From a39fd14e89479ea7112beb04d7a97cfeac883362 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Wed, 30 Sep 2026 13:43:36 +0000 Subject: [PATCH] fix(swift): read a call's shape before reaching a member by name (#2170) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Swift's extractor keeps one receiver level, so super.init(…), a longer chain's removeAll() and a bare min(a, b) all arrive as bare names and exact-matched whichever project member shared the name. A bare or self./ super. call now reaches only a member of the types around it and what they inherit or conform to (source-read heads, UIKit ancestry, the standard collection protocols), nearest first; a standard-named chain link needs a receiver that names the owner, an outside owner, or an argument label only the project's method takes; playground and test-target globals stay put. Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/swift-call-shape.test.ts | 133 ++++++++++++++ src/resolution/name-matcher.ts | 273 ++++++++++++++++++++++++++++- 3 files changed, 406 insertions(+), 1 deletion(-) create mode 100644 __tests__/swift-call-shape.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e2980670..7034f8e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- In Swift, a call written with no receiver, or through `self.` or `super.`, now reaches only a member of the type around it or of what that type inherits or conforms to, and the nearest override wins. A standard-library name later in a chain (`axis.entries.removeAll()`, `names.contains(x)`) no longer lands on a project type's method, unless the receiver or the call's argument label points to it. On Charts, `min(a, b)` and `max(a, b)` used to land on a range type's `min` field and a `Sequence` extension, and `super.init(…)` on an Objective-C demo's `init` over 170 times. A global in a playground or a test target is no longer linked from the main code. Re-index after upgrading. - A call on a variable declared with a type from outside the project (`List list`, `var sb = new StringBuilder()`, `signer = TimestampSigner()`) no longer lands on a project type's method with the same name. On gson, `list.add(…)` used to land on a project list wrapper; on commons-lang, `s.length()` and `buffer.append(…)` on project classes. In Python, a keyword argument inside a call (`prefix=IPNetwork(…)`) is no longer taken for an assignment to that name. - Kotlin classes whose primary constructor sits on the line after the class name, before a supertype list (`expect open class ByteString` / `internal constructor(…) : Comparable`, common in Kotlin Multiplatform code), no longer lose their members to loose functions. A call later in a chain to a Kotlin standard-library function (`apply`, `let`, `toString`, `map`, `contains`…) no longer lands on a project type's same-named method. On okhttp, `.apply { … }` used to land on `ConnectionSpec.apply`. Re-index after upgrading. - In Rust and Go, a call later in a chain, like `sym.filename().map(…)` or `c.Flags().String(…)`, no longer lands on the project's own method of that name when the name is a standard-library one (`unwrap`, `clone`, `map`, `iter`, Go's `String` or `Error`). A plain call like `get(handler)` or `drop(x)` never lands on a method. On tokio, `.map(…)` on an `Option` used to land on `MutexGuard::map`; on axum, the router's `get(handler)` on a cookie jar's `get`; on ripgrep, `.clone()` on an error type. diff --git a/__tests__/swift-call-shape.test.ts b/__tests__/swift-call-shape.test.ts new file mode 100644 index 00000000..0f999609 --- /dev/null +++ b/__tests__/swift-call-shape.test.ts @@ -0,0 +1,133 @@ +/** + * Swift's extractor keeps one receiver level, so `super.reset()`, + * `axis.entries.removeAll()` and `min(a, b)` reach the resolver as bare + * names. How the call is written decides what it can mean: + * + * - bare or through `self.`: a member of a type around the call or of what it + * inherits or conforms to (UIKit ancestry and the standard collection + * protocols included), the nearest first — `super.`: the latter only. + * Charts' `min(a, b)` went to a range type's `min` field, `max(a, b)` to a + * `Sequence` extension, `super.init(…)` to an Objective-C demo's `init`; + * - down a longer chain, a name the standard types all carry is theirs unless + * the receiver names the owner or the call gives a label only the project's + * method takes (Kingfisher's `data.kf.contains(jpeg:)`); + * - a global `let` in a playground is nobody else's call target. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; + +let root = ''; +let cg: CodeGraph; + +beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-swift-shape-')); + const files: Record = { + 'Sources/Charts/Utils/Sequence+KeyPath.swift': `extension Sequence { + func max(by keyPath: KeyPath) -> Element? { + return nil + } +} +`, + 'Sources/Charts/Renderers/Renderer.swift': `open class Renderer { + public struct XBounds { + public var min: Int = 0 + } +} +`, + 'Sources/Charts/Data/ChartData.swift': `open class ChartData { + open func removeAll() { } +} +`, + 'Sources/Charts/Formatters/Formatter.swift': `open class Formatter { + open func reset() { } +} +`, + 'Sources/Charts/Charts/ChartViewBase.swift': `open class ChartViewBase: NSObject { + open func highlightValue(_ h: Int) { } + open func reset() { } +} +`, + 'Sources/Charts/Charts/BarChartView.swift': `open class BarChartView: ChartViewBase { + open override func highlightValue(_ h: Int) { } +} +`, + 'Sources/Charts/Charts/BarLineChartViewBase.swift': `open class BarLineChartViewBase: ChartViewBase { + open func tapped(axis: Axis) { + highlightValue(1) + let widest = max(1, 2) + let narrowest = min(3, 4) + super.reset() + axis.entries.removeAll() + } +} +`, + 'Sources/Charts/Wrapper/KingfisherWrapper.swift': `public struct KingfisherWrapper { + public let base: Base +} + +extension KingfisherWrapper where Base == Data { + func contains(jpeg marker: Int) -> Bool { + return false + } +} +`, + 'Sources/Charts/Wrapper/Decoder.swift': `final class Decoder { + func scan(data: Data) -> Bool { + return data.kf.contains(jpeg: 0xC2) + } +} +`, + 'Demo/Extensions/UIViewController+Bar.swift': `extension UIViewController { + func setupOperationBar() { } +} +`, + 'Demo/Controllers/ListController.swift': `class ListController: UICollectionViewController { + override func viewDidLoad() { + setupOperationBar() + } +} +`, + 'Demo/Chart.playground/Contents.swift': `let min = 20.0 +`, + }; + for (const [rel, content] of Object.entries(files)) { + fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); + fs.writeFileSync(path.join(root, rel), content); + } + cg = await CodeGraph.init(root, { index: true }); +}); + +afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); +}); + +/** `Owner::member` of every call edge out of a file. */ +function callsFrom(file: string): string[] { + const ids = cg.getNodesInFile(file).map((n) => n.id); + return cg + .getOutgoingEdgesFrom(ids) + .filter((e) => e.kind === 'calls') + .map((e) => cg.getNode(e.target)!.qualifiedName) + .sort(); +} + +describe('Swift calls reached by name alone', () => { + it('reach the nearest inherited member, and nothing outside the hierarchy', () => { + expect(callsFrom('Sources/Charts/Charts/BarLineChartViewBase.swift')).toEqual([ + 'ChartViewBase::highlightValue', + 'ChartViewBase::reset', + ]); + }); + + it('keep a standard-named method the call labels as the project’s', () => { + expect(callsFrom('Sources/Charts/Wrapper/Decoder.swift')).toEqual(['KingfisherWrapper::contains']); + }); + + it('reach an extension of a UIKit ancestor', () => { + expect(callsFrom('Demo/Controllers/ListController.swift')).toEqual(['UIViewController::setupOperationBar']); + }); +}); diff --git a/src/resolution/name-matcher.ts b/src/resolution/name-matcher.ts index 7d9d0bff..ee53037d 100644 --- a/src/resolution/name-matcher.ts +++ b/src/resolution/name-matcher.ts @@ -1616,6 +1616,260 @@ function isKotlinStdChainTarget(n: Node, receiver: string): boolean { return receiver === 'this' || (receiver !== '' && sharesReceiverWord(receiver, n)); } +/** + * How a Swift call is written: `bare` (implicit self, or a free function), + * through `self.` / `super.`, or `chained` on some other receiver. The + * extractor keeps one receiver level, so `super.init(…)`, + * `axis.entries.removeAll()` and `min(a, b)` all arrive as bare names. + */ +interface SwiftCallShape { + shape: 'bare' | 'self' | 'super' | 'chained'; + /** A chained call's receiver, calls and subscripts dropped. */ + receiver: string; + /** The first argument's label, if any. */ + label: string; + /** `name[…]`: a subscript of a value, not a call. */ + subscript: boolean; +} + +function swiftCallShape(ref: UnresolvedRef, context: ResolutionContext): SwiftCallShape | null { + const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1]; + if (line === undefined) return null; + const name = ref.referenceName; + let start = line.startsWith(name, ref.column) && !/[\w$]/.test(line[ref.column - 1] ?? '') ? ref.column : -1; + if (start < 0) { + const m = new RegExp(`(? = new Set([ + 'contentsOf', 'where', 'by', 'at', 'keepingCapacity', 'separator', 'forKey', 'of', 'into', 'in', 'options', + 'maxSplits', 'omittingEmptySubsequences', 'with', 'after', 'before', 'upTo', 'through', 'offsetBy', 'default', +]); + +/** + * Whether a Swift method's declaration takes the argument label a call gives + * first (`func contains(jpeg marker: JPEGMarker)` for `data.kf.contains(jpeg: + * .SOF2)`). Labels are part of a Swift method's name, so one the standard + * library's namesakes never take identifies the project's. + */ +function swiftDeclaresLabel(n: Node, label: string, context: ResolutionContext): boolean { + if (label === '' || SWIFT_STD_LABELS.has(label)) return false; + const lines = context.getFileLines?.(n.filePath) ?? context.readFile(n.filePath)?.split(/\r?\n/) ?? []; + const head = lines.slice(n.startLine - 1, n.startLine + 3).join(' '); + return new RegExp(`\\bfunc\\s+${n.name}\\s*(?:<[^>]*>)?\\s*\\(\\s*${label}\\b`).test(head); +} + +/** + * What the standard library's collection types and protocols refine, so a + * project extension of `Collection` is in reach of a type that conforms to + * `RandomAccessCollection`. UIKit / AppKit ancestry comes from the + * Objective-C table. + */ +const SWIFT_STD_SUPERS: Readonly> = { + RandomAccessCollection: ['BidirectionalCollection'], BidirectionalCollection: ['Collection'], + MutableCollection: ['Collection'], RangeReplaceableCollection: ['Collection'], Collection: ['Sequence'], + LazySequenceProtocol: ['Sequence'], LazyCollectionProtocol: ['Collection', 'LazySequenceProtocol'], + StringProtocol: ['BidirectionalCollection'], + Array: ['RandomAccessCollection', 'MutableCollection', 'RangeReplaceableCollection'], + ArraySlice: ['RandomAccessCollection', 'MutableCollection', 'RangeReplaceableCollection'], + ContiguousArray: ['RandomAccessCollection', 'MutableCollection', 'RangeReplaceableCollection'], + String: ['StringProtocol', 'RangeReplaceableCollection'], Substring: ['StringProtocol', 'RangeReplaceableCollection'], + Dictionary: ['Collection'], Set: ['Collection', 'SetAlgebra'], Range: ['RandomAccessCollection'], + ClosedRange: ['RandomAccessCollection'], +}; + +/** Swift declarations a member hangs off: types, protocols, and extensions (extracted as classes). */ +const SWIFT_TYPE_KINDS: ReadonlySet = new Set(['class', 'struct', 'enum', 'interface']); +const SWIFT_MEMBER_KINDS: ReadonlySet = new Set(['method', 'property', 'field', 'enum_member']); +const SWIFT_DECLS = new WeakMap>(); +const SWIFT_HIERARCHIES = new WeakMap>>(); + +/** + * What the project declares a Swift type as: the supertypes and protocols + * every declaration and extension of it names, and whether any of them is + * the type itself rather than an extension of an outside one + * (`extension Sequence`). + */ +function swiftDeclOf(typeName: string, context: ResolutionContext): { supers: string[]; projectType: boolean } { + let memo = SWIFT_DECLS.get(context); + if (!memo) { + memo = new Map(); + SWIFT_DECLS.set(context, memo); + } + const hit = memo.get(typeName); + if (hit) return hit; + const system = OBJC_SYSTEM_SUPERS[typeName]; + const info = { supers: [...(SWIFT_STD_SUPERS[typeName] ?? (system ? [system] : []))], projectType: false }; + for (const decl of context.getNodesByName(typeName)) { + if (decl.language !== 'swift' || !SWIFT_TYPE_KINDS.has(decl.kind)) continue; + const head = swiftHeadOf(decl, context); + info.supers.push(...head.supers); + if (!head.extension) info.projectType = true; + } + memo.set(typeName, info); + return info; +} + +/** + * A Swift declaration's head, read up to its body with comments, attribute + * arguments and generic parameters dropped: `@objc(ChartViewBase) open class + * ChartViewBase: NSUIView, ChartDataProvider where T: Equatable {` → + * NSUIView, ChartDataProvider. + */ +function swiftHeadOf(decl: Node, context: ResolutionContext): { supers: string[]; extension: boolean } { + const lines = context.getFileLines?.(decl.filePath) ?? context.readFile(decl.filePath)?.split(/\r?\n/) ?? []; + const text = lines + .slice(decl.startLine - 1, Math.min(decl.endLine, decl.startLine + 20)) + .join('\n') + .replace(/\/\*[\s\S]*?\*\//g, ' ') + .replace(/\/\/[^\n]*/g, ' '); + let depth = 0; + let flat = ''; + for (const ch of text) { + if (ch === '<' || ch === '(') depth++; + else if (ch === '>' || ch === ')') depth = Math.max(0, depth - 1); + else if (depth === 0) { + if (ch === '{') break; + flat += ch; + } + } + const head = /\b(class|struct|enum|protocol|extension|actor)\s+[\w.]+\s*([\s\S]*)$/.exec(flat); + if (!head) return { supers: [], extension: false }; + const clause = /^:([\s\S]*?)(?:\bwhere\b|$)/.exec(head[2]!.trim())?.[1] ?? ''; + return { + supers: clause.split(',').map((item) => /([A-Za-z_]\w*)\s*$/.exec(item.trim())?.[1]).filter((w): w is string => !!w), + extension: head[1] === 'extension', + }; +} + +/** The Swift types around a call (depth 0) and what they inherit or conform to, by distance. */ +function swiftHierarchyAt(ref: UnresolvedRef, context: ResolutionContext): Map { + let memo = SWIFT_HIERARCHIES.get(context); + if (!memo) { + memo = new WeakMap(); + SWIFT_HIERARCHIES.set(context, memo); + } + const hit = memo.get(ref); + if (hit) return hit; + const depths = new Map(); + const queue: Array<[string, number]> = context + .getNodesInFile(ref.filePath) + .filter((n) => SWIFT_TYPE_KINDS.has(n.kind) && n.startLine <= ref.line && n.endLine >= ref.line) + .map((n) => [n.name.split('.').pop()!, 0]); + while (queue.length > 0 && depths.size < 40) { + const [name, depth] = queue.shift()!; + if (depths.has(name)) continue; + depths.set(name, depth); + for (const sup of swiftDeclOf(name, context).supers) queue.push([sup, depth + 1]); + } + memo.set(ref, depths); + return depths; +} + +/** + * Of the in-scope members a bare / `self.` / `super.` Swift call could mean, + * the nearest: the type's own, else its superclass's — Alamofire's + * `self.cancel()` in a DownloadRequest extension is DownloadRequest's + * override, not Request's. + */ +function nearestSwiftMembers(candidates: Node[], ref: UnresolvedRef, context: ResolutionContext): Node[] { + const hierarchy = swiftHierarchyAt(ref, context); + const depthOf = (n: Node): number | undefined => { + if (!SWIFT_MEMBER_KINDS.has(n.kind) && n.kind !== 'constant' && n.kind !== 'variable') return undefined; + const cut = n.qualifiedName.lastIndexOf('::'); + return cut > 0 ? hierarchy.get(n.qualifiedName.slice(0, cut).split(/::|\./).pop()!) : undefined; + }; + const depths = candidates.map(depthOf).filter((d): d is number => d !== undefined); + if (depths.length < 2) return candidates; + const nearest = Math.min(...depths); + return candidates.filter((n) => { + const d = depthOf(n); + return d === undefined || d === nearest; + }); +} + +/** + * Methods of Swift's standard collections, strings and optionals and of + * Foundation / UIKit views — names a project type rarely carries itself. + * Ones it often does (`cancel`, `resume`, `store`, `validate`, `load`) are + * left out: Alamofire's `request.resume()`, Kingfisher's `cache.store(…)`. + */ +const SWIFT_STD_METHODS: ReadonlySet = new Set([ + 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', 'removeValue', 'contains', 'map', + 'compactMap', 'flatMap', 'filter', 'reduce', 'forEach', 'sorted', 'sort', 'first', 'last', 'min', 'max', + 'firstIndex', 'lastIndex', 'index', 'enumerated', 'reversed', 'joined', 'split', 'prefix', 'suffix', + 'dropFirst', 'dropLast', 'allSatisfy', 'randomElement', 'shuffled', 'popLast', 'replaceSubrange', + 'replacingOccurrences', 'components', 'trimmingCharacters', 'hasPrefix', 'hasSuffix', 'lowercased', + 'uppercased', 'appending', 'updateValue', 'merge', 'merging', 'union', 'intersection', 'subtracting', + 'formUnion', 'isEqual', 'addSubview', 'removeFromSuperview', 'setNeedsDisplay', 'setNeedsLayout', + 'layoutIfNeeded', 'addGestureRecognizer', 'addTarget', 'addObserver', 'removeObserver', 'eraseToAnyPublisher', +]); + +/** + * Whether a Swift file's declarations are visible from another file as far as + * separate modules go: one under a `…Tests` directory (SwiftPM's + * `Tests/VaporTests`, Xcode's `KingfisherTests`) or in a playground only from + * inside it. + */ +function isSwiftTargetVisible(declFile: string, fromFile: string): boolean { + const segments = declFile.split('/').slice(0, -1); + const target = segments.findIndex((seg) => /Tests$|\.playground$/.test(seg)); + if (target < 0) return true; + const prefix = segments.slice(0, target + 1).join('/') + '/'; + return fromFile.startsWith(prefix); +} + +/** + * Whether a Swift call of that shape can mean `n`. A member reached with no + * receiver or through `self.` belongs to a type around the call or to what + * it inherits or conforms to — `super.` to the latter only; top-level code + * has no implicit self. Charts' `min(a, b)` went to a range type's `min` + * field, `super.init(…)` to an Objective-C demo's `init` 170 times. A member + * reached down a longer chain keeps its match unless its name is one the + * standard types all carry: `axis.entries.removeAll()` is the array's, not + * a chart data class's (unless the receiver names the owner, or the owner + * is an outside type the project only extends). + */ +function isSwiftCallTarget(n: Node, shape: SwiftCallShape | null, ref: UnresolvedRef, context: ResolutionContext): boolean { + if (!shape) return true; + // A global `let` / `var` from another file is subscripted, not called — + // Charts' `min(a, b)` went to a playground page's `let min = 20.0`. + if ((n.kind === 'constant' || n.kind === 'variable') && n.filePath !== ref.filePath && !shape.subscript) return false; + // A test target imports the module it tests, never the reverse. + if (!isSwiftTargetVisible(n.filePath, ref.filePath)) return false; + const cut = n.qualifiedName.lastIndexOf('::'); + const owner = cut > 0 ? n.qualifiedName.slice(0, cut).split(/::|\./).pop()! : null; + if (!SWIFT_MEMBER_KINDS.has(n.kind) && !(owner !== null && (n.kind === 'constant' || n.kind === 'variable'))) { + if (n.kind !== 'function') return true; + return shape.shape === 'bare' || owner !== null || (shape.shape === 'chained' && /^[A-Z]/.test(shape.receiver)); + } + if (owner === null) return true; + if (shape.shape === 'chained') { + if (shape.receiver === '' || !SWIFT_STD_METHODS.has(n.name)) return true; + return sharesReceiverWord(shape.receiver.split('.').pop()!, n) || !swiftDeclOf(owner, context).projectType || + swiftDeclaresLabel(n, shape.label, context); + } + const depth = swiftHierarchyAt(ref, context).get(owner); + return depth !== undefined && (shape.shape !== 'super' || depth > 0); +} + /** * Methods of Rust's Option / Result / iterators / collections / strings / * smart pointers — names a project type rarely carries itself. Ones it often @@ -2026,6 +2280,14 @@ function isObjcSelfSendTarget(n: Node, ref: UnresolvedRef, context: ResolutionCo const OBJC_SYSTEM_SUPERS: Readonly> = { UIResponder: 'NSObject', UIView: 'UIResponder', UIViewController: 'UIResponder', UIWindow: 'UIView', UIControl: 'UIView', UIButton: 'UIControl', UITextField: 'UIControl', UISwitch: 'UIControl', UISlider: 'UIControl', + UISegmentedControl: 'UIControl', UIStepper: 'UIControl', UIPageControl: 'UIControl', UIDatePicker: 'UIControl', + UIRefreshControl: 'UIControl', UIStackView: 'UIView', UINavigationBar: 'UIView', UIToolbar: 'UIView', + UITabBar: 'UIView', UISearchBar: 'UIView', UIVisualEffectView: 'UIView', UIActivityIndicatorView: 'UIView', + UIProgressView: 'UIView', UIPickerView: 'UIView', UITableViewHeaderFooterView: 'UIView', + UITableViewController: 'UIViewController', UICollectionViewController: 'UIViewController', + UINavigationController: 'UIViewController', UITabBarController: 'UIViewController', + UIPageViewController: 'UIViewController', UISplitViewController: 'UIViewController', + UIAlertController: 'UIViewController', UIHostingController: 'UIViewController', UIImageView: 'UIView', UILabel: 'UIView', UIScrollView: 'UIView', UITableView: 'UIScrollView', UICollectionView: 'UIScrollView', UITextView: 'UIScrollView', UITableViewCell: 'UIView', UICollectionReusableView: 'UIView', UICollectionViewCell: 'UICollectionReusableView', @@ -2798,9 +3060,12 @@ export function matchByExactName( ? rustGoCallShape(ref, context) : null; const kotlinStdChain = ref.language === 'kotlin' && ref.referenceKind === 'calls' && KOTLIN_STD_METHODS.has(ref.referenceName) ? kotlinChainReceiver(ref, context) : null; + const swiftShape = ref.language === 'swift' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName) + ? swiftCallShape(ref, context) : null; const phpSelf = phpSelfReceiver(ref, context); const filtered = sameName.filter((n) => !(kotlinStdChain !== null && !isKotlinStdChainTarget(n, kotlinStdChain)) && + !(swiftShape && !isSwiftCallTarget(n, swiftShape, ref, context)) && !(rustGoShape && !isRustGoCallTarget(n, rustGoShape)) && !(scalaBare && !isScalaMemberInScope(n, ref, context)) && !(csharpBare && !isCsharpMemberInScope(n, ref, context)) && @@ -2857,7 +3122,8 @@ export function matchByExactName( // file's symbol of that name, so a bare call has no cross-file candidate. !(bareJs && n.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)) ); - const candidates = dartBare ? nearestDartMembers(filtered, ref, context) : filtered; + const candidates = dartBare ? nearestDartMembers(filtered, ref, context) + : swiftShape && swiftShape.shape !== 'chained' ? nearestSwiftMembers(filtered, ref, context) : filtered; if (candidates.length === 0) { return null; @@ -3924,6 +4190,8 @@ export function clearNameMatcherMemos(context: ResolutionContext): void { PHP_SUPERS.delete(context); DART_SUPERS.delete(context); DART_HIERARCHIES.delete(context); + SWIFT_DECLS.delete(context); + SWIFT_HIERARCHIES.delete(context); KOTLIN_FILE_SCOPES.delete(context); RUBY_ANCESTRY.delete(context); CFML_CHAINS.delete(context); @@ -6076,6 +6344,8 @@ export function matchFuzzy( ? rustGoCallShape(ref, context) : null; const kotlinStdChain = ref.language === 'kotlin' && ref.referenceKind === 'calls' && KOTLIN_STD_METHODS.has(ref.referenceName) ? kotlinChainReceiver(ref, context) : null; + const swiftShape = ref.language === 'swift' && ref.referenceKind === 'calls' && /^[A-Za-z_]\w*$/.test(ref.referenceName) + ? swiftCallShape(ref, context) : null; const phpSelf = phpSelfReceiver(ref, context); // Names are case-sensitive in every language but a handful: Rust's // `Bytes` is not the method `bytes`, Python's builtin `dir(…)` not a class @@ -6101,6 +6371,7 @@ export function matchFuzzy( !(scalaBare && !isScalaMemberInScope(n, ref, context)) && !(rustGoShape && !isRustGoCallTarget(n, rustGoShape)) && !(kotlinStdChain !== null && !isKotlinStdChainTarget(n, kotlinStdChain)) && + !(swiftShape && !isSwiftCallTarget(n, swiftShape, ref, context)) && !(phpSelf && (n.kind !== 'method' || !isPhpMethodInScope(n, ref, phpSelf, context)))) .filter((n) => (ref.referenceKind !== 'references' && ref.referenceKind !== 'function_ref') || sameLanguageFamily(n.language, ref.language));