EvidenceReportCollector.java

1
// SPDX-License-Identifier: Apache-2.0
2
// Copyright 2026 Egothor
3
// Copyright 2026 Accenture
4
package org.egothor.methodatlas.evidencereport;
5
6
import java.nio.file.Path;
7
import java.time.Instant;
8
import java.util.ArrayList;
9
import java.util.Collections;
10
import java.util.LinkedHashMap;
11
import java.util.List;
12
import java.util.Map;
13
14
import org.egothor.methodatlas.ai.AiMethodSuggestion;
15
import org.egothor.methodatlas.ai.CweMapping;
16
import org.egothor.methodatlas.emit.TestMethodSink;
17
18
/**
19
 * Streaming sink that builds an {@link EvidenceReport} from scan records observed during
20
 * a single run.
21
 *
22
 * <p>
23
 * The collector implements {@link TestMethodSink} so the orchestration layer can
24
 * hand it the same record stream that drives SARIF/CSV emitters, without a second
25
 * scan and without coupling the emitters to evidence-report logic.
26
 * </p>
27
 *
28
 * <p>
29
 * Only AI-classified, security-relevant methods whose confidence meets the configured
30
 * minimum are collected. Methods without an AI suggestion, or whose suggestion is not
31
 * security-relevant, are silently skipped.
32
 * </p>
33
 *
34
 * <p>
35
 * Package-private because nothing outside the {@code evidencereport} package needs to
36
 * construct one; {@link EvidenceReportFacade} is the sole external entry point.
37
 * </p>
38
 */
39
final class EvidenceReportCollector implements TestMethodSink {
40
41
    /** Minimum AI confidence required to include a method in the report. */
42
    private final double minConfidence;
43
44
    /** Scan-root paths forwarded to the report header; stored as strings for serialisation. */
45
    private final List<String> scanRoots;
46
47
    /** Total methods seen (all, not just security-relevant). */
48
    private int totalScanned;
49
50
    /**
51
     * Accumulated entries grouped by FQCN in encounter order. Each FQCN maps to
52
     * the ordered list of qualifying methods in that class.
53
     */
54
    private final Map<String, List<EvidenceReportEntry>> accumulator = new LinkedHashMap<>();
55
56
    /**
57
     * Creates a collector.
58
     *
59
     * @param minConfidence minimum AI confidence required to include a method;
60
     *                      must be in {@code [0.0, 1.0]}
61
     * @param scanRoots     scan-root paths included verbatim in the report header
62
     */
63
    /* default */ EvidenceReportCollector(double minConfidence, List<Path> scanRoots) {
64
        this.minConfidence = minConfidence;
65
        List<String> roots = new ArrayList<>(scanRoots.size());
66
        for (Path p : scanRoots) {
67
            roots.add(p.toString());
68
        }
69
        this.scanRoots = Collections.unmodifiableList(roots);
70
    }
71
72
    /**
73
     * Records a single test method.
74
     *
75
     * <p>
76
     * Every call increments the total-scanned counter. Only methods with a non-null,
77
     * security-relevant AI suggestion meeting the minimum confidence threshold are
78
     * added to the evidence set.
79
     * </p>
80
     *
81
     * @param fqcn        fully qualified class name
82
     * @param method      test method name
83
     * @param beginLine   first source line of the method
84
     * @param loc         lines of code
85
     * @param contentHash SHA-256 of the enclosing class source, or {@code null}
86
     * @param tags        source-declared {@code @Tag} values
87
     * @param displayName {@code @DisplayName} annotation value, or {@code null}
88
     * @param suggestion  AI classification, or {@code null}
89
     */
90
    @Override
91
    @SuppressWarnings("PMD.UseObjectForClearerAPI")
92
    public void record(String fqcn, String method, int beginLine, int loc, String contentHash,
93
            List<String> tags, String displayName, AiMethodSuggestion suggestion) {
94 1 1. record : Replaced integer addition with subtraction → NO_COVERAGE
        totalScanned++;
95 4 1. record : removed conditional - replaced equality check with false → NO_COVERAGE
2. record : removed conditional - replaced equality check with false → NO_COVERAGE
3. record : removed conditional - replaced equality check with true → NO_COVERAGE
4. record : removed conditional - replaced equality check with true → NO_COVERAGE
        if (suggestion == null || !suggestion.securityRelevant()) {
96
            return;
97
        }
98 3 1. record : changed conditional boundary → NO_COVERAGE
2. record : removed conditional - replaced comparison check with true → NO_COVERAGE
3. record : removed conditional - replaced comparison check with false → NO_COVERAGE
        if (suggestion.confidence() < minConfidence) {
99
            return;
100
        }
101 2 1. record : removed conditional - replaced equality check with false → NO_COVERAGE
2. record : removed conditional - replaced equality check with true → NO_COVERAGE
        List<String> aiTags = suggestion.tags() != null
102
                ? Collections.unmodifiableList(new ArrayList<>(suggestion.tags()))
103
                : List.of();
104
        List<String> cwe = CweMapping.forTags(aiTags);
105
        EvidenceReportEntry entry = new EvidenceReportEntry(
106
                fqcn, method, beginLine, loc, contentHash,
107 2 1. record : removed conditional - replaced equality check with false → NO_COVERAGE
2. record : removed conditional - replaced equality check with true → NO_COVERAGE
                tags != null ? Collections.unmodifiableList(new ArrayList<>(tags)) : List.of(),
108
                displayName,
109
                suggestion.displayName(),
110
                suggestion.securityRelevant(),
111
                aiTags,
112
                suggestion.reason(),
113
                suggestion.confidence(),
114
                suggestion.interactionScore(),
115
                cwe);
116 1 1. lambda$record$0 : replaced return value with Collections.emptyList for org/egothor/methodatlas/evidencereport/EvidenceReportCollector::lambda$record$0 → NO_COVERAGE
        accumulator.computeIfAbsent(fqcn, k -> new ArrayList<>()).add(entry);
117
    }
118
119
    /**
120
     * Builds the final {@link EvidenceReport}.
121
     *
122
     * @param toolVersion resolved tool version string; never {@code null}
123
     * @return populated report
124
     */
125
    /* default */ EvidenceReport buildReport(String toolVersion) {
126
        int secCount = accumulator.values().stream().mapToInt(List::size).sum();
127
        Map<String, List<EvidenceReportEntry>> snapshot = new LinkedHashMap<>();
128
        for (Map.Entry<String, List<EvidenceReportEntry>> e : accumulator.entrySet()) {
129
            snapshot.put(e.getKey(), Collections.unmodifiableList(new ArrayList<>(e.getValue()))); // NOPMD AvoidInstantiatingObjectsInLoops
130
        }
131 1 1. buildReport : replaced return value with null for org/egothor/methodatlas/evidencereport/EvidenceReportCollector::buildReport → NO_COVERAGE
        return new EvidenceReport(
132
                Instant.now().toString(),
133
                toolVersion,
134
                scanRoots,
135
                totalScanned,
136
                secCount,
137
                Collections.unmodifiableMap(snapshot));
138
    }
139
}

Mutations

94

1.1
Location : record
Killed by : none
Replaced integer addition with subtraction → NO_COVERAGE

95

1.1
Location : record
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : record
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

3.3
Location : record
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

4.4
Location : record
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

98

1.1
Location : record
Killed by : none
changed conditional boundary → NO_COVERAGE

2.2
Location : record
Killed by : none
removed conditional - replaced comparison check with true → NO_COVERAGE

3.3
Location : record
Killed by : none
removed conditional - replaced comparison check with false → NO_COVERAGE

101

1.1
Location : record
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : record
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

107

1.1
Location : record
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

2.2
Location : record
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

116

1.1
Location : lambda$record$0
Killed by : none
replaced return value with Collections.emptyList for org/egothor/methodatlas/evidencereport/EvidenceReportCollector::lambda$record$0 → NO_COVERAGE

131

1.1
Location : buildReport
Killed by : none
replaced return value with null for org/egothor/methodatlas/evidencereport/EvidenceReportCollector::buildReport → NO_COVERAGE

Active mutators

Tests examined


Report generated by PIT 1.30.1 support