mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-06 02:49:56 +00:00
fix(swift): avoid false calls for injected closure properties (#3434)
This commit is contained in:
parent
64fd67388f
commit
a8f18f00b9
10 changed files with 545 additions and 7 deletions
|
|
@ -119,7 +119,8 @@
|
|||
"_rebaselined_3354_callable_alternatives": "#3354 callable alternatives: a callable chosen by a value-selecting source now flows every branch it can yield (`a ?? b`, `a || b`, `a or b`, `c ? a : b`, statement `if`, elvis), and an operator branch (`x == f || g`) stays opaque instead of seeding a qualified name. Verified by running the BASE (merge-base 233ca2849) and HEAD emitters over the SAME HEAD fixture corpus: every added or removed match is an `@callable-flow.*` match on one of those sources, and the pre-existing corpus is byte-identical (all other languages: zero delta). The rest of the drift is corpus growth from this PR's regression fixture, which this bench globs. Corpus growth: ruby-callable-alternatives/app.rb (+1 file, +58 groups). Emitter delta: +5 / -0: seeds for single-statement `if` / `elsif` branches (run_then, run_else, run_sweep, run_a, run_b); the multi-statement branch contributes nothing. capture_groups_fp 1358 -> 1416, fixture_count 91 -> 92; synthetic counts unchanged; scaling 1.04 < 1.5. Prior 1c8c9c4b54036fa24c2a81e39ea530e938645c856d369075e5f437da78218c57 -> 45e65d9fa8a9e5e905ddb596b179b77c86ed13ab5139c6b17ccaaf7315dfe46a."
|
||||
},
|
||||
"swift": {
|
||||
"fingerprint": "d56406c2645637042899cfcc8dc73f603d77caef0a9a2bac179ffbec848258a3",
|
||||
"fingerprint": "2507ac75ba6fb47c272a5caa97b5e8b9f6ebc5098c15295d05c7bff3cabc321a",
|
||||
"_rebaselined_3425_injected_closure": "Swift capture fixture growth: 83 to 85 fixtures, 1436 to 1553 capture groups; scaling budget unchanged.",
|
||||
"_rebaselined_3355_xcode_and_import_fixtures": "#3355 follow-up: new swift-xcode-targets fixture (four sources) and two sources added to swift-nested-packages (a Docs/Net folder decoy and an `import Net` caller). The capture query is unchanged; this is fixture-corpus growth only. capture_groups_fp 1393 -> 1436 and fixture_count 77 -> 83; synthetic scale counts remain 5012/16012. Prior aea33bf12f57561be7e3125929fb98cec7aed5a681e25aad435c7bb558747853 -> d56406c2645637042899cfcc8dc73f603d77caef0a9a2bac179ffbec848258a3; measured scaling 1.036 < 1.5.",
|
||||
"_rebaselined_3355_nested_packages": "#3355: new swift-nested-packages fixture (three nested Package.swift manifests, six sources) for nested-package module grouping. The capture query is unchanged; this is fixture-corpus growth only. capture_groups_fp 1333 -> 1393 and fixture_count 68 -> 77; synthetic scale counts remain 5012/16012. Prior c9fc553662f0db18027fba882e3ff730744f6153cc46eca7b00667fabd6a0df8 -> aea33bf12f57561be7e3125929fb98cec7aed5a681e25aad435c7bb558747853; measured scaling 1.023 < 1.5.",
|
||||
"scaling_budget": 1.5,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import type { ParsedFile, ScopeId, SymbolDefinition, TypeRef } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { findClassBindingInScope } from '../../scope-resolution/scope/walkers.js';
|
||||
|
||||
export function swiftIsCallableVisibleFromCaller(ctx: {
|
||||
readonly candidate: SymbolDefinition;
|
||||
readonly callerParsed?: ParsedFile;
|
||||
readonly callArity?: number;
|
||||
readonly callerScope?: ScopeId;
|
||||
readonly scopes?: ScopeResolutionIndexes;
|
||||
}): boolean {
|
||||
const indexes = ctx.scopes;
|
||||
if (ctx.callerScope === undefined || indexes === undefined || ctx.callArity !== 0) return true;
|
||||
|
||||
const name = ctx.candidate.qualifiedName?.split('.').at(-1);
|
||||
if (name === undefined) return true;
|
||||
|
||||
let scopeId: ScopeId | null = ctx.callerScope;
|
||||
let selfType: TypeRef | undefined;
|
||||
while (scopeId !== null) {
|
||||
const scope = indexes.scopeTree.getScope(scopeId);
|
||||
if (scope === undefined) break;
|
||||
// A local function selected inside the caller wins before member lookup.
|
||||
if (
|
||||
scope.kind !== 'Class' &&
|
||||
scope.ownedDefs.some((def) => def.nodeId === ctx.candidate.nodeId)
|
||||
)
|
||||
return true;
|
||||
selfType ??= scope.typeBindings.get('self');
|
||||
if (scope.kind === 'Class') {
|
||||
const classDef =
|
||||
scope.ownedDefs.find((def) => def.type === 'Class') ??
|
||||
(selfType === undefined
|
||||
? undefined
|
||||
: findClassBindingInScope(ctx.callerScope, selfType.rawName, indexes, undefined, {
|
||||
uniqueQualifiedNameFallback: false,
|
||||
}));
|
||||
const propertyOnOwner = (ownerId: string, ownerName: string): boolean =>
|
||||
indexes.qualifiedNames.get(`${ownerName}.${name}`).some((defId) => {
|
||||
const def = indexes.defs.get(defId);
|
||||
return def?.type === 'Property' && def.ownerId === ownerId;
|
||||
});
|
||||
const ownProperty =
|
||||
scope.ownedDefs.some(
|
||||
(def) => def.type === 'Property' && def.qualifiedName?.split('.').at(-1) === name,
|
||||
) ||
|
||||
(classDef !== undefined &&
|
||||
classDef.qualifiedName !== undefined &&
|
||||
propertyOnOwner(classDef.nodeId, classDef.qualifiedName));
|
||||
const inheritedProperty =
|
||||
classDef !== undefined &&
|
||||
indexes.methodDispatch.mroFor(classDef.nodeId).some((ownerId) => {
|
||||
const ownerName = indexes.defs.get(ownerId)?.qualifiedName;
|
||||
return ownerName !== undefined && propertyOnOwner(ownerId, ownerName);
|
||||
});
|
||||
if (!ownProperty && !inheritedProperty) return true;
|
||||
|
||||
// Swift's nested function is owned by its own Function scope. A
|
||||
// same-file sibling must not count as a nearer lexical binding.
|
||||
const declarationScope = ctx.callerParsed?.scopes.find((candidateScope) =>
|
||||
candidateScope.ownedDefs.some((def) => def.nodeId === ctx.candidate.nodeId),
|
||||
);
|
||||
if (
|
||||
declarationScope !== undefined &&
|
||||
declarationScope.kind === 'Function' &&
|
||||
(declarationScope.id === ctx.callerScope ||
|
||||
indexes.scopeTree.getAncestors(declarationScope.id).includes(ctx.callerScope))
|
||||
)
|
||||
return true;
|
||||
|
||||
// Scope defs do not carry Swift access modifiers. A method positively
|
||||
// selected on this type must not be vetoed by an uncertain ancestor.
|
||||
return classDef !== undefined && ctx.candidate.ownerId === classDef.nodeId;
|
||||
}
|
||||
scopeId = scope.parent;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ import {
|
|||
import { stripSwiftTypePreservingDecoration } from './interpret.js';
|
||||
import { groupSwiftFilesByModule } from './target-grouping.js';
|
||||
import { swiftIsGlobalNameFallbackPlausible } from './name-fallback-visibility.js';
|
||||
import { swiftIsCallableVisibleFromCaller } from './callable-visibility.js';
|
||||
|
||||
const ZERO_RANGE = { startLine: 0, startCol: 0, endLine: 0, endCol: 0 } as const;
|
||||
|
||||
|
|
@ -155,6 +156,7 @@ const swiftScopeResolver: ScopeResolver = {
|
|||
// no-`new` constructor + cross-file free-call shape).
|
||||
allowGlobalFreeCallFallback: true,
|
||||
isGlobalNameFallbackPlausible: swiftIsGlobalNameFallbackPlausible,
|
||||
isCallableVisibleFromCaller: swiftIsCallableVisibleFromCaller,
|
||||
|
||||
// Swift's call graph models `Type(...)` as a reference to the type
|
||||
// itself, not its `init` — both the legacy DAG and this test suite link
|
||||
|
|
|
|||
|
|
@ -1235,6 +1235,9 @@ export interface ScopeResolver {
|
|||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
readonly candidate: SymbolDefinition;
|
||||
/** Arity of the actual call, when known. A visibility veto must not
|
||||
* infer applicability from a name match alone. */
|
||||
readonly callArity?: number;
|
||||
/** Caller's enclosing scope id. Languages that gate visibility on
|
||||
* caller scope (e.g. C++ two-phase template lookup) consult it;
|
||||
* others ignore. Optional so existing implementations stay valid. */
|
||||
|
|
|
|||
|
|
@ -84,12 +84,7 @@ export function emitFreeCallFallback(
|
|||
* appended to its reason. See `ScopeResolver.markConstructionSites`. */
|
||||
readonly markConstructionSites?: boolean;
|
||||
readonly isFileLocalDef?: (def: SymbolDefinition) => boolean;
|
||||
readonly isCallableVisibleFromCaller?: (ctx: {
|
||||
readonly callerParsed: ParsedFile;
|
||||
readonly candidate: SymbolDefinition;
|
||||
readonly callerScope?: ScopeId;
|
||||
readonly scopes?: ScopeResolutionIndexes;
|
||||
}) => boolean;
|
||||
readonly isCallableVisibleFromCaller?: ScopeResolver['isCallableVisibleFromCaller'];
|
||||
readonly resolveAdlCandidates?: (
|
||||
site: {
|
||||
readonly name: string;
|
||||
|
|
@ -620,6 +615,7 @@ export function emitFreeCallFallback(
|
|||
options.isCallableVisibleFromCaller!({
|
||||
callerParsed: parsed,
|
||||
candidate,
|
||||
callArity: site.arity,
|
||||
callerScope: site.inScope,
|
||||
scopes,
|
||||
})
|
||||
|
|
@ -698,6 +694,7 @@ export function emitFreeCallFallback(
|
|||
!options.isCallableVisibleFromCaller({
|
||||
callerParsed: parsed,
|
||||
candidate: fnDef,
|
||||
callArity: site.arity,
|
||||
callerScope: site.inScope,
|
||||
scopes,
|
||||
})
|
||||
|
|
|
|||
54
gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Caller.swift
vendored
Normal file
54
gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Caller.swift
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import Foundation
|
||||
|
||||
enum Example {
|
||||
static func runScenario() -> Int {
|
||||
makeValue(input: 1)
|
||||
}
|
||||
}
|
||||
|
||||
final class Service {
|
||||
private let clock: () -> Date
|
||||
|
||||
init(clock: @escaping () -> Date) {
|
||||
self.clock = clock
|
||||
}
|
||||
|
||||
func refreshValue() -> Date {
|
||||
clock()
|
||||
}
|
||||
|
||||
func refreshWithLocalClock() -> Int {
|
||||
func clock() -> Int { 2 }
|
||||
return clock()
|
||||
}
|
||||
}
|
||||
|
||||
class BaseService {
|
||||
let clock: () -> Date
|
||||
|
||||
init(clock: @escaping () -> Date) {
|
||||
self.clock = clock
|
||||
}
|
||||
}
|
||||
|
||||
final class DerivedService: BaseService {
|
||||
func refreshInheritedValue() -> Date {
|
||||
clock()
|
||||
}
|
||||
}
|
||||
|
||||
final class LabeledService {
|
||||
let first: Int = 1
|
||||
|
||||
func first(where value: Bool) -> Int {
|
||||
value ? 2 : 0
|
||||
}
|
||||
|
||||
func refreshLabeled() -> Int {
|
||||
first(where: true)
|
||||
}
|
||||
}
|
||||
|
||||
class PrivateBase {
|
||||
private let clock: () -> Int = { 1 }
|
||||
}
|
||||
39
gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Helpers.swift
vendored
Normal file
39
gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Helpers.swift
vendored
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import Foundation
|
||||
|
||||
extension Example {
|
||||
static func makeValue(input: Int) -> Int {
|
||||
input + 1
|
||||
}
|
||||
}
|
||||
|
||||
enum Other {
|
||||
private static func makeValue(other: String) -> Int {
|
||||
-1
|
||||
}
|
||||
|
||||
static func clock() -> Date {
|
||||
Date.distantPast
|
||||
}
|
||||
}
|
||||
|
||||
extension DerivedService {
|
||||
func refreshInheritedFromExtension() -> Date {
|
||||
clock()
|
||||
}
|
||||
}
|
||||
|
||||
extension BaseService {
|
||||
func refreshOwnFromExtension() -> Date {
|
||||
clock()
|
||||
}
|
||||
}
|
||||
|
||||
final class PrivateDerived: PrivateBase {
|
||||
func clock() -> Int {
|
||||
2
|
||||
}
|
||||
|
||||
func refreshPrivateAncestor() -> Int {
|
||||
clock()
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +127,14 @@
|
|||
"captureGroups": 9,
|
||||
"digest": "c4d442b67247406de3b1158b958f6f3e294f11c0bbb360ed32489582769a1187"
|
||||
},
|
||||
"swift-injected-closure-call/Caller.swift": {
|
||||
"captureGroups": 69,
|
||||
"digest": "6768d62722a6b173dbe953507fec53661ea6c9786a49d8edf7caedb0a3cef0e4"
|
||||
},
|
||||
"swift-injected-closure-call/Helpers.swift": {
|
||||
"captureGroups": 48,
|
||||
"digest": "6e6bfe83084bf189a7789702cfccb5fcc5753986b0de3c0d7f81fc3ae59270b2"
|
||||
},
|
||||
"swift-member-write-access/App.swift": {
|
||||
"captureGroups": 14,
|
||||
"digest": "7be68b7e510fefac99c94a4ebab3eb96556a0e587155b6c82b7602705e270987"
|
||||
|
|
|
|||
|
|
@ -359,6 +359,95 @@ describe.skipIf(!swiftAvailable)('Swift protocol-extension implicit self (#3273)
|
|||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!swiftAvailable)('Swift injected closure property call (#3425)', () => {
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(
|
||||
path.join(FIXTURES, 'swift-injected-closure-call'),
|
||||
() => {},
|
||||
);
|
||||
}, 60000);
|
||||
|
||||
it('does not resolve an injected closure call to an unrelated method', () => {
|
||||
expect(
|
||||
getNodesByLabelFull(result, 'Property').some(
|
||||
(node) => node.name === 'clock' && node.properties.filePath === 'Caller.swift',
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
getNodesByLabelFull(result, 'Function').some(
|
||||
(node) => node.name === 'clock' && node.properties.filePath === 'Helpers.swift',
|
||||
),
|
||||
).toBe(true);
|
||||
const calls = getRelationships(result, 'CALLS').filter((c) => c.source === 'refreshValue');
|
||||
expect(calls.filter((c) => c.target === 'clock')).toEqual([]);
|
||||
});
|
||||
|
||||
it('does not resolve an inherited closure property call to the unrelated method', () => {
|
||||
const extendsEdges = getRelationships(result, 'EXTENDS');
|
||||
expect(
|
||||
extendsEdges.some(
|
||||
(edge) => edge.source === 'DerivedService' && edge.target === 'BaseService',
|
||||
),
|
||||
).toBe(true);
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'refreshInheritedValue',
|
||||
);
|
||||
expect(calls.filter((c) => c.target === 'clock')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps inherited closure calls in extensions unlinked', () => {
|
||||
expect(
|
||||
getNodesByLabelFull(result, 'Function').some(
|
||||
(node) =>
|
||||
node.name === 'refreshInheritedFromExtension' &&
|
||||
node.properties.filePath === 'Helpers.swift',
|
||||
),
|
||||
).toBe(true);
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'refreshInheritedFromExtension',
|
||||
);
|
||||
expect(calls.filter((c) => c.target === 'clock')).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps same-type closure calls in extensions unlinked', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'refreshOwnFromExtension',
|
||||
);
|
||||
expect(calls.filter((c) => c.target === 'clock')).toEqual([]);
|
||||
});
|
||||
|
||||
it('still resolves the concrete-type extension call', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter((c) => c.source === 'runScenario');
|
||||
expect(calls.map((c) => c.rel.targetId)).toEqual([
|
||||
'Function:Helpers.swift:Example.makeValue#1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a nested function that shadows the stored closure', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'refreshWithLocalClock',
|
||||
);
|
||||
expect(calls.filter((c) => c.target === 'clock')).toHaveLength(1);
|
||||
expect(calls.find((c) => c.target === 'clock')?.targetFilePath).toBe('Caller.swift');
|
||||
});
|
||||
|
||||
it('keeps a labeled method selected alongside a same-name property', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter((c) => c.source === 'refreshLabeled');
|
||||
expect(calls.map((c) => c.target)).toContain('first');
|
||||
expect(calls.find((c) => c.target === 'first')?.targetFilePath).toBe('Caller.swift');
|
||||
});
|
||||
|
||||
it('keeps a derived method despite an inaccessible ancestor property', () => {
|
||||
const calls = getRelationships(result, 'CALLS').filter(
|
||||
(c) => c.source === 'refreshPrivateAncestor',
|
||||
);
|
||||
expect(calls.map((c) => c.target)).toContain('clock');
|
||||
expect(calls.find((c) => c.target === 'clock')?.targetFilePath).toBe('Helpers.swift');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor fallback: Swift constructors look like free function calls
|
||||
// (no `new` keyword). The resolver retries with constructor form when
|
||||
|
|
|
|||
|
|
@ -0,0 +1,267 @@
|
|||
import {
|
||||
buildDefIndex,
|
||||
buildMethodDispatchIndex,
|
||||
buildQualifiedNameIndex,
|
||||
buildScopeTree,
|
||||
type Scope,
|
||||
type SymbolDefinition,
|
||||
} from 'gitnexus-shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { swiftIsCallableVisibleFromCaller } from '../../../../src/core/ingestion/languages/swift/callable-visibility.js';
|
||||
import type { ScopeResolutionIndexes } from '../../../../src/core/ingestion/model/scope-resolution-indexes.js';
|
||||
|
||||
const filePath = 'Service.swift';
|
||||
const moduleRange = { startLine: 1, startCol: 0, endLine: 12, endCol: 0 };
|
||||
const classRange = { startLine: 2, startCol: 0, endLine: 10, endCol: 0 };
|
||||
const functionRange = { startLine: 5, startCol: 0, endLine: 8, endCol: 0 };
|
||||
|
||||
function scope(
|
||||
id: string,
|
||||
parent: string | null,
|
||||
kind: Scope['kind'],
|
||||
ownedDefs: SymbolDefinition[],
|
||||
range: Scope['range'],
|
||||
): Scope {
|
||||
return {
|
||||
id,
|
||||
parent,
|
||||
kind,
|
||||
range,
|
||||
filePath,
|
||||
ownedDefs,
|
||||
bindings: new Map(),
|
||||
imports: [],
|
||||
typeBindings: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
const classDef: SymbolDefinition = {
|
||||
nodeId: 'Service',
|
||||
filePath,
|
||||
type: 'Class',
|
||||
qualifiedName: 'Service',
|
||||
};
|
||||
const property: SymbolDefinition = {
|
||||
nodeId: 'Service.clock',
|
||||
filePath,
|
||||
type: 'Property',
|
||||
qualifiedName: 'Service.clock',
|
||||
ownerId: 'Service',
|
||||
};
|
||||
const method: SymbolDefinition = {
|
||||
nodeId: 'Service.refresh',
|
||||
filePath,
|
||||
type: 'Method',
|
||||
qualifiedName: 'Service.refresh',
|
||||
ownerId: 'Service',
|
||||
};
|
||||
const scopes = {
|
||||
scopeTree: buildScopeTree([
|
||||
scope('module', null, 'Module', [], moduleRange),
|
||||
scope('class', 'module', 'Class', [classDef, property], classRange),
|
||||
scope('function', 'class', 'Function', [method], functionRange),
|
||||
]),
|
||||
defs: buildDefIndex([classDef, property, method]),
|
||||
qualifiedNames: buildQualifiedNameIndex([classDef, property, method]),
|
||||
methodDispatch: buildMethodDispatchIndex({
|
||||
owners: [classDef.nodeId],
|
||||
computeMro: () => [],
|
||||
implementsOf: () => [],
|
||||
}),
|
||||
} as ScopeResolutionIndexes;
|
||||
|
||||
describe('Swift caller-side callable visibility', () => {
|
||||
it('rejects an unrelated same-name method shadowed by a stored property', () => {
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: {
|
||||
nodeId: 'Other.clock',
|
||||
filePath: 'Other.swift',
|
||||
type: 'Method',
|
||||
qualifiedName: 'Other.clock',
|
||||
},
|
||||
callerScope: 'function',
|
||||
callArity: 0,
|
||||
scopes,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps differently named methods and unknown caller scopes eligible', () => {
|
||||
const candidate: SymbolDefinition = {
|
||||
nodeId: 'Other.run',
|
||||
filePath: 'Other.swift',
|
||||
type: 'Method',
|
||||
qualifiedName: 'Other.run',
|
||||
};
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate,
|
||||
callerScope: 'function',
|
||||
callArity: 0,
|
||||
scopes,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: { ...candidate, qualifiedName: 'Other.clock' },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves a selected local function before checking the enclosing property', () => {
|
||||
const local: SymbolDefinition = {
|
||||
nodeId: 'local.clock',
|
||||
filePath,
|
||||
type: 'Function',
|
||||
qualifiedName: 'Service.refresh.clock',
|
||||
};
|
||||
const localScopes = {
|
||||
...scopes,
|
||||
scopeTree: buildScopeTree([
|
||||
scope('module', null, 'Module', [], moduleRange),
|
||||
scope('class', 'module', 'Class', [classDef, property], classRange),
|
||||
scope('function', 'class', 'Function', [method], functionRange),
|
||||
scope('local', 'function', 'Function', [local], {
|
||||
startLine: 6,
|
||||
startCol: 0,
|
||||
endLine: 7,
|
||||
endCol: 0,
|
||||
}),
|
||||
]),
|
||||
} as ScopeResolutionIndexes;
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: local,
|
||||
callerParsed: {
|
||||
filePath,
|
||||
moduleScope: 'module',
|
||||
scopes: [...localScopes.scopeTree.byId.values()],
|
||||
localDefs: [classDef, property, method, local],
|
||||
parsedImports: [],
|
||||
referenceSites: [],
|
||||
},
|
||||
callerScope: 'function',
|
||||
callArity: 0,
|
||||
scopes: localScopes,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('preserves a selected method owned by the current type', () => {
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: { ...method, nodeId: 'Service.clock', qualifiedName: 'Service.clock' },
|
||||
callerScope: 'function',
|
||||
callArity: 0,
|
||||
scopes,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('does not veto a call with arguments or unknown arity', () => {
|
||||
const candidate: SymbolDefinition = {
|
||||
nodeId: 'Other.clock',
|
||||
filePath: 'Other.swift',
|
||||
type: 'Method',
|
||||
qualifiedName: 'Other.clock',
|
||||
};
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate,
|
||||
callerScope: 'function',
|
||||
callArity: 1,
|
||||
scopes,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(swiftIsCallableVisibleFromCaller({ candidate, callerScope: 'function', scopes })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a decoy when a superclass owns the closure property', () => {
|
||||
const base: SymbolDefinition = {
|
||||
nodeId: 'BaseService',
|
||||
filePath,
|
||||
type: 'Class',
|
||||
qualifiedName: 'BaseService',
|
||||
};
|
||||
const derived: SymbolDefinition = {
|
||||
nodeId: 'DerivedService',
|
||||
filePath,
|
||||
type: 'Class',
|
||||
qualifiedName: 'DerivedService',
|
||||
};
|
||||
const inheritedProperty: SymbolDefinition = {
|
||||
nodeId: 'BaseService.clock',
|
||||
filePath,
|
||||
type: 'Property',
|
||||
qualifiedName: 'BaseService.clock',
|
||||
ownerId: base.nodeId,
|
||||
};
|
||||
const inheritedScopes = {
|
||||
scopeTree: buildScopeTree([
|
||||
scope('derivedModule', null, 'Module', [], moduleRange),
|
||||
scope('derivedClass', 'derivedModule', 'Class', [derived], classRange),
|
||||
scope('derivedFunction', 'derivedClass', 'Function', [method], functionRange),
|
||||
]),
|
||||
defs: buildDefIndex([base, derived, inheritedProperty]),
|
||||
qualifiedNames: buildQualifiedNameIndex([base, derived, inheritedProperty]),
|
||||
methodDispatch: buildMethodDispatchIndex({
|
||||
owners: [derived.nodeId],
|
||||
computeMro: () => [base.nodeId],
|
||||
implementsOf: () => [],
|
||||
}),
|
||||
} as ScopeResolutionIndexes;
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: {
|
||||
nodeId: 'Other.clock',
|
||||
filePath: 'Other.swift',
|
||||
type: 'Method',
|
||||
qualifiedName: 'Other.clock',
|
||||
},
|
||||
callerScope: 'derivedFunction',
|
||||
callArity: 0,
|
||||
scopes: inheritedScopes,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
const extensionScopes = {
|
||||
...inheritedScopes,
|
||||
bindings: new Map([
|
||||
[
|
||||
'extensionModule',
|
||||
new Map([['DerivedService', [{ def: derived, origin: 'local' as const }]]]),
|
||||
],
|
||||
]),
|
||||
bindingAugmentations: new Map(),
|
||||
scopeTree: buildScopeTree([
|
||||
scope('extensionModule', null, 'Module', [], moduleRange),
|
||||
scope('extensionClass', 'extensionModule', 'Class', [], classRange),
|
||||
{
|
||||
...scope('extensionFunction', 'extensionClass', 'Function', [method], functionRange),
|
||||
typeBindings: new Map([
|
||||
[
|
||||
'self',
|
||||
{ rawName: 'DerivedService', declaredAtScope: 'extensionFunction', source: 'self' },
|
||||
],
|
||||
]),
|
||||
},
|
||||
]),
|
||||
} as ScopeResolutionIndexes;
|
||||
expect(
|
||||
swiftIsCallableVisibleFromCaller({
|
||||
candidate: {
|
||||
nodeId: 'Other.clock',
|
||||
filePath: 'Other.swift',
|
||||
type: 'Method',
|
||||
qualifiedName: 'Other.clock',
|
||||
},
|
||||
callerScope: 'extensionFunction',
|
||||
callArity: 0,
|
||||
scopes: extensionScopes,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue