Skip to content

Commit 442591d

Browse files
aksOpsclaude
andcommitted
feat: zero data loss — ANTLR timeout triggers regex fallback instead of skipping files
Previously, files that timed out during ANTLR parsing (30s) were skipped entirely, causing data loss and non-deterministic graphs (system-dependent). Now: - ANTLR timeout → regex-only detection runs on the same file - File still produces nodes/edges via regex patterns (fewer than ANTLR but not zero) - Nodes tagged with detection_method=regex_fallback so quality is transparent - Added detectRegexOnly() to AbstractAntlrDetector for direct regex path - Applied to all 3 analysis paths (run, runIndex, runSmartIndex) Size-based ANTLR routing (deterministic, not CPU-dependent): - Centralized 200KB guard in AntlrParserFactory.parse() - Removed per-detector 500KB checks (AbstractPythonAntlrDetector, NestJSControllerDetector, TypeScriptStructuresDetector) - < 200KB: ANTLR (always fast) - > 200KB: regex only (AntlrParserFactory returns null → regex fallback) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 543749c commit 442591d

6 files changed

Lines changed: 80 additions & 18 deletions

File tree

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

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.github.randomcodespace.iq.config.CodeIqConfig;
88
import io.github.randomcodespace.iq.config.ProjectConfig;
99
import io.github.randomcodespace.iq.config.ProjectConfigLoader;
10+
import io.github.randomcodespace.iq.detector.AbstractAntlrDetector;
1011
import io.github.randomcodespace.iq.detector.Detector;
1112
import io.github.randomcodespace.iq.detector.DetectorContext;
1213
import io.github.randomcodespace.iq.detector.DetectorRegistry;
@@ -297,7 +298,9 @@ private AnalysisResult runWithCache(Path root, Integer parallelism, AnalysisCach
297298
futures.get(i).get(30, java.util.concurrent.TimeUnit.SECONDS);
298299
} catch (java.util.concurrent.TimeoutException e) {
299300
futures.get(i).cancel(true);
300-
log.warn("Analysis timed out for {} (30s limit), skipping", files.get(i).path());
301+
DiscoveredFile timedOutFile = files.get(i);
302+
log.warn("⏱️ ANTLR timed out for {} (30s), running regex fallback", timedOutFile.path());
303+
resultSlots[i] = analyzeFileRegexOnly(timedOutFile, root, detectorRegistry);
301304
} catch (ExecutionException e) {
302305
log.warn("Analysis failed for {}", files.get(i).path(), e.getCause());
303306
} catch (InterruptedException e) {
@@ -586,7 +589,10 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b
586589
futures.get(i).get(30, java.util.concurrent.TimeUnit.SECONDS);
587590
} catch (java.util.concurrent.TimeoutException e) {
588591
futures.get(i).cancel(true);
589-
log.warn("Analysis timed out for {} (30s limit), skipping", batch.get(i).path());
592+
// Zero data loss: run regex-only fallback instead of skipping
593+
DiscoveredFile timedOutFile = batch.get(i);
594+
log.warn("⏱️ ANTLR timed out for {} (30s), running regex fallback", timedOutFile.path());
595+
resultSlots[i] = analyzeFileRegexOnly(timedOutFile, root, detectorRegistry);
590596
} catch (ExecutionException e) {
591597
log.warn("Analysis failed for {}", batch.get(i).path(), e.getCause());
592598
} catch (InterruptedException e) {
@@ -963,7 +969,9 @@ private int[] processSmartBatch(
963969
futures.get(i).get(30, java.util.concurrent.TimeUnit.SECONDS);
964970
} catch (java.util.concurrent.TimeoutException e) {
965971
futures.get(i).cancel(true);
966-
log.warn("Analysis timed out for {} (30s limit), skipping", batch.get(i).path());
972+
DiscoveredFile timedOutFile = batch.get(i);
973+
log.warn("⏱️ ANTLR timed out for {} (30s), running regex fallback", timedOutFile.path());
974+
slots[i] = analyzeFileRegexOnly(timedOutFile, root, detectorRegistry);
967975
} catch (ExecutionException e) {
968976
log.warn("Analysis failed for {}", batch.get(i).path(), e.getCause());
969977
} catch (InterruptedException e) {
@@ -1351,6 +1359,58 @@ DetectorResult analyzeFile(DiscoveredFile file, Path repoPath, DetectorRegistry
13511359
return DetectorResult.of(allNodes, allEdges);
13521360
}
13531361

1362+
/**
1363+
* Regex-only analysis fallback for files where ANTLR timed out.
1364+
* Ensures zero data loss — every file produces nodes via regex detection.
1365+
* Nodes are tagged with detection_method=regex_fallback.
1366+
*/
1367+
private DetectorResult analyzeFileRegexOnly(DiscoveredFile file, Path repoPath,
1368+
DetectorRegistry detectorRegistry) {
1369+
Path absPath = repoPath.resolve(file.path());
1370+
String content;
1371+
try {
1372+
byte[] raw = Files.readAllBytes(absPath);
1373+
content = DetectorUtils.decodeContent(raw);
1374+
} catch (IOException e) {
1375+
log.debug("Could not read file for regex fallback: {}", absPath, e);
1376+
return DetectorResult.empty();
1377+
}
1378+
1379+
String moduleName = DetectorUtils.deriveModuleName(file.path().toString(), file.language());
1380+
var ctx = new DetectorContext(file.path().toString(), file.language(), content, null, moduleName);
1381+
1382+
List<Detector> detectors = detectorRegistry.detectorsForLanguage(file.language());
1383+
var allNodes = new ArrayList<CodeNode>();
1384+
var allEdges = new ArrayList<io.github.randomcodespace.iq.model.CodeEdge>();
1385+
1386+
for (Detector detector : detectors) {
1387+
try {
1388+
DetectorResult result;
1389+
if (detector instanceof AbstractAntlrDetector antlrDet) {
1390+
result = antlrDet.detectRegexOnly(ctx);
1391+
} else {
1392+
result = detector.detect(ctx);
1393+
}
1394+
allNodes.addAll(result.nodes());
1395+
allEdges.addAll(result.edges());
1396+
} catch (Throwable e) {
1397+
log.debug("Regex fallback detector {} failed on {}: {}",
1398+
detector.getName(), file.path(), e.getMessage());
1399+
}
1400+
}
1401+
1402+
// Tag all nodes with detection method so users know quality level
1403+
for (CodeNode node : allNodes) {
1404+
node.getProperties().put("detection_method", "regex_fallback");
1405+
if (moduleName != null && (node.getModule() == null || node.getModule().isEmpty())) {
1406+
node.setModule(moduleName);
1407+
}
1408+
}
1409+
1410+
AntlrParserFactory.clearCache();
1411+
return DetectorResult.of(allNodes, allEdges);
1412+
}
1413+
13541414
/**
13551415
* Build a deterministic FileInventory from the list of discovered files.
13561416
* Content hashes are reused from {@code cache} when available (no re-read of files).

src/main/java/io/github/randomcodespace/iq/detector/AbstractAntlrDetector.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ protected DetectorResult detectWithRegex(DetectorContext ctx) {
6161
return DetectorResult.empty();
6262
}
6363

64+
/**
65+
* Run regex-only detection, bypassing ANTLR entirely.
66+
* Used when ANTLR times out on a file — ensures the file still produces
67+
* nodes/edges via regex fallback instead of being skipped (zero data loss).
68+
*/
69+
public DetectorResult detectRegexOnly(DetectorContext ctx) {
70+
return detectWithRegex(ctx);
71+
}
72+
6473
/**
6574
* Create a lexer from source content with error output suppressed.
6675
*

src/main/java/io/github/randomcodespace/iq/detector/python/AbstractPythonAntlrDetector.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ public Set<String> getSupportedLanguages() {
2727

2828
@Override
2929
protected ParseTree parse(DetectorContext ctx) {
30-
// Skip ANTLR for very large files (>500KB) — regex fallback is faster
31-
if (ctx.content().length() > 500_000) {
32-
return null;
33-
}
30+
// Size guard is centralized in AntlrParserFactory.parse() (200KB limit)
3431
return AntlrParserFactory.parse("python", ctx.content());
3532
}
3633

src/main/java/io/github/randomcodespace/iq/detector/typescript/NestJSControllerDetector.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ protected ParseTree parse(DetectorContext ctx) {
5656
// Use the dedicated TypeScript ANTLR grammar for parsing;
5757
// detection itself still uses regex for NestJS-specific decorator patterns,
5858
// but the TS grammar is available for future AST-based enhancement.
59-
if (ctx.content().length() > 500_000) {
60-
return null;
61-
}
59+
// Size guard is centralized in AntlrParserFactory.parse() (200KB limit)
6260
return AntlrParserFactory.parse("typescript", ctx.content());
6361
}
6462

src/main/java/io/github/randomcodespace/iq/detector/typescript/TypeScriptStructuresDetector.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,7 @@ public DetectorResult detect(DetectorContext ctx) {
7979

8080
@Override
8181
protected ParseTree parse(DetectorContext ctx) {
82-
// Use the dedicated TypeScript ANTLR grammar for .ts files;
83-
// for .js files or very large files, fall back to regex
84-
if (ctx.content().length() > 500_000) {
85-
return null; // triggers regex fallback
86-
}
82+
// Size guard is centralized in AntlrParserFactory.parse() (200KB limit)
8783
if ("typescript".equals(ctx.language())) {
8884
return AntlrParserFactory.parse(PROP_TYPESCRIPT, ctx.content());
8985
}

src/main/java/io/github/randomcodespace/iq/grammar/AntlrParserFactory.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ public static ParseTree parse(String language, String content) {
9494
}
9595

9696
// Skip files that are too large for ANTLR — regex fallback handles them.
97-
// Files >500KB cause exponential parse times in some grammars (especially TS/JS).
98-
if (content.length() > 500_000) {
99-
log.debug("Skipping ANTLR parse for {} ({} bytes > 500KB limit)", language, content.length());
97+
// Files >200KB can cause exponential parse times in some grammars (especially TS/JS).
98+
// This is a deterministic, size-based guard — same result regardless of CPU speed.
99+
if (content.length() > 200_000) {
100+
log.debug("Skipping ANTLR parse for {} ({} bytes > 200KB limit), regex fallback will handle",
101+
language, content.length());
100102
return null;
101103
}
102104

0 commit comments

Comments
 (0)