mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
* fix(deps): pin tree-sitter-c/cpp to fix Windows segfault (#1242) `tree-sitter-c@0.23.2` ships native prebuilds compiled against tree-sitter ABI 14 (tree-sitter-cli >=0.24), while GitNexus is pinned to the tree-sitter@0.21.1 JS runtime. On Windows the JS runtime hits `Cannot read properties of undefined (reading '161')` inside `unmarshalNode` and a native segfault in the parse-worker pipeline on real C codebases (e.g. STM32 headers from the issue reporter). Two coordinated registry pins fix the root cause without any override gymnastics or vendoring: - `tree-sitter-c` -> `0.21.4` (last release built against the tree-sitter@0.21 ABI; declared peer `^0.21.0`). - `tree-sitter-cpp` -> `0.23.2` (last 0.23.x release before tree-sitter-cpp added a runtime dep on the broken-ABI `tree-sitter-c@^0.23.1`; pinning here lets us drop the previous global override entirely). `npm ls tree-sitter-c` is now clean: single deduped 0.21.4, no `overridden` annotations, no nested copy. Parser loader collapsed to one declarative table: - One `SOURCES` map with `{ load, unavailableNote, optional? }` rows for every grammar including TSX. Adding/removing a grammar is one entry; `unavailableNote` is mandatory and the type checker enforces it, so failures are never silent and never generic. - Single `loadGrammar(key)` does lazy require + cache + per-failure classification. Required failures `console.error` the note and rethrow the original (preserves stack); optional failures `console.warn` and report the language as Unsupported. One warn-once `Set` deduplicates per language key. - The previous bespoke `warnCUnavailable` + `cWarningEmitted` state and 4 conditional spreads in the language map are gone. Per-grammar `unavailableNote` strings name the package, list the most likely failure mode for that grammar, and link the relevant tracking issue (#1013, #1125, #1130, #1242) where applicable. Tests: new `C parser ABI compatibility (#1242)` block under parser-loader.test.ts exercises the actual failure paths (non-trivial parse + tree walk + Query.captures + TreeCursor descent). The original report's `unmarshalNode` crash sits on exactly the traversal hot path these tests now cover. Validation: - npx tsc --noEmit: clean - npx vitest run test/unit: 4808 passed, 10 skipped - npx vitest run test/integration/resolvers/cpp.test.ts: 133/133 - minimal C parse + walk + query + cursor verified manually under tree-sitter@0.21.1 + tree-sitter-c@0.21.4 on Win11 x64 / Node 22 Closes #1242. Does not unblock the broader tree-sitter@0.25 upgrade tracked in #858. Made-with: Cursor * chore(ci): redesign tree-sitter upgrade-readiness report (#858) The daily script that owns the body of #858 used to dump one giant matrix and leave a human to figure out which grammars are actually ready to bump. After pinning `tree-sitter-c@0.21.4` and `tree-sitter-cpp@0.23.2` for #1242, several rows in that matrix now look like regressions when in fact they are deliberate. The report now classifies each grammar instead of just listing them. What changed in `check-tree-sitter-upgrade-readiness.py`: - New `INTENTIONAL_PINS` table documents grammars deliberately held below `npm latest`, with a one-line rationale and a tracking issue per row (#1242 for C and C++, #1013 for C#). The script reads pins straight from `gitnexus/package.json` so a future bump cannot drift away from this report. - New `_classify_grammar(...)` produces one primary disposition per grammar: Ready for 0.25 / Intentionally pinned / Waiting on upstream npm release / Blocked on upstream / Could not check. The dispositions drive the report layout. - New `vendored_drift_summary(...)` covers all three vendored parsers (`tree-sitter-proto`, `tree-sitter-dart`, `tree-sitter-swift`) uniformly: ABI from `parser.c` when present, upstream npm + GitHub status, and the rationale extracted from each vendor's `_vendoredBy` field. Prebuilt-only vendors (Swift today) report `ABI 'prebuilt'` instead of `None`. - Report layout: top-of-page TL;DR + counts, an actionable "What you can do today" section, then one section per disposition bucket, then a dedicated "Vendored parsers" section. The original raw matrix is preserved inside a collapsible `<details>` block so the row-diff bot that watches this issue still has stable input. - `sys.stdout.reconfigure(encoding="utf-8")` so the workflow no longer crashes on Windows when the report contains arrows or em-dashes. No workflow / cron changes; the daily job posts the new body the next time it runs. #858 itself was updated by hand in the meantime to keep the tracker readable. Made-with: Cursor * fix(parser-loader): log C grammar load failures at error severity (#1242) Addresses review feedback on #1243. `tree-sitter-c` is in `dependencies` (not `optionalDependencies`) so a load failure on a supported platform always indicates a real install problem the user needs to see — corrupted node_modules, unsupported Node version, or an ABI mismatch with the bundled runtime. Previously the optional-grammar machinery downgraded that to `console.warn`, which can be missed in long log streams and silently drops C analysis for an entire repo. Decouples log severity from throw behavior: - `GrammarSource.severity?: 'warn' | 'error'` is a new optional field that overrides the default log level for a load failure. Default is `error` for required grammars and `warn` for optional ones, matching the prior behavior for every existing row. - `LoadResult` carries the resolved severity through `loadGrammar` so `logFailure` no longer derives it from `fatal`. - `tree-sitter-c` row sets `optional: true, severity: 'error'`. The pipeline still degrades gracefully (callers see Unsupported instead of a thrown error), but the diagnostic is loud and the `unavailableNote` now spells out what to try first (`npm rebuild tree-sitter-c`, reinstall) and links the tracker. No test changes needed: `parser-loader.test.ts` exercises behavior on the success path and on optional-failure dispatch; severity is a display-only concern routed through `console.error` vs `console.warn`, which the existing tests don't assert on. Made-with: Cursor * fix(ci): treat intentional pins as 0.25 blockers in readiness report Addresses review feedback on #1243. `_classify_grammar` returned bucket `intentional` before checking `target_compat`, and the per-grammar status loop only added a row to `blockers` when npm-latest was incompatible with the target runtime. The combination meant: if every other grammar resolved tomorrow but we were still holding `tree-sitter-c@0.21.4` and `tree-sitter-cpp@0.23.2` (both incompatible with `tree-sitter@0.25.x`), the script would emit "**Ready** — all grammars are 0.25-compatible" and mislead maintainers into thinking the runtime upgrade was unblocked. Fix: - The status loop now adds an entry to `blockers` whenever a grammar is in `INTENTIONAL_PINS`, regardless of npm-latest's peer dep. The blocker message names the pinned spec, embeds the rationale from `INTENTIONAL_PINS`, and tells the reader the pin must be lifted before the target runtime upgrade. When the pin is removed (entry deleted from `INTENTIONAL_PINS`), the grammar resumes standard classification on the next run. - `bump_now` now excludes intentional pins so they never show up in the "What you can do today" section. Bumping an intentional pin requires a deliberate edit to both `INTENTIONAL_PINS` and `package.json`, not a one-line dependency bump. Verified locally: TL;DR now reports 8 blockers (6 upstream + 2 intentional) where it previously reported 6, and the verdict correctly remains **Blocked** even in the hypothetical future where all upstream blockers clear. Made-with: Cursor
255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
import Parser from 'tree-sitter';
|
|
import { createRequire } from 'node:module';
|
|
import { SupportedLanguages } from 'gitnexus-shared';
|
|
|
|
const _require = createRequire(import.meta.url);
|
|
|
|
/**
|
|
* One row per (language, optional variant) describes how to obtain a
|
|
* grammar object suitable for `Parser.setLanguage`.
|
|
*
|
|
* - `load` — returns the grammar object (lazy, called on
|
|
* first use, then cached).
|
|
* - `unavailableNote` — actionable message surfaced *whenever* the
|
|
* grammar can't be loaded. Mandatory for every
|
|
* row so failures are never silent and never
|
|
* generic.
|
|
* - `optional` — when true, a load failure does not throw:
|
|
* we report the language as unavailable and
|
|
* let callers skip files of this language.
|
|
* When false (the default), a load failure
|
|
* re-throws the original error so the
|
|
* pipeline halts loudly.
|
|
* - `severity` — log level for failure diagnostics. Defaults
|
|
* to `error` for required grammars and `warn`
|
|
* for optional ones. Set explicitly to `error`
|
|
* on optional rows whose package is listed in
|
|
* `dependencies` (not `optionalDependencies`):
|
|
* those failures indicate a real install
|
|
* problem and should never be hidden behind
|
|
* a low-severity warning.
|
|
*
|
|
* Adding or removing a grammar is one entry in this table — there is
|
|
* no second list, no conditional spread, and no per-grammar branch in
|
|
* the resolver.
|
|
*/
|
|
interface GrammarSource {
|
|
load: () => unknown;
|
|
unavailableNote: string;
|
|
optional?: boolean;
|
|
severity?: 'warn' | 'error';
|
|
}
|
|
|
|
const ISSUES_URL = 'https://github.com/abhigyanpatwari/GitNexus/issues';
|
|
|
|
const SOURCES: Record<string, GrammarSource> = {
|
|
[SupportedLanguages.JavaScript]: {
|
|
load: () => _require('tree-sitter-javascript'),
|
|
unavailableNote:
|
|
'JavaScript parsing requires `tree-sitter-javascript`. ' +
|
|
'Check that the package and its native binding installed cleanly (`npm ci`).',
|
|
},
|
|
[SupportedLanguages.TypeScript]: {
|
|
load: () => _require('tree-sitter-typescript').typescript,
|
|
unavailableNote:
|
|
'TypeScript parsing requires `tree-sitter-typescript`. ' +
|
|
'Check that the package and its native binding installed cleanly (`npm ci`).',
|
|
},
|
|
[`${SupportedLanguages.TypeScript}:tsx`]: {
|
|
load: () => _require('tree-sitter-typescript').tsx,
|
|
unavailableNote:
|
|
'TSX parsing requires `tree-sitter-typescript` (re-uses the same native binding as TS).',
|
|
},
|
|
[SupportedLanguages.Python]: {
|
|
load: () => _require('tree-sitter-python'),
|
|
unavailableNote:
|
|
'Python parsing requires `tree-sitter-python`. Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.Java]: {
|
|
load: () => _require('tree-sitter-java'),
|
|
unavailableNote:
|
|
'Java parsing requires `tree-sitter-java`. Check the install and native binding.',
|
|
},
|
|
// tree-sitter-c-sharp declares `type: "module"` with `main: "bindings/node"`
|
|
// (no extension) and no `exports` field, which triggers Node 22's DEP0151
|
|
// deprecation warning on the bare-package import. The explicit subpath
|
|
// bypasses the deprecated ESM main-field resolution. (#1013)
|
|
[SupportedLanguages.CSharp]: {
|
|
load: () => _require('tree-sitter-c-sharp/bindings/node/index.js'),
|
|
unavailableNote:
|
|
'C# parsing requires `tree-sitter-c-sharp/bindings/node/index.js`. ' +
|
|
`If the subpath is missing, see ${ISSUES_URL}/1013.`,
|
|
},
|
|
[SupportedLanguages.CPlusPlus]: {
|
|
load: () => _require('tree-sitter-cpp'),
|
|
unavailableNote:
|
|
'C++ parsing requires `tree-sitter-cpp`. Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.Go]: {
|
|
load: () => _require('tree-sitter-go'),
|
|
unavailableNote: 'Go parsing requires `tree-sitter-go`. Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.Rust]: {
|
|
load: () => _require('tree-sitter-rust'),
|
|
unavailableNote:
|
|
'Rust parsing requires `tree-sitter-rust`. Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.PHP]: {
|
|
load: () => _require('tree-sitter-php').php_only,
|
|
unavailableNote:
|
|
'PHP parsing requires `tree-sitter-php` (the `php_only` export). ' +
|
|
'Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.Ruby]: {
|
|
load: () => _require('tree-sitter-ruby'),
|
|
unavailableNote:
|
|
'Ruby parsing requires `tree-sitter-ruby`. Check the install and native binding.',
|
|
},
|
|
[SupportedLanguages.Vue]: {
|
|
load: () => _require('tree-sitter-typescript').typescript,
|
|
unavailableNote:
|
|
'Vue parsing piggybacks on `tree-sitter-typescript`. Check the install and native binding.',
|
|
},
|
|
|
|
// tree-sitter-c is a required dependency, but its native binding has
|
|
// historically been ABI-incompatible with the bundled tree-sitter@0.21.1
|
|
// runtime on some platforms (#1242, #858). Loading it through the
|
|
// optional machinery turns a would-be segfault into a clean degradation
|
|
// while preserving every other language's analysis. Severity is pinned
|
|
// to `error` because the package is in `dependencies`: a failure here
|
|
// is always an install/platform problem the user needs to see, never an
|
|
// expected "user opted out" condition like Swift/Dart/Kotlin.
|
|
[SupportedLanguages.C]: {
|
|
load: () => _require('tree-sitter-c'),
|
|
optional: true,
|
|
severity: 'error',
|
|
unavailableNote:
|
|
'C parsing disabled: `tree-sitter-c` could not be loaded. ' +
|
|
'This package is in `dependencies` and prebuilds ship for all supported ' +
|
|
'platforms (win32/darwin/linux x64+arm64, Node 18/20/22), so this ' +
|
|
'usually indicates a corrupted install, an unsupported Node version, ' +
|
|
'or a native ABI mismatch with the bundled tree-sitter runtime. ' +
|
|
'Try `npm rebuild tree-sitter-c` or reinstalling, then re-run analyze. ' +
|
|
`If the failure persists, file details at ${ISSUES_URL}/1242.`,
|
|
},
|
|
|
|
// optionalDependencies — may be absent on platforms without prebuilds
|
|
// or when users skip optional installs.
|
|
[SupportedLanguages.Swift]: {
|
|
load: () => _require('tree-sitter-swift'),
|
|
optional: true,
|
|
unavailableNote:
|
|
'Swift parsing disabled: vendored `tree-sitter-swift` (under ' +
|
|
'`gitnexus/vendor/tree-sitter-swift`) failed to load. ' +
|
|
'Likely cause: no prebuilt `.node` for this platform/architecture. ' +
|
|
`See ${ISSUES_URL}/1130.`,
|
|
},
|
|
[SupportedLanguages.Dart]: {
|
|
load: () => _require('tree-sitter-dart'),
|
|
optional: true,
|
|
unavailableNote:
|
|
'Dart parsing disabled: vendored `tree-sitter-dart` (under ' +
|
|
'`gitnexus/vendor/tree-sitter-dart`) failed to load. ' +
|
|
'Likely cause: native compile failed at install (missing python3/make/g++). ' +
|
|
`See ${ISSUES_URL}/1125.`,
|
|
},
|
|
[SupportedLanguages.Kotlin]: {
|
|
load: () => _require('tree-sitter-kotlin'),
|
|
optional: true,
|
|
unavailableNote:
|
|
'Kotlin parsing disabled: `tree-sitter-kotlin` is an optionalDependency ' +
|
|
'and is not installed (or its native binding failed to build).',
|
|
},
|
|
};
|
|
|
|
type LoadResult =
|
|
| { ok: true; grammar: unknown }
|
|
| { ok: false; error: Error; note: string; fatal: boolean; severity: 'warn' | 'error' };
|
|
|
|
const loadCache = new Map<string, LoadResult>();
|
|
const logged = new Set<string>();
|
|
|
|
const logFailure = (key: string, result: LoadResult): void => {
|
|
if (result.ok === true) return;
|
|
if (logged.has(key)) return;
|
|
logged.add(key);
|
|
const message = `[gitnexus] ${result.note} (${result.error.message})`;
|
|
|
|
if (result.severity === 'error') console.error(message);
|
|
else console.warn(message);
|
|
};
|
|
|
|
export const resolveLanguageKey = (language: SupportedLanguages, filePath?: string): string =>
|
|
language === SupportedLanguages.TypeScript && filePath?.endsWith('.tsx')
|
|
? `${language}:tsx`
|
|
: language;
|
|
|
|
const loadGrammar = (key: string): LoadResult => {
|
|
const cached = loadCache.get(key);
|
|
if (cached) return cached;
|
|
|
|
const source = SOURCES[key];
|
|
if (!source) {
|
|
const result: LoadResult = {
|
|
ok: false,
|
|
error: new Error(`Unsupported language: ${key}`),
|
|
note: `No grammar registered for language key \`${key}\`. Add a row to SOURCES.`,
|
|
fatal: true,
|
|
severity: 'error',
|
|
};
|
|
loadCache.set(key, result);
|
|
return result;
|
|
}
|
|
|
|
let result: LoadResult;
|
|
try {
|
|
result = { ok: true, grammar: source.load() };
|
|
} catch (err) {
|
|
const fatal = !source.optional;
|
|
result = {
|
|
ok: false,
|
|
error: err as Error,
|
|
note: source.unavailableNote,
|
|
fatal,
|
|
severity: source.severity ?? (fatal ? 'error' : 'warn'),
|
|
};
|
|
}
|
|
loadCache.set(key, result);
|
|
if (result.ok === false) logFailure(key, result);
|
|
return result;
|
|
};
|
|
|
|
export const isLanguageAvailable = (language: SupportedLanguages, filePath?: string): boolean =>
|
|
loadGrammar(resolveLanguageKey(language, filePath)).ok;
|
|
|
|
export const getLanguageGrammar = (language: SupportedLanguages, filePath?: string): unknown => {
|
|
const key = resolveLanguageKey(language, filePath);
|
|
const result = loadGrammar(key);
|
|
if (result.ok === true) return result.grammar;
|
|
// Fatal failures throw the original underlying error (preserving stack)
|
|
// after the note has been logged. Optional failures fall through to the
|
|
// standard "Unsupported language" message that callers already handle.
|
|
if (result.fatal) throw result.error;
|
|
throw new Error(`Unsupported language: ${language}`);
|
|
};
|
|
|
|
let sharedParser: Parser | null = null;
|
|
|
|
export const loadParser = async (): Promise<Parser> => (sharedParser ??= new Parser());
|
|
|
|
export const loadLanguage = async (
|
|
language: SupportedLanguages,
|
|
filePath?: string,
|
|
): Promise<void> => {
|
|
const parser = await loadParser();
|
|
parser.setLanguage(getLanguageGrammar(language, filePath));
|
|
};
|
|
|
|
export const createParserForLanguage = async (
|
|
language: SupportedLanguages,
|
|
filePath?: string,
|
|
): Promise<Parser> => {
|
|
const parser = new Parser();
|
|
parser.setLanguage(getLanguageGrammar(language, filePath));
|
|
return parser;
|
|
};
|