Skip to content

Commit 1901be6

Browse files
committed
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 f4808f8 commit 1901be6

File tree

5 files changed

+396
-0
lines changed

5 files changed

+396
-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
@@ -93,4 +93,19 @@ data class ScanResult(
9393
)
9494
)
9595
}
96+
97+
operator fun plus(other: ScanResult): ScanResult {
98+
require(provenance == other.provenance) {
99+
"Cannot merge ScanResults with different provenance: $provenance != ${other.provenance}."
100+
}
101+
102+
require(scanner == other.scanner) {
103+
"Cannot merge ScanResults with different scanners: $scanner != ${other.scanner}."
104+
}
105+
106+
return copy(
107+
summary = summary + other.summary,
108+
additionalData = additionalData + other.additionalData
109+
)
110+
}
96111
}

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
@@ -310,6 +310,50 @@ data class ScannerRun(
310310

311311
return copy(scanResults = paddedScanResults)
312312
}
313+
314+
operator fun plus(other: ScannerRun): ScannerRun {
315+
require(environment == other.environment) {
316+
"Cannot merge ScannerRuns with different environments: $environment != ${other.environment}."
317+
}
318+
319+
require(config == other.config) {
320+
"Cannot merge ScannerRuns with different configurations: $config != ${other.config}."
321+
}
322+
323+
val mergedFiles = (files.toList() + other.files.toList())
324+
.groupBy { it.provenance }
325+
.values
326+
.map { fileLists ->
327+
fileLists.reduce { acc, next -> acc + next }
328+
}
329+
.toSet()
330+
331+
val mergedScanResults = (scanResults + other.scanResults)
332+
.groupBy { it.provenance to it.scanner }
333+
.values
334+
.map { scanResults ->
335+
scanResults.reduce { acc, next -> acc + next }
336+
}
337+
.toSet()
338+
339+
val mergedIssues = (issues.keys union other.issues.keys).associateWith { key ->
340+
issues[key].orEmpty() union other.issues[key].orEmpty()
341+
}
342+
343+
val mergedScanners = (scanners.keys union other.scanners.keys).associateWith { key ->
344+
scanners[key].orEmpty() union other.scanners[key].orEmpty()
345+
}
346+
347+
return copy(
348+
startTime = minOf(startTime, other.startTime),
349+
endTime = maxOf(endTime, other.endTime),
350+
provenances = provenances + other.provenances,
351+
scanResults = mergedScanResults,
352+
issues = mergedIssues,
353+
scanners = mergedScanners,
354+
files = mergedFiles
355+
)
356+
}
313357
}
314358

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

0 commit comments

Comments
 (0)