Skip to content

Commit 7c59584

Browse files
maennchensschuberth
authored andcommitted
feat(model): Add + merge operators for scan-domain objects
Prep work for upcoming PR #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 3753063 commit 7c59584

File tree

5 files changed

+421
-0
lines changed

5 files changed

+421
-0
lines changed

model/src/main/kotlin/FileList.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,19 @@ data class FileList(
6464
}
6565
}
6666
}
67+
68+
/**
69+
* Merge this [FileList] with the given [other] [FileList].
70+
*
71+
* Both [FileList]s must have the same [provenance], otherwise an [IllegalArgumentException] is thrown.
72+
*/
73+
operator fun plus(other: FileList) =
74+
FileList(
75+
provenance = provenance.also {
76+
require(it == other.provenance) {
77+
"Cannot merge FileLists with different provenance: $it != ${other.provenance}."
78+
}
79+
},
80+
files = files + other.files
81+
)
6782
}

model/src/main/kotlin/ScanResult.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,26 @@ data class ScanResult(
7373
*/
7474
fun filterByIgnorePatterns(ignorePatterns: Collection<String>): ScanResult =
7575
copy(summary = summary.filterByIgnorePatterns(ignorePatterns))
76+
77+
/**
78+
* Merge this [ScanResult] with the given [other] [ScanResult].
79+
*
80+
* Both [ScanResult]s must have the same [provenance] and [scanner], otherwise an [IllegalArgumentException] is
81+
* thrown.
82+
*/
83+
operator fun plus(other: ScanResult) =
84+
ScanResult(
85+
provenance = provenance.also {
86+
require(it == other.provenance) {
87+
"Cannot merge ScanResults with different provenance: $it != ${other.provenance}."
88+
}
89+
},
90+
scanner = scanner.also {
91+
require(it == other.scanner) {
92+
"Cannot merge ScanResults with different scanners: $it != ${other.scanner}."
93+
}
94+
},
95+
summary = summary + other.summary,
96+
additionalData = additionalData + other.additionalData
97+
)
7698
}

model/src/main/kotlin/ScanSummary.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,23 @@ data class ScanSummary(
146146
issues = issues.filter { it.affectedPath == null || !matcher.matches(it.affectedPath) }
147147
)
148148
}
149+
150+
/**
151+
* Merge this [ScanSummary] with the given [other] [ScanSummary].
152+
*
153+
* The [startTime] and [endTime] are widened to the earliest and latest time of both summaries.
154+
*
155+
* The [licenseFindings], [copyrightFindings], [snippetFindings] and [issues] are merged by concatenation.
156+
*/
157+
operator fun plus(other: ScanSummary) =
158+
ScanSummary(
159+
startTime = minOf(startTime, other.startTime),
160+
endTime = maxOf(endTime, other.endTime),
161+
licenseFindings = licenseFindings + other.licenseFindings,
162+
copyrightFindings = copyrightFindings + other.copyrightFindings,
163+
snippetFindings = snippetFindings + other.snippetFindings,
164+
issues = issues + other.issues
165+
)
149166
}
150167

151168
/**

model/src/main/kotlin/ScannerRun.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,53 @@ data class ScannerRun(
273273
scanResultsById.mapValues { (_, scanResults) ->
274274
scanResults.flatMapTo(mutableSetOf()) { it.summary.issues }
275275
}.zipWithSets(issues)
276+
277+
/**
278+
* Merge this [ScannerRun] with the given [other] [ScannerRun].
279+
*
280+
* Both [ScannerRun]s must have the same [environment] and [config], otherwise an [IllegalArgumentException] is
281+
* thrown.
282+
*
283+
* files and scanResults are merged by their [KnownProvenance]s, while issues and scanners are merged by their
284+
* names.
285+
*
286+
* The [startTime] and [endTime] are widened to the earliest and latest values of both [ScannerRun]s.
287+
*/
288+
operator fun plus(other: ScannerRun): ScannerRun {
289+
val mergedFiles = (files + other.files)
290+
.groupBy { it.provenance }
291+
.values
292+
.mapTo(mutableSetOf()) { fileLists ->
293+
fileLists.reduce { acc, next -> acc + next }
294+
}
295+
296+
val mergedScanResults = (scanResults + other.scanResults)
297+
.groupBy { it.provenance to it.scanner }
298+
.values
299+
.mapTo(mutableSetOf()) { scanResults ->
300+
scanResults.reduce { acc, next -> acc + next }
301+
}
302+
303+
return ScannerRun(
304+
startTime = minOf(startTime, other.startTime),
305+
endTime = maxOf(endTime, other.endTime),
306+
environment = environment.also {
307+
require(it == other.environment) {
308+
"Cannot merge ScannerRuns with different environments: $it != ${other.environment}."
309+
}
310+
},
311+
config = config.also {
312+
require(it == other.config) {
313+
"Cannot merge ScannerRuns with different configurations: $it != ${other.config}."
314+
}
315+
},
316+
provenances = provenances + other.provenances,
317+
scanResults = mergedScanResults,
318+
issues = issues.zipWithSets(other.issues),
319+
scanners = scanners.zipWithSets(other.scanners),
320+
files = mergedFiles
321+
)
322+
}
276323
}
277324

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

0 commit comments

Comments
 (0)