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
33 changes: 27 additions & 6 deletions src/license_expression/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -750,12 +750,33 @@ def dedup(self, expression):
),
):
relation = exp.__class__.__name__
deduped = combine_expressions(
expressions,
relation=relation,
unique=True,
licensing=self,
)
# Flatten nested 'AND' expressions only (not OR) to maintain precedence
# Example: (A AND B) AND (C OR D) will become A AND B AND (C OR D)
# The OR will not be flattened to avoid changing the expression logic
if relation == "AND":
flattened = []
for e in expressions:
if isinstance(e, self.AND):
# Flatten nested ANDs by extending with their args
flattened.extend(e.args)
else:
flattened.append(e)
expressions = flattened

unique_expressions = []
for e in expressions:
if e not in unique_expressions:
unique_expressions.append(e)

if len(unique_expressions) == 1:
deduped = unique_expressions[0]
else:
deduped = combine_expressions(
expressions,
relation=relation,
unique=True,
licensing=self,
)
else:
raise ExpressionError(f"Unknown expression type: {expression!r}")
return deduped
Expand Down
42 changes: 42 additions & 0 deletions tests/test_license_expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,48 @@ def test_dedup_expressions_can_be_simplified_2(self):
expected = l.parse("(mit AND (mit OR bsd-new)) OR mit")
assert result == expected

def test_dedup_expressions_can_be_simplified_3(self):
l = Licensing()
exp = "(gpl AND mit) AND mit AND (gpl OR mit)"
result = l.dedup(exp)
expected = l.parse("gpl AND mit AND (gpl OR mit)")
assert result == expected

def test_dedup_expressions_can_be_simplified_4(self):
l = Licensing()
exp = "(gpl AND mit) AND (mit AND gpl) AND (gpl OR mit)"
result = l.dedup(exp)
expected = l.parse("gpl AND mit AND (gpl OR mit)")
assert result == expected

def test_dedup_expressions_logically_equivalent_1(self):
l = Licensing()
exp = "(gpl OR mit) AND (mit OR gpl)"
result = l.dedup(exp)
expected = l.parse("gpl OR mit")
assert result == expected

def test_dedup_expressions_logically_equivalent_2(self):
l = Licensing()
exp = "(gpl AND mit) AND (mit AND gpl)"
result = l.dedup(exp)
expected = l.parse("gpl AND mit")
assert result == expected

def test_dedup_expressions_logically_equivalent_3(self):
l = Licensing()
exp = "(gpl OR mit) OR (mit OR gpl)"
result = l.dedup(exp)
expected = l.parse("gpl OR mit")
assert result == expected

def test_dedup_expressions_logically_equivalent_4(self):
l = Licensing()
exp = "(gpl AND mit) OR (mit AND gpl)"
result = l.dedup(exp)
expected = l.parse("gpl AND mit")
assert result == expected

def test_dedup_expressions_multiple_occurrences(self):
l = Licensing()
exp = " GPL-2.0 or (mit and LGPL-2.1) or bsd Or GPL-2.0 or (mit and LGPL-2.1)"
Expand Down
Loading