mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-08 22:22:52 +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>
119 lines
3.8 KiB
TypeScript
119 lines
3.8 KiB
TypeScript
import { afterAll, describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { runPipelineFromRepo } from '../../src/core/ingestion/pipeline.js';
|
|
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
|
|
import { SupportedLanguages } from '../../src/config/supported-languages.js';
|
|
import { getNodesForFile } from './resolvers/helpers.js';
|
|
import { preprocessSwiftConditionalDirectives } from '../../src/core/ingestion/languages/swift/conditional-directive-preprocess.js';
|
|
|
|
const swiftFixture = `class Outer {
|
|
enum A { case x }
|
|
#if os(iOS)
|
|
enum B { case y }
|
|
#endif
|
|
}
|
|
`;
|
|
|
|
const swiftMultilineStringFixture = `class StringHolder {
|
|
let payload = """
|
|
#if string-data
|
|
#elseif more-string-data
|
|
#else
|
|
#endif
|
|
"""
|
|
#if REAL_DIRECTIVE
|
|
func afterString() {}
|
|
#endif
|
|
}
|
|
`;
|
|
|
|
const swiftColumnZeroFixture = `class ColumnZero {
|
|
enum A { case x }
|
|
#if os(iOS)
|
|
enum B { case y }
|
|
#endif
|
|
}
|
|
`;
|
|
|
|
const swiftHeaderSplitFixture = `class NetworkClient {
|
|
#if swift(>=5.5)
|
|
func fetch() async {
|
|
#else
|
|
func fetch() {
|
|
#endif
|
|
perform()
|
|
}
|
|
}
|
|
|
|
struct SessionStore {}
|
|
`;
|
|
|
|
const swiftAvailable = isLanguageAvailable(SupportedLanguages.Swift);
|
|
const scratchDirs: string[] = [];
|
|
|
|
/** Analyze a one-file Swift repo through the real worker pool. */
|
|
async function runFixture(prefix: string, source: string) {
|
|
const repo = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
scratchDirs.push(repo);
|
|
fs.writeFileSync(path.join(repo, 'Fixture.swift'), source, 'utf8');
|
|
|
|
const result = await runPipelineFromRepo(repo, () => {}, { workerPoolSize: 1 });
|
|
return getNodesForFile(result, 'Fixture.swift');
|
|
}
|
|
|
|
describe.skipIf(!swiftAvailable)('Swift conditional-directive pipeline regression', () => {
|
|
afterAll(() => {
|
|
for (const scratchDir of scratchDirs) fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('keeps Outer and both nested declarations in the real worker pipeline', async () => {
|
|
const { names } = await runFixture('gitnexus-swift-directive-', swiftFixture);
|
|
|
|
expect(names).toEqual(['A', 'B', 'Fixture.swift', 'Outer', 'x', 'y']);
|
|
}, 60000);
|
|
|
|
it('keeps a column-zero directive inside a class body from discarding the class', async () => {
|
|
const { names } = await runFixture('gitnexus-swift-column-zero-', swiftColumnZeroFixture);
|
|
|
|
expect(names).toEqual(['A', 'B', 'ColumnZero', 'Fixture.swift', 'x', 'y']);
|
|
}, 60000);
|
|
|
|
it('keeps later top-level types out of a class whose header is split across branches', async () => {
|
|
// Blanking an unbalanced group re-parents unrelated declarations, which
|
|
// shows up as a fabricated `NetworkClient.` qualified-name prefix.
|
|
expect(preprocessSwiftConditionalDirectives(swiftHeaderSplitFixture)).toBe(
|
|
swiftHeaderSplitFixture,
|
|
);
|
|
|
|
const { qualified } = await runFixture('gitnexus-swift-header-split-', swiftHeaderSplitFixture);
|
|
|
|
expect(qualified).toEqual([
|
|
'Class:NetworkClient',
|
|
'File:Fixture.swift',
|
|
'Function:fetch',
|
|
'Function:fetch',
|
|
'Struct:SessionStore',
|
|
]);
|
|
}, 60000);
|
|
|
|
it('preserves a multiline string property while blanking a real directive between strings', async () => {
|
|
const rewritten = preprocessSwiftConditionalDirectives(swiftMultilineStringFixture);
|
|
const opening = swiftMultilineStringFixture.indexOf('"""') + 3;
|
|
const closing = swiftMultilineStringFixture.indexOf('"""', opening);
|
|
expect([opening, closing]).toEqual([40, 105]);
|
|
expect(rewritten.slice(opening, closing)).toBe(
|
|
swiftMultilineStringFixture.slice(opening, closing),
|
|
);
|
|
|
|
const { labelled } = await runFixture('gitnexus-swift-string-', swiftMultilineStringFixture);
|
|
|
|
expect(labelled).toEqual([
|
|
'Class:StringHolder',
|
|
'File:Fixture.swift',
|
|
'Function:afterString',
|
|
'Property:payload',
|
|
]);
|
|
}, 60000);
|
|
});
|