Skip to content

Commit 3d0c187

Browse files
aksOpsclaude
andcommitted
Add @DetectorInfo annotation to all 97 detectors with category-based registry queries
Introduces runtime metadata annotation for detectors declaring name, category, description, parser type, supported languages, node/edge kinds, and properties. Enhances DetectorRegistry with detectorsForCategory(), allCategories(), getInfo(), and pre-built category index. All 1,173 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1b07042 commit 3d0c187

109 files changed

Lines changed: 2617 additions & 77 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import io.github.randomcodespace.iq.cache.AnalysisCache;
55
import io.github.randomcodespace.iq.cache.FileHasher;
66
import io.github.randomcodespace.iq.config.CodeIqConfig;
7+
import io.github.randomcodespace.iq.config.ProjectConfig;
8+
import io.github.randomcodespace.iq.config.ProjectConfigLoader;
79
import io.github.randomcodespace.iq.detector.Detector;
810
import io.github.randomcodespace.iq.detector.DetectorContext;
911
import io.github.randomcodespace.iq.detector.DetectorRegistry;
@@ -23,6 +25,7 @@
2325
import java.time.Instant;
2426
import java.util.ArrayList;
2527
import java.util.HashMap;
28+
import java.util.HashSet;
2629
import java.util.List;
2730
import java.util.Map;
2831
import java.util.Set;
@@ -143,9 +146,52 @@ public AnalysisResult run(Path repoPath, Integer parallelism, boolean incrementa
143146

144147
private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCache cache,
145148
Consumer<String> report, Instant start) {
149+
// 0. Load project config for pipeline filtering
150+
ProjectConfig projectConfig = ProjectConfigLoader.loadProjectConfig(root);
151+
DetectorRegistry effectiveRegistry = registry;
152+
153+
// Apply detector category filter from project config
154+
if (projectConfig.hasDetectorCategoryFilter()) {
155+
effectiveRegistry = effectiveRegistry.filterByCategories(
156+
projectConfig.getDetectorCategories());
157+
report.accept("Detector categories: " + projectConfig.getDetectorCategories());
158+
}
159+
160+
// Apply detector include filter from project config
161+
if (projectConfig.hasDetectorIncludeFilter()) {
162+
effectiveRegistry = effectiveRegistry.filterByNames(
163+
projectConfig.getDetectorInclude());
164+
report.accept("Detector include: " + projectConfig.getDetectorInclude());
165+
}
166+
167+
// Apply parallelism override from project config
168+
if (parallelism == null && projectConfig.getPipelineParallelism() != null) {
169+
parallelism = projectConfig.getPipelineParallelism();
170+
report.accept("Pipeline parallelism: " + parallelism + " (from config)");
171+
}
172+
146173
// 1. Discover files
147174
report.accept("Discovering files...");
148175
List<DiscoveredFile> files = fileDiscovery.discover(root);
176+
177+
// Apply language filter from project config
178+
if (projectConfig.hasLanguageFilter()) {
179+
Set<String> allowedLanguages = new HashSet<>(projectConfig.getLanguages());
180+
files = files.stream()
181+
.filter(f -> allowedLanguages.contains(f.language()))
182+
.toList();
183+
report.accept("Language filter active: " + projectConfig.getLanguages());
184+
}
185+
186+
// Apply exclude patterns from project config
187+
if (projectConfig.hasExcludePatterns()) {
188+
List<String> excludes = projectConfig.getExclude();
189+
files = files.stream()
190+
.filter(f -> !matchesAnyExclude(f.path().toString(), excludes))
191+
.toList();
192+
report.accept("Exclude patterns: " + excludes);
193+
}
194+
149195
int totalFiles = files.size();
150196
report.accept("Found " + totalFiles + " files");
151197

@@ -160,6 +206,7 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
160206
DetectorResult[] resultSlots = new DetectorResult[files.size()];
161207
int[] cacheHits = {0};
162208

209+
final DetectorRegistry detectorRegistry = effectiveRegistry;
163210
var executorService = parallelism != null && parallelism > 0
164211
? Executors.newFixedThreadPool(parallelism)
165212
: Executors.newVirtualThreadPerTaskExecutor();
@@ -187,18 +234,18 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
187234
}
188235

189236
// Run detectors and cache result
190-
DetectorResult result = analyzeFile(file, root);
237+
DetectorResult result = analyzeFile(file, root, detectorRegistry);
191238
resultSlots[idx] = result;
192239
if (result != null && (!result.nodes().isEmpty() || !result.edges().isEmpty())) {
193240
cacheRef.storeResults(hash, file.path().toString(), file.language(),
194241
result.nodes(), result.edges());
195242
}
196243
} catch (IOException e) {
197244
log.debug("Could not hash file {}", file.path(), e);
198-
resultSlots[idx] = analyzeFile(file, root);
245+
resultSlots[idx] = analyzeFile(file, root, detectorRegistry);
199246
}
200247
} else {
201-
resultSlots[idx] = analyzeFile(file, root);
248+
resultSlots[idx] = analyzeFile(file, root, detectorRegistry);
202249
}
203250
return null;
204251
}));
@@ -300,9 +347,6 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
300347
);
301348
}
302349

303-
/**
304-
* Analyze a single file: read content, parse if structured, run matching detectors.
305-
*/
306350
/**
307351
* Check whether a file is minified (e.g. *.min.js, *.bundle.js) and large
308352
* enough that running detectors would be wasteful.
@@ -329,7 +373,17 @@ private boolean isMinified(DiscoveredFile file, String content) {
329373
return (totalChars / lines.length) > 500;
330374
}
331375

376+
/**
377+
* Analyze a single file using the default registry.
378+
*/
332379
DetectorResult analyzeFile(DiscoveredFile file, Path repoPath) {
380+
return analyzeFile(file, repoPath, registry);
381+
}
382+
383+
/**
384+
* Analyze a single file using the given (possibly filtered) registry.
385+
*/
386+
DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry detectorRegistry) {
333387
Instant fileStart = Instant.now();
334388
Path absPath = repoPath.resolve(file.path());
335389

@@ -376,7 +430,7 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath) {
376430
);
377431

378432
// Run matching detectors and merge results
379-
List<Detector> detectors = registry.detectorsForLanguage(file.language());
433+
List<Detector> detectors = detectorRegistry.detectorsForLanguage(file.language());
380434
if (detectors.isEmpty()) {
381435
return DetectorResult.empty();
382436
}
@@ -440,4 +494,57 @@ private String getGitHead(Path repoPath) {
440494
}
441495
return null;
442496
}
497+
498+
/**
499+
* Check whether a file path matches any of the given exclude patterns.
500+
* Supports simple glob-like patterns: '*' matches any sequence of chars,
501+
* '**' matches across directory separators.
502+
*/
503+
private static boolean matchesAnyExclude(String filePath, List<String> excludePatterns) {
504+
if (excludePatterns == null) return false;
505+
String normalized = filePath.replace('\\', '/');
506+
for (String pattern : excludePatterns) {
507+
if (matchGlob(normalized, pattern.replace('\\', '/'))) {
508+
return true;
509+
}
510+
}
511+
return false;
512+
}
513+
514+
/**
515+
* Simple glob matching: '*' matches any non-separator sequence,
516+
* '**' matches everything (including separators).
517+
*/
518+
private static boolean matchGlob(String text, String pattern) {
519+
// Convert glob to regex
520+
StringBuilder regex = new StringBuilder("^");
521+
int i = 0;
522+
while (i < pattern.length()) {
523+
char c = pattern.charAt(i);
524+
if (c == '*') {
525+
if (i + 1 < pattern.length() && pattern.charAt(i + 1) == '*') {
526+
regex.append(".*");
527+
i += 2;
528+
// skip trailing /
529+
if (i < pattern.length() && pattern.charAt(i) == '/') {
530+
i++;
531+
}
532+
} else {
533+
regex.append("[^/]*");
534+
i++;
535+
}
536+
} else if (c == '?') {
537+
regex.append("[^/]");
538+
i++;
539+
} else if (c == '.') {
540+
regex.append("\\.");
541+
i++;
542+
} else {
543+
regex.append(c);
544+
i++;
545+
}
546+
}
547+
regex.append("$");
548+
return text.matches(regex.toString());
549+
}
443550
}

0 commit comments

Comments
 (0)