From d0793f4dde9357cc5ca0a6e0718aa53b7e091521 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Tue, 9 Jun 2026 12:51:59 +0000 Subject: [PATCH] fix(ingestion): lazy-load tree-sitter-c to prevent module-load crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tree-sitter-c is now vendored prebuild-only (#2116) with 0/6 committed prebuilds, so on a toolchain-less or `--ignore-scripts` install C has no native binding. Three modules loaded it via a hard top-level `import C from 'tree-sitter-c'`, which throws ERR_MODULE_NOT_FOUND at module-load — crashing `analyze` before parser-loader's optional/severity:error degradation can run. This is the #2091/#2093 bug class (previously fixed for swift/dart/kotlin); C was left static because it used to be an always-present npm dependency. - languages/c/query.ts: load via the lazy guarded getLanguageGrammar(C), mirroring swift/query.ts; the main-thread isLanguageAvailable filter ensures the getters are reached only when C is present. - workers/parse-worker.ts: guarded `_require('tree-sitter-c')` + conditional languageMap spread, like swift/dart/kotlin. - group/extractors/include-extractor.ts: guarded `_require`; getLanguageForFile returns null for .c/.h when absent, so C include-extraction degrades to a no-op (C++ unaffected). - extend the registry-import-closure regression test (#2091/#2093) to assert C also loads lazily at registry static-import time. --- .../group/extractors/include-extractor.ts | 16 +++++++++- .../src/core/ingestion/languages/c/query.ts | 21 ++++++++++++-- .../core/ingestion/workers/parse-worker.ts | 13 +++++++-- .../registry-import-closure.test.ts | 29 +++++++++++-------- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/gitnexus/src/core/group/extractors/include-extractor.ts b/gitnexus/src/core/group/extractors/include-extractor.ts index 98cbd371f..e5510f048 100644 --- a/gitnexus/src/core/group/extractors/include-extractor.ts +++ b/gitnexus/src/core/group/extractors/include-extractor.ts @@ -1,9 +1,23 @@ import * as path from 'node:path'; import * as fs from 'node:fs/promises'; +import { createRequire } from 'node:module'; import { glob } from 'glob'; import Parser from 'tree-sitter'; -import C from 'tree-sitter-c'; import Cpp from 'tree-sitter-cpp'; + +// `tree-sitter-c` is vendored prebuild-only (#2116) and may be absent on a +// toolchain-less / `--ignore-scripts` install. Load it via a guarded `_require` +// rather than a top-level `import C from 'tree-sitter-c'`, which would throw +// ERR_MODULE_NOT_FOUND at module-load and crash analyze (#2091/#2093). When the +// binding is absent, `getLanguageForFile` returns null for `.c`/`.h` so C +// include-extraction is skipped (C++ is unaffected — its binding always ships). +const _require = createRequire(import.meta.url); +let C: unknown = null; +try { + C = _require('tree-sitter-c'); +} catch { + /* C grammar unavailable — C include extraction degrades to a no-op. */ +} import type { ContractExtractor, CypherExecutor } from '../contract-extractor.js'; import type { ExtractedContract, RepoHandle } from '../types.js'; import { readSafe } from './fs-utils.js'; diff --git a/gitnexus/src/core/ingestion/languages/c/query.ts b/gitnexus/src/core/ingestion/languages/c/query.ts index 373e1e7a7..065603b1a 100644 --- a/gitnexus/src/core/ingestion/languages/c/query.ts +++ b/gitnexus/src/core/ingestion/languages/c/query.ts @@ -1,5 +1,15 @@ import Parser from 'tree-sitter'; -import C from 'tree-sitter-c'; +import { SupportedLanguages } from 'gitnexus-shared'; +// `tree-sitter-c` is vendored prebuild-only (#2116) and may be absent on a +// toolchain-less / `--ignore-scripts` install. It is loaded lazily + guarded via +// parser-loader rather than statically imported: this module is pulled onto the +// main thread eagerly by the scope-resolution registry and the language-provider +// index, so a top-level `import C from 'tree-sitter-c'` would throw +// ERR_MODULE_NOT_FOUND at module-load and crash `analyze` even for repos with no +// C files (#2091, #2093). The grammar is only ever needed inside the lazy getters +// below, and the main-thread `isLanguageAvailable` filter ensures they are +// reached only when the binding is present. +import { getLanguageGrammar } from '../../../tree-sitter/parser-loader.js'; const C_SCOPE_QUERY = ` ;; Scopes @@ -167,14 +177,19 @@ let _query: Parser.Query | null = null; export function getCParser(): Parser { if (_parser === null) { _parser = new Parser(); - _parser.setLanguage(C as Parameters[0]); + _parser.setLanguage( + getLanguageGrammar(SupportedLanguages.C) as Parameters[0], + ); } return _parser; } export function getCScopeQuery(): Parser.Query { if (_query === null) { - _query = new Parser.Query(C as Parameters[0], C_SCOPE_QUERY); + _query = new Parser.Query( + getLanguageGrammar(SupportedLanguages.C) as Parameters[0], + C_SCOPE_QUERY, + ); } return _query; } diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index b32d95758..681804ee5 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -4,7 +4,6 @@ import JavaScript from 'tree-sitter-javascript'; import TypeScript from 'tree-sitter-typescript'; import Python from 'tree-sitter-python'; import Java from 'tree-sitter-java'; -import C from 'tree-sitter-c'; import CPP from 'tree-sitter-cpp'; // Explicit subpath import — see parser-loader.ts for rationale (#1013). import CSharp from 'tree-sitter-c-sharp/bindings/node/index.js'; @@ -67,6 +66,16 @@ let Kotlin: TreeSitterLanguage | null = null; try { Kotlin = _require('tree-sitter-kotlin'); } catch {} + +// tree-sitter-c is now vendored prebuild-only (#2116) and may be absent on a +// toolchain-less / `--ignore-scripts` install. Guard it like Swift/Dart/Kotlin so +// a missing binding cannot crash the worker at module-load (#2091/#2093); the +// main-thread `isLanguageAvailable` filter keeps C files from being dispatched +// here when the entry is absent. +let C: TreeSitterLanguage | null = null; +try { + C = _require('tree-sitter-c'); +} catch {} import { getLanguageFromFilename } from 'gitnexus-shared'; import { buildConcreteTypedefDefinitionRanges, @@ -404,7 +413,7 @@ const languageMap: Record = { [`${SupportedLanguages.TypeScript}:tsx`]: TypeScript.tsx, [SupportedLanguages.Python]: Python, [SupportedLanguages.Java]: Java, - [SupportedLanguages.C]: C, + ...(C ? { [SupportedLanguages.C]: C } : {}), [SupportedLanguages.CPlusPlus]: CPP, [SupportedLanguages.CSharp]: CSharp, [SupportedLanguages.Go]: Go, diff --git a/gitnexus/test/integration/optional-grammars/registry-import-closure.test.ts b/gitnexus/test/integration/optional-grammars/registry-import-closure.test.ts index 09acea026..fa02d625e 100644 --- a/gitnexus/test/integration/optional-grammars/registry-import-closure.test.ts +++ b/gitnexus/test/integration/optional-grammars/registry-import-closure.test.ts @@ -4,15 +4,17 @@ * The scope-resolution registry (`scope-resolution/pipeline/registry.ts`) and * the language-provider index statically import all 16 language providers. Each * per-language `query.ts` used to do a top-level `import X from 'tree-sitter-Y'`. - * For the OPTIONAL grammars (swift/dart/kotlin) that import resolved — and on a - * default install where the vendored/optional binding is absent, THREW - * `ERR_MODULE_NOT_FOUND` — at module-load on the main thread, before any runtime - * gate, crashing `gitnexus analyze` regardless of the repo's actual languages. + * For the prebuild-only / optional grammars (swift/dart/kotlin, and — since + * #2116 — vendored-prebuild-only C) that import resolved — and on a default + * install where the binding is absent, THREW `ERR_MODULE_NOT_FOUND` — at + * module-load on the main thread, before any runtime gate, crashing + * `gitnexus analyze` regardless of the repo's actual languages. * - * The fix routes those three `query.ts` modules through the lazy, guarded + * The fix routes those `query.ts` modules through the lazy, guarded * `parser-loader.getLanguageGrammar()` so the grammar binding is only required * at first use (inside the worker, for a file of that language) — never at - * module-load. + * module-load. (C joined this set when it became vendored prebuild-only; it used + * to be an always-present npm dependency.) * * This test locks the fix in WITHOUT needing to simulate a missing grammar: * spawn a child Node process, import the built scope-resolution `registry.js` @@ -57,10 +59,13 @@ const PROBE = ` process.stdout.write(JSON.stringify([...after].filter((k) => !before.has(k)))); `; -const OPTIONAL_GRAMMAR_RE = /tree-sitter-(swift|dart|kotlin)[\\/]/; +// `tree-sitter-c[\\/]` matches only the exact `tree-sitter-c/` package — NOT +// `tree-sitter-cpp/` or `tree-sitter-c-sharp/` (those need a non-separator after +// the `c`), so the required C++/C# eager loads are unaffected. +const OPTIONAL_GRAMMAR_RE = /tree-sitter-(swift|dart|kotlin|c)[\\/]/; -describe('optional-grammar static-import closure (#2091/#2093)', () => { - it('importing the scope-resolution registry loads NO optional grammar binding', () => { +describe('optional-grammar static-import closure (#2091/#2093, #2116)', () => { + it('importing the scope-resolution registry loads NO lazy grammar binding (swift/dart/kotlin/c)', () => { if (!fs.existsSync(DIST_REGISTRY)) { throw new Error( `${DIST_REGISTRY} missing — run \`npm run build\` first (or \`npm run test:integration\`, ` + @@ -117,13 +122,13 @@ describe('optional-grammar static-import closure (#2091/#2093)', () => { `Newly-loaded (${newlyLoaded.length}):\n${newlyLoaded.join('\n')}`, ).toBeGreaterThan(0); - // Headline assertion: no OPTIONAL grammar binding (swift/dart/kotlin) is + // Headline assertion: no lazy grammar binding (swift/dart/kotlin/c) is // loaded at registry static-import time — they must load lazily. const optionalLoaded = newlyLoaded.filter((p) => OPTIONAL_GRAMMAR_RE.test(p)); expect( optionalLoaded, - `Optional tree-sitter grammar binding(s) loaded at registry static-import time. ` + - `query.ts must load swift/dart/kotlin lazily via parser-loader, not via a ` + + `Lazy tree-sitter grammar binding(s) loaded at registry static-import time. ` + + `query.ts must load swift/dart/kotlin/c lazily via parser-loader, not via a ` + `top-level \`import\`. Offending paths:\n${optionalLoaded.join('\n')}`, ).toEqual([]); });