From ca0eac905dbfe749559574c016345baca91ce8c1 Mon Sep 17 00:00:00 2001 From: Saquib Saifee Date: Tue, 3 Mar 2026 09:19:17 -0500 Subject: [PATCH] Fix validate() to report errors for invalid SPDX expressions with trailing operators The validate() method failed silently when encountering expressions with trailing operators (e.g. 'GPL-3.0-or-later AND'). The root cause was that the exception handler accessed e.token_string on ExpressionError, but only ExpressionParseError (a subclass) has that attribute. Fix by catching ExpressionParseError and ExpressionError separately. Fixes #114 Signed-off-by: Saquib Saifee --- CHANGELOG.rst | 8 ++++++ src/license_expression/__init__.py | 5 +++- tests/test_license_expression.py | 39 ++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 80c86fc..e3f5030 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,14 @@ Changelog ========= +v30.4.5 - 2026-03-03 +-------------------- + +This is a minor bugfix release: + +- Fix validate() to properly report errors for invalid expressions with + trailing operators (e.g. "GPL-3.0-or-later AND"). + v30.4.4 - 2025-01-10 -------------------- diff --git a/src/license_expression/__init__.py b/src/license_expression/__init__.py index dc1ab31..712e2df 100644 --- a/src/license_expression/__init__.py +++ b/src/license_expression/__init__.py @@ -785,10 +785,13 @@ def validate(self, expression, strict=True, **kwargs): # Check `expression` type and syntax try: parsed_expression = self.parse(expression, strict=strict) - except ExpressionError as e: + except ExpressionParseError as e: expression_info.errors.append(str(e)) expression_info.invalid_symbols.append(e.token_string) return expression_info + except ExpressionError as e: + expression_info.errors.append(str(e)) + return expression_info # Check `expression` keys (validate) try: diff --git a/tests/test_license_expression.py b/tests/test_license_expression.py index 193fafd..e43e057 100644 --- a/tests/test_license_expression.py +++ b/tests/test_license_expression.py @@ -2472,6 +2472,45 @@ def test_validation_invalid_license_exception_strict_false(self): assert result.errors == [] assert result.invalid_symbols == [] + def test_validate_trailing_and_operator(self): + result = self.licensing.validate("GPL-2.0-or-later AND") + assert result.original_expression == "GPL-2.0-or-later AND" + assert not result.normalized_expression + assert len(result.errors) == 1 + assert "AND" in result.errors[0] + + def test_validate_trailing_or_operator(self): + result = self.licensing.validate("GPL-2.0-or-later OR") + assert result.original_expression == "GPL-2.0-or-later OR" + assert not result.normalized_expression + assert len(result.errors) == 1 + assert "OR" in result.errors[0] + + def test_validate_trailing_with_operator(self): + result = self.licensing.validate("GPL-2.0-or-later WITH") + assert result.original_expression == "GPL-2.0-or-later WITH" + assert not result.normalized_expression + assert len(result.errors) == 1 + + def test_validate_multiple_trailing_operators(self): + result = self.licensing.validate("GPL-2.0-or-later AND MIT OR") + assert result.original_expression == "GPL-2.0-or-later AND MIT OR" + assert not result.normalized_expression + assert len(result.errors) == 1 + assert "OR" in result.errors[0] + + def test_validate_leading_operator(self): + result = self.licensing.validate("AND MIT") + assert result.original_expression == "AND MIT" + assert not result.normalized_expression + assert len(result.errors) == 1 + + def test_validate_only_operators(self): + result = self.licensing.validate("AND OR") + assert result.original_expression == "AND OR" + assert not result.normalized_expression + assert len(result.errors) == 1 + class UtilTest(TestCase): test_data_dir = join(dirname(__file__), "data")