YamlConfig.java

1
package org.egothor.methodatlas;
2
3
import java.io.IOException;
4
import java.nio.file.Path;
5
import java.util.List;
6
import java.util.Map;
7
import java.util.logging.Level;
8
import java.util.logging.Logger;
9
10
import com.fasterxml.jackson.annotation.JsonProperty;
11
import tools.jackson.core.JacksonException;
12
import tools.jackson.core.JsonParser;
13
import tools.jackson.databind.DeserializationContext;
14
import tools.jackson.databind.DeserializationFeature;
15
import tools.jackson.databind.ObjectMapper;
16
import tools.jackson.databind.ValueDeserializer;
17
import tools.jackson.databind.deser.DeserializationProblemHandler;
18
import tools.jackson.dataformat.yaml.YAMLMapper;
19
20
/**
21
 * Loads a YAML configuration file that provides default values for
22
 * command-line options.
23
 *
24
 * <p>
25
 * When a {@code -config <file>} argument is present, {@link CliArgs} calls
26
 * {@link #load(Path)} before processing the remaining arguments. The returned
27
 * {@link YamlConfigFile} seeds the initial values; any matching command-line
28
 * flag then overrides the YAML value.
29
 * </p>
30
 *
31
 * <h2>Supported fields</h2>
32
 *
33
 * <pre>
34
 * outputMode: csv          # csv | plain | sarif | json  (default: csv)
35
 * emitMetadata: false      # (default: false)
36
 * contentHash: false       # (default: false)
37
 * securityOnly: false      # (default: false)
38
 * includeNonSecurity: false  # opt-in: include non-security methods in SARIF output (default: false)
39
 * sarifOmitScores: false   # opt-out: omit interaction score / confidence from SARIF message text (default: false)
40
 * minConfidence: 0.0       # drop AI results below this threshold (requires ai.confidence: true; default: 0.0 = off)
41
 * driftDetect: false       # (default: false)
42
 * promoteAi: false         # RISKY, not recommended: -apply-tags-from-csv falls back to ai_tags/ai_display_name
43
 *                          # for methods whose curated tags/display_name are blank, writing UNVALIDATED AI
44
 *                          # output into source (default: false)
45
 * overrideFile: .methodatlas-overrides.yaml  # optional
46
 * fileSuffixes:
47
 *   - Test.java
48
 * testMarkers:             # annotation/attribute names; empty = provider defaults
49
 *   - Test
50
 *   - ParameterizedTest
51
 * properties:              # plugin-specific key/multi-value pairs (optional)
52
 *   functionNames:         # example: for a Jest/Mocha/Vitest TypeScript plugin
53
 *     - test
54
 *     - it
55
 * ai:
56
 *   enabled: true
57
 *   provider: ollama       # auto | ollama | openai | openrouter | anthropic | azure_openai | groq | xai | github_models | mistral
58
 *   model: qwen2.5-coder:7b
59
 *   baseUrl: http://localhost:11434
60
 *   apiKey: sk-...
61
 *   apiKeyEnv: MY_KEY_ENV
62
 *   taxonomyFile: /path/to/taxonomy.txt
63
 *   taxonomyMode: default  # default | optimized
64
 *   maxClassChars: 100000
65
 *   timeoutSec: 30
66
 *   maxRetries: 3
67
 *   confidence: false
68
 *   apiVersion: 2024-02-01 # Azure OpenAI REST API version (azure_openai only)
69
 * detectSecrets: false         # enable credential detection (default: false)
70
 * secretsInclude: "**&#47;*.java" # glob override for file mask (default: null = use fileSuffixes)
71
 * secretsRules: /path/to/rules.yaml  # custom rule catalog (default: null = built-in)
72
 * secretsOut: methodatlas-credentials.csv  # output path for secrets CSV (default: methodatlas-credentials.csv)
73
 * secretsSeparateLlm: false    # force standalone triage LLM call (default: false)
74
 * secretsShowValues: false     # print unmasked values (default: false)
75
 * secretsErrorThreshold: 0.8  # SARIF error score floor (default: 0.8)
76
 * secretsWarningThreshold: 0.4 # SARIF warning score floor (default: 0.4)
77
 * secretsMinScore: 0.0         # suppress findings below this score (default: 0.0 = keep all)
78
 * </pre>
79
 *
80
 * <p>
81
 * Unknown fields in the YAML file are tolerated (they never fail the load) but
82
 * each is logged at {@code WARNING} so operator typos are visible.
83
 * </p>
84
 *
85
 * @see CliArgs
86
 */
87
final class YamlConfig {
88
89
    private static final Logger LOG = Logger.getLogger(YamlConfig.class.getName());
90
91
    /**
92
     * Prevents instantiation of this utility class.
93
     */
94
    private YamlConfig() {
95
    }
96
97
    /**
98
     * Logs every unknown YAML key at {@code WARNING} so operator typos are
99
     * visible, then declines to handle it so Jackson skips the value (the mapper
100
     * has {@code FAIL_ON_UNKNOWN_PROPERTIES} disabled, preserving the
101
     * "unknown keys are tolerated" contract).
102
     */
103
    private static final class UnknownKeyLogger extends DeserializationProblemHandler {
104
        @Override
105
        public boolean handleUnknownProperty(DeserializationContext ctxt, JsonParser parser,
106
                ValueDeserializer<?> deserializer, Object beanOrClass, String propertyName) {
107
            if (LOG.isLoggable(Level.WARNING)) {
108 2 1. handleUnknownProperty : removed conditional - replaced equality check with false → SURVIVED
2. handleUnknownProperty : removed conditional - replaced equality check with true → KILLED
                Class<?> target = beanOrClass instanceof Class<?> c ? c : beanOrClass.getClass();
109
                LOG.log(Level.WARNING,
110
                        "Unknown configuration key ''{0}'' (in {1}) ignored — check for a typo.",
111
                        new Object[] { propertyName, target.getSimpleName() });
112
            }
113 1 1. handleUnknownProperty : replaced boolean return with true for org/egothor/methodatlas/YamlConfig$UnknownKeyLogger::handleUnknownProperty → SURVIVED
            return false; // not consumed here; Jackson skips it (FAIL_ON_UNKNOWN_PROPERTIES=false)
114
        }
115
    }
116
117
    /**
118
     * Loads a YAML configuration file.
119
     *
120
     * @param configFile path to the YAML file
121
     * @return parsed configuration; never {@code null}
122
     * @throws IOException              if the file cannot be read
123
     * @throws IllegalArgumentException if the file cannot be parsed as valid YAML
124
     */
125
    /* default */ static YamlConfigFile load(Path configFile) throws IOException {
126
        ObjectMapper mapper = YAMLMapper.builder()
127
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
128
                .addHandler(new UnknownKeyLogger())
129
                .build();
130
        try {
131 1 1. load : replaced return value with null for org/egothor/methodatlas/YamlConfig::load → KILLED
            return mapper.readValue(configFile.toFile(), YamlConfigFile.class);
132
        } catch (JacksonException e) {
133
            throw new IOException("Cannot read or parse configuration file '" + configFile + "'", e);
134
        }
135
    }
136
137
    // -------------------------------------------------------------------------
138
    // POJO classes
139
    // -------------------------------------------------------------------------
140
141
    /**
142
     * Top-level YAML configuration structure.
143
     *
144
     * <p>
145
     * Unknown keys are tolerated (the mapper disables
146
     * {@code FAIL_ON_UNKNOWN_PROPERTIES}) but each is logged at {@code WARNING}
147
     * by {@link UnknownKeyLogger}; hence no class-level
148
     * {@code @JsonIgnoreProperties(ignoreUnknown = true)} — that would silently
149
     * drop unknowns before the handler could see them.
150
     * </p>
151
     */
152
    /* default */ static final class YamlConfigFile {
153
154
        /** Output mode: {@code csv}, {@code plain}, or {@code sarif}. */
155
        @JsonProperty("outputMode")
156
        /* default */ String outputMode;
157
158
        /** Whether to emit {@code # key: value} metadata comment lines. */
159
        @JsonProperty("emitMetadata")
160
        /* default */ boolean emitMetadata;
161
162
        /** File name suffixes used to select test source files. */
163
        @JsonProperty("fileSuffixes")
164
        /* default */ List<String> fileSuffixes;
165
166
        /**
167
         * Language-neutral test-marker identifiers (annotation/attribute simple
168
         * names for JVM and .NET providers; ignored by TypeScript providers).
169
         * Empty or absent means "use provider defaults".
170
         */
171
        @JsonProperty("testMarkers")
172
        /* default */ List<String> testMarkers;
173
174
        /**
175
         * Plugin-specific key/multi-value pairs forwarded verbatim to each
176
         * {@link org.egothor.methodatlas.api.TestDiscovery} provider.
177
         * Providers ignore keys they do not recognise.
178
         */
179
        @JsonProperty("properties")
180
        /* default */ Map<String, List<String>> properties;
181
182
        /**
183
         * Whether to include a SHA-256 content-hash fingerprint of each class
184
         * source as a {@code content_hash} column.
185
         */
186
        @JsonProperty("contentHash")
187
        /* default */ boolean contentHash;
188
189
        /**
190
         * Path to a YAML classification override file. When set, human-authored
191
         * corrections are applied after AI classification on every run.
192
         */
193
        @JsonProperty("overrideFile")
194
        /* default */ String overrideFile;
195
196
        /**
197
         * When {@code true}, only security-relevant methods are emitted; all
198
         * other methods are silently dropped from the output.
199
         */
200
        @JsonProperty("securityOnly")
201
        /* default */ boolean securityOnly;
202
203
        /**
204
         * When {@code true}, non-security methods are included in SARIF output
205
         * even though SARIF mode applies the security-only filter by default.
206
         * Has no effect in CSV or plain-text modes.
207
         */
208
        @JsonProperty("includeNonSecurity")
209
        /* default */ boolean includeNonSecurity;
210
211
        /**
212
         * When {@code true}, a {@code tag_ai_drift} column is added to CSV/plain
213
         * output comparing the source-level {@code @Tag("security")} annotation
214
         * against the AI security-relevance classification.
215
         */
216
        @JsonProperty("driftDetect")
217
        /* default */ boolean driftDetect;
218
219
        /**
220
         * <strong>Risky, not recommended.</strong> When {@code true}, the
221
         * {@code -apply-tags-from-csv} engine falls back to the {@code ai_tags}
222
         * and {@code ai_display_name} columns for any method whose curated
223
         * {@code tags} / {@code display_name} column is blank, writing the raw,
224
         * unvalidated AI suggestion into source. This bypasses the human review
225
         * step the apply-from-csv workflow exists to enforce. Default:
226
         * {@code false}.
227
         */
228
        @JsonProperty("promoteAi")
229
        /* default */ boolean promoteAi;
230
231
        /**
232
         * When {@code true}, the interaction score and confidence percentage are
233
         * omitted from SARIF result message text. Use this when the consuming
234
         * system already renders the {@code properties} bag and the extra text is
235
         * unwanted. Default: {@code false} (scores are embedded in messages).
236
         */
237
        @JsonProperty("sarifOmitScores")
238
        /* default */ boolean sarifOmitScores;
239
240
        /**
241
         * Minimum AI confidence score (inclusive) required for a method to be
242
         * emitted. Methods whose {@code ai_confidence} is below this threshold
243
         * are silently dropped. Only meaningful when {@code ai.confidence: true}
244
         * is also set. Default: {@code 0.0} (no filtering).
245
         */
246
        @JsonProperty("minConfidence")
247
        /* default */ Double minConfidence;
248
249
        /**
250
         * When {@code true}, enable credential detection in addition to the normal
251
         * test-method scan. Default: {@code false}.
252
         */
253
        @JsonProperty("detectSecrets")
254
        /* default */ boolean detectSecrets;
255
256
        /**
257
         * Glob pattern overriding the default test-file mask when scanning for
258
         * secrets. {@code null} means use the default mask derived from
259
         * {@code fileSuffixes}.
260
         */
261
        @JsonProperty("secretsInclude")
262
        /* default */ String secretsInclude;
263
264
        /**
265
         * Path to a custom rule catalog YAML file. {@code null} uses the built-in
266
         * catalog bundled with the detect-secrets module.
267
         */
268
        @JsonProperty("secretsRules")
269
        /* default */ String secretsRules;
270
271
        /**
272
         * Output path for the secrets CSV. {@code null} causes the default
273
         * {@code methodatlas-credentials.csv} in the current working directory to be
274
         * used.
275
         */
276
        @JsonProperty("secretsOut")
277
        /* default */ String secretsOut;
278
279
        /**
280
         * When {@code true}, force a standalone triage LLM call instead of
281
         * appending the secret-triage prompt to the normal test-classification
282
         * call. Default: {@code false}.
283
         */
284
        @JsonProperty("secretsSeparateLlm")
285
        /* default */ boolean secretsSeparateLlm;
286
287
        /**
288
         * When {@code true}, print unmasked secret values in CSV and SARIF output.
289
         * Default: {@code false} (values are redacted).
290
         */
291
        @JsonProperty("secretsShowValues")
292
        /* default */ boolean secretsShowValues;
293
294
        /**
295
         * SARIF error score floor. Findings at or above this value are emitted as
296
         * {@code error}-level SARIF results. Default: {@code 0.8}.
297
         */
298
        @JsonProperty("secretsErrorThreshold")
299
        /* default */ Double secretsErrorThreshold;
300
301
        /**
302
         * SARIF warning score floor. Findings at or above this value (but below
303
         * {@code secretsErrorThreshold}) are emitted as {@code warning}-level SARIF
304
         * results. Default: {@code 0.4}.
305
         */
306
        @JsonProperty("secretsWarningThreshold")
307
        /* default */ Double secretsWarningThreshold;
308
309
        /**
310
         * Suppress findings whose triage score is below this value. Default:
311
         * {@code 0.0} keeps all findings.
312
         */
313
        @JsonProperty("secretsMinScore")
314
        /* default */ Double secretsMinScore;
315
316
        /** AI enrichment settings. */
317
        @JsonProperty("ai")
318
        /* default */ YamlAiConfig ai;
319
    }
320
321
    /**
322
     * AI subsystem configuration within the YAML file. Unknown keys are logged
323
     * and skipped (see {@link YamlConfigFile}).
324
     */
325
    /* default */ static final class YamlAiConfig {
326
327
        /** Whether AI enrichment is enabled. */
328
        @JsonProperty("enabled")
329
        /* default */ Boolean enabled;
330
331
        /**
332
         * AI provider: {@code auto}, {@code ollama}, {@code openai},
333
         * {@code openrouter}, {@code anthropic}, {@code azure_openai},
334
         * {@code groq}, {@code xai}, {@code github_models}, or {@code mistral}.
335
         */
336
        @JsonProperty("provider")
337
        /* default */ String provider;
338
339
        /** Provider-specific model name. */
340
        @JsonProperty("model")
341
        /* default */ String model;
342
343
        /** Provider base URL override. */
344
        @JsonProperty("baseUrl")
345
        /* default */ String baseUrl;
346
347
        /** API key supplied directly. */
348
        @JsonProperty("apiKey")
349
        /* default */ String apiKey;
350
351
        /** Name of the environment variable that holds the API key. */
352
        @JsonProperty("apiKeyEnv")
353
        /* default */ String apiKeyEnv;
354
355
        /** Path to an external taxonomy file. */
356
        @JsonProperty("taxonomyFile")
357
        /* default */ String taxonomyFile;
358
359
        /** Built-in taxonomy variant: {@code default} or {@code optimized}. */
360
        @JsonProperty("taxonomyMode")
361
        /* default */ String taxonomyMode;
362
363
        /** Maximum number of characters of class source sent to the AI. */
364
        @JsonProperty("maxClassChars")
365
        /* default */ Integer maxClassChars;
366
367
        /** AI request timeout in seconds. */
368
        @JsonProperty("timeoutSec")
369
        /* default */ Long timeoutSec;
370
371
        /** Maximum number of retries for AI requests. */
372
        @JsonProperty("maxRetries")
373
        /* default */ Integer maxRetries;
374
375
        /** Whether to request a confidence score for each classification. */
376
        @JsonProperty("confidence")
377
        /* default */ Boolean confidence;
378
379
        /**
380
         * Azure OpenAI REST API version appended as the {@code api-version} query
381
         * parameter; only used when {@code provider: azure_openai} is set.
382
         */
383
        @JsonProperty("apiVersion")
384
        /* default */ String apiVersion;
385
386
        /** Path to a custom method-classification prompt template (default: built-in). */
387
        @JsonProperty("classificationPrompt")
388
        /* default */ String classificationPrompt;
389
390
        /** Path to a custom folded credential-triage appendix template (default: built-in). */
391
        @JsonProperty("triagePrompt")
392
        /* default */ String triagePrompt;
393
394
        /** Path to a custom standalone credential-triage template (default: built-in). */
395
        @JsonProperty("dedicatedTriagePrompt")
396
        /* default */ String dedicatedTriagePrompt;
397
    }
398
}

Mutations

108

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

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

113

1.1
Location : handleUnknownProperty
Killed by : none
replaced boolean return with true for org/egothor/methodatlas/YamlConfig$UnknownKeyLogger::handleUnknownProperty → SURVIVED
Covering tests

131

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

Active mutators

Tests examined


Report generated by PIT 1.22.1