From 1f0d716421881a3510839aedc0d1c54fa993bcfc Mon Sep 17 00:00:00 2001 From: SHIVANSH-ux-ys Date: Tue, 27 Oct 2026 12:11:55 +0530 Subject: [PATCH] fix(validation): support '+' or-later operator on license references (#887) According to the SPDX specification, any SPDX-listed license short-form identifier, user-defined license reference (LicenseRef-), or external license reference can be followed by a '+' operator indicating 'or any later version'. Currently, when a license expression contains a license identifier with '+' (such as 'Apache-1.0+'), spdx_licensing.validate reports the token as an unknown license key because the underlying license list contains base keys without the operator suffix. Consequently, validate_license_expression falsely reports valid expressions as unrecognized license references. Support the '+' operator in validate_license_expression by checking whether the base token (stripped of '+') resolves to a known SPDX license (non-exception) or a defined license reference. Fixes #887 Signed-off-by: SHIVANSH-ux-ys --- .../validation/license_expression_validator.py | 14 +++++++++++--- .../test_license_expression_validator.py | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/spdx_tools/spdx/validation/license_expression_validator.py b/src/spdx_tools/spdx/validation/license_expression_validator.py index 26ba22510..a213b168d 100644 --- a/src/spdx_tools/spdx/validation/license_expression_validator.py +++ b/src/spdx_tools/spdx/validation/license_expression_validator.py @@ -43,8 +43,16 @@ def validate_license_expression( license_ref_ids: List[str] = [license_ref.license_id for license_ref in document.extracted_licensing_info] for non_spdx_token in spdx_licensing.validate(license_expression).invalid_symbols: - if ":" in non_spdx_token: - split_token: List[str] = non_spdx_token.split(":") + token = non_spdx_token + if token.endswith("+"): + base_token = token[:-1] + known_sym = spdx_licensing.known_symbols.get(base_token) + if known_sym and not getattr(known_sym, "is_exception", False): + continue + token = base_token + + if ":" in token: + split_token: List[str] = token.split(":") if len(split_token) != 2: validation_messages.append( ValidationMessage( @@ -72,7 +80,7 @@ def validate_license_expression( ) ) - elif non_spdx_token not in license_ref_ids: + elif token not in license_ref_ids: validation_messages.append( ValidationMessage( f"Unrecognized license reference: {non_spdx_token}. license_expression must only use IDs from the " diff --git a/tests/spdx/validation/test_license_expression_validator.py b/tests/spdx/validation/test_license_expression_validator.py index e965f4803..9351b40fe 100644 --- a/tests/spdx/validation/test_license_expression_validator.py +++ b/tests/spdx/validation/test_license_expression_validator.py @@ -25,6 +25,11 @@ "expression_string", [ "MIT", + "Apache-1.0+", + "Apache-2.0+", + f"{FIXTURE_LICENSE_ID}+", + f"{EXTERNAL_DOCUMENT_ID}:LicenseRef-007+", + "Apache-1.0+ or MIT", FIXTURE_LICENSE_ID, f"GPL-2.0-only with GPL-CC-1.0 and {FIXTURE_LICENSE_ID} with 389-exception or Beerware", f"{EXTERNAL_DOCUMENT_ID}:LicenseRef-007", @@ -71,6 +76,8 @@ def test_valid_license_expressions(expression_list): [ (f"{FIXTURE_LICENSE_ID} or LicenseRef-22", ["LicenseRef-22"]), ("nope with 389-exception and _.- or LicenseRef-10", ["nope", "_.-", "LicenseRef-10"]), + ("InvalidLicense+", ["InvalidLicense+"]), + ("LLVM-exception+", ["LLVM-exception+"]), ], ) def test_invalid_license_expression_with_unknown_symbols(expression_string, unknown_symbols):