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

Associate stand-alone ScanCode exceptions to licenses #5078

Merged
merged 4 commits into from
Feb 23, 2022
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
24 changes: 24 additions & 0 deletions model/src/main/kotlin/TextLocation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

package org.ossreviewtoolkit.model

import kotlin.math.abs
import kotlin.math.min

/**
* A [TextLocation] references text located in a file.
*/
Expand Down Expand Up @@ -59,4 +62,25 @@ data class TextLocation(
constructor(path: String, line: Int) : this(path, line, line)

override fun compareTo(other: TextLocation) = COMPARATOR.compare(this, other)

/**
* Return whether the given [line] is contained in the location.
*/
operator fun contains(line: Int) = line != UNKNOWN_LINE && line in startLine..endLine

/**
* Return whether this and the [other] locations are overlapping, i.e. they share at least a single line. Note that
* the [path] is not compared.
*/
fun linesOverlapWith(other: TextLocation) = startLine in other || other.startLine in this

/**
* Return the minimum distance between this and the [other] location. A distance of 0 means that the locations are
* overlapping.
*/
fun distanceTo(other: TextLocation) =
when {
linesOverlapWith(other) -> 0
else -> min(abs(other.startLine - endLine), abs(startLine - other.endLine))
}
}
62 changes: 62 additions & 0 deletions model/src/main/kotlin/utils/FindingsMatcher.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import kotlin.math.min
import org.ossreviewtoolkit.model.CopyrightFinding
import org.ossreviewtoolkit.model.LicenseFinding
import org.ossreviewtoolkit.model.TextLocation
import org.ossreviewtoolkit.utils.spdx.SpdxConstants.NOASSERTION
import org.ossreviewtoolkit.utils.spdx.SpdxLicenseException
import org.ossreviewtoolkit.utils.spdx.toSpdx

/**
* A class for matching copyright findings to license findings. Copyright statements may be matched either to license
Expand Down Expand Up @@ -204,3 +207,62 @@ private fun MutableMap<LicenseFinding, MutableSet<CopyrightFinding>>.merge(
getOrPut(licenseFinding) { mutableSetOf() } += copyrightFindings
}
}

/**
* Process [findings] for stand-alone license exceptions and associate them with nearby (according to [toleranceLines])
* applicable licenses. Orphan license exceptions will get associated by [NOASSERTION]. Return the list of resulting
* findings.
*/
fun associateLicensesWithExceptions(
findings: List<LicenseFinding>,
toleranceLines: Int = FindingsMatcher.DEFAULT_TOLERANCE_LINES
): List<LicenseFinding> {
val (exceptions, licenses) = findings.partition { SpdxLicenseException.forId(it.license.toString()) != null }

val remainingExceptions = exceptions.toMutableList()
val fixedLicenses = licenses.toMutableList()

val i = remainingExceptions.iterator()

while (i.hasNext()) {
val exception = i.next()

// Determine all licenses exception is applicable to.
val applicableLicenses = SpdxLicenseException.mapping[exception.license.toString()].orEmpty().map { it.id }

// Determine applicable license findings from the same path.
val applicableLicenseFindings = licenses.filter {
it.location.path == exception.location.path && it.license.toString() in applicableLicenses
}

// Find the closest license within the tolerance.
val associatedLicenseFinding = applicableLicenseFindings
.map { it to it.location.distanceTo(exception.location) }
.sortedBy { it.second }
.firstOrNull { it.second <= toleranceLines }
?.first

if (associatedLicenseFinding != null) {
// Add the fixed-up license with the exception.
fixedLicenses += associatedLicenseFinding.copy(
license = "${associatedLicenseFinding.license} WITH ${exception.license}".toSpdx(),
location = associatedLicenseFinding.location.copy(
startLine = min(associatedLicenseFinding.location.startLine, exception.location.startLine),
endLine = max(associatedLicenseFinding.location.endLine, exception.location.endLine)
)
)

// Remove the original license and the stand-alone exception.
fixedLicenses.remove(associatedLicenseFinding)
i.remove()
}
}

// Associate remaining "orphan" exceptions with "NOASSERTION" to turn them into valid SPDX expressions and at the
// same time "marking" them for review as "NOASSERTION" is not a real license.
remainingExceptions.mapTo(fixedLicenses) { exception ->
exception.copy(license = "$NOASSERTION WITH ${exception.license}".toSpdx())
}

return fixedLicenses
}
39 changes: 39 additions & 0 deletions model/src/test/kotlin/utils/FindingsMatcherTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -223,5 +223,44 @@ class FindingsMatcherTest : WordSpec() {
result.getCopyrights("root-license-1").map { it.statement } should containExactly("statement 1")
}
}

"associateLicensesWithExceptions()" should {
"merge with the nearest license" {
associateLicensesWithExceptions(
listOf(
LicenseFinding("Apache-2.0", TextLocation("file", 1)),
LicenseFinding("Apache-2.0", TextLocation("file", 100)),
LicenseFinding("LLVM-exception", TextLocation("file", 5))
)
) should containExactlyInAnyOrder(
LicenseFinding("Apache-2.0 WITH LLVM-exception", TextLocation("file", 1, 5)),
LicenseFinding("Apache-2.0", TextLocation("file", 100))
)
}

"associate orphan exceptions by NOASSERTION" {
associateLicensesWithExceptions(
listOf(
LicenseFinding("GPL-2.0-only", TextLocation("file", 1)),
LicenseFinding("389-exception", TextLocation("file", 100))
)
) should containExactlyInAnyOrder(
LicenseFinding("GPL-2.0-only", TextLocation("file", 1)),
LicenseFinding("NOASSERTION WITH 389-exception", TextLocation("file", 100))
)
}

"not associate findings from different files" {
associateLicensesWithExceptions(
listOf(
LicenseFinding("Apache-2.0", TextLocation("fileA", 1)),
LicenseFinding("LLVM-exception", TextLocation("fileB", 5))
)
) should containExactlyInAnyOrder(
LicenseFinding("Apache-2.0", TextLocation("fileA", 1)),
LicenseFinding("NOASSERTION WITH LLVM-exception", TextLocation("fileB", 5))
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import org.ossreviewtoolkit.model.OrtIssue
import org.ossreviewtoolkit.model.ScanSummary
import org.ossreviewtoolkit.model.ScannerDetails
import org.ossreviewtoolkit.model.TextLocation
import org.ossreviewtoolkit.model.utils.associateLicensesWithExceptions
import org.ossreviewtoolkit.utils.common.textValueOrEmpty
import org.ossreviewtoolkit.utils.spdx.SpdxConstants
import org.ossreviewtoolkit.utils.spdx.SpdxConstants.LICENSE_REF_PREFIX
Expand Down Expand Up @@ -201,7 +202,7 @@ private fun getLicenseFindings(result: JsonNode, parseExpressions: Boolean): Lis
}
}

return licenseFindings
return associateLicensesWithExceptions(licenseFindings)
}

/**
Expand Down
Loading