CommandSupport.java

1
package org.egothor.methodatlas.command;
2
3
import java.io.IOException;
4
import java.nio.charset.StandardCharsets;
5
import java.nio.file.Path;
6
import java.nio.file.Paths;
7
import java.security.MessageDigest;
8
import java.security.NoSuchAlgorithmException;
9
import java.util.ArrayList;
10
import java.util.HexFormat;
11
import java.util.LinkedHashMap;
12
import java.util.LinkedHashSet;
13
import java.util.List;
14
import java.util.Locale;
15
import java.util.Map;
16
import java.util.ServiceLoader;
17
import java.util.Set;
18
import java.util.logging.Level;
19
import java.util.logging.Logger;
20
import java.util.stream.Collectors;
21
22
import org.egothor.methodatlas.AiResultCache;
23
import org.egothor.methodatlas.ClassificationOverride;
24
import org.egothor.methodatlas.CliConfig;
25
import org.egothor.methodatlas.TestMethodSink;
26
import org.egothor.methodatlas.ai.AiClassSuggestion;
27
import org.egothor.methodatlas.ai.AiMethodSuggestion;
28
import org.egothor.methodatlas.ai.AiOptions;
29
import org.egothor.methodatlas.ai.AiSuggestionEngine;
30
import org.egothor.methodatlas.ai.AiSuggestionEngineImpl;
31
import org.egothor.methodatlas.ai.AiSuggestionException;
32
import org.egothor.methodatlas.ai.PromptBuilder;
33
import org.egothor.methodatlas.ai.SuggestionLookup;
34
import org.egothor.methodatlas.api.DiscoveredMethod;
35
import org.egothor.methodatlas.api.SourcePatcher;
36
import org.egothor.methodatlas.api.TestDiscovery;
37
import org.egothor.methodatlas.api.TestDiscoveryConfig;
38
39
/**
40
 * Shared static infrastructure used by two or more {@link Command} implementations.
41
 *
42
 * <p>
43
 * This utility class centralises provider loading, scan orchestration, AI suggestion
44
 * resolution, content hashing, and other cross-cutting concerns. All methods are
45
 * static; this class cannot be instantiated.
46
 * </p>
47
 *
48
 * <p>
49
 * Methods marked {@code public} ({@link #requireUniqueDiscoveryIds},
50
 * {@link #requireUniquePatcherIds}, {@link #computeFilePrefix},
51
 * {@link #buildAiEngine}, {@link #buildAiCache}, and
52
 * {@link #loadClassificationOverride}) are called either by
53
 * {@link org.egothor.methodatlas.MethodAtlasApp} (a different package) or
54
 * directly by unit tests; all other methods are package-private and intended
55
 * for use within the {@code org.egothor.methodatlas.command} package only.
56
 * </p>
57
 */
58
@SuppressWarnings("PMD.CyclomaticComplexity") // centralised utility class; total CC naturally high across many small methods
59
public final class CommandSupport {
60
61
    private static final Logger LOG = Logger.getLogger(CommandSupport.class.getName());
62
63
    private CommandSupport() {
64
    }
65
66
    // -------------------------------------------------------------------------
67
    // AI runtime bundle (package-private record, shared by scan commands)
68
    // -------------------------------------------------------------------------
69
70
    /**
71
     * Bundles the AI infrastructure that is constant for the duration of a scan run.
72
     *
73
     * @param options  AI configuration
74
     * @param engine   AI engine; {@code null} when AI is disabled
75
     * @param override human classification overrides
76
     * @param cache    AI result cache
77
     */
78
    /* default */ record AiRuntime(AiOptions options, AiSuggestionEngine engine,
79
            ClassificationOverride override, AiResultCache cache) {
80
    }
81
82
    // -------------------------------------------------------------------------
83
    // Factory / loader methods
84
    // -------------------------------------------------------------------------
85
86
    /**
87
     * Creates the AI suggestion engine for the current run.
88
     *
89
     * <p>
90
     * Returns {@code null} when AI support is disabled. Initialization failures
91
     * are wrapped in an {@link IllegalStateException}.
92
     * </p>
93
     *
94
     * @param aiOptions AI configuration for the current run
95
     * @return initialized AI suggestion engine, or {@code null} when AI is disabled
96
     * @throws IllegalStateException if engine initialization fails
97
     */
98
    public static AiSuggestionEngine buildAiEngine(AiOptions aiOptions) {
99 2 1. buildAiEngine : removed conditional - replaced equality check with false → KILLED
2. buildAiEngine : removed conditional - replaced equality check with true → KILLED
        if (!aiOptions.enabled()) {
100
            return null;
101
        }
102
        try {
103 1 1. buildAiEngine : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiEngine → KILLED
            return new AiSuggestionEngineImpl(aiOptions);
104
        } catch (AiSuggestionException e) {
105
            throw new IllegalStateException("Failed to initialize AI engine", e);
106
        }
107
    }
108
109
    /**
110
     * Loads the AI result cache from the given CSV file, or returns the empty
111
     * no-op cache when no cache file is configured.
112
     *
113
     * @param cacheFile path to a previous MethodAtlas CSV output, or {@code null}
114
     * @return loaded cache; never {@code null}
115
     * @throws IllegalArgumentException if the file exists but cannot be read or
116
     *                                  parsed
117
     */
118
    public static AiResultCache buildAiCache(Path cacheFile) {
119 2 1. buildAiCache : removed conditional - replaced equality check with false → KILLED
2. buildAiCache : removed conditional - replaced equality check with true → KILLED
        if (cacheFile == null) {
120 1 1. buildAiCache : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiCache → KILLED
            return AiResultCache.empty();
121
        }
122
        try {
123 1 1. buildAiCache : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiCache → KILLED
            return AiResultCache.load(cacheFile);
124
        } catch (IOException e) {
125
            throw new IllegalArgumentException("Cannot load AI cache file: " + cacheFile, e);
126
        }
127
    }
128
129
    /**
130
     * Loads the classification override file, or returns the empty no-op
131
     * singleton when no override file is configured.
132
     *
133
     * @param overrideFile path to the YAML override file, or {@code null}
134
     * @return loaded override set; never {@code null}
135
     * @throws IllegalArgumentException if the file exists but cannot be read or
136
     *                                  contains invalid YAML
137
     */
138
    public static ClassificationOverride loadClassificationOverride(Path overrideFile) {
139 2 1. loadClassificationOverride : removed conditional - replaced equality check with true → SURVIVED
2. loadClassificationOverride : removed conditional - replaced equality check with false → KILLED
        if (overrideFile == null) {
140 1 1. loadClassificationOverride : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::loadClassificationOverride → KILLED
            return ClassificationOverride.empty();
141
        }
142
        try {
143 1 1. loadClassificationOverride : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::loadClassificationOverride → NO_COVERAGE
            return ClassificationOverride.load(overrideFile);
144
        } catch (IOException e) {
145
            throw new IllegalArgumentException("Cannot load override file: " + overrideFile, e);
146
        }
147
    }
148
149
    // -------------------------------------------------------------------------
150
    // Provider and patcher loading
151
    // -------------------------------------------------------------------------
152
153
    /**
154
     * Loads all {@link TestDiscovery} providers registered via {@link ServiceLoader},
155
     * configures each with {@code config}, and returns the list.
156
     *
157
     * <p>
158
     * Providers are discovered from the classpath using the standard
159
     * {@code META-INF/services/org.egothor.methodatlas.api.TestDiscovery}
160
     * service file.
161
     * </p>
162
     *
163
     * @param config runtime configuration forwarded to every provider via
164
     *               {@link TestDiscovery#configure}
165
     * @return non-empty list of configured providers
166
     * @throws IllegalStateException if no providers are found on the classpath
167
     */
168
    @SuppressWarnings("PMD.CloseResource") // callers are responsible for closing providers via closeAll()
169
    /* default */ static List<TestDiscovery> loadProviders(TestDiscoveryConfig config) {
170
        List<TestDiscovery> providers = new ArrayList<>();
171
        for (TestDiscovery provider : ServiceLoader.load(TestDiscovery.class)) {
172 1 1. loadProviders : removed call to org/egothor/methodatlas/api/TestDiscovery::configure → KILLED
            provider.configure(config);
173
            providers.add(provider);
174
        }
175 2 1. loadProviders : removed conditional - replaced equality check with false → SURVIVED
2. loadProviders : removed conditional - replaced equality check with true → KILLED
        if (providers.isEmpty()) {
176
            throw new IllegalStateException(
177
                    "No TestDiscovery providers found on the classpath. "
178
                    + "Ensure at least one provider JAR ships the service registration file "
179
                    + "META-INF/services/org.egothor.methodatlas.api.TestDiscovery.");
180
        }
181 1 1. loadProviders : removed call to org/egothor/methodatlas/command/CommandSupport::requireUniqueDiscoveryIds → SURVIVED
        requireUniqueDiscoveryIds(providers);
182 1 1. loadProviders : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CommandSupport::loadProviders → KILLED
        return providers;
183
    }
184
185
    /**
186
     * Closes every provider in the list, logging any {@link IOException} at
187
     * {@link Level#FINE} and continuing so that all providers are attempted.
188
     *
189
     * @param providers list of providers to close; never {@code null}
190
     */
191
    @SuppressWarnings("PMD.CloseResource") // this method IS the close mechanism; p.close() is called explicitly
192
    /* default */ static void closeAll(List<TestDiscovery> providers) {
193
        for (TestDiscovery p : providers) {
194
            try {
195 1 1. closeAll : removed call to org/egothor/methodatlas/api/TestDiscovery::close → SURVIVED
                p.close();
196
            } catch (IOException e) {
197
                if (LOG.isLoggable(Level.FINE)) {
198
                    LOG.log(Level.FINE, "Failed to close provider " + p.pluginId(), e);
199
                }
200
            }
201
        }
202
    }
203
204
    /**
205
     * Loads all {@link SourcePatcher} providers registered via {@link ServiceLoader},
206
     * configures each with {@code config}, and returns the list.
207
     *
208
     * @param config runtime configuration forwarded to every patcher via
209
     *               {@link SourcePatcher#configure}
210
     * @return possibly-empty list of configured patchers
211
     */
212
    /* default */ static List<SourcePatcher> loadPatchers(TestDiscoveryConfig config) {
213
        List<SourcePatcher> patchers = new ArrayList<>();
214
        for (SourcePatcher patcher : ServiceLoader.load(SourcePatcher.class)) {
215 1 1. loadPatchers : removed call to org/egothor/methodatlas/api/SourcePatcher::configure → SURVIVED
            patcher.configure(config);
216
            patchers.add(patcher);
217
        }
218 1 1. loadPatchers : removed call to org/egothor/methodatlas/command/CommandSupport::requireUniquePatcherIds → SURVIVED
        requireUniquePatcherIds(patchers);
219 1 1. loadPatchers : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CommandSupport::loadPatchers → KILLED
        return patchers;
220
    }
221
222
    /**
223
     * Verifies that every {@link TestDiscovery} provider in the list has a
224
     * unique {@link TestDiscovery#pluginId()}.
225
     *
226
     * @param providers list of configured providers
227
     * @throws IllegalStateException if two or more providers share the same ID
228
     */
229
    @SuppressWarnings("PMD.CloseResource") // providers are owned by the caller; this method does not close them
230
    public static void requireUniqueDiscoveryIds(List<TestDiscovery> providers) {
231
        Set<String> seen = new LinkedHashSet<>();
232
        for (TestDiscovery p : providers) {
233
            String id = p.pluginId();
234 2 1. requireUniqueDiscoveryIds : removed conditional - replaced equality check with true → KILLED
2. requireUniqueDiscoveryIds : removed conditional - replaced equality check with false → KILLED
            if (!seen.add(id)) {
235
                throw new IllegalStateException(
236
                        "Duplicate TestDiscovery plugin ID \"" + id + "\": two or more "
237
                        + "registered providers claim the same pluginId(). "
238
                        + "Each provider must declare a unique identifier.");
239
            }
240
        }
241
    }
242
243
    /**
244
     * Verifies that every {@link SourcePatcher} in the list has a unique
245
     * {@link SourcePatcher#pluginId()}.
246
     *
247
     * @param patchers list of configured patchers
248
     * @throws IllegalStateException if two or more patchers share the same ID
249
     */
250
    public static void requireUniquePatcherIds(List<SourcePatcher> patchers) {
251
        Set<String> seen = new LinkedHashSet<>();
252
        for (SourcePatcher p : patchers) {
253
            String id = p.pluginId();
254 2 1. requireUniquePatcherIds : removed conditional - replaced equality check with false → KILLED
2. requireUniquePatcherIds : removed conditional - replaced equality check with true → KILLED
            if (!seen.add(id)) {
255
                throw new IllegalStateException(
256
                        "Duplicate SourcePatcher plugin ID \"" + id + "\": two or more "
257
                        + "registered patchers claim the same pluginId(). "
258
                        + "Each patcher must declare a unique identifier.");
259
            }
260
        }
261
    }
262
263
    // -------------------------------------------------------------------------
264
    // Scan orchestration
265
    // -------------------------------------------------------------------------
266
267
    /**
268
     * Scans all roots and forwards each discovered test method to {@code sink}.
269
     *
270
     * @param roots           source roots to scan
271
     * @param cliConfig       full parsed CLI configuration
272
     * @param discoveryConfig discovery configuration forwarded to providers
273
     * @param aiEngine        AI engine providing suggestions; may be {@code null}
274
     * @param sink            receiver of discovered test method records
275
     * @param override        human classification overrides
276
     * @param aiCache         AI result cache
277
     * @return {@code 0} if all files were processed successfully, {@code 1} if any
278
     *         file produced a parse or processing error
279
     * @throws IOException if traversing a file tree fails
280
     */
281
    /* default */ static int scan(List<Path> roots, CliConfig cliConfig, TestDiscoveryConfig discoveryConfig,
282
            AiSuggestionEngine aiEngine, TestMethodSink sink,
283
            ClassificationOverride override, AiResultCache aiCache) throws IOException {
284
        List<TestDiscovery> providers = loadProviders(discoveryConfig);
285
        boolean hadErrors = false;
286
        try {
287
            for (Path root : roots) {
288 2 1. scan : removed conditional - replaced equality check with true → SURVIVED
2. scan : removed conditional - replaced equality check with false → SURVIVED
                if (runDiscovery(root, providers, cliConfig.aiOptions(), aiEngine, sink,
289
                        cliConfig.contentHash(), override, aiCache)) {
290
                    hadErrors = true;
291
                }
292
            }
293
        } finally {
294 1 1. scan : removed call to org/egothor/methodatlas/command/CommandSupport::closeAll → SURVIVED
            closeAll(providers);
295
        }
296 2 1. scan : removed conditional - replaced equality check with false → SURVIVED
2. scan : removed conditional - replaced equality check with true → SURVIVED
        return hadErrors ? 1 : 0;
297
    }
298
299
    /**
300
     * Runs all configured {@link TestDiscovery} providers on {@code root},
301
     * merges their results, orchestrates AI analysis per class, and forwards
302
     * each method record to {@code sink}.
303
     *
304
     * <p>
305
     * All providers are run against every root, and their streams are merged
306
     * before grouping by class. This supports multi-language scanning: a JVM
307
     * provider and a .NET provider on the classpath will each scan their own
308
     * file types and contribute distinct {@link DiscoveredMethod} records.
309
     * </p>
310
     *
311
     * @param root               directory to scan
312
     * @param providers          list of pre-configured discovery providers
313
     * @param aiOptions          AI configuration for the current run
314
     * @param aiEngine           AI engine, or {@code null} when AI is disabled
315
     * @param sink               receiver of discovered test method records
316
     * @param contentHashEnabled whether to include the class content hash
317
     * @param override           human classification overrides
318
     * @param aiCache            AI result cache
319
     * @return {@code true} if any provider encountered a parse or processing error
320
     * @throws IOException if traversing the file tree fails
321
     */
322
    @SuppressWarnings("PMD.CloseResource") // providers are owned by the caller; this method does not close them
323
    /* default */ static boolean runDiscovery(Path root, List<TestDiscovery> providers,
324
            AiOptions aiOptions, AiSuggestionEngine aiEngine, TestMethodSink sink,
325
            boolean contentHashEnabled, ClassificationOverride override,
326
            AiResultCache aiCache) throws IOException {
327
328
        List<DiscoveredMethod> methods = new ArrayList<>();
329
        boolean hadErrors = false;
330
        for (TestDiscovery provider : providers) {
331 1 1. runDiscovery : removed call to java/util/stream/Stream::forEach → KILLED
            provider.discover(root).forEach(methods::add);
332 2 1. runDiscovery : removed conditional - replaced equality check with true → SURVIVED
2. runDiscovery : removed conditional - replaced equality check with false → KILLED
            if (provider.hadErrors()) {
333
                hadErrors = true;
334
            }
335
        }
336
337
        Map<String, List<DiscoveredMethod>> byClass = methods.stream()
338
                .collect(Collectors.groupingBy(DiscoveredMethod::fqcn,
339
                        LinkedHashMap::new, Collectors.toList()));
340
341
        AiRuntime ai = new AiRuntime(aiOptions, aiEngine, override, aiCache);
342
343
        for (Map.Entry<String, List<DiscoveredMethod>> entry : byClass.entrySet()) {
344
            String fqcn = entry.getKey();
345
            List<DiscoveredMethod> classMethods = entry.getValue();
346
347
            String classSource = classMethods.get(0).sourceContent().get().orElse(null);
348
349 6 1. runDiscovery : removed conditional - replaced equality check with false → SURVIVED
2. runDiscovery : removed conditional - replaced equality check with true → SURVIVED
3. runDiscovery : removed conditional - replaced equality check with false → SURVIVED
4. runDiscovery : removed conditional - replaced equality check with true → SURVIVED
5. runDiscovery : removed conditional - replaced equality check with false → KILLED
6. runDiscovery : removed conditional - replaced equality check with true → KILLED
            String lookupHash = (contentHashEnabled || aiCache.isActive()) && classSource != null
350
                    ? computeContentHash(classSource) : null;
351 2 1. runDiscovery : removed conditional - replaced equality check with true → SURVIVED
2. runDiscovery : removed conditional - replaced equality check with false → KILLED
            String outputHash = contentHashEnabled ? lookupHash : null;
352
353
            String fileStem = classMethods.get(0).fileStem();
354
            List<String> methodNames = classMethods.stream().map(DiscoveredMethod::method).toList();
355
            List<PromptBuilder.TargetMethod> targetMethods = classMethods.stream()
356
                    .map(CommandSupport::toTargetMethod)
357
                    .toList();
358
359
            SuggestionLookup suggestions = resolveSuggestionLookup(
360
                    fileStem, fqcn, classSource, methodNames, targetMethods, ai, lookupHash);
361
362
            for (DiscoveredMethod m : classMethods) {
363 1 1. runDiscovery : removed call to org/egothor/methodatlas/TestMethodSink::record → KILLED
                sink.record(m.fqcn(), m.method(), m.beginLine(), m.loc(), outputHash,
364
                        m.tags(), m.displayName(),
365
                        suggestions.find(m.method()).orElse(null));
366
            }
367
        }
368
369 2 1. runDiscovery : replaced boolean return with true for org/egothor/methodatlas/command/CommandSupport::runDiscovery → SURVIVED
2. runDiscovery : replaced boolean return with false for org/egothor/methodatlas/command/CommandSupport::runDiscovery → KILLED
        return hadErrors;
370
    }
371
372
    /**
373
     * Collects all discovered methods from every root and provider, keyed by
374
     * source-file path. Methods whose {@link DiscoveredMethod#filePath()} is
375
     * {@code null} are silently skipped.
376
     *
377
     * @param roots     scan roots
378
     * @param providers configured and already-loaded {@link TestDiscovery} providers
379
     * @return mutable map from source-file path to the methods found in that file;
380
     *         insertion order matches discovery order
381
     * @throws IOException if directory traversal fails for any root
382
     */
383
    @SuppressWarnings({"PMD.AvoidInstantiatingObjectsInLoops",
384
            "PMD.CloseResource"}) // providers are owned by the caller; this method does not close them
385
    /* default */ static Map<Path, List<DiscoveredMethod>> collectMethodsByFile(
386
            List<Path> roots, List<TestDiscovery> providers) throws IOException {
387
        Map<Path, List<DiscoveredMethod>> byFile = new LinkedHashMap<>();
388
        for (Path root : roots) {
389
            for (TestDiscovery provider : providers) {
390 1 1. collectMethodsByFile : removed call to java/util/stream/Stream::forEach → KILLED
                provider.discover(root).forEach(m -> {
391 2 1. lambda$collectMethodsByFile$1 : removed conditional - replaced equality check with true → SURVIVED
2. lambda$collectMethodsByFile$1 : removed conditional - replaced equality check with false → KILLED
                    if (m.filePath() != null) {
392 1 1. lambda$collectMethodsByFile$0 : replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CommandSupport::lambda$collectMethodsByFile$0 → KILLED
                        byFile.computeIfAbsent(m.filePath(), k -> new ArrayList<>()).add(m);
393
                    }
394
                });
395
            }
396
        }
397 1 1. collectMethodsByFile : replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CommandSupport::collectMethodsByFile → KILLED
        return byFile;
398
    }
399
400
    /**
401
     * Resolves AI security-classification suggestions for every class in
402
     * {@code byClass} and populates {@code tagsToApply} and {@code displayNames}
403
     * with the results for methods that are security-relevant.
404
     *
405
     * <p>A display-name suggestion is only placed into {@code displayNames} when
406
     * the discovered method has no existing {@code @DisplayName} in source
407
     * (i.e. {@link DiscoveredMethod#displayName()} returns {@code null}).
408
     * This prevents AI-generated names from overwriting manually authored ones.</p>
409
     *
410
     * @param byClass      discovered methods grouped by FQCN for one source file
411
     * @param ai           AI runtime carrying the engine, override, and cache
412
     * @param aiCache      AI result cache used to compute the content-hash lookup key
413
     * @param tagsToApply  output accumulator: method name → tag values to write
414
     * @param displayNames output accumulator: method name → display name to write
415
     */
416
    /* default */ static void gatherAiSuggestionsForFile(Map<String, List<DiscoveredMethod>> byClass,
417
            AiRuntime ai, AiResultCache aiCache,
418
            Map<String, List<String>> tagsToApply, Map<String, String> displayNames) {
419
        for (Map.Entry<String, List<DiscoveredMethod>> classEntry : byClass.entrySet()) {
420
            String fqcn = classEntry.getKey();
421
            List<DiscoveredMethod> classMethods = classEntry.getValue();
422
423
            String classSource = classMethods.get(0).sourceContent().get().orElse(null);
424 4 1. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → NO_COVERAGE
2. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → NO_COVERAGE
3. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → SURVIVED
4. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → SURVIVED
            String lookupHash = aiCache.isActive() && classSource != null
425
                    ? computeContentHash(classSource) : null;
426
            String fileStem = classMethods.get(0).fileStem();
427
            List<String> methodNames = classMethods.stream().map(DiscoveredMethod::method).toList();
428
            List<PromptBuilder.TargetMethod> targetMethods = classMethods.stream()
429
                    .map(CommandSupport::toTargetMethod).toList();
430
431
            SuggestionLookup suggestions = resolveSuggestionLookup(
432
                    fileStem, fqcn, classSource, methodNames, targetMethods, ai, lookupHash);
433
434
            for (DiscoveredMethod m : classMethods) {
435
                AiMethodSuggestion suggestion = suggestions.find(m.method()).orElse(null);
436 4 1. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → KILLED
2. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
3. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → KILLED
4. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
                if (suggestion == null || !suggestion.securityRelevant()) {
437
                    continue;
438
                }
439 4 1. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → SURVIVED
2. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
3. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → KILLED
4. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
                if (suggestion.displayName() != null && !suggestion.displayName().isBlank()
440 2 1. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
2. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → KILLED
                        && m.displayName() == null) {
441
                    displayNames.putIfAbsent(m.method(), suggestion.displayName());
442
                }
443 4 1. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → SURVIVED
2. gatherAiSuggestionsForFile : removed conditional - replaced equality check with true → SURVIVED
3. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
4. gatherAiSuggestionsForFile : removed conditional - replaced equality check with false → KILLED
                if (suggestion.tags() != null && !suggestion.tags().isEmpty()) {
444
                    tagsToApply.putIfAbsent(m.method(), suggestion.tags());
445
                }
446
            }
447
        }
448
    }
449
450
    /**
451
     * Resolves method-level AI suggestions for a class.
452
     *
453
     * <p>
454
     * Returns an empty lookup when no AI engine is available, the method list is
455
     * empty, or (for regular provider-based AI) the class source exceeds the
456
     * configured maximum size. The {@code maxClassChars} limit is only enforced
457
     * when the automated provider is enabled ({@link AiOptions#enabled()}); it is
458
     * not applied in the manual consume phase.
459
     * </p>
460
     *
461
     * @param fileStem      dot-separated path stem identifying the source file;
462
     *                      forwarded to {@link AiSuggestionEngine#suggestForClass}
463
     * @param fqcn          fully qualified class name
464
     * @param classSource   pretty-printed source text of the class; may be
465
     *                      {@code null} when source is unavailable
466
     * @param methodNames   names of discovered test methods
467
     * @param targetMethods prompt target descriptors for the test methods
468
     * @param ai            AI infrastructure for this scan run
469
     * @param contentHash   hash of the class source for cache lookup; may be
470
     *                      {@code null}
471
     * @return lookup of AI suggestions keyed by method name; never {@code null}
472
     */
473
    /* default */ static SuggestionLookup resolveSuggestionLookup(String fileStem, String fqcn,
474
            String classSource, List<String> methodNames, List<PromptBuilder.TargetMethod> targetMethods,
475
            AiRuntime ai, String contentHash) {
476 2 1. resolveSuggestionLookup : removed conditional - replaced equality check with false → SURVIVED
2. resolveSuggestionLookup : removed conditional - replaced equality check with true → KILLED
        if (methodNames.isEmpty()) {
477 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → NO_COVERAGE
            return SuggestionLookup.from(null);
478
        }
479
480 2 1. resolveSuggestionLookup : removed conditional - replaced equality check with false → KILLED
2. resolveSuggestionLookup : removed conditional - replaced equality check with true → KILLED
        if (ai.engine() == null) {
481 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED
            return SuggestionLookup.from(ai.override().apply(fqcn, null, methodNames));
482
        }
483
484
        // Check the cache before making an API call.
485
        AiClassSuggestion cached = ai.cache().lookup(contentHash).orElse(null);
486 2 1. resolveSuggestionLookup : removed conditional - replaced equality check with false → KILLED
2. resolveSuggestionLookup : removed conditional - replaced equality check with true → KILLED
        if (cached != null) {
487 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED
            return SuggestionLookup.from(ai.override().apply(fqcn, cached, methodNames));
488
        }
489
490 2 1. resolveSuggestionLookup : removed conditional - replaced equality check with false → SURVIVED
2. resolveSuggestionLookup : removed conditional - replaced equality check with true → KILLED
        if (classSource == null) {
491 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → NO_COVERAGE
            return SuggestionLookup.from(ai.override().apply(fqcn, null, methodNames));
492
        }
493
494 5 1. resolveSuggestionLookup : removed conditional - replaced equality check with true → SURVIVED
2. resolveSuggestionLookup : changed conditional boundary → SURVIVED
3. resolveSuggestionLookup : removed conditional - replaced equality check with false → KILLED
4. resolveSuggestionLookup : removed conditional - replaced comparison check with false → KILLED
5. resolveSuggestionLookup : removed conditional - replaced comparison check with true → KILLED
        if (ai.options().enabled() && classSource.length() > ai.options().maxClassChars()) {
495
            if (LOG.isLoggable(Level.INFO)) {
496
                LOG.log(Level.INFO, "Skipping AI for {0}: class source too large ({1} chars)",
497
                        new Object[] { fqcn, classSource.length() });
498
            }
499 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED
            return SuggestionLookup.from(ai.override().apply(fqcn, null, methodNames));
500
        }
501
502
        if (LOG.isLoggable(Level.INFO)) {
503
            LOG.log(Level.INFO, "Querying AI for {0} ({1} methods)", new Object[] { fqcn, targetMethods.size() });
504
        }
505
506
        try {
507
            AiClassSuggestion aiClassSuggestion =
508
                    ai.engine().suggestForClass(fileStem, fqcn, classSource, targetMethods);
509 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED
            return SuggestionLookup.from(ai.override().apply(fqcn, aiClassSuggestion, methodNames));
510
        } catch (AiSuggestionException e) {
511
            if (LOG.isLoggable(Level.WARNING)) {
512
                LOG.log(Level.WARNING, "AI suggestion failed for class " + fqcn, e);
513
            }
514 1 1. resolveSuggestionLookup : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED
            return SuggestionLookup.from(ai.override().apply(fqcn, null, methodNames));
515
        }
516
    }
517
518
    // -------------------------------------------------------------------------
519
    // Utility methods
520
    // -------------------------------------------------------------------------
521
522
    /**
523
     * Computes a SHA-256 content fingerprint of a class source string.
524
     *
525
     * <p>
526
     * The hash is derived from the JavaParser pretty-printed form of the class
527
     * declaration, which normalizes whitespace so that insignificant formatting
528
     * changes do not alter the fingerprint. The result is a 64-character
529
     * lowercase hexadecimal string.
530
     * </p>
531
     *
532
     * @param classSource JavaParser pretty-print of the class declaration
533
     * @return 64-character lowercase hex SHA-256 digest
534
     * @throws IllegalStateException if SHA-256 is unavailable (never in practice;
535
     *                               SHA-256 is mandated by the Java SE spec)
536
     */
537
    /* default */ static String computeContentHash(String classSource) {
538
        try {
539
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
540
            byte[] bytes = digest.digest(classSource.getBytes(StandardCharsets.UTF_8));
541 1 1. computeContentHash : replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::computeContentHash → KILLED
            return HexFormat.of().formatHex(bytes);
542
        } catch (NoSuchAlgorithmException e) {
543
            throw new IllegalStateException("SHA-256 not available", e);
544
        }
545
    }
546
547
    /**
548
     * Derives the file path prefix used in GitHub Actions workflow command
549
     * annotations from the first configured scan root.
550
     *
551
     * <p>
552
     * The prefix is made relative to the current working directory so that the
553
     * resulting annotation paths (e.g. {@code src/test/java/com/acme/AuthTest.java})
554
     * match what GitHub resolves as inline positions in the PR diff.
555
     * </p>
556
     *
557
     * @param roots configured scan roots; may be empty
558
     * @return forward-slash path ending with {@code /}, or empty string when
559
     *         {@code roots} is empty
560
     */
561
    public static String computeFilePrefix(List<Path> roots) {
562 2 1. computeFilePrefix : removed conditional - replaced equality check with false → KILLED
2. computeFilePrefix : removed conditional - replaced equality check with true → KILLED
        if (roots.isEmpty()) {
563
            return "";
564
        }
565
        Path root = roots.get(0).toAbsolutePath().normalize();
566
        String prefix;
567
        try {
568
            Path cwd = Paths.get("").toAbsolutePath();
569
            prefix = cwd.relativize(root).toString().replace('\\', '/');
570
        } catch (IllegalArgumentException e) {
571
            // Different drive on Windows — fall back to the absolute path.
572
            prefix = root.toString().replace('\\', '/');
573
        }
574 4 1. computeFilePrefix : removed conditional - replaced equality check with true → SURVIVED
2. computeFilePrefix : removed conditional - replaced equality check with true → SURVIVED
3. computeFilePrefix : removed conditional - replaced equality check with false → KILLED
4. computeFilePrefix : removed conditional - replaced equality check with false → KILLED
        if (!prefix.isEmpty() && !prefix.endsWith("/")) {
575
            prefix += "/";
576
        }
577 1 1. computeFilePrefix : replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::computeFilePrefix → KILLED
        return prefix;
578
    }
579
580
    /**
581
     * Wraps a {@link TestMethodSink} so that only security-relevant records are
582
     * forwarded to {@code delegate}.
583
     *
584
     * <p>
585
     * When {@code securityOnly} is {@code false} the original {@code delegate} is
586
     * returned unchanged (zero overhead). When {@code true}, a wrapper is returned
587
     * that drops any record whose {@link AiMethodSuggestion} is {@code null} or
588
     * has {@code securityRelevant=false}.
589
     * </p>
590
     *
591
     * @param delegate     the underlying sink to forward matching records to
592
     * @param securityOnly whether to enable the filter
593
     * @return filtered sink, or {@code delegate} unchanged when filtering is off
594
     */
595
    /* default */ static TestMethodSink filterSink(TestMethodSink delegate, boolean securityOnly) {
596 2 1. filterSink : removed conditional - replaced equality check with true → SURVIVED
2. filterSink : removed conditional - replaced equality check with false → KILLED
        if (!securityOnly) {
597 1 1. filterSink : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::filterSink → KILLED
            return delegate;
598
        }
599 1 1. filterSink : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::filterSink → KILLED
        return (fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion) -> {
600 4 1. lambda$filterSink$2 : removed conditional - replaced equality check with true → NO_COVERAGE
2. lambda$filterSink$2 : removed conditional - replaced equality check with false → NO_COVERAGE
3. lambda$filterSink$2 : removed conditional - replaced equality check with false → SURVIVED
4. lambda$filterSink$2 : removed conditional - replaced equality check with true → KILLED
            if (suggestion != null && suggestion.securityRelevant()) {
601 1 1. lambda$filterSink$2 : removed call to org/egothor/methodatlas/TestMethodSink::record → NO_COVERAGE
                delegate.record(fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion);
602
            }
603
        };
604
    }
605
606
    /**
607
     * Converts a single discovered test method into a prompt target descriptor.
608
     *
609
     * @param m discovered test method
610
     * @return corresponding prompt target descriptor; never {@code null}
611
     * @see PromptBuilder.TargetMethod
612
     */
613
    /* default */ static PromptBuilder.TargetMethod toTargetMethod(DiscoveredMethod m) {
614 1 1. toTargetMethod : replaced return value with null for org/egothor/methodatlas/command/CommandSupport::toTargetMethod → KILLED
        return new PromptBuilder.TargetMethod(
615
                m.method(),
616 3 1. toTargetMethod : removed conditional - replaced comparison check with true → SURVIVED
2. toTargetMethod : removed conditional - replaced comparison check with false → SURVIVED
3. toTargetMethod : changed conditional boundary → SURVIVED
                m.beginLine() > 0 ? m.beginLine() : null,
617 3 1. toTargetMethod : changed conditional boundary → SURVIVED
2. toTargetMethod : removed conditional - replaced comparison check with false → SURVIVED
3. toTargetMethod : removed conditional - replaced comparison check with true → SURVIVED
                m.endLine() > 0 ? m.endLine() : null);
618
    }
619
620
    /**
621
     * Produces a human-readable string identifying which taxonomy configuration
622
     * is in effect, for use in scan metadata output.
623
     *
624
     * @param aiOptions AI configuration for the current run
625
     * @param aiActive  whether an AI engine is active for this run
626
     * @return taxonomy descriptor string; never {@code null}
627
     */
628
    /* default */ static String resolveTaxonomyInfo(AiOptions aiOptions, boolean aiActive) {
629 2 1. resolveTaxonomyInfo : removed conditional - replaced equality check with true → KILLED
2. resolveTaxonomyInfo : removed conditional - replaced equality check with false → KILLED
        if (!aiActive) {
630 1 1. resolveTaxonomyInfo : replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED
            return "n/a (AI disabled)";
631
        }
632 2 1. resolveTaxonomyInfo : removed conditional - replaced equality check with false → KILLED
2. resolveTaxonomyInfo : removed conditional - replaced equality check with true → KILLED
        if (aiOptions.taxonomyFile() != null) {
633 1 1. resolveTaxonomyInfo : replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED
            return "file:" + aiOptions.taxonomyFile().toAbsolutePath();
634
        }
635 1 1. resolveTaxonomyInfo : replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED
        return "built-in/" + aiOptions.taxonomyMode().name().toLowerCase(Locale.ROOT);
636
    }
637
}

Mutations

99

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

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

103

1.1
Location : buildAiEngine
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiEngine → KILLED

119

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

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

120

1.1
Location : buildAiCache
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiCache → KILLED

123

1.1
Location : buildAiCache
Killed by : org.egothor.methodatlas.MethodAtlasAppAiTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppAiTest]/[method:aiCache_miss_engineCalledForUnknownHash(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::buildAiCache → KILLED

139

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

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

140

1.1
Location : loadClassificationOverride
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_exitCode0WhenNoErrors(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::loadClassificationOverride → KILLED

143

1.1
Location : loadClassificationOverride
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::loadClassificationOverride → NO_COVERAGE

172

1.1
Location : loadProviders
Killed by : org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:app_githubAnnotationsMode_emptyDirectoryProducesNoOutput(java.nio.file.Path)]
removed call to org/egothor/methodatlas/api/TestDiscovery::configure → KILLED

175

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

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

181

1.1
Location : loadProviders
Killed by : none
removed call to org/egothor/methodatlas/command/CommandSupport::requireUniqueDiscoveryIds → SURVIVED
Covering tests

182

1.1
Location : loadProviders
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CommandSupport::loadProviders → KILLED

195

1.1
Location : closeAll
Killed by : none
removed call to org/egothor/methodatlas/api/TestDiscovery::close → SURVIVED
Covering tests

215

1.1
Location : loadPatchers
Killed by : none
removed call to org/egothor/methodatlas/api/SourcePatcher::configure → SURVIVED
Covering tests

218

1.1
Location : loadPatchers
Killed by : none
removed call to org/egothor/methodatlas/command/CommandSupport::requireUniquePatcherIds → SURVIVED
Covering tests

219

1.1
Location : loadPatchers
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsFromCsvTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsFromCsvTest]/[method:applyTagsFromCsv_addsDisplayNameImportWhenDisplayNameSet(java.nio.file.Path)]
replaced return value with Collections.emptyList for org/egothor/methodatlas/command/CommandSupport::loadPatchers → KILLED

234

1.1
Location : requireUniqueDiscoveryIds
Killed by : org.egothor.methodatlas.PluginIdUniquenessTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.PluginIdUniquenessTest]/[method:requireUniqueDiscoveryIds_distinctIds_doesNotThrow()]
removed conditional - replaced equality check with true → KILLED

2.2
Location : requireUniqueDiscoveryIds
Killed by : org.egothor.methodatlas.PluginIdUniquenessTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.PluginIdUniquenessTest]/[method:requireUniqueDiscoveryIds_duplicateId_throwsWithIdInMessage()]
removed conditional - replaced equality check with false → KILLED

254

1.1
Location : requireUniquePatcherIds
Killed by : org.egothor.methodatlas.PluginIdUniquenessTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.PluginIdUniquenessTest]/[method:requireUniquePatcherIds_duplicateId_throwsWithIdInMessage()]
removed conditional - replaced equality check with false → KILLED

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

288

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

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

294

1.1
Location : scan
Killed by : none
removed call to org/egothor/methodatlas/command/CommandSupport::closeAll → SURVIVED
Covering tests

296

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

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

331

1.1
Location : runDiscovery
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
removed call to java/util/stream/Stream::forEach → KILLED

332

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

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

349

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

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

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

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

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

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

351

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

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

363

1.1
Location : runDiscovery
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
removed call to org/egothor/methodatlas/TestMethodSink::record → KILLED

369

1.1
Location : runDiscovery
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_unparseableFile_returnsExitCode1(java.nio.file.Path)]
replaced boolean return with false for org/egothor/methodatlas/command/CommandSupport::runDiscovery → KILLED

2.2
Location : runDiscovery
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/command/CommandSupport::runDiscovery → SURVIVED
Covering tests

390

1.1
Location : collectMethodsByFile
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_addsDisplayNameImport(java.nio.file.Path)]
removed call to java/util/stream/Stream::forEach → KILLED

391

1.1
Location : lambda$collectMethodsByFile$1
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

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

392

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

397

1.1
Location : collectMethodsByFile
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_addsDisplayNameImport(java.nio.file.Path)]
replaced return value with Collections.emptyMap for org/egothor/methodatlas/command/CommandSupport::collectMethodsByFile → KILLED

424

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

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

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

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

436

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

2.2
Location : gatherAiSuggestionsForFile
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

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

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

439

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

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

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

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

440

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

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

443

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

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

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

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

476

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

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

477

1.1
Location : resolveSuggestionLookup
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → NO_COVERAGE

480

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

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

481

1.1
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_exitCode0WhenNoErrors(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED

486

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

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

487

1.1
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppAiTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppAiTest]/[method:aiCache_hit_engineNotCalledForUnchangedClass(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED

490

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

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

491

1.1
Location : resolveSuggestionLookup
Killed by : none
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → NO_COVERAGE

494

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

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

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

4.4
Location : resolveSuggestionLookup
Killed by : none
changed conditional boundary → SURVIVED Covering tests

5.5
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_addsDisplayNameImport(java.nio.file.Path)]
removed conditional - replaced comparison check with true → KILLED

499

1.1
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppAiTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppAiTest]/[method:csvMode_oversizedClass_skipsAiLookup_andLeavesAiColumnsEmpty(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED

509

1.1
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_nonSecurityMethod_notAnnotated(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED

514

1.1
Location : resolveSuggestionLookup
Killed by : org.egothor.methodatlas.MethodAtlasAppAiTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppAiTest]/[method:plainMode_aiFailureForOneClass_continuesScanningAndFallsBackForThatClass(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::resolveSuggestionLookup → KILLED

541

1.1
Location : computeContentHash
Killed by : org.egothor.methodatlas.MethodAtlasAppContentHashTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppContentHashTest]/[method:csvMode_distinctClasses_produceDifferentHashes(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::computeContentHash → KILLED

562

1.1
Location : computeFilePrefix
Killed by : org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:computeFilePrefix_emptyList_returnsEmptyString()]
removed conditional - replaced equality check with false → KILLED

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

574

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

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

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

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

577

1.1
Location : computeFilePrefix
Killed by : org.egothor.methodatlas.GitHubAnnotationsEmitterTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.GitHubAnnotationsEmitterTest]/[method:computeFilePrefix_relativeRoot_endsWithSlash(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::computeFilePrefix → KILLED

596

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

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

597

1.1
Location : filterSink
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:csvMode_noPackageDeclaration_usesSimpleClassNameAsFqcn(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::filterSink → KILLED

599

1.1
Location : filterSink
Killed by : org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_outputIsNotCsvHeader(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::filterSink → KILLED

600

1.1
Location : lambda$filterSink$2
Killed by : none
removed conditional - replaced equality check with true → NO_COVERAGE

2.2
Location : lambda$filterSink$2
Killed by : none
removed conditional - replaced equality check with false → NO_COVERAGE

3.3
Location : lambda$filterSink$2
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

4.4
Location : lambda$filterSink$2
Killed by : org.egothor.methodatlas.MethodAtlasAppSarifTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppSarifTest]/[method:sarifMode_outputIsNotCsvHeader(java.nio.file.Path)]
removed conditional - replaced equality check with true → KILLED

601

1.1
Location : lambda$filterSink$2
Killed by : none
removed call to org/egothor/methodatlas/TestMethodSink::record → NO_COVERAGE

614

1.1
Location : toTargetMethod
Killed by : org.egothor.methodatlas.MethodAtlasAppManualTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppManualTest]/[method:manualPrepare_producesNoCSVOutput(java.nio.file.Path)]
replaced return value with null for org/egothor/methodatlas/command/CommandSupport::toTargetMethod → KILLED

616

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

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

3.3
Location : toTargetMethod
Killed by : none
changed conditional boundary → SURVIVED Covering tests

617

1.1
Location : toTargetMethod
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

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

3.3
Location : toTargetMethod
Killed by : none
removed conditional - replaced comparison check with true → SURVIVED Covering tests

629

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

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

630

1.1
Location : resolveTaxonomyInfo
Killed by : org.egothor.methodatlas.MethodAtlasAppTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppTest]/[method:emitMetadata_prependsCommentLinesBeforeHeader(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED

632

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

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

633

1.1
Location : resolveTaxonomyInfo
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_taxonomyFile_emitsFilePath(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED

635

1.1
Location : resolveTaxonomyInfo
Killed by : org.egothor.methodatlas.MethodAtlasAppScanCoverageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppScanCoverageTest]/[method:emitMetadata_aiEnabled_emitsBuiltinDefaultTaxonomy(java.nio.file.Path)]
replaced return value with "" for org/egothor/methodatlas/command/CommandSupport::resolveTaxonomyInfo → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1