1818 */
1919
2020package org.ossreviewtoolkit.plugins.scanners.scanoss
21-
21+ import com.scanoss.dto.LicenseDetails
2222import com.scanoss.dto.ScanFileDetails
2323import com.scanoss.dto.ScanFileResult
2424import com.scanoss.dto.enums.MatchType
25+ import com.scanoss.dto.enums.StatusType
2526
27+ import java.lang.invoke.MethodHandles
2628import java.time.Instant
2729
30+ import org.apache.logging.log4j.kotlin.loggerOf
31+
2832import org.ossreviewtoolkit.downloader.VcsHost
2933import org.ossreviewtoolkit.model.CopyrightFinding
3034import org.ossreviewtoolkit.model.LicenseFinding
@@ -36,7 +40,8 @@ import org.ossreviewtoolkit.model.TextLocation
3640import org.ossreviewtoolkit.utils.spdx.SpdxConstants
3741import org.ossreviewtoolkit.utils.spdx.SpdxExpression
3842import org.ossreviewtoolkit.utils.spdx.SpdxLicenseIdExpression
39- import org.ossreviewtoolkit.utils.spdx.toExpression
43+
44+ private val logger = loggerOf(MethodHandles .lookup().lookupClass())
4045
4146/* *
4247 * Generate a summary from the given SCANOSS [result], using [startTime], [endTime] as metadata. This variant can be
@@ -56,16 +61,29 @@ internal fun generateSummary(startTime: Instant, endTime: Instant, results: List
5661 }
5762
5863 MatchType .snippet -> {
59- val file = requireNotNull(details.file)
60- val lines = requireNotNull(details.lines)
61- val sourceLocations = convertLines(file, lines)
62- val snippets = getSnippets(details)
63-
64- snippets.forEach { snippet ->
65- sourceLocations.forEach { sourceLocation ->
66- // TODO: Aggregate the snippet by source file location.
67- snippetFindings + = SnippetFinding (sourceLocation, setOf (snippet))
64+ val file = requireNotNull(result.filePath)
65+ if (details.status == StatusType .pending) {
66+ val lines = requireNotNull(details.lines)
67+ val sourceLocations = convertLines(file, lines)
68+ val snippets = getSnippets(details)
69+
70+ // The number of snippets should match the number of source locations.
71+ if (sourceLocations.size != snippets.size) {
72+ logger.warn {
73+ " Unexpected mismatch in '$file ': " +
74+ " ${sourceLocations.size} source locations vs ${snippets.size} snippets. " +
75+ " This indicates a potential issue with line range conversion."
76+ }
6877 }
78+
79+ // Associate each source location with its corresponding snippet.
80+ sourceLocations.zip(snippets).forEach { (location, snippet) ->
81+ snippetFindings + = SnippetFinding (location, setOf (snippet))
82+ }
83+ } else {
84+ logger.warn { " File '$file ' is identified, not including on snippet findings" }
85+ licenseFindings + = getLicenseFindings(details)
86+ copyrightFindings + = getCopyrightFindings(details)
6987 }
7088 }
7189
@@ -134,36 +152,34 @@ private fun getCopyrightFindings(details: ScanFileDetails): List<CopyrightFindin
134152}
135153
136154/* *
137- * Get the snippet findings from the given [details]. If a snippet returned by ScanOSS contains several Purls,
138- * several snippets are created in ORT each containing a single Purl.
155+ * Get the snippet findings from the given [details]. If a snippet returned by SCANOSS contains several Purls,
156+ * the function uses the first PURL as the primary identifier while storing all PURLs in additionalData
157+ * to preserve the complete information.
139158 */
140- private fun getSnippets (details : ScanFileDetails ): Set <Snippet > {
159+ private fun getSnippets (details : ScanFileDetails ): List <Snippet > {
141160 val matched = requireNotNull(details.matched)
142161 val fileUrl = requireNotNull(details.fileUrl)
143162 val ossLines = requireNotNull(details.ossLines)
144163 val url = requireNotNull(details.url)
145164 val purls = requireNotNull(details.purls)
146165
147- val licenses = details.licenseDetails.orEmpty().mapTo(mutableSetOf ()) { license ->
148- SpdxExpression .parse(license.name)
149- }
166+ val license = getUniqueLicenseExpression(details.licenseDetails.toList())
150167
151168 val score = matched.substringBeforeLast(" %" ).toFloat()
152169 val locations = convertLines(fileUrl, ossLines)
153170 // TODO: No resolved revision is available. Should a ArtifactProvenance be created instead ?
154171 val vcsInfo = VcsHost .parseUrl(url.takeUnless { it == " none" }.orEmpty())
155172 val provenance = RepositoryProvenance (vcsInfo, " ." )
156173
157- val additionalData = mapOf (" release_date" to details.releaseDate)
158-
159- return buildSet {
160- purls.forEach { purl ->
161- locations.forEach { snippetLocation ->
162- val license = licenses.toExpression()?.sorted() ? : SpdxLicenseIdExpression (SpdxConstants .NOASSERTION )
174+ // Store all PURLs in additionalData to preserve the complete information.
175+ val additionalData = mapOf (
176+ " release_date" to details.releaseDate,
177+ " all_purls" to purls.joinToString(" " )
178+ )
163179
164- add( Snippet (score, snippetLocation, provenance, purl, license, additionalData))
165- }
166- }
180+ // Create one snippet per location, using the first PURL as the primary identifier.
181+ return locations.map { snippetLocation ->
182+ Snippet (score, snippetLocation, provenance, purls.firstOrNull().orEmpty(), license, additionalData)
167183 }
168184}
169185
@@ -180,3 +196,14 @@ private fun convertLines(file: String, lineRanges: String): List<TextLocation> =
180196 else -> throw IllegalArgumentException (" Unsupported line range '$lineRange '." )
181197 }
182198 }
199+
200+ fun getUniqueLicenseExpression (licensesDetails : List <LicenseDetails >): SpdxExpression {
201+ if (licensesDetails.isEmpty()) {
202+ return SpdxLicenseIdExpression (SpdxConstants .NOASSERTION )
203+ }
204+
205+ return licensesDetails
206+ .map { license -> SpdxExpression .parse(license.name) }
207+ .reduce { acc, expr -> acc and expr }
208+ .simplify()
209+ }
0 commit comments