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
5 changes: 4 additions & 1 deletion docs/codeiq.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ indexing:
- '**/generated/**'
incremental: true # reuse H2 cache when file hashes match
cache_dir: .code-iq/cache # H2 analysis cache directory
parallelism: auto # "auto" or a positive integer
parallelism: null # null = auto-detect (Runtime.availableProcessors()); positive int to pin
batch_size: 500 # files per H2 flush batch (default: 500)
max_depth: 10 # max impact-trace depth
max_radius: 10 # max ego-graph radius
max_files: null # null = no cap; positive int to bound discovery
max_snippet_lines: null # null = use CodeIqConfig default
parsers: [] # parser-preference names (e.g. ["javaparser","antlr"]); empty = defaults

# ---------------------------------------------------------------------------
# serving
Expand Down Expand Up @@ -92,6 +93,8 @@ observability:
# ---------------------------------------------------------------------------
detectors:
profiles: [default] # named detector bundles to activate
categories: [] # allow-list of detector categories (e.g. ["endpoints","entities"]); empty = all
include: [] # allow-list of detector names (by Detector#getName()); empty = no name filter
overrides: # per-detector feature flags, keyed by SimpleClassName
SpringRestDetector: { enabled: true }
QuarkusRestDetector: { enabled: true }
Expand Down
148 changes: 89 additions & 59 deletions src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import io.github.randomcodespace.iq.cache.FileHasher;
import io.github.randomcodespace.iq.cli.VersionCommand;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.config.ProjectConfig;
import io.github.randomcodespace.iq.config.ProjectConfigLoader;
import io.github.randomcodespace.iq.config.unified.CodeIqUnifiedConfig;
import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
import io.github.randomcodespace.iq.detector.Detector;
import io.github.randomcodespace.iq.detector.DetectorContext;
Expand Down Expand Up @@ -88,9 +87,36 @@
private final LayerClassifier layerClassifier;
private final List<Linker> linkers;
private final CodeIqConfig config;
private final CodeIqUnifiedConfig unifiedConfig;
private final ConfigScanner configScanner;
private final ArchitectureKeywordFilter keywordFilter;

/**
* Projection of the injected {@link CodeIqUnifiedConfig} tree into the flat
* shape the pipeline consumes: detector category/include filters, language +
* exclude filters, and a parallelism override ({@code null} = auto-detect).
*
* <p>Lists are always non-null; an empty list means "no filter" (same
* semantics as the pre-Phase-B legacy {@code ProjectConfig.empty()} path).
*/
private record PipelineFilters(
List<String> categories,
List<String> include,
List<String> languages,
List<String> exclude,
Integer parallelism) {}

private PipelineFilters pipelineFilters() {
var indexing = unifiedConfig.indexing();
var detectors = unifiedConfig.detectors();
return new PipelineFilters(
detectors.categories() == null ? List.of() : detectors.categories(),
detectors.include() == null ? List.of() : detectors.include(),
indexing.languages() == null ? List.of() : indexing.languages(),
indexing.exclude() == null ? List.of() : indexing.exclude(),
indexing.parallelism());
}

/** Primary constructor — used by Spring Boot dependency injection. */
@Autowired
public Analyzer(
Expand All @@ -100,6 +126,7 @@
LayerClassifier layerClassifier,
List<Linker> linkers,
CodeIqConfig config,
CodeIqUnifiedConfig unifiedConfig,
ConfigScanner configScanner,
ArchitectureKeywordFilter keywordFilter
) {
Expand All @@ -109,11 +136,20 @@
this.layerClassifier = layerClassifier;
this.linkers = linkers;
this.config = config;
this.unifiedConfig = unifiedConfig;
this.configScanner = configScanner;
this.keywordFilter = keywordFilter;
}

/** Backward-compatible constructor for tests that don't need smart indexing. */
/**
* Backward-compatible constructor for tests that don't need smart indexing.
*
* <p>Defaults the unified-config overlay to {@link CodeIqUnifiedConfig#empty()} —
* equivalent to the pre-Phase-B "no {@code .osscodeiq.yml} present" path
* (no detector filters, no language filter, auto parallelism). Tests that
* need to exercise filters should use the primary constructor with a
* hand-rolled {@link CodeIqUnifiedConfig}.
*/
public Analyzer(
DetectorRegistry registry,
StructuredParser parser,
Expand All @@ -123,6 +159,7 @@
CodeIqConfig config
) {
this(registry, parser, fileDiscovery, layerClassifier, linkers, config,
CodeIqUnifiedConfig.empty(),
new ConfigScanner(), new ArchitectureKeywordFilter());
}

Expand Down Expand Up @@ -188,51 +225,49 @@

private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCache cache,
Consumer<String> report, Instant start) {
// 0. Load project config for pipeline filtering
ProjectConfig projectConfig = ProjectConfigLoader.loadProjectConfig(root);
// 0. Read pipeline filters from the injected unified config (single source of truth
// resolved at startup by UnifiedConfigBeans — no per-call file I/O).
PipelineFilters filters = pipelineFilters();
DetectorRegistry effectiveRegistry = registry;

// Apply detector category filter from project config
if (projectConfig.hasDetectorCategoryFilter()) {
effectiveRegistry = effectiveRegistry.filterByCategories(
projectConfig.getDetectorCategories());
report.accept("Detector categories: " + projectConfig.getDetectorCategories());
// Apply detector category filter from unified config
if (!filters.categories().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByCategories(filters.categories());
report.accept("Detector categories: " + filters.categories());
}

// Apply detector include filter from project config
if (projectConfig.hasDetectorIncludeFilter()) {
effectiveRegistry = effectiveRegistry.filterByNames(
projectConfig.getDetectorInclude());
report.accept("Detector include: " + projectConfig.getDetectorInclude());
// Apply detector include filter from unified config
if (!filters.include().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByNames(filters.include());
report.accept("Detector include: " + filters.include());
}

// Apply parallelism override from project config
if (parallelism == null && projectConfig.getPipelineParallelism() != null) {
parallelism = projectConfig.getPipelineParallelism();
// Apply parallelism override from unified config (null = auto-detect)
if (parallelism == null && filters.parallelism() != null) {
parallelism = filters.parallelism();
report.accept("Pipeline parallelism: " + parallelism + " (from config)");
}

// 1. Discover files
report.accept("Discovering files...");
List<DiscoveredFile> files = fileDiscovery.discover(root);

// Apply language filter from project config
if (projectConfig.hasLanguageFilter()) {
Set<String> allowedLanguages = new HashSet<>(projectConfig.getLanguages());
// Apply language filter from unified config
if (!filters.languages().isEmpty()) {
Set<String> allowedLanguages = new HashSet<>(filters.languages());
files = files.stream()
.filter(f -> allowedLanguages.contains(f.language()))
.toList();
report.accept("Language filter active: " + projectConfig.getLanguages());
report.accept("Language filter active: " + filters.languages());
}

// Apply exclude patterns from project config
if (projectConfig.hasExcludePatterns()) {
List<String> excludes = projectConfig.getExclude();
List<java.util.regex.Pattern> compiledExcludes = compileExcludePatterns(excludes);
// Apply exclude patterns from unified config
if (!filters.exclude().isEmpty()) {
List<java.util.regex.Pattern> compiledExcludes = compileExcludePatterns(filters.exclude());
files = files.stream()
.filter(f -> !matchesAnyCompiledExclude(f.path().toString(), compiledExcludes))
.toList();
report.accept("Exclude patterns: " + excludes);
report.accept("Exclude patterns: " + filters.exclude());
}

int totalFiles = files.size();
Expand Down Expand Up @@ -486,43 +521,40 @@
private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int batchSize,
boolean incremental, AnalysisCache cache,
Consumer<String> report, Instant start) {
// 0. Load project config for pipeline filtering
ProjectConfig projectConfig = ProjectConfigLoader.loadProjectConfig(root);
// 0. Read pipeline filters from the injected unified config.
PipelineFilters filters = pipelineFilters();
DetectorRegistry effectiveRegistry = registry;

if (projectConfig.hasDetectorCategoryFilter()) {
effectiveRegistry = effectiveRegistry.filterByCategories(
projectConfig.getDetectorCategories());
report.accept("Detector categories: " + projectConfig.getDetectorCategories());
if (!filters.categories().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByCategories(filters.categories());
report.accept("Detector categories: " + filters.categories());
}
if (projectConfig.hasDetectorIncludeFilter()) {
effectiveRegistry = effectiveRegistry.filterByNames(
projectConfig.getDetectorInclude());
report.accept("Detector include: " + projectConfig.getDetectorInclude());
if (!filters.include().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByNames(filters.include());
report.accept("Detector include: " + filters.include());
}
if (parallelism == null && projectConfig.getPipelineParallelism() != null) {
parallelism = projectConfig.getPipelineParallelism();
if (parallelism == null && filters.parallelism() != null) {
parallelism = filters.parallelism();
report.accept("Pipeline parallelism: " + parallelism + " (from config)");
}

// 1. Discover files
report.accept("Discovering files...");
List<DiscoveredFile> files = fileDiscovery.discover(root);

if (projectConfig.hasLanguageFilter()) {
Set<String> allowedLanguages = new HashSet<>(projectConfig.getLanguages());
if (!filters.languages().isEmpty()) {
Set<String> allowedLanguages = new HashSet<>(filters.languages());
files = files.stream()
.filter(f -> allowedLanguages.contains(f.language()))
.toList();
report.accept("Language filter active: " + projectConfig.getLanguages());
report.accept("Language filter active: " + filters.languages());
}
if (projectConfig.hasExcludePatterns()) {
List<String> excludes = projectConfig.getExclude();
List<java.util.regex.Pattern> compiledExcludes = compileExcludePatterns(excludes);
if (!filters.exclude().isEmpty()) {
List<java.util.regex.Pattern> compiledExcludes = compileExcludePatterns(filters.exclude());
files = files.stream()
.filter(f -> !matchesAnyCompiledExclude(f.path().toString(), compiledExcludes))
.toList();
report.accept("Exclude patterns: " + excludes);
report.accept("Exclude patterns: " + filters.exclude());
}

int totalFiles = files.size();
Expand Down Expand Up @@ -790,30 +822,28 @@
Instant phase2Start = Instant.now();
report.accept("Phase 2: Discovering files...");

ProjectConfig projectConfig = ProjectConfigLoader.loadProjectConfig(root);
PipelineFilters filters = pipelineFilters();
DetectorRegistry effectiveRegistry = registry;

if (projectConfig.hasDetectorCategoryFilter()) {
effectiveRegistry = effectiveRegistry.filterByCategories(
projectConfig.getDetectorCategories());
if (!filters.categories().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByCategories(filters.categories());
}
if (projectConfig.hasDetectorIncludeFilter()) {
effectiveRegistry = effectiveRegistry.filterByNames(
projectConfig.getDetectorInclude());
if (!filters.include().isEmpty()) {
effectiveRegistry = effectiveRegistry.filterByNames(filters.include());
}
if (parallelism == null && projectConfig.getPipelineParallelism() != null) {
parallelism = projectConfig.getPipelineParallelism();
if (parallelism == null && filters.parallelism() != null) {
parallelism = filters.parallelism();
}

List<DiscoveredFile> allFiles = fileDiscovery.discover(root);

if (projectConfig.hasLanguageFilter()) {
Set<String> allowed = new HashSet<>(projectConfig.getLanguages());
if (!filters.languages().isEmpty()) {
Set<String> allowed = new HashSet<>(filters.languages());
allFiles = allFiles.stream().filter(f -> allowed.contains(f.language())).toList();
}
if (projectConfig.hasExcludePatterns()) {
if (!filters.exclude().isEmpty()) {
List<java.util.regex.Pattern> compiledExcludes =
compileExcludePatterns(projectConfig.getExclude());
compileExcludePatterns(filters.exclude());
allFiles = allFiles.stream()
.filter(f -> !matchesAnyCompiledExclude(f.path().toString(), compiledExcludes))
.toList();
Expand Down Expand Up @@ -1395,7 +1425,7 @@
/**
* Analyze a single file using the given (possibly filtered) registry.
*/
DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry detectorRegistry) {

Check warning on line 1428 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=AZ26VqKOx1UzvZYd7_J6&open=AZ26VqKOx1UzvZYd7_J6&pullRequest=52
Instant fileStart = Instant.now();
Path absPath = repoPath.resolve(file.path());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public AnalyzeCommand(Analyzer analyzer, CodeIqConfig config) {
public Integer call() {
Path root = path.toAbsolutePath().normalize();

CliOutput.configureFromOptions(config, graphDir, serviceName, root);
CliOutput.configureFromOptions(config, graphDir, serviceName);

NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);
int cores = parallelism != null ? parallelism : Runtime.getRuntime().availableProcessors();
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/io/github/randomcodespace/iq/cli/CliOutput.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,19 @@ static String format(String ansiFormatted) {
}

/**
* Configure shared CLI options: graph directory, service name, and project-level overrides.
* Configure shared CLI options: graph directory + service name.
* Used by both {@code analyze} and {@code index} commands.
*
* <p>Project-level overrides ({@code cache_dir}, {@code max_depth},
* {@code max_radius} from {@code codeiq.yml} / legacy {@code .osscodeiq.yml})
* are already resolved at Spring startup by
* {@link io.github.randomcodespace.iq.config.UnifiedConfigBeans#codeIqConfig}
* via {@code ConfigResolver} + {@code UnifiedConfigAdapter}. The {@code config}
* bean passed in already carries those values, so re-reading the file here
* would be pure redundancy.
*/
static void configureFromOptions(io.github.randomcodespace.iq.config.CodeIqConfig config,
java.nio.file.Path graphDir, String serviceName,
java.nio.file.Path root) {
java.nio.file.Path graphDir, String serviceName) {
if (graphDir != null) {
java.nio.file.Path sharedDir = graphDir.toAbsolutePath().normalize();
config.setCacheDir(sharedDir.toString());
Expand All @@ -95,7 +102,6 @@ static void configureFromOptions(io.github.randomcodespace.iq.config.CodeIqConfi
config.setServiceName(serviceName);
info(" Service name: " + serviceName);
}
io.github.randomcodespace.iq.config.ProjectConfigLoader.loadIfPresent(root, config);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public Integer call() {

Path root = path.toAbsolutePath().normalize();

CliOutput.configureFromOptions(config, graphDir, serviceName, root);
CliOutput.configureFromOptions(config, graphDir, serviceName);

// Use configured batch size if not overridden on command line
int effectiveBatchSize = batchSize > 0 ? batchSize : config.getBatchSize();
Expand Down
Loading
Loading