fix(swift): use official prebuilt parser runtime (#1130)

* fix(swift): use official prebuilt parser runtime

Vendor the official tree-sitter-swift 0.7.1 runtime package so Swift parsing works without source-building, while keeping the repo on the current tree-sitter runtime until the broader upgrade is ready. Also preserves Swift resolver correctness for overloaded owned functions and extension-backed type duplicates now that Swift is available by default.

Made-with: Cursor

* fix(swift): move duplicate type ordering into provider

Keep Swift extension candidate ordering behind the LanguageProvider contract and cover the Swift 0.7 init scanner path so parser runtime changes do not leak language-specific logic into shared resolution.

Made-with: Cursor

* fix(swift): address parser runtime review

Add explicit Swift prebuild checks and vendor guidance so parser runtime packaging remains observable and maintainable.
This commit is contained in:
Gergő Magyar 2026-04-28 09:57:42 +01:00 committed by GitHub
parent 86abc01445
commit 1f6df5fdbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 31127 additions and 231 deletions

View file

@ -3,7 +3,8 @@ WORKDIR /app
RUN apt-get -o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
COPY . .
RUN npm ci --ignore-scripts \
&& node scripts/patch-tree-sitter-swift.cjs \
&& npm rebuild tree-sitter-swift 2>&1 \
&& node -e "require('tree-sitter-swift')" \
&& (npm rebuild 2>&1 || true) \
&& cd node_modules/tree-sitter-kotlin && npx --yes node-gyp rebuild 2>&1
CMD ["npx", "vitest", "run", "test/integration", "--reporter=verbose"]

View file

@ -68,7 +68,7 @@
"tree-sitter-dart": "file:./vendor/tree-sitter-dart",
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-proto": "file:./vendor/tree-sitter-proto",
"tree-sitter-swift": "^0.6.0"
"tree-sitter-swift": "file:./vendor/tree-sitter-swift"
}
},
"../gitnexus-shared": {
@ -5065,20 +5065,6 @@
}
}
},
"node_modules/tree-sitter-cli": {
"version": "0.23.2",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz",
"integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"bin": {
"tree-sitter": "cli.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/tree-sitter-cpp": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.23.4.tgz",
@ -5268,49 +5254,8 @@
}
},
"node_modules/tree-sitter-swift": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz",
"integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0",
"tree-sitter-cli": "^0.23",
"which": "2.0.2"
},
"peerDependencies": {
"tree-sitter": "^0.21.1"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-swift/node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC",
"optional": true
},
"node_modules/tree-sitter-swift/node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"optional": true,
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
"resolved": "vendor/tree-sitter-swift",
"link": true
},
"node_modules/tree-sitter-typescript": {
"version": "0.23.2",
@ -5765,6 +5710,24 @@
"peerDependencies": {
"tree-sitter": ">=0.21.0"
}
},
"vendor/tree-sitter-swift": {
"version": "0.7.1",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"tree-sitter": "^0.21.1 || ^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}
}
}

View file

@ -47,7 +47,7 @@
"test:integration": "vitest run test/integration",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"postinstall": "node scripts/patch-tree-sitter-swift.cjs && node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs",
"postinstall": "node scripts/build-tree-sitter-dart.cjs && node scripts/build-tree-sitter-proto.cjs",
"prepare": "node scripts/build.js",
"prepack": "node scripts/build.js"
},
@ -91,7 +91,7 @@
"tree-sitter-dart": "file:./vendor/tree-sitter-dart",
"tree-sitter-kotlin": "^0.3.8",
"tree-sitter-proto": "file:./vendor/tree-sitter-proto",
"tree-sitter-swift": "^0.6.0"
"tree-sitter-swift": "file:./vendor/tree-sitter-swift"
},
"devDependencies": {
"@types/cli-progress": "^3.11.6",

View file

@ -26,7 +26,7 @@
* `node_modules/tree-sitter-proto/build/Release/tree_sitter_proto_binding.node`
* under npm-managed territory, safe on upgrade.
*
* Mirrors scripts/patch-tree-sitter-swift.cjs. Best-effort: if any
* Mirrors the tree-sitter-dart build helper. Best-effort: if any
* precondition fails (optional dep absent, no toolchain, --ignore-scripts),
* warn and exit 0 so gitnexus install still succeeds.
*/

View file

@ -1,78 +0,0 @@
#!/usr/bin/env node
/**
* WORKAROUND: tree-sitter-swift@0.6.0 binding.gyp build failure
*
* Background:
* tree-sitter-swift@0.6.0's binding.gyp contains an "actions" array that
* invokes `tree-sitter generate` to regenerate parser.c from grammar.js.
* This is intended for grammar developers, but the published npm package
* already ships pre-generated parser files (parser.c, scanner.c), so the
* actions are unnecessary for consumers. Since consumers don't have
* tree-sitter-cli installed, the actions always fail during `npm install`.
*
* Why we can't just upgrade:
* tree-sitter-swift@0.7.1 fixes this (removes postinstall, ships prebuilds),
* but it requires tree-sitter@^0.22.1. The upstream project pins tree-sitter
* to ^0.21.0 and all other grammar packages depend on that version.
* Upgrading tree-sitter would be a separate breaking change.
*
* How this workaround works:
* 1. tree-sitter-swift's own postinstall fails (npm warns but continues)
* 2. This script runs as gitnexus's postinstall
* 3. It removes the "actions" array from binding.gyp
* 4. It rebuilds the native binding with the cleaned binding.gyp
*
* TODO: Remove this script when tree-sitter is upgraded to ^0.22.x,
* which allows using tree-sitter-swift@0.7.1+ directly.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const swiftDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-swift');
const bindingPath = path.join(swiftDir, 'binding.gyp');
try {
if (!fs.existsSync(bindingPath)) {
process.exit(0);
}
const content = fs.readFileSync(bindingPath, 'utf8');
let needsRebuild = false;
if (content.includes('"actions"')) {
// Strip Python-style comments (#) and trailing commas before JSON parsing
const cleaned = content
.replace(/#[^\n]*/g, '') // Remove # comments
.replace(/,(\s*[\]}])/g, '$1'); // Remove trailing commas before ] or }
const gyp = JSON.parse(cleaned);
if (gyp.targets && gyp.targets[0] && gyp.targets[0].actions) {
delete gyp.targets[0].actions;
fs.writeFileSync(bindingPath, JSON.stringify(gyp, null, 2) + '\n');
console.log('[tree-sitter-swift] Patched binding.gyp (removed actions array)');
needsRebuild = true;
}
}
// Check if native binding exists
const bindingNode = path.join(swiftDir, 'build', 'Release', 'tree_sitter_swift_binding.node');
if (!fs.existsSync(bindingNode)) {
needsRebuild = true;
}
if (needsRebuild) {
console.log('[tree-sitter-swift] Rebuilding native binding...');
execSync('npx node-gyp rebuild', {
cwd: swiftDir,
stdio: 'pipe',
timeout: 120000,
});
console.log('[tree-sitter-swift] Native binding built successfully');
}
} catch (err) {
console.warn('[tree-sitter-swift] Could not build native binding:', err.message);
console.warn(
'[tree-sitter-swift] You may need to manually run: cd node_modules/tree-sitter-swift && npx node-gyp rebuild',
);
}

View file

@ -12,7 +12,7 @@ type ReceiverSource = ReceiverEnriched['receiverSource'];
* DAG stage 4 fallback: used when `selectDispatch` is absent or returns null.
* Preserves pre-DAG dispatch semantics:
* - 'constructor' constructor branch
* - 'free' free branch (admits Swift/Kotlin class-target fast path)
* - 'free' free branch (admits class-target fast path)
* - 'member' or undefined owner-scoped branch
*
* `undefined` callForm MUST route through owner-scoped (not free) so bare
@ -1595,41 +1595,30 @@ const disambiguateByOverloadOrArgTypes = (
return null;
};
/**
* Collapse Swift-extension duplicate Class/Struct candidates to the primary
* definition, preferring the shortest file path.
*
* Swift extensions (`extension User { ... }` in a separate file) create
* multiple `Class` nodes sharing the same symbol name one for the primary
* declaration and one per extension file. When overload disambiguation and
* receiver narrowing both fail to converge on a single candidate, this
* heuristic picks the primary definition based on the assumption that it
* lives at the shortest file path (e.g. `User.swift` over `UserExtensions.swift`).
*
* Intentionally narrower than {@link INSTANTIABLE_CLASS_TYPES}: only `Class`
* and `Struct` are considered, not `Record`. Swift extensions only produce
* `Class` duplicates in practice, and C#/Kotlin records do not exhibit the
* same multi-file-definition pattern, so widening this set risks accidental
* dedup of legitimately distinct record types.
*
* Returns a `ResolveResult` when the heuristic fires, `null` when the
* candidate pool does not match the shape (mixed types, non-Class/Struct
* kinds, or `length <= 1`). Callers should fall through to their own null
* return when this helper returns `null`.
*
* Used by `resolveFreeCall`. Having a single source of truth prevents
* duplication if the heuristic is ever tuned.
*/
const dedupSwiftExtensionCandidates = (
const orderProviderSameNameTypeCandidates = (
candidates: readonly SymbolDefinition[],
typeName: string,
filePath: string,
): readonly SymbolDefinition[] | null => {
const language = getLanguageFromFilename(filePath);
if (language == null) return null;
return (
getProvider(language).orderSameNameTypeCandidates?.({
typeName,
callSiteFilePath: filePath,
candidates,
}) ?? null
);
};
const resolveProviderPrimaryTypeCandidate = (
candidates: readonly SymbolDefinition[],
tier: ResolutionTier,
typeName: string,
filePath: string,
): ResolveResult | null => {
if (candidates.length <= 1) return null;
const allSameType = candidates.every((c) => c.type === candidates[0].type);
if (!allSameType) return null;
if (candidates[0].type !== 'Class' && candidates[0].type !== 'Struct') return null;
const sorted = [...candidates].sort((a, b) => a.filePath.length - b.filePath.length);
return toResolveResult(sorted[0], tier);
const ordered = orderProviderSameNameTypeCandidates(candidates, typeName, filePath);
return ordered && ordered.length > 0 ? toResolveResult(ordered[0], tier) : null;
};
/**
@ -2223,6 +2212,35 @@ const resolveMethodByOwner = (
}
}
if (!firstDef && !ambiguous) {
const orderedTypeCandidates = orderProviderSameNameTypeCandidates(
ctx.model.types.lookupClassByName(receiverTypeName),
receiverTypeName,
filePath,
);
if (orderedTypeCandidates) {
for (const candidate of orderedTypeCandidates) {
const def = canWalkMRO
? lookupMethodByOwnerWithMRO(
candidate.nodeId,
methodName,
heritageMap,
ctx.model,
mroStrategy,
argCount,
)
: ctx.model.methods.lookupMethodByOwner(candidate.nodeId, methodName, argCount);
if (!def) continue;
if (!firstDef) {
firstDef = def;
} else if (def.nodeId !== firstDef.nodeId) {
ambiguous = true;
break;
}
}
}
}
if (!firstDef || ambiguous) return undefined;
return { def: firstDef, tier: typeResolved.tier };
};
@ -2290,9 +2308,9 @@ export const resolveMemberCall = (
* resolution via `ctx.resolve()`.
*
* Used for `foo()`, `doStuff()` unqualified calls with no receiver.
* Also handles Swift/Kotlin implicit constructors (`User()` without `new`)
* by delegating to {@link resolveStaticCall} when the tiered pool contains
* class-like targets.
* Also handles implicit constructors (`User()` without `new`) by delegating
* to {@link resolveStaticCall} when the tiered pool contains class-like
* targets.
*
* {@link resolveCallTarget} delegates here for `callForm === 'free'`.
*
@ -2324,33 +2342,30 @@ export const resolveFreeCall = (
let filteredCandidates = filterCallableCandidates(tiered.candidates, argCount, 'free');
// Class-target fast path: Swift/Kotlin `User()` — free-form call targeting a
// class. Delegates to resolveStaticCall for O(1) class + constructor lookup.
// Class-target fast path: free-form call targeting a class. Delegates to
// resolveStaticCall for O(1) class + constructor lookup.
// The `.some()` trigger must stay aligned with `INSTANTIABLE_CLASS_TYPES` —
// any type admitted here that is not in that set will cause resolveStaticCall
// to return null, wasting two lookup passes per call. `Enum` is deliberately
// excluded; `Record` is included so C# records and Kotlin data classes reach
// the fast path.
// excluded; `Record` is included so record-like class targets reach the fast
// path.
// Align with INSTANTIABLE_CLASS_TYPES by reusing the set directly rather
// than enumerating literal strings. This converts an invariant that was
// previously enforced by a comment ("keep this list aligned with
// INSTANTIABLE_CLASS_TYPES") into one enforced structurally — any future
// extension of the set (e.g. Kotlin `object`) propagates here automatically.
// The `dedupSwiftExtensionCandidates` helper used in the tail of this
// function deliberately uses a narrower literal `'Class' | 'Struct'` check
// — Swift extensions only produce Class duplicates in practice, so Record
// is excluded there by design. Do not collapse that helper into
// INSTANTIABLE_CLASS_TYPES.
// extension of the set propagates here automatically.
// Language providers can still choose a primary same-name type candidate in
// the tail of this function when their grammars index one logical type
// multiple times.
const hasClassTarget =
filteredCandidates.length === 0 &&
tiered.candidates.some((c) => INSTANTIABLE_CLASS_TYPES.has(c.type));
if (hasClassTarget) {
const staticResult = resolveStaticCall(calledName, filePath, ctx, argCount, tiered);
if (staticResult) return staticResult;
// Retry with constructor form: Swift/Kotlin constructor calls look like
// free function calls (no `new` keyword). If resolveStaticCall didn't
// match, re-filter with constructor form so CONSTRUCTOR_TARGET_TYPES
// applies.
// Retry with constructor form for languages whose constructor calls look
// like free function calls. If resolveStaticCall didn't match, re-filter
// with constructor form so CONSTRUCTOR_TARGET_TYPES applies.
//
// The retry fires for every null return from `resolveStaticCall`, which
// can happen for three distinct reasons — all three are handled below:
@ -2364,9 +2379,8 @@ export const resolveFreeCall = (
// (b) Homonym ambiguity — two or more instantiable class candidates
// share the name (e.g. `User` in two files, same tier). The
// retry repopulates `filteredCandidates` with both Classes and
// they flow into `dedupSwiftExtensionCandidates` below, which
// either picks the shortest-path primary or null-routes.
// Covered by the R7 Swift-extension dedup test.
// they flow into the provider same-name candidate hook below, which
// can pick a primary definition or null-route.
//
// (c) `resolveStaticCall` step 4 bailed because the tiered pool
// contains ownerless `Constructor` nodes (some extractors emit
@ -2391,10 +2405,13 @@ export const resolveFreeCall = (
}
if (filteredCandidates.length !== 1) {
// See `dedupSwiftExtensionCandidates` — shared helper, single source of
// truth for the Swift-extension same-name collision heuristic.
const deduped = dedupSwiftExtensionCandidates(filteredCandidates, tiered.tier);
if (deduped) return deduped;
const primary = resolveProviderPrimaryTypeCandidate(
filteredCandidates,
tiered.tier,
calledName,
filePath,
);
if (primary) return primary;
return null;
}
@ -2559,9 +2576,16 @@ export const resolveStaticCall = (
// Interface / Trait / Impl). Null-route via the fall-through `return
// null` — this is the dominant Codex-fix case.
// length === 1 → a single instantiable candidate remains, return it.
// length > 1 → two or more instantiable classes share the name (e.g.
// homonym classes across files with no import narrowing). Fall through
// to `return null` so the caller null-routes rather than guess.
// length > 1 → let the call-site provider choose a primary when it can
// prove the candidates are one logical type; otherwise null-route.
const primary = resolveProviderPrimaryTypeCandidate(
instantiableCandidates,
typeResolved.tier,
className,
currentFile,
);
if (primary) return primary;
if (instantiableCandidates.length === 1) {
return toResolveResult(instantiableCandidates[0], typeResolved.tier);
}

View file

@ -498,6 +498,15 @@ interface LanguageProviderConfig {
// ── Resolution phase (RFC §4v2) ────────────────────────────────────
/** Order same-name type candidates when a language can index multiple
* definitions for one logical type. Return null to keep shared ambiguity
* handling. */
readonly orderSameNameTypeCandidates?: (params: {
readonly typeName: string;
readonly callSiteFilePath: string;
readonly candidates: readonly SymbolDefinition[];
}) => readonly SymbolDefinition[] | null;
/**
* Is this callable definition compatible with the given call-site arity?
* Language-specific rules: Python `*args`/`**kwargs`/defaults, JS default

View file

@ -11,7 +11,7 @@
*/
import { SupportedLanguages } from 'gitnexus-shared';
import type { NodeLabel } from 'gitnexus-shared';
import type { NodeLabel, SymbolDefinition } from 'gitnexus-shared';
import { createClassExtractor } from '../class-extractors/generic.js';
import { swiftClassConfig } from '../class-extractors/configs/swift.js';
import { defineLanguage } from '../language-provider.js';
@ -128,6 +128,24 @@ const swiftExtractFunctionName = (
return null; // fall through to generic
};
const orderSwiftSameNameTypeCandidates = ({
callSiteFilePath,
candidates,
}: {
readonly typeName: string;
readonly callSiteFilePath: string;
readonly candidates: readonly SymbolDefinition[];
}): readonly SymbolDefinition[] | null => {
if (!callSiteFilePath.endsWith('.swift')) return null;
if (candidates.length <= 1) return null;
if (!candidates.every((c) => c.type === candidates[0].type)) return null;
if (candidates[0].type !== 'Class' && candidates[0].type !== 'Struct') return null;
if (!candidates.every((c) => c.filePath.endsWith('.swift'))) return null;
return [...candidates].sort(
(a, b) => a.filePath.length - b.filePath.length || a.filePath.localeCompare(b.filePath),
);
};
const BUILT_INS: ReadonlySet<string> = new Set([
'print',
'debugPrint',
@ -257,5 +275,6 @@ export const swiftProvider = defineLanguage({
classExtractor: createClassExtractor(swiftClassConfig),
heritageExtractor: createHeritageExtractor(SupportedLanguages.Swift),
implicitImportWirer: wireSwiftImplicitImports,
orderSameNameTypeCandidates: orderSwiftSameNameTypeCandidates,
builtInNames: BUILT_INS,
});

View file

@ -1,5 +1,5 @@
// gitnexus/src/core/ingestion/method-extractors/configs/swift.ts
// Verified against tree-sitter-swift 0.6.0
// Verified against tree-sitter-swift 0.7.x
import { SupportedLanguages } from 'gitnexus-shared';
import type {
@ -110,7 +110,7 @@ function extractSwiftReturnType(node: SyntaxNode): string | undefined {
function extractSwiftParameters(node: SyntaxNode): ParameterInfo[] {
const params: ParameterInfo[] = [];
// In tree-sitter-swift 0.6.0, parameters are direct children of function_declaration.
// In tree-sitter-swift, parameters are direct children of function_declaration.
// Default value tokens ('=', literal) are siblings of the parameter node at the
// function_declaration level, not children of the parameter node.
for (let i = 0; i < node.childCount; i++) {
@ -264,8 +264,7 @@ function extractSwiftAnnotations(node: SyntaxNode): string[] {
export const swiftMethodConfig: MethodExtractionConfig = {
language: SupportedLanguages.Swift,
// tree-sitter-swift 0.6.0 may use class_declaration for classes, structs, enums, extensions,
// and actors — but this cannot be verified until the grammar installs on Node 22+.
// Keep this conservative until Swift type-shape coverage is expanded.
// TODO: Verify struct_declaration, enum_declaration, extension_declaration, actor_declaration
// node types once tree-sitter-swift loads on Node 22, and add them here if they are distinct.
// protocol_declaration is a separate, confirmed node type.

View file

@ -542,10 +542,13 @@ const processParsingSequential = async (
}
}
// Append #<paramCount> to Method/Constructor IDs to disambiguate overloads.
// Functions are not suffixed — they don't overload by name in the same scope.
// Append #<paramCount> to owned callable IDs to disambiguate overloads.
// Top-level Function IDs stay stable; functions inside an owner may overload.
// When same-arity collisions exist, append ~type1,type2 for further disambiguation.
const needsAritySuffix = nodeLabel === 'Method' || nodeLabel === 'Constructor';
const needsAritySuffix =
nodeLabel === 'Method' ||
nodeLabel === 'Constructor' ||
(nodeLabel === 'Function' && enclosingClassId !== null);
let arityTag = needsAritySuffix && arityForId !== undefined ? `#${arityForId}` : '';
if (arityTag && seqDefMethods && seqDefMethodInfo && seqClassNodeId !== undefined) {
// Use cached method map + collision groups (built once per class, not per method)

View file

@ -41,6 +41,10 @@ function unwrapSwiftExpression(node: SyntaxNode): SyntaxNode {
return node;
}
function swiftNavigationSuffixName(node: SyntaxNode | null): string | undefined {
return node?.type === 'navigation_suffix' ? node.lastNamedChild?.text : node?.text;
}
/** Swift: let x: Foo = ... */
const extractDeclaration: TypeBindingExtractor = (
node: SyntaxNode,
@ -119,8 +123,10 @@ const extractInitializer: InitializerExtractor = (
// Explicit init: User.init(name: "alice") — navigation_expression with .init suffix
if (callee.type === 'navigation_expression') {
const receiver = callee.firstNamedChild;
const suffix = callee.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
if (
receiver?.type === 'simple_identifier' &&
swiftNavigationSuffixName(callee.lastNamedChild) === 'init'
) {
const calleeName = receiver.text;
if (calleeName && classNames.has(calleeName)) {
env.set(varName, calleeName);
@ -133,7 +139,7 @@ const extractInitializer: InitializerExtractor = (
const scanConstructorBinding: ConstructorBindingScanner = (node) => {
if (node.type !== 'property_declaration') return undefined;
if (hasTypeAnnotation(node)) return undefined;
const pattern = node.childForFieldName('pattern');
const pattern = node.childForFieldName('pattern') ?? findChild(node, 'pattern');
if (!pattern) return undefined;
const varName = pattern.text;
if (!varName) return undefined;
@ -162,7 +168,7 @@ const scanConstructorBinding: ConstructorBindingScanner = (node) => {
if (callee.type === 'navigation_expression') {
const receiver = callee.firstNamedChild;
const suffix = callee.lastNamedChild;
if (receiver?.type === 'simple_identifier' && suffix?.text === 'init') {
if (receiver?.type === 'simple_identifier' && swiftNavigationSuffixName(suffix) === 'init') {
return { varName, calleeName: receiver.text };
}
// General qualified call: service.getUser() → extract method name.

View file

@ -2078,10 +2078,13 @@ const processFileGroup = (
}
}
// Append #<paramCount> to Method/Constructor IDs to disambiguate overloads.
// Functions are not suffixed — they don't overload by name in the same scope.
// Append #<paramCount> to owned callable IDs to disambiguate overloads.
// Top-level Function IDs stay stable; functions inside an owner may overload.
// When same-arity collisions exist, append ~type1,type2 for further disambiguation.
const needsAritySuffix = nodeLabel === 'Method' || nodeLabel === 'Constructor';
const needsAritySuffix =
nodeLabel === 'Method' ||
nodeLabel === 'Constructor' ||
(nodeLabel === 'Function' && enclosingClassId !== null);
let arityTag = needsAritySuffix && arityForId !== undefined ? `#${arityForId}` : '';
if (arityTag && defMethodMap && defMethodInfo) {
const groups = buildCollisionGroups(defMethodMap);

View file

@ -3,8 +3,8 @@
* Verifies that `let user = User(name: "alice"); user.save()` resolves to User.save
* without explicit type annotations, using SymbolTable verification.
*
* NOTE: tree-sitter-swift has build issues on Node 22 these tests skip gracefully
* when the Swift parser is not available.
* NOTE: Swift is installed as an optional dependency. These tests skip gracefully
* if a consumer installs without optional dependencies.
*/
import { describe, it, expect, beforeAll } from 'vitest';
import path from 'path';

View file

@ -48,6 +48,20 @@ describe('CLI commands', () => {
'file:./vendor/tree-sitter-dart',
);
});
it('uses the vendored official Swift runtime package instead of source-building on install', async () => {
const pkg = await import('../../package.json', { with: { type: 'json' } });
const swiftPkg = await import('../../vendor/tree-sitter-swift/package.json', {
with: { type: 'json' },
});
expect(pkg.default.dependencies['tree-sitter']).toBe('^0.21.1');
expect(pkg.default.optionalDependencies['tree-sitter-swift']).toBe(
'file:./vendor/tree-sitter-swift',
);
expect(pkg.default.scripts.postinstall).not.toContain('tree-sitter-swift');
expect(swiftPkg.default.version).toBe('0.7.1');
expect(swiftPkg.default.peerDependencies['tree-sitter']).toContain('^0.21.1');
});
});
describe('analyzeCommand', () => {

View file

@ -19,11 +19,8 @@ describe('isLanguageAvailable', () => {
expect(isLanguageAvailable('haskell' as SupportedLanguages)).toBe(false);
});
it('handles Swift based on optional dependency availability', () => {
// Swift is optional — result depends on whether tree-sitter-swift is installed
const result = isLanguageAvailable(SupportedLanguages.Swift);
expect(typeof result).toBe('boolean');
// Either way, it should not throw
it('returns true for Swift in the default install', () => {
expect(isLanguageAvailable(SupportedLanguages.Swift)).toBe(true);
});
it('handles Kotlin based on optional dependency availability', () => {

View file

@ -4385,12 +4385,14 @@ class Foo {
expect(m.parameters[0]).toEqual({
name: 'name',
type: 'String',
rawType: 'String',
isOptional: false,
isVariadic: false,
});
expect(m.parameters[1]).toEqual({
name: 'age',
type: 'Int',
rawType: 'Int',
isOptional: true,
isVariadic: false,
});

View file

@ -81,15 +81,14 @@ describe('parser-loader', () => {
});
describe('Swift optional dependency', () => {
it('handles Swift loading gracefully', async () => {
// Swift is optional — it either loads successfully or throws an error about unsupported language
try {
await loadLanguage(SupportedLanguages.Swift);
// If it succeeds, tree-sitter-swift is installed
} catch (e: any) {
// If it fails, it should be because tree-sitter-swift is not installed
expect(e.message).toContain('Unsupported language');
}
it('loads Swift from the default optional dependency and parses source', async () => {
const parser = await loadParser();
await loadLanguage(SupportedLanguages.Swift);
const tree = parser.parse('class Foo { func bar() {} }');
expect(tree.rootNode.type).toBe('source_file');
expect(tree.rootNode.namedChildCount).toBe(1);
});
});
});

View file

@ -2895,6 +2895,24 @@ class User : BaseModel<string> {
expect(typeEnv.constructorBindings.find((b) => b.varName === 'user')).toBeUndefined();
});
describeSwift('Swift constructor binding scanner', () => {
it('returns constructor binding for explicit User.init(...) calls', () => {
const tree = parseSwift(`
func run() {
let user = User.init(name: "alice")
}
`);
const typeEnv = buildTypeEnv(tree, 'swift');
expect(flatGet(typeEnv, 'user')).toBeUndefined();
expect(typeEnv.constructorBindings).toEqual([
expect.objectContaining({
varName: 'user',
calleeName: 'User',
}),
]);
});
});
it('returns constructor bindings for Python x = UnknownClass()', () => {
const tree = parse(
`

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 alex-pinkus
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,139 @@
## GitNexus vendor notice
This directory is a GitNexus-managed vendored copy of the official
`tree-sitter-swift@0.7.1` npm runtime package, including its official native
prebuilds. GitNexus keeps the top-level `tree-sitter` dependency pinned to
`^0.21.1` until the broader parser runtime upgrade is handled separately.
When updating this vendor package, replace it from an official
`tree-sitter-swift` npm release, keep the native `prebuilds/` artifacts, update
the `_vendoredBy` provenance fields in `package.json`, and verify the packed
GitNexus tarball can load `tree-sitter-swift`.
![Parse rate badge](https://byob.yarr.is/alex-pinkus/tree-sitter-swift/parse_rate)
[![Crates.io badge](https://byob.yarr.is/alex-pinkus/tree-sitter-swift/crates_io_version)](https://crates.io/crates/tree-sitter-swift)
[![NPM badge](https://byob.yarr.is/alex-pinkus/tree-sitter-swift/npm_version)](https://www.npmjs.com/package/tree-sitter-swift)
[![Build](https://github.com/alex-pinkus/tree-sitter-swift/actions/workflows/top-repos.yml/badge.svg)](https://github.com/alex-pinkus/tree-sitter-swift/actions/workflows/top-repos.yml)
# tree-sitter-swift
This contains a [`tree-sitter`](https://tree-sitter.github.io/tree-sitter) grammar for the Swift programming language.
## Getting started
To use this parser to parse Swift code, you'll want to depend on either the Rust crate or the NPM package.
### Rust
To use the Rust crate, you'll add this to your `Cargo.toml`:
```
tree-sitter = "0.23.0"
tree-sitter-swift = "=0.7.0"
```
Then you can use a `tree-sitter` parser with the language declared here:
```
let mut parser = tree_sitter::Parser::new();
parser.set_language(tree_sitter_swift::language())?;
// ...
let tree = parser.parse(&my_source_code, None)
.ok_or_else(|| /* error handling code */)?;
```
### Javascript
To use this from NPM, you'll add similar dependencies to `package.json`:
```
"dependencies: {
"tree-sitter-swift": "0.7.0",
"tree-sitter": "^0.22.1"
}
```
Your usage of the parser will look like:
```
const Parser = require("tree-sitter");
const Swift = require("tree-sitter-swift");
const parser = new Parser();
parser.setLanguage(Swift);
// ...
const tree = parser.parse(mySourceCode);
```
### Editing the grammar
With this package checked out, a common workflow for editing the grammar will look something like:
1. Make a change to `grammar.ts`.
2. Run `npm install && npm test` to see whether the change has had impact on existing parsing behavior. The default
`npm test` target requires `valgrind` to be installed; if you do not have it installed, and do not wish to, you can
substitute `tree-sitter test` directly.
3. Run `tree-sitter parse` on some real Swift codebase and see whether (or where) it fails.
4. Use any failures to create new corpus test cases.
## Contributions
All contributions to this repository are welcome.
If said contribution is to check generated files (e.g., `parser.c`) into the repository, be aware that your contribution will not be accepted. Make sure to read the [FAQ entry](https://github.com/alex-pinkus/tree-sitter-swift?tab=readme-ov-file#where-is-your-parserc) and the [prior](https://github.com/alex-pinkus/tree-sitter-swift/issues/362) [discussions](https://github.com/alex-pinkus/tree-sitter-swift/pull/315) and [compromises](https://github.com/alex-pinkus/tree-sitter-swift/issues/149) that have occurred already on this topic.
## Using tree-sitter-swift in Web Assembly
To use tree-sitter-swift as a language for the web bindings version tree-sitter, which will likely be a more modern version than the published node
module. [see](https://github.com/tree-sitter/tree-sitter/blob/master/lib/binding_web/README.md). Follow the instructions below
1. Install the node modules `npm install web-tree-sitter tree-sitter-swift`
2. Run the tree-sitter cli to create the wasm bundle
```sh
$ npx tree-sitter build-asm ./node_modules/tree-sitter
```
3. Boot tree-sitter wasm like this.
```js
const Parser = require('web-tree-sitter');
async function run() {
//needs to happen first
await Parser.init();
//wait for the load of swift
const Swift = await Parser.Language.load('./tree-sitter-swift.wasm');
const parser = new Parser();
parser.setLanguage(Swift);
//Parse your swift code here.
const tree = parser.parse('print("Hello, World!")');
}
//if you want to run this
run().then(console.log, console.error);
```
## Frequently asked questions
### Where is your `parser.c`?
This repository currently omits most of the code that is autogenerated during a build. This means, for instance, that
`grammar.json` and `parser.c` are both only available following a build. It also significantly reduces noise during
diffs.
The side benefit of not checking in `parser.c` is that you can guarantee backwards compatibility. Parsers generated by
the tree-sitter CLI aren't always backwards compatible. If you need a parser, generate it yourself using the CLI; all
the information to do so is available in this package. By doing that, you'll also know for sure that your parser version
and your library version are compatible.
If you need a `parser.c`, and you don't care about the tree-sitter version, but you don't have a local setup that would
allow you to obtain the parser, you can just download one from a recent workflow run in this package. To do so:
- Go to the [GitHub actions page](https://github.com/alex-pinkus/tree-sitter-swift/actions) for this
repository.
- Click on the "Publish `grammar.json` and `parser.c`" action for the appropriate commit.
- Go down to `Artifacts` and click on `generated-parser-src`. All the relevant parser files will be available in your
download.

View file

@ -0,0 +1,28 @@
type BaseNode = {
type: string;
named: boolean;
};
type ChildNode = {
multiple: boolean;
required: boolean;
types: BaseNode[];
};
type NodeInfo =
| (BaseNode & {
subtypes: BaseNode[];
})
| (BaseNode & {
fields: { [name: string]: ChildNode };
children: ChildNode[];
});
type Language = {
name: string;
language: unknown;
nodeTypeInfo: NodeInfo[];
};
declare const language: Language;
export = language;

View file

@ -0,0 +1,7 @@
const root = require("path").join(__dirname, "..", "..");
module.exports = require("node-gyp-build")(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View file

@ -0,0 +1,28 @@
{
"name": "tree-sitter-swift",
"version": "0.7.1",
"description": "A tree-sitter grammar for the Swift programming language",
"main": "bindings/node/index.js",
"types": "bindings/node/index.d.ts",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/alex-pinkus/tree-sitter-swift.git"
},
"_vendoredBy": "gitnexus - minimal runtime package copied from official tree-sitter-swift@0.7.1 (gitHead 88bfd19a89be9d0481b14566fb6160cccea2fe0a). Keeps upstream prebuilds while allowing GitNexus to stay on tree-sitter@0.21.1 until #858 is resolved.",
"scripts": {
"install": "node-gyp-build"
},
"dependencies": {
"node-addon-api": "^8.0.0",
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"tree-sitter": "^0.21.1 || ^0.22.1"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
}

File diff suppressed because it is too large Load diff