-
Notifications
You must be signed in to change notification settings - Fork 441
/
Copy pathNameMatcher.swift
456 lines (415 loc) · 17.7 KB
/
NameMatcher.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//
#if compiler(>=6)
import SwiftParser
public import SwiftSyntax
#else
import SwiftParser
import SwiftSyntax
#endif
/// Given a set of positions in a source file, resolve the names of the
/// declarations that are referenced at these locations and, if they refer to
/// functions, their argument labels.
@_spi(Compiler)
public class NameMatcher: SyntaxAnyVisitor {
/// The positions that still need to be resolved.
///
/// Elements are removed from this set when they are resoled.
private var positionsToResolve: [AbsolutePosition]
/// The locations that we have already resolved.
///
/// - Note: These are not guaranteed to be in source order since we might eg. generate an entry for a subscript call
/// at `[` when visiting the `SubscriptCallExprSyntax` before resolving the base name at `foo[1]`.
private var resolvedLocs: [DeclNameLocation] = []
/// The topmost element of this stack always represents the context of newly resolved locations.
///
/// As we walk into syntax nodes that contribute context to any locations resolved within them, an entry is pushed to
/// this stack. Elements are popped of the stack when the syntax node with the context is left.
private var contextStack: [DeclNameLocation.Context] = [.default] {
didSet {
precondition(!contextStack.isEmpty)
}
}
/// The topmost element of this stack always represents whether newly resolved locations are active or inactive.
///
/// As we walk into `#if` regions, an element is pushed to this stack depending on whether the region is active. As
/// the region is left, values are popped.
private var isActiveStack: [Bool] = [true] {
didSet {
precondition(!isActiveStack.isEmpty)
}
}
private init(baseNamePositions: some Sequence<AbsolutePosition>) {
self.positionsToResolve = baseNamePositions.sorted()
super.init(viewMode: .sourceAccurate)
}
// MARK: - Public entry
public static func resolve(
baseNamePositions: some Sequence<AbsolutePosition>,
in tree: some SyntaxProtocol
) -> [DeclNameLocation] {
let matcher = NameMatcher(baseNamePositions: baseNamePositions)
matcher.walk(tree)
return matcher.resolvedLocs
}
// MARK: - Utilities
/// Checks if the `position` is in `positionsToResolve`. If so, remove it from `positionsToResolve` and return `true`.
/// Otherwise, return `false`.
private func removePositionToResolveIfExists(at position: AbsolutePosition) -> Bool {
for (index, positionToResolve) in positionsToResolve.enumerated() {
if positionToResolve > position {
// positionToResolve is sorted. If we're already past the position we are looking for, we won't find anything
// later.
return false
}
if positionToResolve == position {
positionsToResolve.remove(at: index)
return true
}
}
return false
}
/// If there is a position to resolve inside `range` return it, otherwise return `nil`.
private func firstPositionToResolve(in range: Range<AbsolutePosition>) -> AbsolutePosition? {
for positionToResolve in positionsToResolve {
if positionToResolve > range.upperBound {
// positionToResolve is sorted. If we're already past the range we are looking for, we won't find anything later
return nil
}
if range.contains(positionToResolve) {
return positionToResolve
}
}
return nil
}
/// Finds the first position to resolve that is in the leading or trailing trivia of this token.
///
/// If one is found, also returns the range of the trivia in which the position was found.
private func firstPositionToResolve(
inTriviaOf token: TokenSyntax
) -> (position: AbsolutePosition, triviaRange: Range<AbsolutePosition>)? {
if let position = firstPositionToResolve(in: token.leadingTriviaRange) {
return (position, token.leadingTriviaRange)
}
if let position = firstPositionToResolve(in: token.trailingTriviaRange) {
return (position, token.trailingTriviaRange)
}
return nil
}
// MARK: - addResolvedLocIfRequested overloads
/// If a position should be resolved at at the start of `baseNameRange`, create a new `DeclNameLocation` to
/// `resolvedLocs`, otherwise a no-op.
private func addResolvedLocIfRequested(
baseNameRange: Range<AbsolutePosition>,
argumentLabels: DeclNameLocation.Arguments,
context: DeclNameLocation.Context? = nil
) {
guard removePositionToResolveIfExists(at: baseNameRange.lowerBound) else {
return
}
resolvedLocs.append(
DeclNameLocation(
baseNameRange: baseNameRange,
arguments: argumentLabels,
context: context ?? contextStack.last!,
isActive: isActiveStack.last!
)
)
}
/// Try resolving `baseName` with the given argument labels.
///
/// This adds a resolved location if the start of `baseName` is in `positionsToResolve` and if the base name starts
/// with `$` or `_` and `positionsToResolve` contains the position after the `$` or `_`. This ensures that we can
/// rename the underlying properties referenced by property wrappers.
private func addResolvedLocIfRequested(
baseName: TokenSyntax,
argumentLabels: DeclNameLocation.Arguments
) {
addResolvedLocIfRequested(
baseNameRange: baseName.rangeWithoutTrivia,
argumentLabels: argumentLabels
)
if baseName.text.first == "$" || baseName.text.first == "_" {
let range = baseName.positionAfterSkippingLeadingTrivia.advanced(by: 1)..<baseName.endPositionBeforeTrailingTrivia
addResolvedLocIfRequested(
baseNameRange: range,
argumentLabels: argumentLabels
)
}
}
/// Try resolving a function-style call at `baseName`.
///
/// This computes the argument labels from the passed arguments and trailing closures.
private func addResolvedLocIfRequested(
baseName: TokenSyntax,
arguments: LabeledExprListSyntax,
trailingClosure: ClosureExprSyntax? = nil,
additionalTrailingClosures: MultipleTrailingClosureElementListSyntax? = nil
) {
var firstTrailingClosureIndex: Int? = nil
var argumentLabels = arguments.map { (argument) -> DeclNameLocation.Argument in
if let label = argument.label, let colon = argument.colon {
return .labeledCall(label: label, colon: colon)
} else {
return .unlabeled(argument: Syntax(argument.colon) ?? Syntax(argument.expression))
}
}
if let trailingClosure {
firstTrailingClosureIndex = argumentLabels.count
argumentLabels.append(.unlabeled(argument: trailingClosure))
}
if let additionalTrailingClosures {
argumentLabels += additionalTrailingClosures.map { (additionalTrailingClosure) -> DeclNameLocation.Argument in
// We need to report additional trailing closure labels in the same way that we report function parameters
// because changing the argument label to `_` should result in an additional trailing closure label `_:` instead
// of removing the label, which is what `labeledCall` does
return .labeled(firstName: additionalTrailingClosure.label, secondName: nil)
}
}
addResolvedLocIfRequested(
baseName: baseName,
argumentLabels: .call(argumentLabels, firstTrailingClosureIndex: firstTrailingClosureIndex)
)
}
/// Try resolving a function-style declaration at `baseName`.
///
/// This computes the argument labels from the passed function signature.
private func addResolvedLocIfRequested(
baseName: TokenSyntax,
signature: FunctionSignatureSyntax
) {
let argumentLabels = signature.parameterClause.parameters.map { (argument) -> DeclNameLocation.Argument in
return .labeled(firstName: argument.firstName, secondName: argument.secondName)
}
addResolvedLocIfRequested(baseName: baseName, argumentLabels: .parameters(argumentLabels))
}
// MARK: - Visit functions
public override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
if firstPositionToResolve(in: node.position..<node.endPosition) == nil {
return .skipChildren
} else {
return .visitChildren
}
}
public override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
while let (baseNamePosition, triviaRange) = firstPositionToResolve(inTriviaOf: token) {
// Parse the comment from the position that we want to resolve. This should parse any function calls or compound decl names, the rest of
// the comment will probably be parsed as garbage but that's OK because we don't actually care about it.
let positionOffsetInToken = baseNamePosition.utf8Offset - token.position.utf8Offset
let triviaRangeEndOffsetInToken = triviaRange.upperBound.utf8Offset - token.position.utf8Offset
let commentTree = token.syntaxTextBytes[positionOffsetInToken..<triviaRangeEndOffsetInToken]
.withUnsafeBufferPointer { (buffer) -> ExprSyntax in
var parser = Parser(buffer)
return ExprSyntax.parse(from: &parser)
}
// Run a new `NameMatcher`. Since the input of that name matcher is the text after the position to resolve, we
// want to resolve the position at offset 0.
let resolvedInComment = NameMatcher.resolve(baseNamePositions: [AbsolutePosition(utf8Offset: 0)], in: commentTree)
let positionRemoved = removePositionToResolveIfExists(at: baseNamePosition)
precondition(
positionRemoved,
"Found a position with `firstPositionToResolve but didn't find it again to remove it?"
)
// Adjust the positions to point back to the original tree, set the context as `comment` and record them.
resolvedLocs += resolvedInComment.map { locationInComment in
DeclNameLocation(
baseNameRange: locationInComment.baseNameRange.advanced(by: baseNamePosition.utf8Offset),
arguments: locationInComment.arguments.advanced(by: baseNamePosition.utf8Offset),
context: .comment,
isActive: isActiveStack.last!
)
}
}
if case .stringSegment = token.tokenKind {
while let baseNamePosition = positionsToResolve.first(where: { token.rangeWithoutTrivia.contains($0) }) {
let positionOffsetInStringSegment = baseNamePosition.utf8Offset - token.position.utf8Offset
guard let tokenLength = getFirstTokenLength(in: token.syntaxTextBytes[positionOffsetInStringSegment...]) else {
continue
}
addResolvedLocIfRequested(
baseNameRange: baseNamePosition..<baseNamePosition.advanced(by: tokenLength.utf8Length),
argumentLabels: .noArguments,
context: .stringLiteral
)
}
}
addResolvedLocIfRequested(baseName: token, argumentLabels: .noArguments)
return .skipChildren
}
public override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
let attributeName: TokenSyntax
if let name = node.attributeName.as(IdentifierTypeSyntax.self) {
attributeName = name.name
} else if let name = node.attributeName.as(MemberTypeSyntax.self) {
attributeName = name.name
} else {
return .visitChildren
}
if node.arguments == nil {
addResolvedLocIfRequested(baseName: attributeName, argumentLabels: .noArguments)
} else if case .argumentList(let argumentList) = node.arguments {
addResolvedLocIfRequested(baseName: attributeName, arguments: argumentList)
}
return .visitChildren
}
public override func visit(_ node: DeclReferenceExprSyntax) -> SyntaxVisitorContinueKind {
if let argumentNames = node.argumentNames {
addResolvedLocIfRequested(
baseName: node.baseName,
argumentLabels: .selector(argumentNames.arguments.map { .labeled(firstName: $0.name, secondName: nil) })
)
} else if let functionCall = node.parentFunctionCall {
addResolvedLocIfRequested(
baseName: node.baseName,
arguments: functionCall.arguments,
trailingClosure: functionCall.trailingClosure,
additionalTrailingClosures: functionCall.additionalTrailingClosures
)
}
return .visitChildren
}
public override func visit(_ node: EnumCaseElementSyntax) -> SyntaxVisitorContinueKind {
if let parameterClause = node.parameterClause {
let argumentLabels = parameterClause.parameters.map { (argument) -> DeclNameLocation.Argument in
if let firstName = argument.firstName {
return .labeled(firstName: firstName, secondName: argument.secondName)
} else {
return .unlabeled(argument: Syntax(argument.secondName) ?? Syntax(argument.colon) ?? Syntax(argument.type))
}
}
addResolvedLocIfRequested(baseName: node.name, argumentLabels: .enumCaseParameters(argumentLabels))
}
return .visitChildren
}
public override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
// Calls to closures are represented by a location to resolve at the opening `(`. We need to match that.
// Renames of the function name are handled by `visit(_:DeclReferenceExprSyntax)`.
if let leftParen = node.leftParen {
addResolvedLocIfRequested(
baseName: leftParen,
arguments: node.arguments,
trailingClosure: node.trailingClosure,
additionalTrailingClosures: node.additionalTrailingClosures
)
}
return .visitChildren
}
public override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
addResolvedLocIfRequested(baseName: node.name, signature: node.signature)
return .visitChildren
}
public override func visit(_ node: IfConfigDeclSyntax) -> SyntaxVisitorContinueKind {
isActiveStack.append(false)
return .visitChildren
}
public override func visitPost(_ node: IfConfigDeclSyntax) {
isActiveStack.removeLast()
}
public override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind {
addResolvedLocIfRequested(baseName: node.initKeyword, signature: node.signature)
return .visitChildren
}
public override func visit(_ node: MacroDeclSyntax) -> SyntaxVisitorContinueKind {
addResolvedLocIfRequested(baseName: node.name, signature: node.signature)
return .visitChildren
}
public override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind {
if node.macroName.text == "selector" {
contextStack.append(.selector)
}
addResolvedLocIfRequested(
baseName: node.macroName,
arguments: node.arguments,
trailingClosure: node.trailingClosure,
additionalTrailingClosures: node.additionalTrailingClosures
)
return .visitChildren
}
public override func visitPost(_ node: MacroExpansionDeclSyntax) {
if node.macroName.text == "selector" {
contextStack.removeLast()
}
}
public override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind {
if node.macroName.text == "selector" {
contextStack.append(.selector)
}
addResolvedLocIfRequested(
baseName: node.macroName,
arguments: node.arguments,
trailingClosure: node.trailingClosure,
additionalTrailingClosures: node.additionalTrailingClosures
)
return .visitChildren
}
public override func visitPost(_ node: MacroExpansionExprSyntax) {
if node.macroName.text == "selector" {
contextStack.removeLast()
}
}
public override func visit(_ node: SubscriptCallExprSyntax) -> SyntaxVisitorContinueKind {
addResolvedLocIfRequested(
baseName: node.leftSquare,
arguments: node.arguments,
trailingClosure: node.trailingClosure,
additionalTrailingClosures: node.additionalTrailingClosures
)
return .visitChildren
}
public override func visit(_ node: SubscriptDeclSyntax) -> SyntaxVisitorContinueKind {
let argumentLabels = node.parameterClause.parameters.map { (argument) -> DeclNameLocation.Argument in
return .labeled(firstName: argument.firstName, secondName: argument.secondName)
}
addResolvedLocIfRequested(
baseName: node.subscriptKeyword,
argumentLabels: .noncollapsibleParameters(argumentLabels)
)
return .visitChildren
}
}
extension TokenSyntax {
var rangeWithoutTrivia: Range<AbsolutePosition> {
return positionAfterSkippingLeadingTrivia..<endPositionBeforeTrailingTrivia
}
var leadingTriviaRange: Range<AbsolutePosition> {
return position..<positionAfterSkippingLeadingTrivia
}
var trailingTriviaRange: Range<AbsolutePosition> {
return endPositionBeforeTrailingTrivia..<endPosition
}
}
extension DeclReferenceExprSyntax {
var parentFunctionCall: FunctionCallExprSyntax? {
if let functionCall = self.parent?.as(FunctionCallExprSyntax.self) {
// E.g `foo(a: 1)`
return functionCall
} else if let memberAccess = self.parent?.as(MemberAccessExprSyntax.self),
memberAccess.declName == self,
let functionCall = memberAccess.parent?.as(FunctionCallExprSyntax.self)
{
// E.g. `foo.bar(a: 1)``
return functionCall
} else {
return nil
}
}
}
private func getFirstTokenLength(in text: ArraySlice<UInt8>) -> SourceLength? {
return text.withUnsafeBufferPointer { (buffer) -> SourceLength? in
// We can force-unwrap the first token because there must be at least the EOF token in the source file.
let firstToken = Parser.parse(source: buffer).firstToken(viewMode: .sourceAccurate)!
guard firstToken.leadingTriviaLength == .zero else {
return nil
}
return firstToken.trimmedLength
}
}