Skip to content

Commit e647880

Browse files
authored
Merge pull request #2401 from ahoppen/ahoppen/fixme-namematcher
Address remaining `FIXME`s for the new `NameMatcher`
2 parents 3c45445 + 46fa0b3 commit e647880

File tree

5 files changed

+108
-18
lines changed

5 files changed

+108
-18
lines changed

Sources/SwiftIDEUtils/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_swift_syntax_library(SwiftIDEUtils
1313
Syntax+Classifications.swift
1414
SyntaxClassification.swift
1515
SyntaxClassifier.swift
16+
Utils.swift
1617
)
1718

1819
target_link_swift_syntax_libraries(SwiftIDEUtils PUBLIC

Sources/SwiftIDEUtils/DeclNameLocation.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,17 @@ public struct DeclNameLocation: Equatable {
2626
/// ## Examples
2727
/// - `a` in `func foo(a: Int) {}`
2828
/// - `a` and `b` in `func foo(a b: Int) {}`
29+
/// - `additionalTrailingClosure:` in `foo {} additionalTrailingClosure: {}`
30+
/// - We cannot use `labeledCall` here because when changing the parameter label to `_`, we need to change the
31+
/// call to `foo {} _: {}`. If we used `labeledCall`, the label would be removed, leaving us with `foo {} {}`.
2932
case labeled(firstName: Range<AbsolutePosition>, secondName: Range<AbsolutePosition>?)
3033

3134
/// The argument of a call.
3235
///
33-
/// The range of the label does not include trivia. The range of the colon *does* include trivia.
36+
/// The range of the label does not include trivia.
37+
///
38+
/// The range of the colon *does* include trivia. This is so we can remove the colon including the space after it
39+
/// when changing a labeled parameter to an unlabeled parameter.
3440
///
3541
/// ## Examples
3642
/// - `a` and `:` in `foo(a: 1)`
@@ -46,10 +52,6 @@ public struct DeclNameLocation: Equatable {
4652
}
4753

4854
static func labeledCall(label: TokenSyntax, colon: TokenSyntax) -> Argument {
49-
// FIXME: (NameMatcher) The `labeledCall` case is problematic for two reasons
50-
// 1. The fact that `colon` includes trivia is inconsistent with the associated values in `label` and `labeledCall`
51-
// 2. If `colon` didn't need to contain trivia, we wouldn't need the `labeledCall` case at all.
52-
// See if we can unify `labeledCall` and `labeled`.
5355
return .labeledCall(label: label.rangeWithoutTrivia, colon: colon.position..<colon.endPosition)
5456
}
5557

@@ -68,6 +70,18 @@ public struct DeclNameLocation: Equatable {
6870
return argumentPosition..<argumentPosition
6971
}
7072
}
73+
74+
/// Shift the ranges `utf8Offset` bytes to the right, ie. add `utf8Offset` to the upper and lower bound.
75+
func advanced(by utf8Offset: Int) -> DeclNameLocation.Argument {
76+
switch self {
77+
case .labeled(firstName: let firstName, secondName: let secondName):
78+
return .labeled(firstName: firstName.advanced(by: utf8Offset), secondName: secondName?.advanced(by: utf8Offset))
79+
case .labeledCall(label: let label, colon: let colon):
80+
return .labeledCall(label: label.advanced(by: utf8Offset), colon: colon.advanced(by: utf8Offset))
81+
case .unlabeled(argumentPosition: let argumentPosition):
82+
return .unlabeled(argumentPosition: argumentPosition.advanced(by: utf8Offset))
83+
}
84+
}
7185
}
7286

7387
/// The arguments of a ``DeclNameLocation``.
@@ -92,6 +106,22 @@ public struct DeclNameLocation: Equatable {
92106

93107
/// The argument label to disambiguate multiple functions with the same base name, like `foo(a:)`.
94108
case selector([Argument])
109+
110+
/// Shift the ranges `utf8Offset` bytes to the right, ie. add `utf8Offset` to the upper and lower bound.
111+
func advanced(by utf8Offset: Int) -> DeclNameLocation.Arguments {
112+
switch self {
113+
case .noArguments:
114+
return .noArguments
115+
case .call(let arguments, firstTrailingClosureIndex: let firstTrailingClosureIndex):
116+
return .call(arguments.map { $0.advanced(by: utf8Offset) }, firstTrailingClosureIndex: firstTrailingClosureIndex)
117+
case .parameters(let parameters):
118+
return .parameters(parameters.map { $0.advanced(by: utf8Offset) })
119+
case .noncollapsibleParameters(let parameters):
120+
return .noncollapsibleParameters(parameters.map { $0.advanced(by: utf8Offset) })
121+
case .selector(let arguments):
122+
return .selector(arguments.map { $0.advanced(by: utf8Offset) })
123+
}
124+
}
95125
}
96126

97127
public enum Context {

Sources/SwiftIDEUtils/NameMatcher.swift

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ public class NameMatcher: SyntaxAnyVisitor {
5555

5656
// MARK: - Public entry
5757

58-
public static func resolve(baseNamePositions: some Sequence<AbsolutePosition>, in sourceFile: SourceFileSyntax) -> [DeclNameLocation] {
58+
public static func resolve(baseNamePositions: some Sequence<AbsolutePosition>, in tree: some SyntaxProtocol) -> [DeclNameLocation] {
5959
let matcher = NameMatcher(baseNamePositions: baseNamePositions)
60-
matcher.walk(sourceFile)
60+
matcher.walk(tree)
6161
return matcher.resolvedLocs
6262
}
6363

@@ -161,7 +161,9 @@ public class NameMatcher: SyntaxAnyVisitor {
161161
}
162162
if let additionalTrailingClosures {
163163
argumentLabels += additionalTrailingClosures.map { (additionalTrailingClosure) -> DeclNameLocation.Argument in
164-
// FIXME: (NameMatcher) This really should be callLabel but the old NameMatcher also reported this as labeled
164+
// We need to report additional trailing closure labels in the same way that we report function parameters
165+
// because changing the argument label to `_` should result in an additional trailing closure label `_:` instead
166+
// of removing the label, which is what `labeledCall` does
165167
return .labeled(firstName: additionalTrailingClosure.label, secondName: nil)
166168
}
167169
}
@@ -193,16 +195,29 @@ public class NameMatcher: SyntaxAnyVisitor {
193195

194196
public override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
195197
while let baseNamePosition = firstPositionToResolve(in: token.leadingTriviaRange) ?? firstPositionToResolve(in: token.trailingTriviaRange) {
198+
// Parse the comment from the position that we want to resolve. This should parse any function calls or compound decl names, the rest of
199+
// the comment will probably be parsed as garbage but that's OK because we don't actually care about it.
196200
let positionOffsetInToken = baseNamePosition.utf8Offset - token.position.utf8Offset
197-
guard let tokenLength = getFirstTokenLength(in: token.syntaxTextBytes[positionOffsetInToken...]) else {
198-
continue
201+
let commentTree = token.syntaxTextBytes[positionOffsetInToken...].withUnsafeBufferPointer { (buffer) -> ExprSyntax in
202+
var parser = Parser(buffer)
203+
return ExprSyntax.parse(from: &parser)
204+
}
205+
// Run a new `NameMatcher`. Since the input of that name matcher is the text after the position to resolve, we
206+
// want to resolve the position at offset 0.
207+
let resolvedInComment = NameMatcher.resolve(baseNamePositions: [AbsolutePosition(utf8Offset: 0)], in: commentTree)
208+
209+
let positionRemoved = removePositionToResolveIfExists(at: baseNamePosition)
210+
precondition(positionRemoved, "Found a position with `firstPositionToResolve but didn't find it again to remove it?")
211+
212+
// Adjust the positions to point back to the original tree, set the context as `comment` and record them.
213+
resolvedLocs += resolvedInComment.map { locationInComment in
214+
DeclNameLocation(
215+
baseNameRange: locationInComment.baseNameRange.advanced(by: baseNamePosition.utf8Offset),
216+
arguments: locationInComment.arguments.advanced(by: baseNamePosition.utf8Offset),
217+
context: .comment,
218+
isActive: isActiveStack.last!
219+
)
199220
}
200-
// FIXME: (NameMatcher) We could also resolve argument labels inside comments if we are parsing anyway.
201-
addResolvedLocIfRequested(
202-
baseNameRange: baseNamePosition..<baseNamePosition.advanced(by: tokenLength.utf8Length),
203-
argumentLabels: .noArguments,
204-
context: .comment
205-
)
206221
}
207222

208223
if case .stringSegment = token.tokenKind {

Sources/SwiftIDEUtils/Utils.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
extension Range<AbsolutePosition> {
16+
/// Shift the range `utf8Offset` bytes to the right, ie. add `utf8Offset` to the upper and lower bound.
17+
func advanced(by utf8Offset: Int) -> Range<AbsolutePosition> {
18+
return self.lowerBound.advanced(by: utf8Offset)..<self.upperBound.advanced(by: utf8Offset)
19+
}
20+
}

Tests/SwiftIDEUtilsTest/NameMatcherTests.swift

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,32 @@ public class NameMatcherTests: XCTestCase {
171171
*/
172172
""",
173173
expected: [
174-
DeclNameLocationSpec(baseName: "foo", context: .comment),
175-
DeclNameLocationSpec(baseName: "foo", context: .comment),
174+
DeclNameLocationSpec(baseName: "foo", argumentLabels: [], type: .call, context: .comment),
175+
DeclNameLocationSpec(baseName: "foo", argumentLabels: ["first"], type: .selector, context: .comment),
176+
]
177+
)
178+
}
179+
180+
func testResolveArgumentLabelsInComments() {
181+
assertNameMatcherResult(
182+
"// 1️⃣fn(x:)",
183+
expected: [
184+
DeclNameLocationSpec(baseName: "fn", argumentLabels: ["x"], type: .selector, context: .comment)
185+
]
186+
)
187+
188+
assertNameMatcherResult(
189+
"// 1️⃣fn(x:) 2️⃣foo(other:)",
190+
expected: [
191+
DeclNameLocationSpec(baseName: "fn", argumentLabels: ["x"], type: .selector, context: .comment),
192+
DeclNameLocationSpec(baseName: "foo", argumentLabels: ["other"], type: .selector, context: .comment),
193+
]
194+
)
195+
196+
assertNameMatcherResult(
197+
"// 1️⃣fn(x: 1)",
198+
expected: [
199+
DeclNameLocationSpec(baseName: "fn", argumentLabels: ["x: "], type: .call, context: .comment)
176200
]
177201
)
178202
}

0 commit comments

Comments
 (0)