Skip to content

Commit b479382

Browse files
authored
Merge pull request #2905 from MAJKFL/composite-names
[SwiftLexicalLookup] Composite names
2 parents 507f323 + 6feca32 commit b479382

File tree

5 files changed

+149
-20
lines changed

5 files changed

+149
-20
lines changed

Diff for: Sources/SwiftLexicalLookup/LookupName.swift

+22-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ import SwiftSyntax
103103
?? subscriptDecl.endPositionBeforeTrailingTrivia
104104
case .variableDecl(let variableDecl):
105105
return variableDecl.bindings.first?.accessorBlock?.positionAfterSkippingLeadingTrivia
106-
?? variableDecl.endPosition
106+
?? variableDecl.bindings.positionAfterSkippingLeadingTrivia
107107
case .accessorDecl(let accessorDecl):
108108
return accessorDecl.accessorSpecifier.positionAfterSkippingLeadingTrivia
109109
case .deinitializerDecl(let deinitializerDecl):
@@ -139,6 +139,19 @@ import SwiftSyntax
139139
case implicit(ImplicitDecl)
140140
/// Dollar identifier introduced by a closure without parameters.
141141
case dollarIdentifier(ClosureExprSyntax, strRepresentation: String)
142+
/// Represents equivalent names grouped together.
143+
/// - Important: The array should be non-empty.
144+
///
145+
/// ### Example:
146+
/// ```swift
147+
/// switch X {
148+
/// case .a(let x), .b(let x):
149+
/// print(x) // <-- lookup here
150+
/// }
151+
/// ```
152+
/// For lookup at the given position, the result
153+
/// contains only one name, that represents both `let x` declarations.
154+
case equivalentNames([LookupName])
142155

143156
/// Syntax associated with this name.
144157
@_spi(Experimental) public var syntax: SyntaxProtocol {
@@ -151,6 +164,8 @@ import SwiftSyntax
151164
return implicitName.syntax
152165
case .dollarIdentifier(let closureExpr, _):
153166
return closureExpr
167+
case .equivalentNames(let names):
168+
return names.first!.syntax
154169
}
155170
}
156171

@@ -165,6 +180,8 @@ import SwiftSyntax
165180
return kind.identifier
166181
case .dollarIdentifier(_, strRepresentation: _):
167182
return nil
183+
case .equivalentNames(let names):
184+
return names.first!.identifier
168185
}
169186
}
170187

@@ -185,6 +202,8 @@ import SwiftSyntax
185202
return implicitName.position
186203
case .dollarIdentifier(let closureExpr, _):
187204
return closureExpr.positionAfterSkippingLeadingTrivia
205+
case .equivalentNames(let names):
206+
return names.first!.position
188207
}
189208
}
190209

@@ -319,6 +338,8 @@ import SwiftSyntax
319338
return "implicit: \(strName)"
320339
case .dollarIdentifier(_, strRepresentation: let str):
321340
return "dollarIdentifier: \(str)"
341+
case .equivalentNames(let names):
342+
return "Composite name: [ \(names.map(\.debugDescription).joined(separator: ", ")) ]"
322343
}
323344
}
324345
}

Diff for: Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift

+59-6
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ import SwiftSyntax
144144

145145
/// Capture, parameter and body names introduced in this scope.
146146
@_spi(Experimental) public var defaultIntroducedNames: [LookupName] {
147-
captureNames + parameterNames + introducedNamesInBody
147+
parameterNames + captureNames + introducedNamesInBody
148148
}
149149

150150
@_spi(Experimental) public var scopeDebugName: String {
@@ -202,7 +202,7 @@ import SwiftSyntax
202202
} else {
203203
signatureResults = LookupResult.getResultArray(
204204
for: self,
205-
withNames: (captureNames + parameterNames).filter { name in
205+
withNames: (parameterNames + captureNames).filter { name in
206206
checkIdentifier(identifier, refersTo: name, at: lookUpPosition)
207207
}
208208
)
@@ -574,13 +574,40 @@ import SwiftSyntax
574574
@_spi(Experimental) extension SwitchCaseSyntax: SequentialScopeSyntax {
575575
/// Names introduced within `case` items.
576576
var namesFromLabel: [LookupName] {
577-
label.as(SwitchCaseLabelSyntax.self)?.caseItems.flatMap { child in
577+
guard let switchCaseItemList = label.as(SwitchCaseLabelSyntax.self)?.caseItems else { return [] }
578+
579+
let extractedNames = switchCaseItemList.flatMap { child in
578580
if let exprPattern = child.pattern.as(ExpressionPatternSyntax.self) {
579581
return LookupName.getNames(from: exprPattern.expression)
580582
} else {
581583
return LookupName.getNames(from: child.pattern)
582584
}
583-
} ?? []
585+
}
586+
587+
if switchCaseItemList.count <= 1 {
588+
return extractedNames
589+
}
590+
591+
var orderedKeys: [Identifier] = []
592+
var partitioned: [Identifier: [LookupName]] = [:]
593+
594+
for extractedName in extractedNames {
595+
guard let identifier = extractedName.identifier else { continue }
596+
597+
if !partitioned.keys.contains(identifier) {
598+
orderedKeys.append(identifier)
599+
}
600+
601+
partitioned[identifier, default: []].append(extractedName)
602+
}
603+
604+
return
605+
orderedKeys
606+
.compactMap { key in
607+
guard let names = partitioned[key] else { return nil }
608+
609+
return .equivalentNames(names)
610+
}
584611
}
585612

586613
/// Names introduced within `case` items
@@ -607,7 +634,7 @@ import SwiftSyntax
607634
checkIdentifier(identifier, refersTo: name, at: lookUpPosition)
608635
}
609636

610-
if label.range.contains(lookUpPosition) {
637+
if label.range.contains(lookUpPosition) && !isInWhereClause(lookUpPosition: lookUpPosition) {
611638
return config.finishInSequentialScope ? [] : lookupInParent(identifier, at: lookUpPosition, with: config)
612639
} else if config.finishInSequentialScope {
613640
return sequentialLookup(
@@ -629,6 +656,20 @@ import SwiftSyntax
629656
+ lookupInParent(identifier, at: lookUpPosition, with: config)
630657
}
631658
}
659+
660+
/// Returns `true` if `lookUpPosition` is inside a `where`
661+
/// clause associated with one of the case items of this scope.
662+
private func isInWhereClause(lookUpPosition: AbsolutePosition) -> Bool {
663+
guard let switchCaseItemList = label.as(SwitchCaseLabelSyntax.self)?.caseItems else { return false }
664+
665+
for item in switchCaseItemList {
666+
if item.whereClause?.range.contains(lookUpPosition) ?? false {
667+
return true
668+
}
669+
}
670+
671+
return false
672+
}
632673
}
633674

634675
@_spi(Experimental) extension ProtocolDeclSyntax: ScopeSyntax, LookInMembersScopeSyntax {
@@ -903,7 +944,9 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe
903944
at lookUpPosition: AbsolutePosition,
904945
with config: LookupConfig
905946
) -> [LookupResult] {
906-
if bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false {
947+
if (bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false)
948+
|| shouldIntroduceSelfIfLazy(lookUpPosition: lookUpPosition)
949+
{
907950
return defaultLookupImplementation(
908951
in: (isMember ? [.implicit(.self(self))] : LookupName.getNames(from: self)),
909952
identifier,
@@ -929,6 +972,16 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe
929972

930973
return resultsToInterleave + lookupInParent(identifier, at: lookUpPosition, with: config)
931974
}
975+
976+
/// Returns `true`, if `lookUpPosition` is in initializer of
977+
/// this variable declaration and the declaration is lazy.
978+
private func shouldIntroduceSelfIfLazy(lookUpPosition: AbsolutePosition) -> Bool {
979+
guard bindings.first?.initializer?.range.contains(lookUpPosition) ?? false else { return false }
980+
981+
return modifiers.contains {
982+
$0.name.tokenKind == .keyword(.lazy)
983+
}
984+
}
932985
}
933986

934987
@_spi(Experimental) extension DeinitializerDeclSyntax: ScopeSyntax {

Diff for: Tests/SwiftLexicalLookupTest/Assertions.swift

+7-3
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,18 @@ func assertLexicalNameLookup(
105105
ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues)
106106

107107
return result.flatMap { lookUpResult in
108-
lookUpResult.names.map { lookupName in
109-
lookupName.syntax
108+
lookUpResult.names.flatMap { lookupName in
109+
if case .equivalentNames(let names) = lookupName {
110+
return names.map(\.syntax)
111+
} else {
112+
return [lookupName.syntax]
113+
}
110114
}
111115
}
112116
},
113117
expected: references.mapValues { expectations in
114118
expectations.flatMap { expectation in
115-
expectation.expectedNames.map { expectedName in
119+
expectation.expectedNames.flatMap { expectedName in
116120
expectedName.marker
117121
}
118122
}

Diff for: Tests/SwiftLexicalLookupTest/ExpectedName.swift

+23-6
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ import XCTest
1616

1717
/// Used to define lookup name assertion.
1818
protocol ExpectedName {
19-
var marker: String { get }
19+
var marker: [String] { get }
2020
}
2121

2222
extension String: ExpectedName {
23-
var marker: String {
24-
self
23+
var marker: [String] {
24+
[self]
2525
}
2626
}
2727

@@ -63,15 +63,22 @@ enum NameExpectation: ExpectedName {
6363
case declaration(String)
6464
case implicit(ImplicitNameExpectation)
6565
case dollarIdentifier(String, String)
66+
case equivalentNames([NameExpectation])
6667

67-
var marker: String {
68+
var marker: [String] {
6869
switch self {
6970
case .identifier(let marker),
7071
.declaration(let marker),
7172
.dollarIdentifier(let marker, _):
72-
return marker
73+
return [marker]
7374
case .implicit(let implicitName):
74-
return implicitName.marker
75+
return [implicitName.marker]
76+
case .equivalentNames(let expectedNames):
77+
return
78+
expectedNames
79+
.flatMap { expectedName in
80+
expectedName.marker
81+
}
7582
}
7683
}
7784

@@ -86,6 +93,16 @@ enum NameExpectation: ExpectedName {
8693
actualStr == expectedStr,
8794
"For marker \(marker), actual identifier \(actualStr) doesn't match expected \(expectedStr)"
8895
)
96+
case (.equivalentNames(let actualNames), .equivalentNames(let expectedNames)):
97+
XCTAssert(
98+
actualNames.count == expectedNames.count,
99+
"For marker \(marker), actual composite name count "
100+
+ "\(actualNames.count) doesn't match expected \(expectedNames.count)"
101+
)
102+
103+
for (actualName, expectedName) in zip(actualNames, expectedNames) {
104+
expectedName.assertExpectation(marker: marker, for: actualName)
105+
}
89106
default:
90107
XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)")
91108
}

Diff for: Tests/SwiftLexicalLookupTest/NameLookupTests.swift

+38-4
Original file line numberDiff line numberDiff line change
@@ -791,9 +791,11 @@ final class testNameLookup: XCTestCase {
791791
assertLexicalNameLookup(
792792
source: """
793793
switch {
794-
case .x(let 1️⃣a, let 2️⃣b), .y(.c(let 3️⃣c), .z):
795-
print(4️⃣a, 5️⃣b, 6️⃣c)
796-
case .z(let 7️⃣a), .smth(let 8️⃣a)
794+
case .x(let 1️⃣a, let 2️⃣b):
795+
print(4️⃣a, 5️⃣b)
796+
case .y(.c(let 3️⃣c), .z):
797+
print(6️⃣c)
798+
case .z(let 7️⃣a)
797799
print(9️⃣a)
798800
default:
799801
print(0️⃣a)
@@ -803,13 +805,45 @@ final class testNameLookup: XCTestCase {
803805
"4️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["1️⃣"])],
804806
"5️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["2️⃣"])],
805807
"6️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["3️⃣"])],
806-
"9️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["7️⃣", "8️⃣"])],
808+
"9️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["7️⃣"])],
807809
"0️⃣": [],
808810
],
809811
expectedResultTypes: .all(IdentifierPatternSyntax.self)
810812
)
811813
}
812814

815+
func testCompositeNames() {
816+
assertLexicalNameLookup(
817+
source: """
818+
switch X {
819+
case .x(let 1️⃣a, let 2️⃣b), .y(let 3️⃣a, let 4️⃣b):
820+
print(5️⃣x)
821+
case .z(let 7️⃣a), .smth(let 8️⃣a):
822+
print(9️⃣x)
823+
}
824+
""",
825+
references: [
826+
"5️⃣": [
827+
.fromScope(
828+
SwitchCaseSyntax.self,
829+
expectedNames: [
830+
NameExpectation.equivalentNames([.identifier("1️⃣"), .identifier("3️⃣")]),
831+
NameExpectation.equivalentNames([.identifier("2️⃣"), .identifier("4️⃣")]),
832+
]
833+
)
834+
],
835+
"9️⃣": [
836+
.fromScope(
837+
SwitchCaseSyntax.self,
838+
expectedNames: [NameExpectation.equivalentNames([.identifier("7️⃣"), .identifier("8️⃣")])]
839+
)
840+
],
841+
],
842+
expectedResultTypes: .all(IdentifierPatternSyntax.self),
843+
useNilAsTheParameter: true
844+
)
845+
}
846+
813847
func testSimpleGenericParameterScope() {
814848
assertLexicalNameLookup(
815849
source: """

0 commit comments

Comments
 (0)