AiResultCache.java

1
package org.egothor.methodatlas;
2
3
import java.io.IOException;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.HashMap;
7
import java.util.List;
8
import java.util.Map;
9
import java.util.Optional;
10
11
import org.egothor.methodatlas.ai.AiClassSuggestion;
12
import org.egothor.methodatlas.ai.AiMethodSuggestion;
13
import org.egothor.methodatlas.api.ScanRecord;
14
15
/**
16
 * In-memory cache of AI classification results loaded from a previous MethodAtlas scan output.
17
 *
18
 * <p>Cache entries are keyed by {@code content_hash} — the per-class SHA-256 fingerprint
19
 * produced by the {@code -content-hash} flag. A cache hit means the class source is identical
20
 * to the previous run; the stored AI classification is reused without an API call.</p>
21
 *
22
 * <p>When the source CSV was produced without {@code -content-hash}, the {@code content_hash}
23
 * column is absent and no entries are loaded. All lookups return empty and the AI is called
24
 * normally for every class.</p>
25
 *
26
 * <p>Instances are obtained via {@link #load(Path)} or the no-op {@link #empty()} singleton.</p>
27
 *
28
 * @see MethodAtlasApp
29
 */
30
public final class AiResultCache {
31
32
    private final Map<String, AiClassSuggestion> byHash;
33
    private int hits;
34
    private int misses;
35
36
    private AiResultCache(Map<String, AiClassSuggestion> byHash) {
37
        this.byHash = byHash;
38
    }
39
40
    /** Returns an empty cache that always produces misses. */
41
    public static AiResultCache empty() {
42 1 1. empty : replaced return value with null for org/egothor/methodatlas/AiResultCache::empty → KILLED
        return new AiResultCache(Map.of());
43
    }
44
45
    /**
46
     * Loads a cache from a MethodAtlas CSV output file.
47
     *
48
     * <p>Only rows with a non-empty {@code content_hash} value and a non-{@code null}
49
     * {@code ai_security_relevant} column (AI was enabled for that scan) are included.
50
     * Rows missing either field are silently skipped.</p>
51
     *
52
     * @param csvPath path to a MethodAtlas CSV produced with {@code -content-hash -ai}
53
     * @return loaded cache; never {@code null}
54
     * @throws IOException if the file cannot be read
55
     */
56
    @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
57
    public static AiResultCache load(Path csvPath) throws IOException {
58
        List<ScanRecord> records = DeltaReport.loadRecords(csvPath);
59
60
        Map<String, List<ScanRecord>> grouped = new HashMap<>();
61
        for (ScanRecord r : records) {
62 4 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
3. load : removed conditional - replaced equality check with false → KILLED
4. load : removed conditional - replaced equality check with true → KILLED
            if (r.contentHash() != null && !r.contentHash().isEmpty()
63 2 1. load : removed conditional - replaced equality check with true → KILLED
2. load : removed conditional - replaced equality check with false → KILLED
                    && r.aiSecurityRelevant() != null) {
64 1 1. lambda$load$0 : replaced return value with Collections.emptyList for org/egothor/methodatlas/AiResultCache::lambda$load$0 → KILLED
                grouped.computeIfAbsent(r.contentHash(), k -> new ArrayList<>()).add(r);
65
            }
66
        }
67
68 1 1. load : Replaced integer multiplication with division → SURVIVED
        Map<String, AiClassSuggestion> cache = new HashMap<>(grouped.size() * 2);
69
        for (Map.Entry<String, List<ScanRecord>> entry : grouped.entrySet()) {
70
            List<AiMethodSuggestion> methods = new ArrayList<>(entry.getValue().size());
71
            for (ScanRecord r : entry.getValue()) {
72
                methods.add(new AiMethodSuggestion(
73
                        r.method(),
74
                        Boolean.TRUE.equals(r.aiSecurityRelevant()),
75
                        r.aiDisplayName(),
76 2 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
                        r.aiTags() != null ? r.aiTags() : List.of(),
77
                        r.aiReason(),
78 2 1. load : removed conditional - replaced equality check with false → SURVIVED
2. load : removed conditional - replaced equality check with true → KILLED
                        r.aiConfidence() != null ? r.aiConfidence() : 0.0,
79 2 1. load : removed conditional - replaced equality check with true → SURVIVED
2. load : removed conditional - replaced equality check with false → KILLED
                        r.aiInteractionScore() != null ? r.aiInteractionScore() : 0.0));
80
            }
81
            cache.put(entry.getKey(), new AiClassSuggestion(null, null, null, null, methods));
82
        }
83
84 1 1. load : replaced return value with null for org/egothor/methodatlas/AiResultCache::load → KILLED
        return new AiResultCache(cache);
85
    }
86
87
    /**
88
     * Returns the cached classification for the class with the given content hash,
89
     * or empty when the hash is absent from the cache.
90
     *
91
     * @param contentHash SHA-256 fingerprint of the class source, or {@code null}
92
     * @return cached suggestion, or empty on a miss or null hash
93
     */
94
    public Optional<AiClassSuggestion> lookup(String contentHash) {
95 2 1. lookup : removed conditional - replaced equality check with true → KILLED
2. lookup : removed conditional - replaced equality check with false → KILLED
        if (contentHash == null) {
96 1 1. lookup : Replaced integer addition with subtraction → KILLED
            misses++;
97
            return Optional.empty();
98
        }
99
        AiClassSuggestion cached = byHash.get(contentHash);
100 2 1. lookup : removed conditional - replaced equality check with false → KILLED
2. lookup : removed conditional - replaced equality check with true → KILLED
        if (cached != null) {
101 1 1. lookup : Replaced integer addition with subtraction → KILLED
            hits++;
102
        } else {
103 1 1. lookup : Replaced integer addition with subtraction → KILLED
            misses++;
104
        }
105 1 1. lookup : replaced return value with Optional.empty for org/egothor/methodatlas/AiResultCache::lookup → KILLED
        return Optional.ofNullable(cached);
106
    }
107
108
    /**
109
     * Returns {@code true} when this cache contains at least one entry.
110
     *
111
     * <p>When {@code false}, content hashes do not need to be computed for lookups
112
     * because all results would be misses regardless.</p>
113
     */
114
    public boolean isActive() {
115 3 1. isActive : replaced boolean return with true for org/egothor/methodatlas/AiResultCache::isActive → KILLED
2. isActive : removed conditional - replaced equality check with true → KILLED
3. isActive : removed conditional - replaced equality check with false → KILLED
        return !byHash.isEmpty();
116
    }
117
118
    /** Returns the number of successful cache lookups so far. */
119
    public int hits() {
120 1 1. hits : replaced int return with 0 for org/egothor/methodatlas/AiResultCache::hits → KILLED
        return hits;
121
    }
122
123
    /** Returns the number of unsuccessful cache lookups so far. */
124
    public int misses() {
125 1 1. misses : replaced int return with 0 for org/egothor/methodatlas/AiResultCache::misses → KILLED
        return misses;
126
    }
127
}

Mutations

42

1.1
Location : empty
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced return value with null for org/egothor/methodatlas/AiResultCache::empty → KILLED

62

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

3.3
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutContentHashColumn_producesInactiveCache(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

4.4
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

63

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutAiColumns_producesInactiveCache(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

64

1.1
Location : lambda$load$0
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/AiResultCache::lambda$load$0 → KILLED

68

1.1
Location : load
Killed by : none
Replaced integer multiplication with division → SURVIVED
Covering tests

76

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_methodSuggestionFieldsRestoredCorrectly(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

78

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : load
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

79

1.1
Location : load
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

2.2
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_methodSuggestionFieldsRestoredCorrectly(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

84

1.1
Location : load
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithoutAiColumns_producesInactiveCache(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/AiResultCache::load → KILLED

95

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

2.2
Location : lookup
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_nonSecurityMethod_notAnnotated(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

96

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:lookup_nullHash_returnsMissAndIncrementsMisses(java.nio.file.Path)]
Replaced integer addition with subtraction → KILLED

100

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

2.2
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
removed conditional - replaced equality check with true → KILLED

101

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
Replaced integer addition with subtraction → KILLED

103

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
Replaced integer addition with subtraction → KILLED

105

1.1
Location : lookup
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_multipleMethodsSameHash_groupedIntoOneSuggestion(java.nio.file.Path)]
replaced return value with Optional.empty for org/egothor/methodatlas/AiResultCache::lookup → KILLED

115

1.1
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced boolean return with true for org/egothor/methodatlas/AiResultCache::isActive → KILLED

2.2
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
removed conditional - replaced equality check with true → KILLED

3.3
Location : isActive
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_nonSecurityMethod_cachedAndRestoredAsNonSecurity(java.nio.file.Path)]
removed conditional - replaced equality check with false → KILLED

120

1.1
Location : hits
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:load_csvWithContentHashAndAiColumns_hitOnMatchingHash(java.nio.file.Path)]
replaced int return with 0 for org/egothor/methodatlas/AiResultCache::hits → KILLED

125

1.1
Location : misses
Killed by : org.egothor.methodatlas.AiResultCacheTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.AiResultCacheTest]/[method:empty_isNotActiveAndAlwaysMisses()]
replaced int return with 0 for org/egothor/methodatlas/AiResultCache::misses → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1