GitNexus/gitnexus/test/integration/doc-comment-description-e2e.test.ts
Karl Lehenbauer 639eb04b31
fix(swift): preprocess indented conditional directives so class bodies survive parsing (#2771)
* 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>
2026-08-01 20:10:58 +00:00

96 lines
3.4 KiB
TypeScript

/**
* End-to-end coverage for issue #2270: a leading doc comment must survive the
* full parse pipeline into the node's `description` property (the field the
* embedding metadata header reads for semantic search). The unit tests stop at
* the `descriptionExtractor` hook; this exercises the real worker pipeline and
* the highest-value case — an EXPORTED TS function, whose JSDoc precedes the
* wrapping `export_statement` (PR #2286 review fix).
*/
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { runPipelineFromRepo } from './resolvers/helpers.js';
import type { PipelineResult } from '../../src/types/pipeline.js';
import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js';
import { SupportedLanguages } from '../../src/config/supported-languages.js';
function createTsRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-desc-e2e-'));
fs.writeFileSync(
path.join(dir, 'package.json'),
JSON.stringify({ name: 'doc-desc-e2e', version: '1.0.0' }),
);
fs.writeFileSync(
path.join(dir, 'index.ts'),
[
'/** Computes the running balance, marker EXPORTEDDOC. */',
'export function computeBalance(userId: number): number {',
' return userId;',
'}',
'',
].join('\n'),
);
return dir;
}
function createSwiftRepo(): string {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-desc-swift-e2e-'));
fs.writeFileSync(
path.join(dir, 'Widget.swift'),
[
'class Widget {',
' /**',
' Renders the widget, marker SWIFTDOC.',
' Example:',
' #if os(iOS)',
' useUIKit()',
' #endif',
' */',
' func render() {}',
'}',
'',
].join('\n'),
);
return dir;
}
describe('doc-comment description end-to-end (issue #2270)', () => {
it('surfaces an exported function JSDoc as its node description through the pipeline', async () => {
const result: PipelineResult = await runPipelineFromRepo(createTsRepo(), () => {}, {
skipGraphPhases: true,
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});
const descriptions = new Map<string, unknown>();
result.graph.forEachNode((node) => {
descriptions.set(`${node.label}:${node.properties.name}`, node.properties.description);
});
expect(result.usedWorkerPool).toBe(true);
expect(descriptions.get('Function:computeBalance')).toContain('EXPORTEDDOC');
});
// Swift preprocessing blanks nested conditional directives before parsing;
// doing that inside a doc comment would delete lines from `description` (#2771).
it.skipIf(!isLanguageAvailable(SupportedLanguages.Swift))(
'keeps directive lines inside a Swift doc comment in the node description',
async () => {
const result: PipelineResult = await runPipelineFromRepo(createSwiftRepo(), () => {}, {
skipGraphPhases: true,
workerThresholdsForTest: { minFiles: 1, minBytes: 1 },
workerPoolSize: 2,
});
const descriptions = new Map<string, unknown>();
result.graph.forEachNode((node) => {
descriptions.set(`${node.label}:${node.properties.name}`, node.properties.description);
});
expect(descriptions.get('Function:render')).toBe(
'Renders the widget, marker SWIFTDOC. Example: #if os(iOS) useUIKit() #endif',
);
},
);
});