fix(ingestion): skip empty scope extraction (#1100)

This commit is contained in:
Gergő Magyar
2026-04-27 07:29:37 +01:00
committed by GitHub
parent 9e62f7c121
commit 09d78cadec
3 changed files with 39 additions and 6 deletions
@@ -13,8 +13,11 @@
* `emitScopeCaptures`. Returns `undefined`; zero work done. This is
* the state of every language today — `ParsedFile` production stays
* dormant until a language migrates.
* 2. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
* 3. **Swallows exceptions from either side.** A failure here returns
* 2. Short-circuits empty / whitespace-only files. There is no scope
* content to extract, and some tree-sitter queries do not match an
* otherwise valid empty root node.
* 3. Invokes the hook + feeds its output to `ScopeExtractor.extract`.
* 4. **Swallows exceptions from either side.** A failure here returns
* `undefined` and emits a warning via `onWarn`; legacy parsing on
* the same file continues unaffected by the scope-extraction miss.
* Scope-based resolution is the new path under construction — it
@@ -41,6 +44,7 @@ export function extractParsedFile(
cachedTree?: unknown,
): ParsedFile | undefined {
if (provider.emitScopeCaptures === undefined) return undefined;
if (sourceText.trim().length === 0) return undefined;
try {
const captures = provider.emitScopeCaptures(sourceText, filePath, cachedTree);
return extractScope(captures, filePath, provider);
@@ -67,6 +67,27 @@ describe('extractParsedFile', () => {
});
describe('provider HAS migrated', () => {
it('returns undefined without warning for whitespace-only source', () => {
const warnings: string[] = [];
let called = false;
const provider = fakeProvider({
emitScopeCaptures: () => {
called = true;
throw new Error('should not inspect empty source');
},
});
for (const src of ['', ' \n\t']) {
const result = extractParsedFile(provider, src, 'pkg/__init__.py', (msg) => {
warnings.push(msg);
});
expect(result).toBeUndefined();
}
expect(called).toBe(false);
expect(warnings).toEqual([]);
});
it('threads emitScopeCaptures output through ScopeExtractor', () => {
const provider = fakeProvider({
emitScopeCaptures: () => [moduleScopeMatch()],
@@ -56,15 +56,23 @@ function findDef(file: ParsedFile, name: string) {
describe('Python scopes — module / class / function', () => {
it('case 01: minimal module produces a single Module scope', () => {
// Empty source produces a zero-range module node; the central
// extractor treats zero-range scopes as malformed (and rightly so —
// they collide with sibling-overlap detection on subsequent reparses).
// Real Python files always have at least a newline.
const f = parse('pass\n');
expect(f.scopes).toHaveLength(1);
expect(f.scopes[0]!.kind).toBe('Module');
});
it('case 01a: empty and whitespace-only files are skipped without warnings', () => {
for (const src of ['', ' \n\t']) {
const warnings: string[] = [];
const parsed = extractParsedFile(pythonProvider, src, 'pkg/__init__.py', (msg) => {
warnings.push(msg);
});
expect(parsed).toBeUndefined();
expect(warnings).toEqual([]);
}
});
it('case 01b: large cache-miss files use the adaptive tree-sitter buffer', () => {
const padding = 'x'.repeat(600 * 1024);
const f = parse(`# ${padding}\ndef after_padding():\n return 1\n`);