mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(swift): read a call's shape before reaching a member by name (#2170)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<String> 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<ByteString>`, 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.
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'Sources/Charts/Utils/Sequence+KeyPath.swift': `extension Sequence {
|
||||
func max<T: Comparable>(by keyPath: KeyPath<Element, T>) -> 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<Base> {
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -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(`(?<![\\w$])${name}\\s*[({<[]`).exec(line);
|
||||
start = m ? m.index : -1;
|
||||
}
|
||||
if (start < 0) return null;
|
||||
const before = line.slice(0, start);
|
||||
const after = line.slice(start + name.length);
|
||||
const base = { receiver: '', label: '', subscript: /^\s*\[/.test(after) };
|
||||
if (!/\.\s*$/.test(before)) return { ...base, shape: 'bare' };
|
||||
// `self.m(` — not `self[i].m(` or `self.items.m(`, which are chains.
|
||||
const plain = /(?:^|[^\w$.)\]])(self|Self|super)\s*[?!]?\s*\.\s*$/.exec(before)?.[1];
|
||||
if (plain) return { ...base, shape: plain === 'super' ? 'super' : 'self' };
|
||||
return {
|
||||
...base,
|
||||
shape: 'chained',
|
||||
receiver: rustGoReceiverName(before.replace(/[?!]\s*\./g, '.')),
|
||||
label: /^\s*\(\s*([A-Za-z_]\w*)\s*:(?!:)/.exec(after)?.[1] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
/** Argument labels the standard library's collection and string methods take. */
|
||||
const SWIFT_STD_LABELS: ReadonlySet<string> = 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<Record<string, readonly string[]>> = {
|
||||
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<string> = new Set(['class', 'struct', 'enum', 'interface']);
|
||||
const SWIFT_MEMBER_KINDS: ReadonlySet<string> = new Set(['method', 'property', 'field', 'enum_member']);
|
||||
const SWIFT_DECLS = new WeakMap<ResolutionContext, Map<string, { supers: string[]; projectType: boolean }>>();
|
||||
const SWIFT_HIERARCHIES = new WeakMap<ResolutionContext, WeakMap<UnresolvedRef, Map<string, number>>>();
|
||||
|
||||
/**
|
||||
* 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<T>: 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<string, number> {
|
||||
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<string, number>();
|
||||
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<string> = 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<Record<string, string>> = {
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user