Skip to content

Commit 6e3a8e6

Browse files
committed
feat(model): add + merge operators for scan-domain objects
Prep work for upcoming PR oss-review-toolkit#10502. * `FileList`, `ScanSummary`, `ScanResult` and `ScannerRun` now implement `operator fun plus(...)`, enabling declarative, type-safe merging of scan data. * Each operator validates invariants (e.g. identical provenance, identical scanner / environment / config) and throws an `IllegalArgumentException` on mismatch. * For compatible objects, collections are union-merged; time ranges are widened (`startTime = min`, `endTime = max`). * `ScannerRun.plus()` * Merges file lists and scan results by `provenance to scanner` using the new operators. * Combines issues / scanners maps by key union. * Carries forward earliest start and latest end timestamps. * **Unit tests** * Extensive coverage in `ScannerRunTest` for all merge scenarios, including negative cases (different config / environment) and positive cases for times, provenances, issues, scanners, file lists and scan results. Signed-off-by: Jonatan Männchen <jonatan@maennchen.ch>
1 parent 9507abf commit 6e3a8e6

File tree

5 files changed

+398
-0
lines changed

5 files changed

+398
-0
lines changed

model/src/main/kotlin/FileList.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,12 @@ data class FileList(
6464
}
6565
}
6666
}
67+
68+
operator fun plus(other: FileList): FileList {
69+
require(provenance == other.provenance) {
70+
"Cannot merge FileLists with different provenance: $provenance != ${other.provenance}."
71+
}
72+
73+
return copy(files = files + other.files)
74+
}
6775
}

model/src/main/kotlin/ScanResult.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,19 @@ data class ScanResult(
7373
*/
7474
fun filterByIgnorePatterns(ignorePatterns: Collection<String>): ScanResult =
7575
copy(summary = summary.filterByIgnorePatterns(ignorePatterns))
76+
77+
operator fun plus(other: ScanResult): ScanResult {
78+
require(provenance == other.provenance) {
79+
"Cannot merge ScanResults with different provenance: $provenance != ${other.provenance}."
80+
}
81+
82+
require(scanner == other.scanner) {
83+
"Cannot merge ScanResults with different scanners: $scanner != ${other.scanner}."
84+
}
85+
86+
return copy(
87+
summary = summary + other.summary,
88+
additionalData = additionalData + other.additionalData
89+
)
90+
}
7691
}

model/src/main/kotlin/ScanSummary.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,17 @@ data class ScanSummary(
146146
issues = issues.filter { it.affectedPath == null || !matcher.matches(it.affectedPath) }
147147
)
148148
}
149+
150+
operator fun plus(other: ScanSummary): ScanSummary {
151+
return copy(
152+
startTime = minOf(startTime, other.startTime),
153+
endTime = maxOf(endTime, other.endTime),
154+
licenseFindings = licenseFindings + other.licenseFindings,
155+
copyrightFindings = copyrightFindings + other.copyrightFindings,
156+
snippetFindings = snippetFindings + other.snippetFindings,
157+
issues = issues + other.issues
158+
)
159+
}
149160
}
150161

151162
/**

model/src/main/kotlin/ScannerRun.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,50 @@ data class ScannerRun(
273273
scanResultsById.mapValues { (_, scanResults) ->
274274
scanResults.flatMapTo(mutableSetOf()) { it.summary.issues }
275275
}.zipWithSets(issues)
276+
277+
operator fun plus(other: ScannerRun): ScannerRun {
278+
require(environment == other.environment) {
279+
"Cannot merge ScannerRuns with different environments: $environment != ${other.environment}."
280+
}
281+
282+
require(config == other.config) {
283+
"Cannot merge ScannerRuns with different configurations: $config != ${other.config}."
284+
}
285+
286+
val mergedFiles = (files.toList() + other.files.toList())
287+
.groupBy { it.provenance }
288+
.values
289+
.map { fileLists ->
290+
fileLists.reduce { acc, next -> acc + next }
291+
}
292+
.toSet()
293+
294+
val mergedScanResults = (scanResults + other.scanResults)
295+
.groupBy { it.provenance to it.scanner }
296+
.values
297+
.map { scanResults ->
298+
scanResults.reduce { acc, next -> acc + next }
299+
}
300+
.toSet()
301+
302+
val mergedIssues = (issues.keys union other.issues.keys).associateWith { key ->
303+
issues[key].orEmpty() union other.issues[key].orEmpty()
304+
}
305+
306+
val mergedScanners = (scanners.keys union other.scanners.keys).associateWith { key ->
307+
scanners[key].orEmpty() union other.scanners[key].orEmpty()
308+
}
309+
310+
return copy(
311+
startTime = minOf(startTime, other.startTime),
312+
endTime = maxOf(endTime, other.endTime),
313+
provenances = provenances + other.provenances,
314+
scanResults = mergedScanResults,
315+
issues = mergedIssues,
316+
scanners = mergedScanners,
317+
files = mergedFiles
318+
)
319+
}
276320
}
277321

278322
private fun scanResultForProvenanceResolutionIssues(packageProvenance: KnownProvenance?, issues: List<Issue>) =

0 commit comments

Comments
 (0)