Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.randomcodespace.iq.cache.AnalysisCache;
import io.github.randomcodespace.iq.cache.FileHasher;
import io.github.randomcodespace.iq.cli.VersionCommand;
import io.github.randomcodespace.iq.config.CliStartupConfigOverrides;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig;
import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
Expand Down Expand Up @@ -814,7 +815,7 @@
report.accept("Service: " + infraRegistry.getServiceName());
// Propagate to config if not already set
if (config.getServiceName() == null || config.getServiceName().isBlank()) {
config.setServiceName(infraRegistry.getServiceName());
CliStartupConfigOverrides.applyServiceName(config, infraRegistry.getServiceName());
}
}

Expand Down Expand Up @@ -1425,7 +1426,7 @@
/**
* Analyze a single file using the given (possibly filtered) registry.
*/
DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry detectorRegistry) {

Check warning on line 1429 in src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

A "Brain Method" was detected. Refactor it to reduce at least one of the following metrics: LOC from 90 to 64, Complexity from 19 to 14, Nesting Level from 3 to 2, Number of Variables from 26 to 6.

See more on https://sonarcloud.io/project/issues?id=RandomCodeSpace_code-iq&issues=AZ26aAjmVk-tm9gMnCon&open=AZ26aAjmVk-tm9gMnCon&pullRequest=53
Instant fileStart = Instant.now();
Path absPath = repoPath.resolve(file.path());

Expand Down
6 changes: 4 additions & 2 deletions src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,13 @@ static void configureFromOptions(io.github.randomcodespace.iq.config.CodeIqConfi
java.nio.file.Path graphDir, String serviceName) {
if (graphDir != null) {
java.nio.file.Path sharedDir = graphDir.toAbsolutePath().normalize();
config.setCacheDir(sharedDir.toString());
io.github.randomcodespace.iq.config.CliStartupConfigOverrides.applyCacheDir(
config, sharedDir.toString());
info(" Graph dir: " + sharedDir + " (shared multi-repo)");
}
if (serviceName != null && !serviceName.isBlank()) {
config.setServiceName(serviceName);
io.github.randomcodespace.iq.config.CliStartupConfigOverrides.applyServiceName(
config, serviceName);
info(" Service name: " + serviceName);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.randomcodespace.iq.analyzer.LayerClassifier;
import io.github.randomcodespace.iq.analyzer.linker.Linker;
import io.github.randomcodespace.iq.cache.AnalysisCache;
import io.github.randomcodespace.iq.config.CliStartupConfigOverrides;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.intelligence.RepositoryIdentity;
import io.github.randomcodespace.iq.intelligence.extractor.LanguageEnricher;
Expand Down Expand Up @@ -91,8 +92,9 @@ public Integer call() {

// If --graph is set, override cache directory to shared location
if (graphDir != null) {
config.setCacheDir(graphDir.toAbsolutePath().normalize().toString());
CliOutput.info(" Graph dir: " + graphDir.toAbsolutePath().normalize() + " (shared multi-repo)");
Path sharedDir = graphDir.toAbsolutePath().normalize();
CliStartupConfigOverrides.applyCacheDir(config, sharedDir.toString());
CliOutput.info(" Graph dir: " + sharedDir + " (shared multi-repo)");
}

// 1. Open H2 file
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.randomcodespace.iq.cli;

import io.github.randomcodespace.iq.config.CliStartupConfigOverrides;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.config.GraphBootstrapper;
import io.github.randomcodespace.iq.graph.GraphStore;
Expand Down Expand Up @@ -71,10 +72,7 @@ public class ServeCommand implements Callable<Integer> {
@Override
public Integer call() {
Path root = path.toAbsolutePath().normalize();
config.setRootPath(root.toString());
if (readOnly) {
config.setReadOnly(true);
}
CliStartupConfigOverrides.applyServeOverrides(config, root, readOnly);
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);

// Bootstrap Neo4j from the H2 analysis cache if Neo4j is empty. This is
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.randomcodespace.iq.config;

import java.nio.file.Path;

/**
* Centralised, CLI-startup-only mutation of the {@link CodeIqConfig} Spring
* singleton.
*
* <p><b>Call contract:</b> these helpers are invoked exactly once per JVM
* invocation, from a Picocli command's {@code call()} entry point, <em>before</em>
* any downstream consumer reads config state. Treat the config as frozen
* afterwards.
*
* <p>Do <b>not</b> invoke from request handlers, background workers, controllers,
* MCP tools, or any serving-layer code path. The {@link CodeIqConfig} bean is a
* Spring singleton shared across every consumer — mutating it at runtime is a
* correctness hazard and was the motivation for collapsing all existing call
* sites into this one package-private surface.
*
* <p>Visibility is package-private by design: only other classes inside
* {@code io.github.randomcodespace.iq.config} can reach {@link CodeIqConfig}'s
* package-private setters via this helper. CLI callers in
* {@code io.github.randomcodespace.iq.cli} and analyzer callers in
* {@code io.github.randomcodespace.iq.analyzer} route through the public
* {@code apply*} methods below.
*/
public final class CliStartupConfigOverrides {

private CliStartupConfigOverrides() {}

/**
* Apply the {@code serve} command's startup overrides to the config bean:
* absolute root path, and read-only mode when the {@code --read-only} flag
* was set.
*
* @param config the Spring-managed {@link CodeIqConfig} singleton
* @param root absolute, normalised root path (must not be {@code null})
* @param readOnly whether the {@code --read-only} CLI flag was set
*/
public static void applyServeOverrides(CodeIqConfig config, Path root, boolean readOnly) {
if (config == null || root == null) {
return;
}
config.setRootPath(root.toString());
if (readOnly) {
config.setReadOnly(true);
}
}

/**
* Override the cache directory. No-op if {@code cacheDir} is {@code null}
* or blank — we never overwrite the in-code default with an absent value.
*/
public static void applyCacheDir(CodeIqConfig config, String cacheDir) {
if (config == null || cacheDir == null || cacheDir.isBlank()) {
return;
}
config.setCacheDir(cacheDir);
}

/**
* Override the service-name tag used in multi-repo graph mode. No-op if
* {@code name} is {@code null} or blank.
*/
public static void applyServiceName(CodeIqConfig config, String name) {
if (config == null || name == null || name.isBlank()) {
return;
}
config.setServiceName(name);
}
}
40 changes: 23 additions & 17 deletions src/main/java/io/github/randomcodespace/iq/config/CodeIqConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
* Task 11 moved bean production to {@link UnifiedConfigBeans#codeIqConfig}, which
* adapts a {@link io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig}
* (single source of truth) via {@link UnifiedConfigAdapter#toCodeIqConfig}. The
* getter/setter surface is preserved unchanged so the ~100 call sites that still
* depend on this bean continue to work.
* getter surface is preserved unchanged so the ~100 call sites that read this
* bean continue to work.
*
* <p>This class is intentionally a plain POJO (no {@code @Configuration},
* no {@code @ConfigurationProperties}); Spring Boot no longer instantiates it
* from {@code application.yml}. Instantiable directly in tests via the public
* no-arg constructor and setters.
* from {@code application.yml}.
*
* <p><b>Setters are package-private.</b> Only {@link UnifiedConfigAdapter}
* (at Spring startup) and {@link CliStartupConfigOverrides} (once per JVM at
* CLI entry) mutate instances of this class. External-package callers go
* through {@link CliStartupConfigOverrides}. External-package tests that need
* a populated instance construct one via
* {@link UnifiedConfigAdapter#toCodeIqConfig(io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig)}.
*/
public class CodeIqConfig {

Expand Down Expand Up @@ -48,7 +54,7 @@ public static class Graph {
private String path = ".code-iq/graph/graph.db";

public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
void setPath(String path) { this.path = path; }
}

/** Read-only mode for serving — no lock files, no writes. For read-only filesystems (AKS). */
Expand All @@ -57,93 +63,93 @@ public static class Graph {
/** Service name tag for multi-repo graph mode. */
private String serviceName;

// --- Getters and Setters ---
// --- Getters (public) and Setters (package-private) ---

public String getRootPath() {
return rootPath;
}

public void setRootPath(String rootPath) {
void setRootPath(String rootPath) {
this.rootPath = rootPath;
}

public String getCacheDir() {
return cacheDir;
}

public void setCacheDir(String cacheDir) {
void setCacheDir(String cacheDir) {
this.cacheDir = cacheDir;
}

public int getMaxDepth() {
return maxDepth;
}

public void setMaxDepth(int maxDepth) {
void setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
}

public int getMaxFiles() {
return maxFiles;
}

public void setMaxFiles(int maxFiles) {
void setMaxFiles(int maxFiles) {
this.maxFiles = Math.max(1, maxFiles);
}

public int getMaxRadius() {
return maxRadius;
}

public void setMaxRadius(int maxRadius) {
void setMaxRadius(int maxRadius) {
this.maxRadius = maxRadius;
}

public int getBatchSize() {
return batchSize;
}

public void setBatchSize(int batchSize) {
void setBatchSize(int batchSize) {
this.batchSize = Math.max(1, batchSize);
}

public boolean isReadOnly() {
return readOnly;
}

public void setReadOnly(boolean readOnly) {
void setReadOnly(boolean readOnly) {
this.readOnly = readOnly;
}

public String getServiceName() {
return serviceName;
}

public void setServiceName(String serviceName) {
void setServiceName(String serviceName) {
this.serviceName = serviceName;
}

public Graph getGraph() {
return graph;
}

public void setGraph(Graph graph) {
void setGraph(Graph graph) {
this.graph = graph;
}

public boolean isUiEnabled() {
return uiEnabled;
}

public void setUiEnabled(boolean uiEnabled) {
void setUiEnabled(boolean uiEnabled) {
this.uiEnabled = uiEnabled;
}

public int getMaxSnippetLines() {
return maxSnippetLines;
}

public void setMaxSnippetLines(int maxSnippetLines) {
void setMaxSnippetLines(int maxSnippetLines) {
this.maxSnippetLines = Math.max(1, maxSnippetLines);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport;

/**
* Tests for the REST API controller using standalone MockMvc (no Spring context needed).
Expand All @@ -46,9 +47,9 @@ class GraphControllerTest {
@BeforeEach
void setUp() {
config = new CodeIqConfig();
config.setMaxDepth(10);
config.setMaxRadius(10);
config.setRootPath(".");
CodeIqConfigTestSupport.override(config).maxDepth(10).done();
CodeIqConfigTestSupport.override(config).maxRadius(10).done();
CodeIqConfigTestSupport.override(config).rootPath(".").done();
var controller = new GraphController(queryService, config);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
Expand Down Expand Up @@ -499,7 +500,7 @@ void searchGraphShouldReturnResults() throws Exception {
@Test
void readFileShouldReturnContent(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("hello.txt"), "Hello World", StandardCharsets.UTF_8);
config.setRootPath(tempDir.toAbsolutePath().toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done();
var controller = new GraphController(queryService, config);
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();

Expand All @@ -510,7 +511,7 @@ void readFileShouldReturnContent(@TempDir Path tempDir) throws Exception {

@Test
void readFileShouldReturn404ForMissing(@TempDir Path tempDir) throws Exception {
config.setRootPath(tempDir.toAbsolutePath().toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done();
var controller = new GraphController(queryService, config);
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();

Expand All @@ -520,7 +521,7 @@ void readFileShouldReturn404ForMissing(@TempDir Path tempDir) throws Exception {

@Test
void readFileShouldBlockPathTraversal(@TempDir Path tempDir) throws Exception {
config.setRootPath(tempDir.toAbsolutePath().toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done();
var controller = new GraphController(queryService, config);
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();

Expand All @@ -533,7 +534,7 @@ void readFileShouldBlockPathTraversal(@TempDir Path tempDir) throws Exception {
void readFileShouldReturnLineRange(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("multi.txt"), "line1\nline2\nline3\nline4\nline5",
StandardCharsets.UTF_8);
config.setRootPath(tempDir.toAbsolutePath().toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done();
var controller = new GraphController(queryService, config);
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();

Expand All @@ -548,7 +549,7 @@ void readFileShouldReturnLineRange(@TempDir Path tempDir) throws Exception {
@Test
void readFileShouldReturnFullContentWithoutLineParams(@TempDir Path tempDir) throws Exception {
Files.writeString(tempDir.resolve("full.txt"), "aaa\nbbb\nccc", StandardCharsets.UTF_8);
config.setRootPath(tempDir.toAbsolutePath().toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toAbsolutePath().toString()).done();
var controller = new GraphController(queryService, config);
var fileMvc = MockMvcBuilders.standaloneSetup(controller).build();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport;

class IntelligenceControllerTest {

Expand All @@ -41,7 +42,7 @@ void setUp() {
when(metadataProvider.current()).thenReturn(metadata);

CodeIqConfig config = new CodeIqConfig();
config.setRootPath(System.getProperty("java.io.tmpdir"));
CodeIqConfigTestSupport.override(config).rootPath(System.getProperty("java.io.tmpdir")).done();

IntelligenceController controller = new IntelligenceController(assembler, metadataProvider, config);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import io.github.randomcodespace.iq.config.CodeIqConfigTestSupport;

/**
* Extended tests for TopologyController that exercise the actual REST endpoints
Expand Down Expand Up @@ -60,7 +61,7 @@ class TopologyControllerExtendedTest {
void setUp() {
var config = new CodeIqConfig();
// Use the temp dir as rootPath so H2 fallback finds no cache file
config.setRootPath(tempDir.toString());
CodeIqConfigTestSupport.override(config).rootPath(tempDir.toString()).done();
controller = new TopologyController(topologyService, graphStore, config);
mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
}
Expand Down
Loading
Loading