diff --git a/gitnexus/bench/scope-capture/baselines.json b/gitnexus/bench/scope-capture/baselines.json index 4845f23fb..8cfddc5a7 100644 --- a/gitnexus/bench/scope-capture/baselines.json +++ b/gitnexus/bench/scope-capture/baselines.json @@ -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, diff --git a/gitnexus/src/core/ingestion/languages/swift/callable-visibility.ts b/gitnexus/src/core/ingestion/languages/swift/callable-visibility.ts new file mode 100644 index 000000000..f89b2b639 --- /dev/null +++ b/gitnexus/src/core/ingestion/languages/swift/callable-visibility.ts @@ -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; +} diff --git a/gitnexus/src/core/ingestion/languages/swift/scope-resolver.ts b/gitnexus/src/core/ingestion/languages/swift/scope-resolver.ts index 0822b8cf8..7fe075abf 100644 --- a/gitnexus/src/core/ingestion/languages/swift/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/languages/swift/scope-resolver.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts index 46a80060a..22a7a2d41 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/contract/scope-resolver.ts @@ -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. */ diff --git a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts index 6693a2bc6..af556d0d3 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/passes/free-call-fallback.ts @@ -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, }) diff --git a/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Caller.swift b/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Caller.swift new file mode 100644 index 000000000..dfea6fe9a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Caller.swift @@ -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 } +} diff --git a/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Helpers.swift b/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Helpers.swift new file mode 100644 index 000000000..64c6b6a10 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/swift-injected-closure-call/Helpers.swift @@ -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() + } +} diff --git a/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json b/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json index b6c40d1b1..037d5d01c 100644 --- a/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json +++ b/gitnexus/test/fixtures/swift-captures-golden/expected-captures.json @@ -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" diff --git a/gitnexus/test/integration/resolvers/swift.test.ts b/gitnexus/test/integration/resolvers/swift.test.ts index 5d3e0bd85..76b25a565 100644 --- a/gitnexus/test/integration/resolvers/swift.test.ts +++ b/gitnexus/test/integration/resolvers/swift.test.ts @@ -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 diff --git a/gitnexus/test/unit/scope-resolution/swift/callable-visibility.test.ts b/gitnexus/test/unit/scope-resolution/swift/callable-visibility.test.ts new file mode 100644 index 000000000..7cc8daed5 --- /dev/null +++ b/gitnexus/test/unit/scope-resolution/swift/callable-visibility.test.ts @@ -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); + }); +});