ApplyTagsCommand.java

1
package org.egothor.methodatlas.command;
2
3
import java.io.IOException;
4
import java.io.PrintWriter;
5
import java.nio.file.Path;
6
import java.nio.file.Paths;
7
import java.util.LinkedHashMap;
8
import java.util.List;
9
import java.util.Map;
10
import java.util.logging.Level;
11
import java.util.logging.Logger;
12
import java.util.stream.Collectors;
13
14
import org.egothor.methodatlas.AiResultCache;
15
import org.egothor.methodatlas.emit.ClassificationOverride;
16
import org.egothor.methodatlas.CliConfig;
17
import org.egothor.methodatlas.ai.AiSuggestionEngine;
18
import org.egothor.methodatlas.api.DiscoveredMethod;
19
import org.egothor.methodatlas.api.SourcePatcher;
20
import org.egothor.methodatlas.api.TestDiscovery;
21
import org.egothor.methodatlas.api.TestDiscoveryConfig;
22
23
/**
24
 * CLI command handler for the {@code -apply-tags} mode.
25
 *
26
 * <p>
27
 * Discovers test methods via the configured {@link TestDiscovery} providers,
28
 * resolves AI suggestions for each class, and delegates the actual source file
29
 * write-back to the matching {@link SourcePatcher} implementation. A summary
30
 * line is written to the supplied writer on completion.
31
 * </p>
32
 *
33
 * <h2>Languages supported for write-back</h2>
34
 * <p>
35
 * Source write-back is only available for languages whose discovery plugin
36
 * ships a {@link SourcePatcher} implementation. At the time of writing this
37
 * is <strong>Java</strong> (JUnit&nbsp;5 / 4 / TestNG) and
38
 * <strong>C#</strong> (xUnit / NUnit / MSTest). Files discovered by any other
39
 * plugin (TypeScript, Go, Python, PowerShell, SAP&nbsp;ABAP, COBOL, …) are
40
 * recognised but skipped during write-back; the command prints a per-file
41
 * notice and an aggregate skip count in the summary line. Skipped files do
42
 * not cause a non-zero exit code on their own.
43
 * </p>
44
 *
45
 * @see ApplyTagsFromCsvCommand
46
 * @see org.egothor.methodatlas.api.SourcePatcher
47
 */
48
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
49
public final class ApplyTagsCommand implements Command {
50
51
    private static final Logger LOG = Logger.getLogger(ApplyTagsCommand.class.getName());
52
53
    private final CliConfig cliConfig;
54
    private final TestDiscoveryConfig discoveryConfig;
55
    private final AiSuggestionEngine aiEngine;
56
    private final ClassificationOverride override;
57
    private final AiResultCache aiCache;
58
    private final PluginLoader pluginLoader;
59
    private final ScanOrchestrator scanOrchestrator;
60
61
    /**
62
     * Creates a new apply-tags command.
63
     *
64
     * @param cliConfig         full parsed CLI configuration
65
     * @param discoveryConfig   discovery configuration forwarded to providers
66
     * @param aiEngine          AI engine providing suggestions; {@code null}
67
     *                          when AI is disabled
68
     * @param override          human classification overrides
69
     * @param aiCache           AI result cache
70
     * @param pluginLoader      loader used to resolve {@link TestDiscovery}
71
     *                          and {@link SourcePatcher} plugins
72
     * @param scanOrchestrator  orchestrator providing
73
     *                          {@code collectMethodsByFile} and
74
     *                          {@code gatherAiSuggestionsForFile} helpers
75
     */
76
    public ApplyTagsCommand(CliConfig cliConfig, TestDiscoveryConfig discoveryConfig,
77
            AiSuggestionEngine aiEngine, ClassificationOverride override,
78
            AiResultCache aiCache, PluginLoader pluginLoader,
79
            ScanOrchestrator scanOrchestrator) {
80
        this.cliConfig = cliConfig;
81
        this.discoveryConfig = discoveryConfig;
82
        this.aiEngine = aiEngine;
83
        this.override = override;
84
        this.aiCache = aiCache;
85
        this.pluginLoader = pluginLoader;
86
        this.scanOrchestrator = scanOrchestrator;
87
    }
88
89
    /**
90
     * Discovers test methods, gathers AI suggestions, and patches source files.
91
     *
92
     * @param out writer for the completion summary line
93
     * @return {@code 0} if all files were processed successfully, {@code 1}
94
     *         if any file produced a parse or processing error
95
     * @throws IOException if traversing a file tree fails
96
     */
97
    @Override
98
    public int execute(PrintWriter out) throws IOException {
99 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → TIMED_OUT
        List<Path> roots = cliConfig.paths().isEmpty() ? List.of(Paths.get(".")) : cliConfig.paths();
100
        List<SourcePatcher> patchers = pluginLoader.loadPatchers(discoveryConfig);
101
        List<TestDiscovery> providers = pluginLoader.loadProviders(discoveryConfig);
102
103
        // Initializers are omitted: both variables are assigned unconditionally in the
104
        // try block, which satisfies JLS §16.2.15 definite-assignment for try-finally
105
        // without a catch clause.
106
        Map<Path, List<DiscoveredMethod>> byFile;
107
        boolean hadErrors;
108
        try {
109
            byFile = scanOrchestrator.collectMethodsByFile(roots, providers);
110
            hadErrors = providers.stream().anyMatch(TestDiscovery::hadErrors);
111
        } finally {
112 1 1. execute : removed call to org/egothor/methodatlas/command/PluginLoader::closeAll → SURVIVED
            pluginLoader.closeAll(providers);
113
        }
114
115
        AiRuntime ai = new AiRuntime(cliConfig.aiOptions(), aiEngine, override, aiCache);
116
117
        int modifiedFiles = 0;
118
        int totalAnnotations = 0;
119
        int skippedFiles = 0;
120
121
        for (Map.Entry<Path, List<DiscoveredMethod>> entry : byFile.entrySet()) {
122
            Path sourceFile = entry.getKey();
123
            List<DiscoveredMethod> methods = entry.getValue();
124
125
            SourcePatcher patcher = patchers.stream()
126 2 1. lambda$execute$0 : replaced boolean return with false for org/egothor/methodatlas/command/ApplyTagsCommand::lambda$execute$0 → KILLED
2. lambda$execute$0 : replaced boolean return with true for org/egothor/methodatlas/command/ApplyTagsCommand::lambda$execute$0 → KILLED
                    .filter(p -> p.supports(sourceFile))
127
                    .findFirst().orElse(null);
128 2 1. execute : removed conditional - replaced equality check with true → KILLED
2. execute : removed conditional - replaced equality check with false → KILLED
            if (patcher == null) {
129 1 1. execute : Changed increment from 1 to -1 → KILLED
                skippedFiles++;
130
                if (LOG.isLoggable(Level.INFO)) {
131
                    LOG.log(Level.INFO,
132
                            "Skipping {0}: no SourcePatcher available for this language",
133
                            sourceFile);
134
                }
135 1 1. execute : removed call to java/io/PrintWriter::println → KILLED
                out.println("Apply-tags: skipped " + sourceFile
136
                        + " — source write-back is not supported for this language "
137
                        + "(currently Java and C# only)");
138
                continue;
139
            }
140
141
            Map<String, List<DiscoveredMethod>> byClass = methods.stream()
142
                    .collect(Collectors.groupingBy(DiscoveredMethod::fqcn,
143
                            LinkedHashMap::new, Collectors.toList()));
144
145
            Map<String, List<String>> tagsToApply = new LinkedHashMap<>();
146
            Map<String, String> displayNames = new LinkedHashMap<>();
147
148 1 1. execute : removed call to org/egothor/methodatlas/command/ScanOrchestrator::gatherAiSuggestionsForFile → KILLED
            scanOrchestrator.gatherAiSuggestionsForFile(byClass, ai, aiCache, tagsToApply, displayNames);
149
150 4 1. execute : removed conditional - replaced equality check with true → SURVIVED
2. execute : removed conditional - replaced equality check with false → SURVIVED
3. execute : removed conditional - replaced equality check with false → KILLED
4. execute : removed conditional - replaced equality check with true → KILLED
            if (!tagsToApply.isEmpty() || !displayNames.isEmpty()) {
151
                try {
152
                    int changes = patcher.patch(sourceFile, tagsToApply, displayNames, out);
153 3 1. execute : changed conditional boundary → SURVIVED
2. execute : removed conditional - replaced comparison check with true → SURVIVED
3. execute : removed conditional - replaced comparison check with false → KILLED
                    if (changes > 0) {
154 1 1. execute : Changed increment from 1 to -1 → KILLED
                        modifiedFiles++;
155 1 1. execute : Replaced integer addition with subtraction → SURVIVED
                        totalAnnotations += changes;
156
                    }
157
                } catch (IOException e) {
158
                    if (LOG.isLoggable(Level.WARNING)) {
159
                        LOG.log(Level.WARNING, "Cannot process: " + sourceFile, e);
160
                    }
161
                    hadErrors = true;
162
                }
163
            }
164
        }
165
166
        // Capacity 192 comfortably covers the worst-case message:
167
        //   "Apply-tags complete: <int> annotation(s) added to <int> file(s);
168
        //    <int> file(s) skipped (no source write-back support for the language)"
169
        // which is ~140 chars including the integer placeholders.
170
        StringBuilder summary = new StringBuilder(192)
171
                .append("Apply-tags complete: ")
172
                .append(totalAnnotations).append(" annotation(s) added to ")
173
                .append(modifiedFiles).append(" file(s)");
174 3 1. execute : changed conditional boundary → KILLED
2. execute : removed conditional - replaced comparison check with true → KILLED
3. execute : removed conditional - replaced comparison check with false → KILLED
        if (skippedFiles > 0) {
175
            summary.append("; ").append(skippedFiles)
176
                    .append(" file(s) skipped (no source write-back support for the language)");
177
        }
178 1 1. execute : removed call to java/io/PrintWriter::println → KILLED
        out.println(summary.toString());
179 2 1. execute : removed conditional - replaced equality check with false → SURVIVED
2. execute : removed conditional - replaced equality check with true → KILLED
        return hadErrors ? 1 : 0;
180
    }
181
}

Mutations

99

1.1
Location : execute
Killed by : none
removed conditional - replaced equality check with true → TIMED_OUT

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

112

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

126

1.1
Location : lambda$execute$0
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest]/[method:applyTags_javaOnly_noSkipCountInSummary(java.nio.file.Path)]
replaced boolean return with false for org/egothor/methodatlas/command/ApplyTagsCommand::lambda$execute$0 → KILLED

2.2
Location : lambda$execute$0
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest]/[method:applyTags_mixedJavaAndGo_javaPatchedAndGoSkipped(java.nio.file.Path)]
replaced boolean return with true for org/egothor/methodatlas/command/ApplyTagsCommand::lambda$execute$0 → KILLED

128

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

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

129

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest]/[method:applyTags_mixedJavaAndGo_javaPatchedAndGoSkipped(java.nio.file.Path)]
Changed increment from 1 to -1 → KILLED

135

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest]/[method:applyTags_mixedJavaAndGo_javaPatchedAndGoSkipped(java.nio.file.Path)]
removed call to java/io/PrintWriter::println → KILLED

148

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_withAi_annotatesSecurityRelevantMethod(java.nio.file.Path)]
removed call to org/egothor/methodatlas/command/ScanOrchestrator::gatherAiSuggestionsForFile → KILLED

150

1.1
Location : execute
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 : execute
Killed by : none
removed conditional - replaced equality check with true → SURVIVED
Covering tests

3.3
Location : execute
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 : execute
Killed by : none
removed conditional - replaced equality check with false → SURVIVED Covering tests

153

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

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

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

154

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_summaryCountsMatchAnnotationsAdded(java.nio.file.Path)]
Changed increment from 1 to -1 → KILLED

155

1.1
Location : execute
Killed by : none
Replaced integer addition with subtraction → SURVIVED
Covering tests

174

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsUnsupportedLanguageTest]/[method:applyTags_javaOnly_noSkipCountInSummary(java.nio.file.Path)]
changed conditional boundary → KILLED

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

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

178

1.1
Location : execute
Killed by : org.egothor.methodatlas.MethodAtlasAppApplyTagsTest.[engine:junit-jupiter]/[class:org.egothor.methodatlas.MethodAtlasAppApplyTagsTest]/[method:applyTags_summaryLineAlwaysEmitted(java.nio.file.Path)]
removed call to java/io/PrintWriter::println → KILLED

179

1.1
Location : execute
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 : execute
Killed by : none
removed conditional - replaced equality check with false → SURVIVED
Covering tests

Active mutators

Tests examined


Report generated by PIT 1.22.1