44import io .github .randomcodespace .iq .cache .AnalysisCache ;
55import io .github .randomcodespace .iq .cache .FileHasher ;
66import io .github .randomcodespace .iq .config .CodeIqConfig ;
7+ import io .github .randomcodespace .iq .config .ProjectConfig ;
8+ import io .github .randomcodespace .iq .config .ProjectConfigLoader ;
79import io .github .randomcodespace .iq .detector .Detector ;
810import io .github .randomcodespace .iq .detector .DetectorContext ;
911import io .github .randomcodespace .iq .detector .DetectorRegistry ;
2325import java .time .Instant ;
2426import java .util .ArrayList ;
2527import java .util .HashMap ;
28+ import java .util .HashSet ;
2629import java .util .List ;
2730import java .util .Map ;
2831import 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