Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions src/licensedcode/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,56 @@ def use_referenced_license_expression(referenced_license_expression, license_det
return True



def combine_expressions_with_exception_handling(matches, licensing):
"""
Combine license expressions from matches, using WITH for license+exception pairs
and AND for other combinations.

Returns a combined LicenseExpression or None.
"""
if not matches:
return None

from licensedcode.cache import get_licenses_db

license_db = get_licenses_db()
expressions = [match.rule.license_expression for match in matches]

all_keys = set()
for expr in expressions:
keys = licensing.license_keys(expr)
all_keys.update(keys)

exceptions = set()
regular_licenses = set()

for key in all_keys:
license_obj = license_db.get(key)
if license_obj and license_obj.is_exception:
exceptions.add(key)
else:
regular_licenses.add(key)

if len(regular_licenses) == 1 and len(exceptions) >= 1:
base_license = list(regular_licenses)[0]
exception_expr = combine_expressions(
expressions=list(exceptions),
relation='AND',
unique=True,
licensing=licensing
)
combined = licensing.parse(f"{base_license} WITH {exception_expr}")
return combined

return combine_expressions(
expressions=expressions,
relation='AND',
unique=True,
licensing=licensing
)


def get_detected_license_expression(
analysis,
license_matches=None,
Expand Down Expand Up @@ -1591,10 +1641,7 @@ def get_detected_license_expression(
if TRACE:
logger_debug(f'matches_for_expression: {matches_for_expression}', f'detection_log: {detection_log}')

combined_expression = combine_expressions(
expressions=[match.rule.license_expression for match in matches_for_expression],
licensing=get_licensing(),
)
combined_expression = combine_expressions_with_exception_handling(matches_for_expression, get_licensing())

if TRACE or TRACE_ANALYSIS:
logger_debug(f'combined_expression {combined_expression}')
Expand Down
39 changes: 39 additions & 0 deletions tests/licensedcode/test_spdx_exception_fix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import os
from licensedcode.detection import get_detected_license_expression
from licensedcode.detection import DetectionCategory
from licensedcode.match import LicenseMatch
from licensedcode.models import Rule
from licensedcode.cache import get_licensing

def test_gpl_with_gcc_exception_uses_with_operator():
"""
Test that GPL-3.0 and GCC-exception are combined with WITH instead of AND
"""
licensing = get_licensing()

gpl_rule = Rule(
license_expression='gpl-3.0',
text='GPL 3.0 text',
)

gcc_exception_rule = Rule(
license_expression='gcc-exception-3.1',
text='GCC exception text',
)

gpl_match = LicenseMatch(rule=gpl_rule, qspan=(0, 10), ispan=(0, 10))
gcc_match = LicenseMatch(rule=gcc_exception_rule, qspan=(11, 20), ispan=(11, 20))

matches = [gpl_match, gcc_match]

detection_log, combined_expression = get_detected_license_expression(
analysis=DetectionCategory.UNKNOWN_MATCH.value,
license_matches=matches,
)

assert 'WITH' in combined_expression
assert 'gpl-3.0 WITH gcc-exception-3.1' == combined_expression

if __name__ == '__main__':
test_gpl_with_gcc_exception_uses_with_operator()
print("Test passed!")
Loading