JsonCommand.java
package org.egothor.methodatlas.command;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.egothor.methodatlas.AiResultCache;
import org.egothor.methodatlas.emit.ClassificationOverride;
import org.egothor.methodatlas.CliConfig;
import org.egothor.methodatlas.emit.TestMethodSink;
import org.egothor.methodatlas.ai.AiSuggestionEngine;
import org.egothor.methodatlas.api.TestDiscovery;
import org.egothor.methodatlas.api.TestDiscoveryConfig;
import org.egothor.methodatlas.emit.JsonEmitter;
/**
* CLI command handler for the {@code -json} output mode.
*
* <p>
* Scans one or more source roots, buffers all discovered test-method records,
* and serializes the result as a flat JSON array once the scan completes.
* </p>
*
* <p>
* The JSON representation differs from CSV in the following ways:
* </p>
* <ul>
* <li>{@code tags} and {@code ai_tags} are JSON arrays, not semicolon-separated
* strings</li>
* <li>Numeric fields are JSON numbers; {@code ai_security_relevant} is a JSON
* boolean</li>
* <li>Optional columns are omitted entirely when the corresponding flag is not
* enabled (rather than being left blank)</li>
* </ul>
*
* @see org.egothor.methodatlas.emit.JsonEmitter
* @see SarifCommand
* @see ScanCommand
*/
public final class JsonCommand implements Command {
private static final Logger LOG = Logger.getLogger(JsonCommand.class.getName());
private final CliConfig cliConfig;
private final TestDiscoveryConfig discoveryConfig;
private final AiSuggestionEngine aiEngine;
private final ClassificationOverride override;
private final AiResultCache aiCache;
private final PluginLoader pluginLoader;
private final ScanOrchestrator scanOrchestrator;
/**
* Creates a new JSON command.
*
* @param cliConfig full parsed CLI configuration
* @param discoveryConfig discovery configuration forwarded to providers
* @param aiEngine AI engine providing suggestions; {@code null}
* when AI is disabled
* @param override human classification overrides
* @param aiCache AI result cache
* @param pluginLoader loader used to resolve and close
* {@link TestDiscovery} providers
* @param scanOrchestrator scan-and-emit orchestrator used to process
* each root with the per-record sink
*/
public JsonCommand(CliConfig cliConfig, TestDiscoveryConfig discoveryConfig,
AiSuggestionEngine aiEngine, ClassificationOverride override,
AiResultCache aiCache, PluginLoader pluginLoader,
ScanOrchestrator scanOrchestrator) {
this.cliConfig = cliConfig;
this.discoveryConfig = discoveryConfig;
this.aiEngine = aiEngine;
this.override = override;
this.aiCache = aiCache;
this.pluginLoader = pluginLoader;
this.scanOrchestrator = scanOrchestrator;
}
/**
* Scans all roots and emits the buffered result as a JSON array.
*
* @param out writer that receives the JSON output
* @return {@code 0} if all files were processed successfully, {@code 1} if
* any file produced a parse or processing error
* @throws IOException if traversing a file tree fails
*/
@Override
public int execute(PrintWriter out) throws IOException {
boolean aiEnabled = aiEngine != null;
boolean confidenceEnabled = aiEnabled && cliConfig.aiOptions().confidence();
boolean contentHashEnabled = cliConfig.contentHash();
boolean emitSourceRoot = cliConfig.emitSourceRoot();
List<Path> roots = cliConfig.paths().isEmpty() ? List.of(Paths.get(".")) : cliConfig.paths();
JsonEmitter jsonEmitter = new JsonEmitter(aiEnabled, confidenceEnabled, contentHashEnabled,
cliConfig.driftDetect(), emitSourceRoot);
List<TestDiscovery> providers = pluginLoader.loadProviders(discoveryConfig);
boolean hadErrors = false;
try {
for (Path root : roots) {
String sourceRoot = emitSourceRoot ? ContentHasher.filePrefix(List.of(root)) : null;
final String finalSourceRoot = sourceRoot;
TestMethodSink rootSink = (fqcn, method, beginLine, loc, contentHash, tags, displayName, suggestion) ->
jsonEmitter.record(fqcn, method, beginLine, loc, contentHash, tags, displayName,
suggestion, finalSourceRoot);
if (scanOrchestrator.runDiscovery(root, providers, cliConfig.aiOptions(), aiEngine,
scanOrchestrator.filterSink(rootSink, cliConfig.securityOnly(),
cliConfig.minConfidence(), confidenceEnabled),
cliConfig.contentHash(), override, aiCache)) {
hadErrors = true;
}
}
} finally {
pluginLoader.closeAll(providers);
}
if (aiCache.isActive() && LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "AI cache: {0} hit(s), {1} miss(es)",
new Object[] { aiCache.hits(), aiCache.misses() });
}
jsonEmitter.flush(out);
return hadErrors ? 1 : 0;
}
}