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

Introduce a license finding score #5131

Merged
merged 7 commits into from
Mar 4, 2022
Merged
14 changes: 12 additions & 2 deletions model/src/main/kotlin/LicenseFinding.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

package org.ossreviewtoolkit.model

import com.fasterxml.jackson.annotation.JsonInclude

import org.ossreviewtoolkit.model.config.LicenseFindingCuration
import org.ossreviewtoolkit.utils.spdx.SpdxExpression
import org.ossreviewtoolkit.utils.spdx.toSpdx
Expand All @@ -28,6 +30,7 @@ import org.ossreviewtoolkit.utils.spdx.toSpdx
* [SpdxExpression]s, depending on the capabilities of the used license scanner. [LicenseFindingCuration]s can also be
* used to create findings with complex expressions.
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
data class LicenseFinding(
/**
* The found SPDX expression.
Expand All @@ -37,13 +40,20 @@ data class LicenseFinding(
/**
* The text location where the license was found.
*/
val location: TextLocation
val location: TextLocation,

/**
* The score of a license finding. Its exact meaning is scanner-specific, but it should give some hint at how much
* the finding can be relied on / how confident the scanner is to be right. In most cases this is a percentage where
* 100.0 means that the scanner is 100% confident that the finding is correct.
*/
val score: Float? = null
) : Comparable<LicenseFinding> {
companion object {
private val COMPARATOR = compareBy<LicenseFinding>({ it.license.toString() }, { it.location })
}

constructor(license: String, location: TextLocation) : this(license.toSpdx(), location)
constructor(license: String, location: TextLocation, score: Float? = null) : this(license.toSpdx(), location, score)

override fun compareTo(other: LicenseFinding) = COMPARATOR.compare(this, other)
}
5 changes: 2 additions & 3 deletions scanner/src/main/kotlin/scanners/Askalono.kt
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,10 @@ class Askalono(
// Turn absolute paths in the native result into relative paths to not expose any information.
relativizePath(scanPath, File(root["path"].textValue())),
TextLocation.UNKNOWN_LINE
)
),
score = result["score"].floatValue()
)

log.info { "Found $licenseFinding with score ${result["score"].floatValue()}." }

licenseFindings += licenseFinding
}
}
Expand Down
3 changes: 2 additions & 1 deletion scanner/src/main/kotlin/scanners/BoyterLc.kt
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ class BoyterLc(
// Turn absolute paths in the native result into relative paths to not expose any information.
relativizePath(scanPath, filePath),
TextLocation.UNKNOWN_LINE
)
),
score = it["Percentage"].floatValue()
)
}
}
Expand Down
3 changes: 2 additions & 1 deletion scanner/src/main/kotlin/scanners/Licensee.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ class Licensee(
// The path is already relative.
filePath.path,
TextLocation.UNKNOWN_LINE
)
),
score = it["matcher"]["confidence"].floatValue()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ import org.ossreviewtoolkit.utils.spdx.SpdxConstants
import org.ossreviewtoolkit.utils.spdx.SpdxConstants.LICENSE_REF_PREFIX
import org.ossreviewtoolkit.utils.spdx.calculatePackageVerificationCode

private data class LicenseExpression(
private data class LicenseMatch(
val expression: String,
val startLine: Int,
val endLine: Int
val endLine: Int,
val score: Float
)

data class LicenseKeyReplacement(
Expand Down Expand Up @@ -176,28 +177,30 @@ private fun getLicenseFindings(result: JsonNode, parseExpressions: Boolean): Lis

licenses.groupBy(
keySelector = {
LicenseExpression(
LicenseMatch(
// Older ScanCode versions do not produce the `license_expression` field.
// Just use the `key` field in this case.
it["matched_rule"]?.get("license_expression")?.textValue().takeIf { parseExpressions }
?: it["key"].textValue(),
it["start_line"].intValue(),
it["end_line"].intValue()
it["end_line"].intValue(),
it["score"].floatValue()
)
},
valueTransform = {
LicenseKeyReplacement(it["key"].textValue(), getSpdxLicenseId(it))
}
).map { (licenseExpression, replacements) ->
val spdxLicenseExpression = replaceLicenseKeys(licenseExpression.expression, replacements)
).map { (licenseMatch, replacements) ->
val spdxLicenseExpression = replaceLicenseKeys(licenseMatch.expression, replacements)

LicenseFinding(
license = spdxLicenseExpression,
location = TextLocation(
path = file["path"].textValue().removePrefix(input),
startLine = licenseExpression.startLine,
endLine = licenseExpression.endLine
)
startLine = licenseMatch.startLine,
endLine = licenseMatch.endLine
),
score = licenseMatch.score
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@ internal fun generateSummary(
/**
* Get the license findings from the given [match]. Use [filename] as a fallback if no file is given in the match.
*/
private fun getLicenseFindings(match: JsonNode, filename: String): Sequence<LicenseFinding> =
match["licenses"]?.asSequence().orEmpty().map {
private fun getLicenseFindings(match: JsonNode, filename: String): Sequence<LicenseFinding> {
val score = match["matched"]?.textValue()?.removeSuffix("%")?.toFloatOrNull()
return match["licenses"]?.asSequence().orEmpty().map {
val licenseName = it["name"].asText()
val licenseExpression = runCatching { SpdxExpression.parse(licenseName) }.getOrNull()

Expand All @@ -94,9 +95,11 @@ private fun getLicenseFindings(match: JsonNode, filename: String): Sequence<Lice
path = match["file"].textValue() ?: filename,
startLine = TextLocation.UNKNOWN_LINE,
endLine = TextLocation.UNKNOWN_LINE
)
),
score = score
)
}
}

/**
* Get the copyright findings from the given [match]. Use [filename] as a fallback if no file is given in the match.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,11 +315,13 @@ class ScanCodeResultParserTest : FreeSpec({
summary.licenseFindings should containExactlyInAnyOrder(
LicenseFinding(
license = "(MPL-2.0 OR EPL-1.0) AND LicenseRef-scancode-proprietary-license",
location = TextLocation("h2/src/main/org/h2/table/Column.java", 2, 3)
location = TextLocation("h2/src/main/org/h2/table/Column.java", 2, 3),
score = 20.37f
),
LicenseFinding(
license = "LicenseRef-scancode-public-domain",
location = TextLocation("h2/src/main/org/h2/table/Column.java", 317)
location = TextLocation("h2/src/main/org/h2/table/Column.java", 317),
score = 70.0f
)
)
}
Expand Down Expand Up @@ -396,15 +398,18 @@ class ScanCodeResultParserTest : FreeSpec({
summary.licenseFindings should containExactlyInAnyOrder(
LicenseFinding(
license = "Apache-2.0",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/Cargo.toml", 23, 23)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/Cargo.toml", 23, 23),
score = 100.0f
),
LicenseFinding(
license = "Apache-2.0",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/Cargo.toml.orig", 5, 5)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/Cargo.toml.orig", 5, 5),
score = 100.0f
),
LicenseFinding(
license = "Apache-2.0",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/LICENSE-APACHE", 1, 201)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/LICENSE-APACHE", 1, 201),
score = 100.0f
),
LicenseFinding(
license = "Apache-2.0 WITH LLVM-exception",
Expand All @@ -413,31 +418,37 @@ class ScanCodeResultParserTest : FreeSpec({
"LICENSE-Apache-2.0_WITH_LLVM-exception",
startLine = 2,
endLine = 219
)
),
score = 100.0f
),
LicenseFinding(
license = "Apache-2.0",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 85, 88)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 85, 88),
score = 66.67f
),
LicenseFinding(
license = "Apache-2.0",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 93, 93)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 93, 93),
score = 100.0f
),
LicenseFinding(
license = "LicenseRef-scancode-free-unknown",
location = TextLocation(
path = "Downloads/wasi-0.10.2+wasi-snapshot-preview1/ORG_CODE_OF_CONDUCT.md",
startLine = 106,
endLine = 106
)
),
score = 50.0f
),
LicenseFinding(
license = "LicenseRef-scancode-unknown-license-reference",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 88, 88)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/README.md", 88, 88),
score = 100.0f
),
LicenseFinding(
license = "MIT",
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/LICENSE-MIT", 1, 23)
location = TextLocation("Downloads/wasi-0.10.2+wasi-snapshot-preview1/LICENSE-MIT", 1, 23),
score = 100.0f
)
)
}
Expand Down