Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify redundant SPDX choices #9641

Merged
merged 6 commits into from
Dec 18, 2024
Merged
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
26 changes: 16 additions & 10 deletions utils/spdx/src/main/kotlin/SpdxExpression.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ sealed class SpdxExpression {
* Return true if this expression offers a license choice. This can only be true if this expression contains the
* [OR operator][SpdxOperator.OR].
*/
open fun offersChoice(): Boolean = false
fun offersChoice(): Boolean = validChoices().size > 1

/**
* Apply a license [choice], optionally limited to the given [subExpression], and return an [SpdxExpression] where
Expand Down Expand Up @@ -249,13 +249,13 @@ class SpdxCompoundExpression(
override fun normalize(mapDeprecated: Boolean) =
SpdxCompoundExpression(operator, children.map { it.normalize(mapDeprecated) })

override fun simplify(): SpdxExpression {
private fun flatten(): SpdxExpression {
val flattenedChildren = children.flatMapTo(mutableSetOf()) { child ->
val simplifiedChild = child.simplify()

if (simplifiedChild is SpdxCompoundExpression && simplifiedChild.operator == operator) {
// Inline nested children of the same operator.
simplifiedChild.children.map { it.simplify() }
simplifiedChild.children.map { if (it is SpdxCompoundExpression) it.flatten() else it }
} else {
setOf(simplifiedChild)
}
Expand All @@ -264,6 +264,12 @@ class SpdxCompoundExpression(
return flattenedChildren.singleOrNull() ?: SpdxCompoundExpression(operator, flattenedChildren)
}

override fun simplify(): SpdxExpression =
// Eliminate redundant choices by creating the set of unique choices and using the choice if there is only one.
validChoices().singleOrNull()?.let {
if (it is SpdxCompoundExpression) it.flatten() else it
} ?: flatten()

override fun sorted(): SpdxExpression {
/**
* Get all transitive children of this expression that are concatenated with the same operator as this compound
Expand Down Expand Up @@ -300,6 +306,8 @@ class SpdxCompoundExpression(
override fun validChoices(): Set<SpdxExpression> =
when (operator) {
SpdxOperator.AND -> {
// If there is a top-level `AND` in an expression, create the combinations of all choices on the left
// and all choices on the right to get the overall valid choices.
children.fold(setOf()) { acc, child ->
if (acc.isEmpty()) {
child.validChoices()
Expand All @@ -317,13 +325,11 @@ class SpdxCompoundExpression(
}
}

SpdxOperator.OR -> children.flatMapTo(mutableSetOf()) { it.validChoices() }
}

override fun offersChoice(): Boolean =
when (operator) {
SpdxOperator.OR -> true
SpdxOperator.AND -> children.any { it.offersChoice() }
SpdxOperator.OR -> {
// If there is a top-level `OR` in an expression, the operands already are the choices and just need to
// be checked themselves for choices.
children.flatMapTo(mutableSetOf()) { it.validChoices() }
}
}

override fun applyChoice(choice: SpdxExpression, subExpression: SpdxExpression): SpdxExpression {
Expand Down
41 changes: 35 additions & 6 deletions utils/spdx/src/test/kotlin/SpdxCompoundExpressionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,54 @@ class SpdxCompoundExpressionTest : WordSpec({
)

// Compare string representations to not rely on semantic equality.
expression.simplify().toString() shouldBe SpdxCompoundExpression(
expression.simplify().sorted().toString() shouldBe "Apache-2.0 AND MIT"
}

"create a single expression for equal AND-operands" {
mnonnenmacher marked this conversation as resolved.
Show resolved Hide resolved
val expression = SpdxCompoundExpression(
SpdxOperator.AND,
listOf(
SpdxLicenseIdExpression("MIT"),
SpdxLicenseIdExpression("Apache-2.0")
SpdxLicenseIdExpression("MIT")
)
).toString()
)

// Compare string representations to not rely on semantic equality.
expression.simplify().toString() shouldBe "MIT"
}

"create a single expression for equal operands" {
"create a single expression for equal OR-operands" {
val expression = SpdxCompoundExpression(
SpdxOperator.AND,
SpdxOperator.OR,
listOf(
SpdxLicenseIdExpression("MIT"),
SpdxLicenseIdExpression("MIT")
)
)

expression.simplify() shouldBe SpdxLicenseIdExpression("MIT")
// Compare string representations to not rely on semantic equality.
expression.simplify().toString() shouldBe "MIT"
}

"remove redundant choices with a nested expression at the beginning" {
val expression = "(Apache-2.0 OR MIT) AND MIT AND Apache-2.0 AND Apache-2.0 AND MIT".toSpdx()

// Compare string representations to not rely on semantic equality.
expression.simplify().sorted().toString() shouldBe "Apache-2.0 AND MIT"
}

"remove redundant choices with a nested expression in the middle" {
val expression = "Apache-2.0 AND (Apache-2.0 OR MIT) AND MIT AND MIT".toSpdx()

// Compare string representations to not rely on semantic equality.
expression.simplify().sorted().toString() shouldBe "Apache-2.0 AND MIT"
}

"remove redundant choices with a nested expression at the end" {
val expression = "MIT AND Apache-2.0 AND Apache-2.0 AND MIT AND (Apache-2.0 OR MIT)".toSpdx()

// Compare string representations to not rely on semantic equality.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this say "structural equality" instead? I'm actually not sure if I understand the comment.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is about simplifying expressions, I want to be sure that e.g. MIT AND MIT is simplified to exactly MIT although it is "MIT AND MIT".toSpdx() == "MIT".toSpdx().

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but isn't "MIT AND MIT" semantically equal with "MIT", but not syntactically?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. The comment is meaning to say: "If we would not use string-comparison here, expressions would be compared for being semantically equal, which is not what we want."

expression.simplify().sorted().toString() shouldBe "Apache-2.0 AND MIT"
}
}
})
4 changes: 4 additions & 0 deletions utils/spdx/src/test/kotlin/SpdxExpressionChoiceTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,10 @@ class SpdxExpressionChoiceTest : WordSpec({
"a AND b".toSpdx().offersChoice() shouldBe false
"a AND b AND c".toSpdx().offersChoice() shouldBe false
}

"return false if the expression contains the OR operator with equal operands" {
"a OR a".toSpdx().offersChoice() shouldBe false
}
}

"validChoices()" should {
Expand Down
Loading