From adfcd4e1b3f0bc237de5ef3d48e34de471a3ea76 Mon Sep 17 00:00:00 2001 From: Gujiassh Date: Mon, 16 Mar 2026 12:03:20 +0900 Subject: [PATCH] fix: skip unavailable parsers in sequential parsing Mirror the existing import/call/heritage availability contract so sequential parsing skips missing language parsers without calling loadLanguage and can emit the same verbose warning path. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../src/core/ingestion/parsing-processor.ts | 27 +++++++------ .../sequential-language-availability.test.ts | 40 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index f37f1a52f..af9fcc9a7 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -9,6 +9,7 @@ import { ASTCache } from './ast-cache.js'; import { getLanguageFromFilename, SupportedLanguages } from 'gitnexus-shared'; import { extractVueScript, isVueSetupTopLevel } from './vue-sfc-extractor.js'; import { yieldToEventLoop } from './utils/event-loop.js'; +import { isVerboseIngestionEnabled } from './utils/verbose.js'; import { getDefinitionNodeFromCaptures, findEnclosingClassInfo, @@ -284,7 +285,8 @@ const processParsingSequential = async ( ) => { const parser = await loadParser(); const total = files.length; - const skippedLanguages = new Map(); + const logSkipped = isVerboseIngestionEnabled(); + const skippedByLang = logSkipped ? new Map() : null; for (let i = 0; i < files.length; i++) { const file = files[i]; @@ -303,10 +305,10 @@ const processParsingSequential = async ( const language = getLanguageFromFilename(file.path); if (!language) continue; - - // Skip unsupported languages (e.g. Swift when tree-sitter-swift not installed) if (!isLanguageAvailable(language)) { - skippedLanguages.set(language, (skippedLanguages.get(language) || 0) + 1); + if (skippedByLang) { + skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1); + } continue; } @@ -331,7 +333,7 @@ const processParsingSequential = async ( continue; // parser unavailable — safety net } - let tree; + let tree: Parser.Tree; try { tree = parser.parse(parseContent, undefined, { bufferSize: getTreeSitterBufferSize(parseContent.length), @@ -349,8 +351,8 @@ const processParsingSequential = async ( continue; } - let query; - let matches; + let query: Parser.Query; + let matches: Parser.QueryMatch[]; try { const language = parser.getLanguage(); query = new Parser.Query(language, queryString); @@ -635,11 +637,12 @@ const processParsingSequential = async ( }); } - if (skippedLanguages.size > 0) { - const summary = Array.from(skippedLanguages.entries()) - .map(([lang, count]) => `${lang}: ${count}`) - .join(', '); - console.warn(` Skipped unsupported languages: ${summary}`); + if (skippedByLang && skippedByLang.size > 0) { + for (const [lang, count] of skippedByLang.entries()) { + console.warn( + `[ingestion] Skipped ${count} ${lang} file(s) in parsing processing — ${lang} parser not available.`, + ); + } } }; diff --git a/gitnexus/test/unit/sequential-language-availability.test.ts b/gitnexus/test/unit/sequential-language-availability.test.ts index 5b84f6702..84ac8e4ca 100644 --- a/gitnexus/test/unit/sequential-language-availability.test.ts +++ b/gitnexus/test/unit/sequential-language-availability.test.ts @@ -11,9 +11,11 @@ vi.mock('../../src/core/tree-sitter/parser-loader.js', () => ({ import { createKnowledgeGraph } from '../../src/core/graph/graph.js'; import { createASTCache } from '../../src/core/ingestion/ast-cache.js'; +import { processParsing } from '../../src/core/ingestion/parsing-processor.js'; import { processImports } from '../../src/core/ingestion/import-processor.js'; import { processCalls } from '../../src/core/ingestion/call-processor.js'; import { processHeritage } from '../../src/core/ingestion/heritage-processor.js'; +import { createSymbolTable } from '../../src/core/ingestion/symbol-table.js'; import { createResolutionContext } from '../../src/core/ingestion/resolution-context.js'; import * as parserLoader from '../../src/core/tree-sitter/parser-loader.js'; @@ -147,4 +149,42 @@ describe('sequential native parser availability', () => { process.env.GITNEXUS_VERBOSE = previous; } }); + + it('skips Swift files in processParsing when the native parser is unavailable', async () => { + vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); + + await expect(processParsing( + createKnowledgeGraph(), + [{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }], + createSymbolTable(), + createASTCache(), + )).resolves.toBeNull(); + + expect(parserLoader.loadLanguage).not.toHaveBeenCalled(); + }); + + it('warns when processParsing skips files in verbose mode', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const previous = process.env.GITNEXUS_VERBOSE; + process.env.GITNEXUS_VERBOSE = '1'; + vi.mocked(parserLoader.isLanguageAvailable).mockReturnValue(false); + + await processParsing( + createKnowledgeGraph(), + [{ path: 'App.swift', content: 'class AppViewController: UIViewController {}' }], + createSymbolTable(), + createASTCache(), + ); + + expect(warnSpy).toHaveBeenCalledWith( + '[ingestion] Skipped 1 swift file(s) in parsing processing — swift parser not available.', + ); + + warnSpy.mockRestore(); + if (previous === undefined) { + delete process.env.GITNEXUS_VERBOSE; + } else { + process.env.GITNEXUS_VERBOSE = previous; + } + }); });