diff --git a/gitnexus/src/core/ingestion/scope-extractor-bridge.ts b/gitnexus/src/core/ingestion/scope-extractor-bridge.ts index 19e16e940..1774cefca 100644 --- a/gitnexus/src/core/ingestion/scope-extractor-bridge.ts +++ b/gitnexus/src/core/ingestion/scope-extractor-bridge.ts @@ -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); diff --git a/gitnexus/test/unit/scope-resolution/parse-worker-scope-integration.test.ts b/gitnexus/test/unit/scope-resolution/parse-worker-scope-integration.test.ts index c824a18b4..6b67db11b 100644 --- a/gitnexus/test/unit/scope-resolution/parse-worker-scope-integration.test.ts +++ b/gitnexus/test/unit/scope-resolution/parse-worker-scope-integration.test.ts @@ -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()], diff --git a/gitnexus/test/unit/scope-resolution/python/python-fixtures.test.ts b/gitnexus/test/unit/scope-resolution/python/python-fixtures.test.ts index 06bf98280..a4b14d53c 100644 --- a/gitnexus/test/unit/scope-resolution/python/python-fixtures.test.ts +++ b/gitnexus/test/unit/scope-resolution/python/python-fixtures.test.ts @@ -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`);