diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index fb0d2b2ecb9..a5b10a0996a 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -103,7 +103,7 @@ import SwiftSyntax ?? subscriptDecl.endPositionBeforeTrailingTrivia case .variableDecl(let variableDecl): return variableDecl.bindings.first?.accessorBlock?.positionAfterSkippingLeadingTrivia - ?? variableDecl.endPosition + ?? variableDecl.bindings.positionAfterSkippingLeadingTrivia case .accessorDecl(let accessorDecl): return accessorDecl.accessorSpecifier.positionAfterSkippingLeadingTrivia case .deinitializerDecl(let deinitializerDecl): @@ -139,6 +139,19 @@ import SwiftSyntax case implicit(ImplicitDecl) /// Dollar identifier introduced by a closure without parameters. case dollarIdentifier(ClosureExprSyntax, strRepresentation: String) + /// Represents equivalent names grouped together. + /// - Important: The array should be non-empty. + /// + /// ### Example: + /// ```swift + /// switch X { + /// case .a(let x), .b(let x): + /// print(x) // <-- lookup here + /// } + /// ``` + /// For lookup at the given position, the result + /// contains only one name, that represents both `let x` declarations. + case equivalentNames([LookupName]) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { @@ -151,6 +164,8 @@ import SwiftSyntax return implicitName.syntax case .dollarIdentifier(let closureExpr, _): return closureExpr + case .equivalentNames(let names): + return names.first!.syntax } } @@ -165,6 +180,8 @@ import SwiftSyntax return kind.identifier case .dollarIdentifier(_, strRepresentation: _): return nil + case .equivalentNames(let names): + return names.first!.identifier } } @@ -185,6 +202,8 @@ import SwiftSyntax return implicitName.position case .dollarIdentifier(let closureExpr, _): return closureExpr.positionAfterSkippingLeadingTrivia + case .equivalentNames(let names): + return names.first!.position } } @@ -319,6 +338,8 @@ import SwiftSyntax return "implicit: \(strName)" case .dollarIdentifier(_, strRepresentation: let str): return "dollarIdentifier: \(str)" + case .equivalentNames(let names): + return "Composite name: [ \(names.map(\.debugDescription).joined(separator: ", ")) ]" } } } diff --git a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift index 0a6f406caf8..348b57b5def 100644 --- a/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/Scopes/ScopeImplementations.swift @@ -144,7 +144,7 @@ import SwiftSyntax /// Capture, parameter and body names introduced in this scope. @_spi(Experimental) public var defaultIntroducedNames: [LookupName] { - captureNames + parameterNames + introducedNamesInBody + parameterNames + captureNames + introducedNamesInBody } @_spi(Experimental) public var scopeDebugName: String { @@ -202,7 +202,7 @@ import SwiftSyntax } else { signatureResults = LookupResult.getResultArray( for: self, - withNames: (captureNames + parameterNames).filter { name in + withNames: (parameterNames + captureNames).filter { name in checkIdentifier(identifier, refersTo: name, at: lookUpPosition) } ) @@ -574,13 +574,40 @@ import SwiftSyntax @_spi(Experimental) extension SwitchCaseSyntax: SequentialScopeSyntax { /// Names introduced within `case` items. var namesFromLabel: [LookupName] { - label.as(SwitchCaseLabelSyntax.self)?.caseItems.flatMap { child in + guard let switchCaseItemList = label.as(SwitchCaseLabelSyntax.self)?.caseItems else { return [] } + + let extractedNames = switchCaseItemList.flatMap { child in if let exprPattern = child.pattern.as(ExpressionPatternSyntax.self) { return LookupName.getNames(from: exprPattern.expression) } else { return LookupName.getNames(from: child.pattern) } - } ?? [] + } + + if switchCaseItemList.count <= 1 { + return extractedNames + } + + var orderedKeys: [Identifier] = [] + var partitioned: [Identifier: [LookupName]] = [:] + + for extractedName in extractedNames { + guard let identifier = extractedName.identifier else { continue } + + if !partitioned.keys.contains(identifier) { + orderedKeys.append(identifier) + } + + partitioned[identifier, default: []].append(extractedName) + } + + return + orderedKeys + .compactMap { key in + guard let names = partitioned[key] else { return nil } + + return .equivalentNames(names) + } } /// Names introduced within `case` items @@ -607,7 +634,7 @@ import SwiftSyntax checkIdentifier(identifier, refersTo: name, at: lookUpPosition) } - if label.range.contains(lookUpPosition) { + if label.range.contains(lookUpPosition) && !isInWhereClause(lookUpPosition: lookUpPosition) { return config.finishInSequentialScope ? [] : lookupInParent(identifier, at: lookUpPosition, with: config) } else if config.finishInSequentialScope { return sequentialLookup( @@ -629,6 +656,20 @@ import SwiftSyntax + lookupInParent(identifier, at: lookUpPosition, with: config) } } + + /// Returns `true` if `lookUpPosition` is inside a `where` + /// clause associated with one of the case items of this scope. + private func isInWhereClause(lookUpPosition: AbsolutePosition) -> Bool { + guard let switchCaseItemList = label.as(SwitchCaseLabelSyntax.self)?.caseItems else { return false } + + for item in switchCaseItemList { + if item.whereClause?.range.contains(lookUpPosition) ?? false { + return true + } + } + + return false + } } @_spi(Experimental) extension ProtocolDeclSyntax: ScopeSyntax, LookInMembersScopeSyntax { @@ -903,7 +944,9 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe at lookUpPosition: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - if bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false { + if (bindings.first?.accessorBlock?.range.contains(lookUpPosition) ?? false) + || shouldIntroduceSelfIfLazy(lookUpPosition: lookUpPosition) + { return defaultLookupImplementation( in: (isMember ? [.implicit(.self(self))] : LookupName.getNames(from: self)), identifier, @@ -929,6 +972,16 @@ extension SubscriptDeclSyntax: WithGenericParametersScopeSyntax, CanInterleaveRe return resultsToInterleave + lookupInParent(identifier, at: lookUpPosition, with: config) } + + /// Returns `true`, if `lookUpPosition` is in initializer of + /// this variable declaration and the declaration is lazy. + private func shouldIntroduceSelfIfLazy(lookUpPosition: AbsolutePosition) -> Bool { + guard bindings.first?.initializer?.range.contains(lookUpPosition) ?? false else { return false } + + return modifiers.contains { + $0.name.tokenKind == .keyword(.lazy) + } + } } @_spi(Experimental) extension DeinitializerDeclSyntax: ScopeSyntax { diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index ee70e4835c6..6b64adc3a63 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -105,14 +105,18 @@ func assertLexicalNameLookup( ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) return result.flatMap { lookUpResult in - lookUpResult.names.map { lookupName in - lookupName.syntax + lookUpResult.names.flatMap { lookupName in + if case .equivalentNames(let names) = lookupName { + return names.map(\.syntax) + } else { + return [lookupName.syntax] + } } } }, expected: references.mapValues { expectations in expectations.flatMap { expectation in - expectation.expectedNames.map { expectedName in + expectation.expectedNames.flatMap { expectedName in expectedName.marker } } diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift index 059a4a6d1db..4bc408a6dab 100644 --- a/Tests/SwiftLexicalLookupTest/ExpectedName.swift +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -16,12 +16,12 @@ import XCTest /// Used to define lookup name assertion. protocol ExpectedName { - var marker: String { get } + var marker: [String] { get } } extension String: ExpectedName { - var marker: String { - self + var marker: [String] { + [self] } } @@ -63,15 +63,22 @@ enum NameExpectation: ExpectedName { case declaration(String) case implicit(ImplicitNameExpectation) case dollarIdentifier(String, String) + case equivalentNames([NameExpectation]) - var marker: String { + var marker: [String] { switch self { case .identifier(let marker), .declaration(let marker), .dollarIdentifier(let marker, _): - return marker + return [marker] case .implicit(let implicitName): - return implicitName.marker + return [implicitName.marker] + case .equivalentNames(let expectedNames): + return + expectedNames + .flatMap { expectedName in + expectedName.marker + } } } @@ -86,6 +93,16 @@ enum NameExpectation: ExpectedName { actualStr == expectedStr, "For marker \(marker), actual identifier \(actualStr) doesn't match expected \(expectedStr)" ) + case (.equivalentNames(let actualNames), .equivalentNames(let expectedNames)): + XCTAssert( + actualNames.count == expectedNames.count, + "For marker \(marker), actual composite name count " + + "\(actualNames.count) doesn't match expected \(expectedNames.count)" + ) + + for (actualName, expectedName) in zip(actualNames, expectedNames) { + expectedName.assertExpectation(marker: marker, for: actualName) + } default: XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index d9fe4f2ccff..711b6950baf 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -791,9 +791,11 @@ final class testNameLookup: XCTestCase { assertLexicalNameLookup( source: """ switch { - case .x(let 1️⃣a, let 2️⃣b), .y(.c(let 3️⃣c), .z): - print(4️⃣a, 5️⃣b, 6️⃣c) - case .z(let 7️⃣a), .smth(let 8️⃣a) + case .x(let 1️⃣a, let 2️⃣b): + print(4️⃣a, 5️⃣b) + case .y(.c(let 3️⃣c), .z): + print(6️⃣c) + case .z(let 7️⃣a) print(9️⃣a) default: print(0️⃣a) @@ -803,13 +805,45 @@ final class testNameLookup: XCTestCase { "4️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["1️⃣"])], "5️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["2️⃣"])], "6️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["3️⃣"])], - "9️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["7️⃣", "8️⃣"])], + "9️⃣": [.fromScope(SwitchCaseSyntax.self, expectedNames: ["7️⃣"])], "0️⃣": [], ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) } + func testCompositeNames() { + assertLexicalNameLookup( + source: """ + switch X { + case .x(let 1️⃣a, let 2️⃣b), .y(let 3️⃣a, let 4️⃣b): + print(5️⃣x) + case .z(let 7️⃣a), .smth(let 8️⃣a): + print(9️⃣x) + } + """, + references: [ + "5️⃣": [ + .fromScope( + SwitchCaseSyntax.self, + expectedNames: [ + NameExpectation.equivalentNames([.identifier("1️⃣"), .identifier("3️⃣")]), + NameExpectation.equivalentNames([.identifier("2️⃣"), .identifier("4️⃣")]), + ] + ) + ], + "9️⃣": [ + .fromScope( + SwitchCaseSyntax.self, + expectedNames: [NameExpectation.equivalentNames([.identifier("7️⃣"), .identifier("8️⃣")])] + ) + ], + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self), + useNilAsTheParameter: true + ) + } + func testSimpleGenericParameterScope() { assertLexicalNameLookup( source: """