mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-24 11:24:49 +08:00
fix(swift): a composition continued on & lines no longer swallows its type (#2108)
typealias EditorClient = AutocompleteService.Client
& MediaUploadService.Client
is valid Swift, but in a type's body tree-sitter-swift ends the member at
the newline and the `&` line is an ERROR that swallows the enclosing
type. IceCubesApp's 980-line `EditorStore` came out as loose variables
and functions — no class, no init, no methods — so a call in its init
counted as file-level code ("a file that runs something"), and calls
into its methods from the views landed on same-named methods of other
services.
A Swift preParse moves a line-leading `&` onto the line before, for an
indented `typealias`/`let`/`var` statement's continuation lines only.
Same length and line count; the continuation line's tokens sit one
column left (the Swift type gate reads the site's qualifier at that one
column over). Measured shapes: the failure is type bodies only — a
top-level declaration, a function body and a parenthesized type parse as
written, and are left alone; `&&`, `&+` and inout `&x` are never
touched. The preParse is hoisted before the kernel call like the C/C#
ones, so both arms parse the same bytes, and the file no longer defers
to wasm.
Across IceCubesApp, SwiftPackageIndex-Server, swift-nio, Alamofire, TCA
and mastodon-ios (3,000 Swift files) it rewrites exactly the one failing
file, which now parses clean; none newly erroring. EditorStore is a class
again with 38 methods; views now reference it and call its
insertStatusText / runAssistant (62 edges into it), and
`urlSession.upload` no longer lands on MediaUploadService.upload.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5753de20f5
commit
e9be68b61d
@@ -40,7 +40,53 @@ function extractSwiftReturnType(node: SyntaxNode, source: string): string | unde
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A declaration in a body whose type may continue onto `&` lines:
|
||||
* `typealias X = A`, `let client: any A`. Indented — a top-level declaration
|
||||
* parses as written, and is left alone.
|
||||
*/
|
||||
const COMPOSITION_HEAD = /^[ \t]+(?:@[\w.]+(?:\([^)]*\))?\s*)*(?:[a-z]+(?:\([a-z]+\))?\s+)*(?:typealias|let|var)\s/;
|
||||
const COMPOSITION_CONTINUATION = /^([ \t]*)&([ \t]+)(?=\S)/;
|
||||
|
||||
/**
|
||||
* Move a protocol composition's line-leading `&` onto the line before it.
|
||||
*
|
||||
* typealias EditorClient = AutocompleteService.Client
|
||||
* & MediaUploadService.Client
|
||||
*
|
||||
* is valid Swift, but in a type's body tree-sitter-swift ends the declaration
|
||||
* at the newline and the `&` line is an ERROR that swallows the enclosing type
|
||||
* (IceCubesApp's 980-line `EditorStore` came out as loose variables, no class).
|
||||
* With the `&` trailing the previous line it parses. Same length and line
|
||||
* count; only the continuation line's tokens sit one column left. Applied to
|
||||
* an indented `typealias`/`let`/`var` statement's `&` lines only — a
|
||||
* line-leading `&&`, `&+` or inout `&x` is never touched.
|
||||
*/
|
||||
export function joinSwiftCompositionContinuations(source: string): string {
|
||||
if (!/\n[ \t]*&[ \t]/.test(source)) return source;
|
||||
const original = source.split('\n');
|
||||
const lines = original.slice();
|
||||
let changed = false;
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cont = COMPOSITION_CONTINUATION.exec(lines[i]!);
|
||||
if (!cont) continue;
|
||||
let head = i - 1;
|
||||
while (head > 0 && COMPOSITION_CONTINUATION.test(original[head]!)) head--;
|
||||
if (!COMPOSITION_HEAD.test(original[head]!)) continue;
|
||||
const prev = lines[i - 1]!;
|
||||
const cr = prev.endsWith('\r') ? '\r' : '';
|
||||
const body = cr ? prev.slice(0, -1) : prev;
|
||||
// The previous line must end on a type; a trailing comment would swallow the `&`.
|
||||
if (!/[\w>)\]?!]$/.test(body) || body.includes('//')) continue;
|
||||
lines[i - 1] = `${body}&${cr}`;
|
||||
lines[i] = `${cont[1]}${cont[2]}${lines[i]!.slice(cont[0].length)}`;
|
||||
changed = true;
|
||||
}
|
||||
return changed ? lines.join('\n') : source;
|
||||
}
|
||||
|
||||
export const swiftExtractor: LanguageExtractor = {
|
||||
preParse: joinSwiftCompositionContinuations,
|
||||
functionTypes: ['function_declaration'],
|
||||
classTypes: ['class_declaration'],
|
||||
methodTypes: ['function_declaration'], // Methods are functions inside classes
|
||||
|
||||
@@ -219,8 +219,11 @@ function writtenName(ref: UnresolvedRef, context: ResolutionContext): string {
|
||||
if (!text) return name;
|
||||
// `EventLoopFuture<Success>.Isolated`: the qualifier's generic arguments are not part of its path.
|
||||
const clean = (qualifier: string): string => `${qualifier.replace(GENERIC_ARGS, '').replace(/\s+/g, '')}${name}`;
|
||||
if (text.startsWith(name, ref.column)) {
|
||||
const before = QUALIFIER_BEFORE.exec(text.slice(Math.max(0, ref.column - 240), ref.column));
|
||||
// A composition's `&` line is parsed one column left of the file, its `&`
|
||||
// moved onto the line before (joinSwiftCompositionContinuations).
|
||||
const column = !text.startsWith(name, ref.column) && /^[ \t]*&[ \t]/.test(text) && text.startsWith(name, ref.column + 1) ? ref.column + 1 : ref.column;
|
||||
if (text.startsWith(name, column)) {
|
||||
const before = QUALIFIER_BEFORE.exec(text.slice(Math.max(0, column - 240), column));
|
||||
return before ? clean(before[1]!) : name;
|
||||
}
|
||||
const chain = new RegExp(String.raw`^((?:${QUALIFIER_SEGMENT})+)${name.replace(/[$\\]/g, '\\$&')}\b`).exec(text.slice(ref.column, ref.column + 240));
|
||||
|
||||
Reference in New Issue
Block a user