mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
* fix(swift): preprocess indented conditional directives so class bodies survive parsing * fix(swift): make conditional-directive blanking comment-, string- and brace-aware (#2771) Addresses the review findings on PR #2771. The transform fired unconditionally, which turned valid Swift into parse errors while missing the most common shape it was written for. - The blank/keep decision now consults `blockCommentDepth`, so ` #endif */` — the result of commenting out a conditional block — keeps its comment terminator. Previously `hasError` went raw=false -> preprocessed=true and the rest of the file was swallowed. - The decision keys on the scanner's brace depth instead of indentation. A column-0 `#if` inside a class body is blanked (6 of 7 body shapes previously still lost the enclosing declaration) and an indented file-scope directive is not — matching what the doc comment already claimed. Bare-CR line endings, NBSP/ideographic indentation and a leading BOM are recognized too. - A group is blanked only when every branch is brace-balanced. An `#if`/`#else` that splits a declaration header leaves one unmatched `{` once both branches survive, which collapsed five top-level nodes into one and gave unrelated types fabricated `NetworkClient.` qualified names. Such a group now degrades to the pre-fix behavior. - Multiline strings honour `\"""` escapes, and a plain `"""` closes even when a `#` follows it, so the scanner no longer wedges in string state and silently stops blanking for the rest of the file. - The pound run is counted once per position and skipped. It was quadratic: 10.6s for one 64k-`#` line, well inside the 512 KB walker limit. - Extended regex literals (`#/.../#`) no longer open a phantom block comment. - Directive-free files return early, matching `stripUeMacros`. Worker parity: `emitSwiftScopeCaptures` and `emitCppScopeCaptures` re-apply their provider's `preprocessSource` on the parse-cache-miss path — Dart already did this — and the embedding parse in `ensureAndParse` applies the hook as well. Before this the worker and the scope-capture/embedding halves analyzed different programs, turning a consistent degradation into cold-run/warm-run non-determinism. A new parity test pins the equivalence for every provider that defines the hook. SCHEMA_BUMP 37 -> 38: this changes parse semantics, the chunk key hashes raw on-disk bytes, and `preprocessSource` runs after the key is computed — so a same-package-version warm cache would replay pre-fix Swift results verbatim, including across `--force`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(ingestion): apply preprocessSource once in the scope bridge (#2771) Follow-up cleanup on the review fixes. The previous commit re-applied each provider's `preprocessSource` inside `emitSwiftScopeCaptures` and `emitCppScopeCaptures`, mirroring what Dart already did — three copies of the same rule, and a contract that asked every future emitter to remember it. `extractParsedFile` is the single funnel every `emitScopeCaptures` caller passes through (parse worker, scope-resolution run, Vue script extraction), and it already receives the provider. Applying the hook there on the cache-miss path covers all three languages and every future one, names no language in shared code, and drops Dart's unconditional transform on the cache-hit path. Verified the three emitters use `sourceText` for nothing but the parse, so the substitution is output-identical — which the parity test asserts directly. Also from the cleanup pass: - the parity test derives its language list from the provider registry, so a new provider adopting the hook fails until it adds a fixture - `ensureAndParse` resolves the provider from the language it already computed, instead of a second extension table (`getProviderForFile`) - the preprocessor returns `sourceText` unchanged when no group was blanked, which is the common case for files whose only directives are top-level - `split(/(\r\n|\n|\r)/)` replaces the hand-rolled line splitter, and the per-group brace bookkeeping is two scalars instead of an array - the hint regex is derived from the line regex so the two cannot drift - unit assertions compare the WHOLE preprocessed file against the expected blanking, replacing per-line spot checks; the pipeline tests share one `runFixture` helper and `getNodesForFile` in the resolver test helpers - `LanguageProvider.preprocessSource` documents the real call sites and says plainly that the set is not closed — `populateRangeBindings` still hands language helpers raw text Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(autofix): apply prettier + eslint fixes via /autofix command --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
105 lines
4.4 KiB
TypeScript
105 lines
4.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const { createParserForLanguage, getLanguageFromFilename, parseSourceSafeSpy } = vi.hoisted(() => ({
|
|
createParserForLanguage: vi.fn(),
|
|
getLanguageFromFilename: vi.fn((filePath: string) =>
|
|
filePath.endsWith('.py') ? 'python' : 'typescript',
|
|
),
|
|
parseSourceSafeSpy: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/core/tree-sitter/parser-loader.js', () => ({
|
|
createParserForLanguage,
|
|
isLanguageAvailable: vi.fn().mockReturnValue(true),
|
|
resolveLanguageKey: vi.fn((language: string, filePath?: string) =>
|
|
language === 'typescript' && filePath?.endsWith('.tsx') ? 'typescript:tsx' : language,
|
|
),
|
|
}));
|
|
|
|
vi.mock('../../src/core/tree-sitter/safe-parse.js', async () => {
|
|
const { buildSafeParseMock } = await import('../helpers/parse-source-safe-mock.js');
|
|
return buildSafeParseMock(parseSourceSafeSpy);
|
|
});
|
|
|
|
// Partial mock: `ast-utils` now resolves the LanguageProvider registry to apply
|
|
// `preprocessSource`, and that graph needs the real shared exports (#2771).
|
|
vi.mock('gitnexus-shared', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('gitnexus-shared')>()),
|
|
getLanguageFromFilename,
|
|
}));
|
|
|
|
describe('ensureAndParse', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
createParserForLanguage.mockReset();
|
|
getLanguageFromFilename.mockClear();
|
|
});
|
|
|
|
it('reuses the parser for the same grammar key across interleaved languages', async () => {
|
|
const tsParse = vi
|
|
.fn()
|
|
.mockReturnValueOnce({ lang: 'ts', content: 'first' })
|
|
.mockReturnValueOnce({ lang: 'ts', content: 'second' });
|
|
const pyParse = vi.fn().mockReturnValue({ lang: 'py', content: 'middle' });
|
|
|
|
createParserForLanguage.mockImplementation(async (language: string, filePath?: string) => {
|
|
if (language === 'typescript') return { parse: tsParse, key: filePath };
|
|
if (language === 'python') return { parse: pyParse, key: filePath };
|
|
throw new Error(`unexpected language ${language}`);
|
|
});
|
|
|
|
const { ensureAndParse } = await import('../../src/core/embeddings/ast-utils.js');
|
|
|
|
const tsFirst = await ensureAndParse('const one = 1;', 'first.ts');
|
|
const pyMiddle = await ensureAndParse('value = 1', 'middle.py');
|
|
const tsSecond = await ensureAndParse('const two = 2;', 'second.ts');
|
|
|
|
expect(tsFirst).toEqual({ lang: 'ts', content: 'first' });
|
|
expect(pyMiddle).toEqual({ lang: 'py', content: 'middle' });
|
|
expect(tsSecond).toEqual({ lang: 'ts', content: 'second' });
|
|
expect(createParserForLanguage).toHaveBeenCalledTimes(2);
|
|
expect(tsParse).toHaveBeenCalledTimes(2);
|
|
expect(pyParse).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('uses separate parser instances for .ts and .tsx', async () => {
|
|
const tsParse = vi.fn().mockReturnValue({ lang: 'ts' });
|
|
const tsxParse = vi.fn().mockReturnValue({ lang: 'tsx' });
|
|
|
|
createParserForLanguage.mockImplementation(async (_language: string, filePath?: string) => {
|
|
if (filePath?.endsWith('.tsx')) return { parse: tsxParse };
|
|
return { parse: tsParse };
|
|
});
|
|
|
|
const { ensureAndParse } = await import('../../src/core/embeddings/ast-utils.js');
|
|
|
|
await ensureAndParse('const value = 1;', 'plain.ts');
|
|
await ensureAndParse('export const View = <div />;', 'view.tsx');
|
|
await ensureAndParse('const other = 2;', 'other.ts');
|
|
|
|
expect(createParserForLanguage).toHaveBeenCalledTimes(2);
|
|
expect(tsParse).toHaveBeenCalledTimes(2);
|
|
expect(tsxParse).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
// Windows SIGSEGV regression: ensureAndParse must route through parseSourceSafe
|
|
// so >32 767-char inputs do not crash the process. Direct parser.parse(content)
|
|
// on strings that size SIGSEGVs on Windows; the spy assertion is what catches
|
|
// a bypass since parser.parse(40 000 chars) succeeds on Linux/macOS.
|
|
it('routes >32 767-char input through parseSourceSafe', async () => {
|
|
parseSourceSafeSpy.mockClear();
|
|
|
|
const fakeParse = vi.fn().mockReturnValue({ rootNode: { type: 'module' } });
|
|
createParserForLanguage.mockResolvedValue({ parse: fakeParse });
|
|
|
|
const { ensureAndParse } = await import('../../src/core/embeddings/ast-utils.js');
|
|
|
|
const largeInput = 'const x = 1;\n'.repeat(4000); // ~52 000 chars
|
|
expect(largeInput.length).toBeGreaterThan(40_000);
|
|
|
|
const result = await ensureAndParse(largeInput, 'big.ts');
|
|
|
|
expect(parseSourceSafeSpy).toHaveBeenCalled();
|
|
expect(result).not.toBeNull();
|
|
});
|
|
});
|