From b9c4f51c8d7006cf28dbc815819482d6c528ea66 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:01:16 +0200 Subject: [PATCH 01/10] Add initial name lookup functionality --- Sources/SwiftLexicalLookup/LookupName.swift | 141 +++++++++++ .../ScopeImplementations.swift | 150 ++++++++++++ Sources/SwiftLexicalLookup/ScopeSyntax.swift | 76 ++++++ .../SimpleLookupQueries.swift | 8 +- Tests/SwiftLexicalLookupTest/Assertions.swift | 92 +++++++- .../NameLookupTests.swift | 220 ++++++++++++++++++ 6 files changed, 672 insertions(+), 15 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/LookupName.swift create mode 100644 Sources/SwiftLexicalLookup/ScopeImplementations.swift create mode 100644 Sources/SwiftLexicalLookup/ScopeSyntax.swift create mode 100644 Tests/SwiftLexicalLookupTest/NameLookupTests.swift diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift new file mode 100644 index 00000000000..9e0615435a7 --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -0,0 +1,141 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +@_spi(Experimental) public enum LookupName { + /// Identifier associated with the name. + /// Could be an identifier of a variable, function or closure parameter and more + case identifier(String, SyntaxProtocol) + /// Declaration associated with the name. + /// Could be class, struct, actor, protocol, function and more + case declaration(String, DeclSyntaxProtocol) + + /// Syntax associated with this name. + @_spi(Experimental) public var syntax: SyntaxProtocol { + switch self { + case .identifier(_, let syntax): + syntax + case .declaration(_, let syntax): + syntax + } + } + + /// Introduced name. + @_spi(Experimental) public var name: String { + switch self { + case .identifier(let name, _): + name + case .declaration(let name, _): + name + } + } + + /// Checks if this name was introduced before the syntax used for lookup. + func isBefore(_ lookedUpSyntax: SyntaxProtocol) -> Bool { + syntax.position < lookedUpSyntax.position + } + + /// Checks if this name refers to the looked up phrase. + func refersTo(_ lookedUpName: String) -> Bool { + name == lookedUpName + } + + /// Extracts names introduced by the given `from` structure. + static func getNames(from syntax: SyntaxProtocol) -> [LookupName] { + switch Syntax(syntax).as(SyntaxEnum.self) { + case .variableDecl(let variableDecl): + variableDecl.bindings.flatMap { binding in + getNames(from: binding.pattern) + } + case .tuplePattern(let tuplePattern): + tuplePattern.elements.flatMap { tupleElement in + getNames(from: tupleElement.pattern) + } + case .valueBindingPattern(let valueBindingPattern): + getNames(from: valueBindingPattern.pattern) + case .expressionPattern(let expressionPattern): + getNames(from: expressionPattern.expression) + case .sequenceExpr(let sequenceExpr): + sequenceExpr.elements.flatMap { expression in + getNames(from: expression) + } + case .patternExpr(let patternExpr): + getNames(from: patternExpr.pattern) + case .optionalBindingCondition(let optionalBinding): + getNames(from: optionalBinding.pattern) + case .identifierPattern(let identifierPattern): + handle(identifierPattern: identifierPattern) + case .closureShorthandParameter(let closureShorthandParameter): + handle(closureShorthandParameter: closureShorthandParameter) + case .closureParameter(let closureParameter): + handle(closureParameter: closureParameter) + case .functionDecl(let functionDecl): + handle(functionDecl: functionDecl) + case .classDecl(let classDecl): + handle(classDecl: classDecl) + case .structDecl(let structDecl): + handle(structDecl: structDecl) + case .actorDecl(let actorDecl): + handle(actorDecl: actorDecl) + case .protocolDecl(let protocolDecl): + handle(protocolDecl: protocolDecl) + default: + [] + } + } + + /// Extracts name introduced by `identifierPattern`. + private static func handle(identifierPattern: IdentifierPatternSyntax) -> [LookupName] { + [.identifier(identifierPattern.identifier.text, identifierPattern)] + } + + /// Extracts name introduced by `closureParameter`. + private static func handle(closureParameter: ClosureParameterSyntax) -> [LookupName] { + [.identifier(closureParameter.secondName?.text ?? closureParameter.firstName.text, closureParameter)] + } + + /// Extracts name introduced by `closureShorthandParameter`. + private static func handle(closureShorthandParameter: ClosureShorthandParameterSyntax) -> [LookupName] { + let name = closureShorthandParameter.name.text + if name != "_" { + return [.identifier(name, closureShorthandParameter)] + } else { + return [] + } + } + + /// Extracts name introduced by `functionDecl`. + private static func handle(functionDecl: FunctionDeclSyntax) -> [LookupName] { + [.declaration(functionDecl.name.text, functionDecl)] + } + + /// Extracts name introduced by `classDecl`. + private static func handle(classDecl: ClassDeclSyntax) -> [LookupName] { + [.declaration(classDecl.name.text, classDecl)] + } + + /// Extracts name introduced by `structDecl`. + private static func handle(structDecl: StructDeclSyntax) -> [LookupName] { + [.declaration(structDecl.name.text, structDecl)] + } + + /// Extracts name introduced by `actorDecl`. + private static func handle(actorDecl: ActorDeclSyntax) -> [LookupName] { + [.declaration(actorDecl.name.text, actorDecl)] + } + + /// Extracts name introduced by `protocolDecl`. + private static func handle(protocolDecl: ProtocolDeclSyntax) -> [LookupName] { + [.declaration(protocolDecl.name.text, protocolDecl)] + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift new file mode 100644 index 00000000000..0e862fa5b49 --- /dev/null +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension SyntaxProtocol { + /// Parent scope of this syntax node, or scope introduced by this syntax node. + var scope: ScopeSyntax? { + switch Syntax(self).as(SyntaxEnum.self) { + case .sourceFile(let sourceFile): + sourceFile + case .codeBlock(let codeBlock): + codeBlock + case .forStmt(let forStmt): + forStmt + case .closureExpr(let closureExpr): + closureExpr + case .whileStmt(let whileStmt): + whileStmt + case .ifExpr(let ifExpr): + ifExpr + case .memberBlock(let memberBlock): + memberBlock + default: + self.parent?.scope + } + } +} + +extension SourceFileSyntax: ScopeSyntax { + var parentScope: ScopeSyntax? { + nil + } + + var introducedNames: [LookupName] { + [] + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + [] + } +} + +extension CodeBlockSyntax: ScopeSyntax { + var introducedNames: [LookupName] { + statements.flatMap { codeBlockItem in + LookupName.getNames(from: codeBlockItem.item) + } + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax, positionSensitive: true) + } +} + +extension ForStmtSyntax: ScopeSyntax { + var introducedNames: [LookupName] { + LookupName.getNames(from: pattern) + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax) + } +} + +extension ClosureExprSyntax: ScopeSyntax { + var introducedNames: [LookupName] { + signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in + if let parameterList = parameter.as(ClosureParameterListSyntax.self) { + parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in + LookupName.getNames(from: parameter) + } + } else { + LookupName.getNames(from: parameter) + } + } ?? [] + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax) + } +} + +extension WhileStmtSyntax: ScopeSyntax { + var introducedNames: [LookupName] { + conditions.flatMap { element in + LookupName.getNames(from: element.condition) + } + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax) + } +} + +extension IfExprSyntax: ScopeSyntax { + var parentScope: ScopeSyntax? { + getParent(for: self.parent, previousIfElse: self.elseKeyword == nil) + } + + /// Finds the parent scope, omitting parent `if` statements if part of their `else if` clause. + private func getParent(for syntax: Syntax?, previousIfElse: Bool) -> ScopeSyntax? { + guard let syntax else { return nil } + + if let lookedUpScope = syntax.scope, lookedUpScope.id != self.id { + if let currentIfExpr = lookedUpScope.as(IfExprSyntax.self), previousIfElse { + return getParent(for: syntax.parent, previousIfElse: currentIfExpr.elseKeyword == nil) + } else { + return lookedUpScope + } + } else { + return getParent(for: syntax.parent, previousIfElse: previousIfElse) + } + } + + var introducedNames: [LookupName] { + conditions.flatMap { element in + LookupName.getNames(from: element.condition) + } + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { + parentScope?.lookup(for: name, at: syntax) ?? [] + } else { + defaultLookupImplementation(for: name, at: syntax, positionSensitive: true) + } + } +} + +extension MemberBlockSyntax: ScopeSyntax { + var introducedNames: [LookupName] { + members.flatMap { member in + LookupName.getNames(from: member.decl) + } + } + + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax) + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift new file mode 100644 index 00000000000..07ac283ee23 --- /dev/null +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +extension SyntaxProtocol { + /// Returns all names that `for` refers to at this syntax node. + /// + /// - Returns: An array of names referred to by `for` at this syntax node, + /// ordered by visibility. The order is from the innermost to the outermost + /// scope, and within each scope, names are ordered by their introduction + /// in the source code. + /// + /// Example usage: + /// ```swift + /// class C { + /// var a = 42 + /// + /// func a(a: Int) { + /// a // <--- lookup here + /// + /// let a = 0 + /// } + /// + /// func a() { + /// // ... + /// } + /// } + /// ``` + /// When calling this function on the declaration reference `a` within its name, + /// the function returns the parameter first, then the identifier of the variable + /// declaration, followed by the first function name, and then the second function name, + /// in this exact order. The constant declaration within the function body is omitted + /// due to the ordering rules that prioritize visibility within the function body. + @_spi(Experimental) public func lookup(for name: String) -> [LookupName] { + scope?.lookup(for: name, at: self) ?? [] + } +} + +protocol ScopeSyntax: SyntaxProtocol { + /// Parent of this scope, or `nil` if it is the root. + var parentScope: ScopeSyntax? { get } + /// Names found in this scope. Ordered from first to last introduced. + var introducedNames: [LookupName] { get } + /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] +} + +extension ScopeSyntax { + var parentScope: ScopeSyntax? { + self.parent?.scope + } + + /// Returns all names introduced in this scope that `name` refers to and then + /// passes lookup to the parent. Optionally, if `positionSensitive` is set to `true`, + /// the method filters names that were introduced in this scope after `syntax`. + func defaultLookupImplementation( + for name: String, + at syntax: SyntaxProtocol, + positionSensitive: Bool = false + ) -> [LookupName] { + introducedNames + .filter { introducedName in + (!positionSensitive || introducedName.isBefore(syntax)) && introducedName.refersTo(name) + } + (parentScope?.lookup(for: name, at: syntax) ?? []) + } +} diff --git a/Sources/SwiftLexicalLookup/SimpleLookupQueries.swift b/Sources/SwiftLexicalLookup/SimpleLookupQueries.swift index f530c87856e..461ad0fc6c2 100644 --- a/Sources/SwiftLexicalLookup/SimpleLookupQueries.swift +++ b/Sources/SwiftLexicalLookup/SimpleLookupQueries.swift @@ -68,7 +68,8 @@ extension SyntaxProtocol { // MARK: - lookupCatchNode - /// Given syntax node location, finds where an error could be caught. If `traverseCatchClause` is set to `true` lookup will skip the next do statement. + /// Given syntax node location, finds where an error could be caught. + /// If `traverseCatchClause` is set to `true` lookup will skip the next do statement. private func lookupCatchNodeHelper(traversedCatchClause: Bool) -> Syntax? { guard let parent else { return nil } @@ -108,7 +109,8 @@ extension SyntaxProtocol { collectNodesOfTypeUpToFunctionBoundary(type, stopWithFirstMatch: true).first } - /// Collect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. The nodes in the array are inside out, with the innermost node being the first. + /// Collect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. + /// The nodes in the array are inside out, with the innermost node being the first. fileprivate func collectNodesOfTypeUpToFunctionBoundary( _ type: T.Type, stopWithFirstMatch: Bool = false @@ -128,7 +130,7 @@ extension SyntaxProtocol { ) } - /// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. + /// Collect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. private func collectNodes( ofType type: T.Type, upTo stopAt: [SyntaxProtocol.Type], diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index d4126c02a1d..fa9f3b040cc 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -17,11 +17,50 @@ import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport -/// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. +/// Used to define result type expectectations for given markers. +enum MarkerExpectation { + /// Specifies a separate type for each result marker. + case distinct([String: SyntaxProtocol.Type]) + /// Specifies a common type for all results + /// apart from the ones defined explicitly in `except`. + case all(SyntaxProtocol.Type, except: [String: SyntaxProtocol.Type] = [:]) + /// Does not assert result types. + case none + + /// Assert `actual` result labeled with `marker` + /// according to the rules represented by this expectation. + fileprivate func assertMarkerType(marker: String, actual: SyntaxProtocol) { + switch self { + case .all(let expectedType, except: let dictionary): + assertMarkerType(marker: marker, actual: actual, expectedType: dictionary[marker] ?? expectedType) + case .distinct(let dictionary): + if let expectedType = dictionary[marker] { + assertMarkerType(marker: marker, actual: actual, expectedType: expectedType) + } else { + XCTFail("For result \(marker), could not find type expectation") + } + case .none: + break + } + } + + /// Assert whether `actual` type matches `expectedType`. + private func assertMarkerType(marker: String, actual: SyntaxProtocol, expectedType: SyntaxProtocol.Type) { + XCTAssert( + actual.is(expectedType), + "For result \(marker), expected type \(expectedType) doesn't match the actual type \(actual.syntaxNodeType)" + ) + } +} + +/// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. +/// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. +/// It also checks whether result types match rules specified in `expectedResultTypes`. func assertLexicalScopeQuery( source: String, methodUnderTest: (TokenSyntax) -> ([SyntaxProtocol?]), - expected: [String: [String?]] + expected: [String: [String?]], + expectedResultTypes: MarkerExpectation = .none ) { // Extract markers let (markerDict, textWithoutMarkers) = extractMarkers(source) @@ -44,12 +83,12 @@ func assertLexicalScopeQuery( let result = methodUnderTest(testArgument) // Extract the expected results for the test argument - let expectedValues: [AbsolutePosition?] = expectedMarkers.map { expectedMarker in + let expectedPositions: [AbsolutePosition?] = expectedMarkers.map { expectedMarker in guard let expectedMarker else { return nil } guard let expectedPosition = markerDict[expectedMarker] else { - XCTFail("Could not find token at location \(marker)") + XCTFail("Could not find position for \(marker)") return nil } @@ -57,27 +96,56 @@ func assertLexicalScopeQuery( } // Compare number of actual results to the number of expected results - if result.count != expectedValues.count { + if result.count != expectedPositions.count { XCTFail( - "For marker \(marker), actual number of elements: \(result.count) doesn't match the expected: \(expectedValues.count)" + "For marker \(marker), actual number of elements: \(result.count) doesn't match the expected: \(expectedPositions.count)" ) } // Assert validity of the output - for (actual, expected) in zip(result, expectedValues) { - if actual == nil && expected == nil { continue } + for (actual, expected) in zip(result, zip(expectedMarkers, expectedPositions)) { + if actual == nil && expected.1 == nil { continue } - guard let actual, let expected else { + guard let actual else { XCTFail( - "For marker \(marker), actual result: \(actual?.description ?? "nil"), expected position: \(expected.debugDescription)" + "For marker \(marker), actual is nil while expected is \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" ) continue } + guard let expectedPosition = expected.1 else { + XCTFail("For marker \(marker), actual is \(actual) while expected position is nil") + continue + } + XCTAssert( - actual.positionAfterSkippingLeadingTrivia == expected, - "For marker \(marker), actual result: \(actual.description) doesn't match expected value: \(sourceFileSyntax.token(at: expected) ?? "nil")" + actual.positionAfterSkippingLeadingTrivia == expectedPosition, + "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expected.1!)?.description ?? "nil")" ) + + if let expectedMarker = expected.0 { + expectedResultTypes.assertMarkerType(marker: expectedMarker, actual: actual) + } } } } + +/// Name lookup is called with the token at every position marker in the keys of `expected`. +/// It then asserts that the positions of the syntax nodes returned by the lookup are the values in `expected`. +/// It also checks whether result types match rules specified in `expectedResultTypes`. +func assertLexicalNameLookup( + source: String, + references: [String: [String]], + expectedResultTypes: MarkerExpectation = .none +) { + assertLexicalScopeQuery( + source: source, + methodUnderTest: { argument in + return argument.lookup(for: argument.text).map { lookUpResult in + lookUpResult.syntax + } + }, + expected: references, + expectedResultTypes: expectedResultTypes + ) +} diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift new file mode 100644 index 00000000000..7cf33be3402 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -0,0 +1,220 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import Foundation +@_spi(Testing) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +final class testNameLookup: XCTestCase { + func testCodeBlockSimpleCase() { + assertLexicalNameLookup( + source: """ + for i in 1..<4 { + let 1️⃣a = i + let 2️⃣b = 3️⃣a + + for j in 1..<4 { + let 4️⃣c = 5️⃣a + let 6️⃣a = j + + let d = 7️⃣a + 8️⃣b + 9️⃣c + } + } + """, + references: ["3️⃣": ["1️⃣"], "5️⃣": ["1️⃣"], "7️⃣": ["6️⃣", "1️⃣"], "8️⃣": ["2️⃣"], "9️⃣": ["4️⃣"]], + expectedResultTypes: .all(IdentifierPatternSyntax.self) + ) + } + + func testLookupForComplexDeclarationsInCodeBlock() { + assertLexicalNameLookup( + source: """ + for i in 1..<4 { + let (1️⃣a, 2️⃣b) = (1, 2) + let 3️⃣c = 3, 4️⃣d = 4 + + 5️⃣a + 6️⃣b + 7️⃣c + 8️⃣d + } + """, + references: ["5️⃣": ["1️⃣"], "6️⃣": ["2️⃣"], "7️⃣": ["3️⃣"], "8️⃣": ["4️⃣"]], + expectedResultTypes: .all(IdentifierPatternSyntax.self) + ) + } + + func testLookupForLoop() { + assertLexicalNameLookup( + source: """ + for 1️⃣i in 1..<4 { + let (a, b) = (2️⃣i, 3️⃣j) + for (4️⃣i, 5️⃣j) in foo { + let (c, d) = (6️⃣i, 7️⃣j) + } + } + """, + references: ["2️⃣": ["1️⃣"], "3️⃣": [], "6️⃣": ["4️⃣", "1️⃣"], "7️⃣": ["5️⃣"]], + expectedResultTypes: .all(IdentifierPatternSyntax.self) + ) + } + + func testLookupForCaseLetLoop() { + assertLexicalNameLookup( + source: """ + for case let 1️⃣a as T in arr { + 2️⃣a.foo() + } + """, + references: ["2️⃣": ["1️⃣"]], + expectedResultTypes: .all(IdentifierPatternSyntax.self) + ) + } + + func testShorthandParameterLookupClosure() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let 2️⃣b = 2 + let 3️⃣x: (Int, Int, Int) = { 4️⃣a, _, 5️⃣c in + print(6️⃣a, 7️⃣b, 8️⃣c, 0️⃣$0) + } + 9️⃣x() + } + """, + references: ["6️⃣": ["4️⃣", "1️⃣"], "7️⃣": ["2️⃣"], "8️⃣": ["5️⃣"], "9️⃣": ["3️⃣"], "0️⃣": []], + expectedResultTypes: .all( + IdentifierPatternSyntax.self, + except: [ + "4️⃣": ClosureShorthandParameterSyntax.self, + "5️⃣": ClosureShorthandParameterSyntax.self, + ] + ) + ) + } + + func testParameterLookupClosure() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let 2️⃣b = 2 + let 3️⃣x = { (4️⃣a b: Int, 5️⃣c: Int) in + print(6️⃣a, 7️⃣b, 8️⃣c, 0️⃣$0) + } + 9️⃣x() + } + """, + references: ["6️⃣": ["1️⃣"], "7️⃣": ["4️⃣", "2️⃣"], "8️⃣": ["5️⃣"], "9️⃣": ["3️⃣"], "0️⃣": []], + expectedResultTypes: .all( + IdentifierPatternSyntax.self, + except: [ + "4️⃣": ClosureParameterSyntax.self, + "5️⃣": ClosureParameterSyntax.self, + ] + ) + ) + } + + func testWhileOptionalBindingLookup() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣b = x + while let 2️⃣a = 3️⃣b { + let 4️⃣b = x + print(5️⃣a, 6️⃣b) + } + } + """, + references: ["3️⃣": ["1️⃣"], "5️⃣": ["2️⃣"], "6️⃣": ["4️⃣", "1️⃣"]], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } + + func testIfLetOptionalBindingSimpleCaseWithPrecedence() { + assertLexicalNameLookup( + source: """ + if let 1️⃣a = 2️⃣b, let 3️⃣b = 4️⃣a { + print(5️⃣a, 6️⃣b) + } else { + print(7️⃣a, 8️⃣b) + } + """, + references: ["2️⃣": [], "4️⃣": ["1️⃣"], "5️⃣": ["1️⃣"], "6️⃣": ["3️⃣"], "7️⃣": [], "8️⃣": []], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } + + func testIfLetWithElseIfAndNesting() { + assertLexicalNameLookup( + source: """ + if let 1️⃣a = x { + if let 2️⃣a = x { + print(3️⃣a) + } else if let 4️⃣a = x { + print(5️⃣a) + } else { + print(6️⃣a) + } + print(7️⃣a) + } else if let 8️⃣a = x { + print(9️⃣a) + } else { + print(0️⃣a) + } + """, + references: ["3️⃣": ["2️⃣", "1️⃣"], "5️⃣": ["4️⃣", "1️⃣"], "6️⃣": ["1️⃣"], "7️⃣": ["1️⃣"], "9️⃣": ["8️⃣"], "0️⃣": []], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } + + func testMemberBlockScope() { + assertLexicalNameLookup( + source: """ + class x { + var 1️⃣a = 1 + + 2️⃣class b {} + 3️⃣struct b {} + + 4️⃣func a { + 5️⃣a + 6️⃣b + 7️⃣c + 8️⃣d + } + + 9️⃣actor c {} + 0️⃣protocol d {} + } + """, + references: ["5️⃣": ["1️⃣", "4️⃣"], "6️⃣": ["2️⃣", "3️⃣"], "7️⃣": ["9️⃣"], "8️⃣": ["0️⃣"]], + expectedResultTypes: .distinct([ + "1️⃣": IdentifierPatternSyntax.self, + "2️⃣": ClassDeclSyntax.self, + "3️⃣": StructDeclSyntax.self, + "4️⃣": FunctionDeclSyntax.self, + "9️⃣": ActorDeclSyntax.self, + "0️⃣": ProtocolDeclSyntax.self, + ]) + ) + } +} From 34500294a44245bcb43c9ea918d711487cb34476 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:27:24 +0200 Subject: [PATCH 02/10] Move name accessibility evaluation to `LookupName`. Simplify name extraction in `LookupName`. --- .../IdentifiableSyntax.swift | 32 +++++ Sources/SwiftLexicalLookup/LookupName.swift | 119 +++++++----------- .../ScopeImplementations.swift | 53 +------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 16 ++- .../NameLookupTests.swift | 24 ++++ 5 files changed, 117 insertions(+), 127 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/IdentifiableSyntax.swift diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift new file mode 100644 index 00000000000..750e3ef4220 --- /dev/null +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -0,0 +1,32 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Syntax node that can be refered to with an identifier. +public protocol IdentifiableSyntax: SyntaxProtocol { + var identifier: TokenSyntax { get } +} + +extension IdentifierPatternSyntax: IdentifiableSyntax {} + +extension ClosureParameterSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax { + secondName ?? firstName + } +} + +extension ClosureShorthandParameterSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax { + name + } +} diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 9e0615435a7..2fc512797e2 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -15,17 +15,17 @@ import SwiftSyntax @_spi(Experimental) public enum LookupName { /// Identifier associated with the name. /// Could be an identifier of a variable, function or closure parameter and more - case identifier(String, SyntaxProtocol) + case identifier(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Declaration associated with the name. /// Could be class, struct, actor, protocol, function and more - case declaration(String, DeclSyntaxProtocol) + case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { switch self { - case .identifier(_, let syntax): + case .identifier(let syntax, _): syntax - case .declaration(_, let syntax): + case .declaration(let syntax, _): syntax } } @@ -33,16 +33,28 @@ import SwiftSyntax /// Introduced name. @_spi(Experimental) public var name: String { switch self { - case .identifier(let name, _): - name - case .declaration(let name, _): - name + case .identifier(let syntax, _): + syntax.identifier.text + case .declaration(let syntax, _): + syntax.name.text + } + } + + /// Point, after which the name is available in scope. + /// If set to `nil`, the name is available at any point in scope. + var accessibleAfter: AbsolutePosition? { + switch self { + case .identifier(_, let absolutePosition): + absolutePosition + case .declaration(_, let absolutePosition): + absolutePosition } } /// Checks if this name was introduced before the syntax used for lookup. - func isBefore(_ lookedUpSyntax: SyntaxProtocol) -> Bool { - syntax.position < lookedUpSyntax.position + func isAccessible(at lookedUpSyntax: SyntaxProtocol) -> Bool { + guard let accessibleAfter else { return true } + return accessibleAfter <= lookedUpSyntax.position } /// Checks if this name refers to the looked up phrase. @@ -51,91 +63,50 @@ import SwiftSyntax } /// Extracts names introduced by the given `from` structure. - static func getNames(from syntax: SyntaxProtocol) -> [LookupName] { + static func getNames(from syntax: SyntaxProtocol, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { switch Syntax(syntax).as(SyntaxEnum.self) { case .variableDecl(let variableDecl): variableDecl.bindings.flatMap { binding in - getNames(from: binding.pattern) + getNames(from: binding.pattern, accessibleAfter: accessibleAfter) } case .tuplePattern(let tuplePattern): tuplePattern.elements.flatMap { tupleElement in - getNames(from: tupleElement.pattern) + getNames(from: tupleElement.pattern, accessibleAfter: accessibleAfter) } case .valueBindingPattern(let valueBindingPattern): - getNames(from: valueBindingPattern.pattern) + getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) case .expressionPattern(let expressionPattern): - getNames(from: expressionPattern.expression) + getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) case .sequenceExpr(let sequenceExpr): sequenceExpr.elements.flatMap { expression in - getNames(from: expression) + getNames(from: expression, accessibleAfter: accessibleAfter) } case .patternExpr(let patternExpr): - getNames(from: patternExpr.pattern) + getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) case .optionalBindingCondition(let optionalBinding): - getNames(from: optionalBinding.pattern) - case .identifierPattern(let identifierPattern): - handle(identifierPattern: identifierPattern) - case .closureShorthandParameter(let closureShorthandParameter): - handle(closureShorthandParameter: closureShorthandParameter) - case .closureParameter(let closureParameter): - handle(closureParameter: closureParameter) - case .functionDecl(let functionDecl): - handle(functionDecl: functionDecl) - case .classDecl(let classDecl): - handle(classDecl: classDecl) - case .structDecl(let structDecl): - handle(structDecl: structDecl) - case .actorDecl(let actorDecl): - handle(actorDecl: actorDecl) - case .protocolDecl(let protocolDecl): - handle(protocolDecl: protocolDecl) + getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) default: - [] + if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { + handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) + } else if let identifiable = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? IdentifiableSyntax { + handle(identifiable: identifiable, accessibleAfter: accessibleAfter) + } else { + [] + } } } - /// Extracts name introduced by `identifierPattern`. - private static func handle(identifierPattern: IdentifierPatternSyntax) -> [LookupName] { - [.identifier(identifierPattern.identifier.text, identifierPattern)] - } - - /// Extracts name introduced by `closureParameter`. - private static func handle(closureParameter: ClosureParameterSyntax) -> [LookupName] { - [.identifier(closureParameter.secondName?.text ?? closureParameter.firstName.text, closureParameter)] - } - - /// Extracts name introduced by `closureShorthandParameter`. - private static func handle(closureShorthandParameter: ClosureShorthandParameterSyntax) -> [LookupName] { - let name = closureShorthandParameter.name.text - if name != "_" { - return [.identifier(name, closureShorthandParameter)] + /// Extracts name introduced by `IdentifiableSyntax` node. + private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + if identifiable.identifier.text != "_" { + return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] } } - - /// Extracts name introduced by `functionDecl`. - private static func handle(functionDecl: FunctionDeclSyntax) -> [LookupName] { - [.declaration(functionDecl.name.text, functionDecl)] - } - - /// Extracts name introduced by `classDecl`. - private static func handle(classDecl: ClassDeclSyntax) -> [LookupName] { - [.declaration(classDecl.name.text, classDecl)] - } - - /// Extracts name introduced by `structDecl`. - private static func handle(structDecl: StructDeclSyntax) -> [LookupName] { - [.declaration(structDecl.name.text, structDecl)] - } - - /// Extracts name introduced by `actorDecl`. - private static func handle(actorDecl: ActorDeclSyntax) -> [LookupName] { - [.declaration(actorDecl.name.text, actorDecl)] - } - - /// Extracts name introduced by `protocolDecl`. - private static func handle(protocolDecl: ProtocolDeclSyntax) -> [LookupName] { - [.declaration(protocolDecl.name.text, protocolDecl)] + + /// Extracts name introduced by `NamedDeclSyntax` node. + private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + [.declaration(namedDecl, accessibleAfter: accessibleAfter)] } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 0e862fa5b49..17b5f428140 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -15,61 +15,32 @@ import SwiftSyntax extension SyntaxProtocol { /// Parent scope of this syntax node, or scope introduced by this syntax node. var scope: ScopeSyntax? { - switch Syntax(self).as(SyntaxEnum.self) { - case .sourceFile(let sourceFile): - sourceFile - case .codeBlock(let codeBlock): - codeBlock - case .forStmt(let forStmt): - forStmt - case .closureExpr(let closureExpr): - closureExpr - case .whileStmt(let whileStmt): - whileStmt - case .ifExpr(let ifExpr): - ifExpr - case .memberBlock(let memberBlock): - memberBlock - default: + if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { + scopeSyntax + } else { self.parent?.scope } } } extension SourceFileSyntax: ScopeSyntax { - var parentScope: ScopeSyntax? { - nil - } - var introducedNames: [LookupName] { [] } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - [] - } } extension CodeBlockSyntax: ScopeSyntax { var introducedNames: [LookupName] { statements.flatMap { codeBlockItem in - LookupName.getNames(from: codeBlockItem.item) + LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.item.endPosition) } } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - defaultLookupImplementation(for: name, at: syntax, positionSensitive: true) - } } extension ForStmtSyntax: ScopeSyntax { var introducedNames: [LookupName] { LookupName.getNames(from: pattern) } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - defaultLookupImplementation(for: name, at: syntax) - } } extension ClosureExprSyntax: ScopeSyntax { @@ -84,10 +55,6 @@ extension ClosureExprSyntax: ScopeSyntax { } } ?? [] } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - defaultLookupImplementation(for: name, at: syntax) - } } extension WhileStmtSyntax: ScopeSyntax { @@ -96,10 +63,6 @@ extension WhileStmtSyntax: ScopeSyntax { LookupName.getNames(from: element.condition) } } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - defaultLookupImplementation(for: name, at: syntax) - } } extension IfExprSyntax: ScopeSyntax { @@ -124,7 +87,7 @@ extension IfExprSyntax: ScopeSyntax { var introducedNames: [LookupName] { conditions.flatMap { element in - LookupName.getNames(from: element.condition) + LookupName.getNames(from: element.condition, accessibleAfter: element.condition.endPosition) } } @@ -132,7 +95,7 @@ extension IfExprSyntax: ScopeSyntax { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { parentScope?.lookup(for: name, at: syntax) ?? [] } else { - defaultLookupImplementation(for: name, at: syntax, positionSensitive: true) + defaultLookupImplementation(for: name, at: syntax) } } } @@ -143,8 +106,4 @@ extension MemberBlockSyntax: ScopeSyntax { LookupName.getNames(from: member.decl) } } - - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { - defaultLookupImplementation(for: name, at: syntax) - } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 07ac283ee23..76ca798f764 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -59,18 +59,22 @@ extension ScopeSyntax { var parentScope: ScopeSyntax? { self.parent?.scope } + + /// Returns all names introduced in this scope that `name` refers to and + /// is accessible at given syntax node then passes lookup to the parent. + func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + defaultLookupImplementation(for: name, at: syntax) + } - /// Returns all names introduced in this scope that `name` refers to and then - /// passes lookup to the parent. Optionally, if `positionSensitive` is set to `true`, - /// the method filters names that were introduced in this scope after `syntax`. + /// Returns all names introduced in this scope that `name` refers to and + /// is accessible at given syntax node then passes lookup to the parent. func defaultLookupImplementation( for name: String, - at syntax: SyntaxProtocol, - positionSensitive: Bool = false + at syntax: SyntaxProtocol ) -> [LookupName] { introducedNames .filter { introducedName in - (!positionSensitive || introducedName.isBefore(syntax)) && introducedName.refersTo(name) + introducedName.isAccessible(at: syntax) && introducedName.refersTo(name) } + (parentScope?.lookup(for: name, at: syntax) ?? []) } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 7cf33be3402..10a302b7360 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -217,4 +217,28 @@ final class testNameLookup: XCTestCase { ]) ) } + + func testLookupInDeclaration() { + assertLexicalNameLookup( + source: """ + class foo { + let 1️⃣a = 2️⃣a + + func foo() { + let 3️⃣a = 4️⃣a + + if let 5️⃣a = 6️⃣a { + let (a, b) = 8️⃣a + } + } + + let 9️⃣a = 0️⃣a + } + """, + references: ["2️⃣": ["1️⃣", "9️⃣"], "0️⃣": ["1️⃣", "9️⃣"], "4️⃣": ["1️⃣", "9️⃣"], "6️⃣": ["3️⃣", "1️⃣", "9️⃣"], "8️⃣": ["5️⃣", "3️⃣", "1️⃣", "9️⃣"]], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } } From 6c402030d61a3289dd7563fb8488403e4c0a8a17 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:15:49 +0200 Subject: [PATCH 03/10] Add new result structure using `LookupResult` enum array. --- Sources/SwiftLexicalLookup/LookupName.swift | 7 +- Sources/SwiftLexicalLookup/LookupResult.swift | 35 ++++ .../ScopeImplementations.swift | 36 ++--- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 51 +++--- Tests/SwiftLexicalLookupTest/Assertions.swift | 66 +++++++- .../NameLookupTests.swift | 152 ++++++++++++++++-- .../SimpleQueryTests.swift | 18 +-- 7 files changed, 293 insertions(+), 72 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/LookupResult.swift diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 2fc512797e2..89d0f71eeb8 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -39,7 +39,7 @@ import SwiftSyntax syntax.name.text } } - + /// Point, after which the name is available in scope. /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { @@ -97,14 +97,15 @@ import SwiftSyntax } /// Extracts name introduced by `IdentifiableSyntax` node. - private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { + private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] + { if identifiable.identifier.text != "_" { return [.identifier(identifiable, accessibleAfter: accessibleAfter)] } else { return [] } } - + /// Extracts name introduced by `NamedDeclSyntax` node. private static func handle(namedDecl: NamedDeclSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] { [.declaration(namedDecl, accessibleAfter: accessibleAfter)] diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift new file mode 100644 index 00000000000..1aa552f549d --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Represents resul +@_spi(Experimental) public enum LookupResult { + /// Scope and the names that matched lookup. + case fromScope(ScopeSyntax, withNames: [LookupName]) + + /// Associated scope. + @_spi(Experimental) public var scope: ScopeSyntax? { + switch self { + case .fromScope(let scopeSyntax, _): + scopeSyntax + } + } + + /// Names that matched lookup. + @_spi(Experimental) public var names: [LookupName] { + switch self { + case .fromScope(_, let names): + names + } + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 17b5f428140..fabda38ce44 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -23,28 +23,28 @@ extension SyntaxProtocol { } } -extension SourceFileSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { [] } } -extension CodeBlockSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { statements.flatMap { codeBlockItem in - LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.item.endPosition) + LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) } } } -extension ForStmtSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) } } -extension ClosureExprSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in if let parameterList = parameter.as(ClosureParameterListSyntax.self) { parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in @@ -57,16 +57,16 @@ extension ClosureExprSyntax: ScopeSyntax { } } -extension WhileStmtSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension WhileStmtSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition) } } } -extension IfExprSyntax: ScopeSyntax { - var parentScope: ScopeSyntax? { +@_spi(Experimental) extension IfExprSyntax: ScopeSyntax { + public var parentScope: ScopeSyntax? { getParent(for: self.parent, previousIfElse: self.elseKeyword == nil) } @@ -85,13 +85,13 @@ extension IfExprSyntax: ScopeSyntax { } } - var introducedNames: [LookupName] { + public var introducedNames: [LookupName] { conditions.flatMap { element in - LookupName.getNames(from: element.condition, accessibleAfter: element.condition.endPosition) + LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) } } - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { parentScope?.lookup(for: name, at: syntax) ?? [] } else { @@ -100,8 +100,8 @@ extension IfExprSyntax: ScopeSyntax { } } -extension MemberBlockSyntax: ScopeSyntax { - var introducedNames: [LookupName] { +@_spi(Experimental) extension MemberBlockSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { members.flatMap { member in LookupName.getNames(from: member.decl) } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 76ca798f764..412ad01338e 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -15,9 +15,10 @@ import SwiftSyntax extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. /// - /// - Returns: An array of names referred to by `for` at this syntax node, - /// ordered by visibility. The order is from the innermost to the outermost - /// scope, and within each scope, names are ordered by their introduction + /// - Returns: An array of `LookupResult` for name `for` at this syntax node, + /// ordered by visibility. If set to `nil`, returns all available names ordered by visibility. + /// The order is from the innermost to the outermost scope, + /// and within each result, names are ordered by their introduction /// in the source code. /// /// Example usage: @@ -41,40 +42,50 @@ extension SyntaxProtocol { /// declaration, followed by the first function name, and then the second function name, /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. - @_spi(Experimental) public func lookup(for name: String) -> [LookupName] { + @_spi(Experimental) public func lookup(for name: String?) -> [LookupResult] { scope?.lookup(for: name, at: self) ?? [] } } -protocol ScopeSyntax: SyntaxProtocol { +@_spi(Experimental) public protocol ScopeSyntax: SyntaxProtocol { /// Parent of this scope, or `nil` if it is the root. var parentScope: ScopeSyntax? { get } /// Names found in this scope. Ordered from first to last introduced. var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] + /// If `name` set to `nil`, returns all available names at the given node. + func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] } -extension ScopeSyntax { - var parentScope: ScopeSyntax? { +@_spi(Experimental) extension ScopeSyntax { + public var parentScope: ScopeSyntax? { self.parent?.scope } - - /// Returns all names introduced in this scope that `name` refers to and - /// is accessible at given syntax node then passes lookup to the parent. - func lookup(for name: String, at syntax: SyntaxProtocol) -> [LookupName] { + + /// Returns `LookupResult` of all names introduced in this scope that `name` + /// refers to and is accessible at given syntax node then passes lookup to the parent. + /// If `name` set to `nil`, returns all available names at the given node. + public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] { defaultLookupImplementation(for: name, at: syntax) } - /// Returns all names introduced in this scope that `name` refers to and - /// is accessible at given syntax node then passes lookup to the parent. - func defaultLookupImplementation( - for name: String, + /// Returns `LookupResult` of all names introduced in this scope that `name` + /// refers to and is accessible at given syntax node then passes lookup to the parent. + /// If `name` set to `nil`, returns all available names at the given node. + public func defaultLookupImplementation( + for name: String?, at syntax: SyntaxProtocol - ) -> [LookupName] { - introducedNames + ) -> [LookupResult] { + let filteredNames = + introducedNames .filter { introducedName in - introducedName.isAccessible(at: syntax) && introducedName.refersTo(name) - } + (parentScope?.lookup(for: name, at: syntax) ?? []) + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + } + + if filteredNames.isEmpty { + return parentScope?.lookup(for: name, at: syntax) ?? [] + } else { + return [.fromScope(self, withNames: filteredNames)] + (parentScope?.lookup(for: name, at: syntax) ?? []) + } } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index fa9f3b040cc..2bd9f025737 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -53,12 +53,31 @@ enum MarkerExpectation { } } +/// Used to define +enum ResultExpectation { + case fromScope(ScopeSyntax.Type, expectedNames: [String]) + + var expectedNames: [String] { + switch self { + case .fromScope(_, let expectedNames): + expectedNames + } + } + + static func == (lhs: ResultExpectation, rhs: LookupResult) -> Bool { + switch (lhs, rhs) { + case (.fromScope, .fromScope): + return true + } + } +} + /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. /// It then asserts that the positions of the syntax nodes returned by `methodUnderTest` are the values in `expected`. /// It also checks whether result types match rules specified in `expectedResultTypes`. func assertLexicalScopeQuery( source: String, - methodUnderTest: (TokenSyntax) -> ([SyntaxProtocol?]), + methodUnderTest: (String, TokenSyntax) -> ([SyntaxProtocol?]), expected: [String: [String?]], expectedResultTypes: MarkerExpectation = .none ) { @@ -80,7 +99,7 @@ func assertLexicalScopeQuery( } // Execute the tested method - let result = methodUnderTest(testArgument) + let result = methodUnderTest(marker, testArgument) // Extract the expected results for the test argument let expectedPositions: [AbsolutePosition?] = expectedMarkers.map { expectedMarker in @@ -135,17 +154,48 @@ func assertLexicalScopeQuery( /// It also checks whether result types match rules specified in `expectedResultTypes`. func assertLexicalNameLookup( source: String, - references: [String: [String]], - expectedResultTypes: MarkerExpectation = .none + references: [String: [ResultExpectation]], + expectedResultTypes: MarkerExpectation = .none, + useNilAsTheParameter: Bool = false ) { assertLexicalScopeQuery( source: source, - methodUnderTest: { argument in - return argument.lookup(for: argument.text).map { lookUpResult in - lookUpResult.syntax + methodUnderTest: { marker, argument in + let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text) + + guard let expectedValues = references[marker] else { + XCTFail("For marker \(marker), couldn't find expectation") + return [] + } + + for (actual, expected) in zip(result, expectedValues) { + XCTAssert( + expected == actual, + "For marker \(marker), expected actual result \(actual) doesn't match expected \(expected)" + ) + + switch actual { + case .fromScope(let scope, withNames: _): + if case .fromScope(let expectedType, expectedNames: _) = expected { + XCTAssert( + scope.syntaxNodeType == expectedType, + "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" + ) + } + } + } + + return result.flatMap { lookUpResult in + lookUpResult.names.map { lookupName in + lookupName.syntax + } + } + }, + expected: references.mapValues { expectations in + expectations.flatMap { expectation in + expectation.expectedNames } }, - expected: references, expectedResultTypes: expectedResultTypes ) } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 10a302b7360..e7d2dac28f5 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -31,7 +31,16 @@ final class testNameLookup: XCTestCase { } } """, - references: ["3️⃣": ["1️⃣"], "5️⃣": ["1️⃣"], "7️⃣": ["6️⃣", "1️⃣"], "8️⃣": ["2️⃣"], "9️⃣": ["4️⃣"]], + references: [ + "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "7️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["6️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "8️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"])], + "9️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣"])], + ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) } @@ -49,7 +58,12 @@ final class testNameLookup: XCTestCase { 8️⃣d } """, - references: ["5️⃣": ["1️⃣"], "6️⃣": ["2️⃣"], "7️⃣": ["3️⃣"], "8️⃣": ["4️⃣"]], + references: [ + "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"])], + "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "8️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣"])], + ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) } @@ -64,7 +78,15 @@ final class testNameLookup: XCTestCase { } } """, - references: ["2️⃣": ["1️⃣"], "3️⃣": [], "6️⃣": ["4️⃣", "1️⃣"], "7️⃣": ["5️⃣"]], + references: [ + "2️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["1️⃣"])], + "3️⃣": [], + "6️⃣": [ + .fromScope(ForStmtSyntax.self, expectedNames: ["4️⃣"]), + .fromScope(ForStmtSyntax.self, expectedNames: ["1️⃣"]), + ], + "7️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["5️⃣"])], + ], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) } @@ -76,7 +98,7 @@ final class testNameLookup: XCTestCase { 2️⃣a.foo() } """, - references: ["2️⃣": ["1️⃣"]], + references: ["2️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["1️⃣"])]], expectedResultTypes: .all(IdentifierPatternSyntax.self) ) } @@ -93,7 +115,16 @@ final class testNameLookup: XCTestCase { 9️⃣x() } """, - references: ["6️⃣": ["4️⃣", "1️⃣"], "7️⃣": ["2️⃣"], "8️⃣": ["5️⃣"], "9️⃣": ["3️⃣"], "0️⃣": []], + references: [ + "6️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: ["4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"])], + "8️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["5️⃣"])], + "9️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "0️⃣": [], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self, except: [ @@ -116,7 +147,16 @@ final class testNameLookup: XCTestCase { 9️⃣x() } """, - references: ["6️⃣": ["1️⃣"], "7️⃣": ["4️⃣", "2️⃣"], "8️⃣": ["5️⃣"], "9️⃣": ["3️⃣"], "0️⃣": []], + references: [ + "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "7️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: ["4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"]), + ], + "8️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["5️⃣"])], + "9️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "0️⃣": [], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self, except: [ @@ -138,7 +178,14 @@ final class testNameLookup: XCTestCase { } } """, - references: ["3️⃣": ["1️⃣"], "5️⃣": ["2️⃣"], "6️⃣": ["4️⃣", "1️⃣"]], + references: [ + "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [.fromScope(WhileStmtSyntax.self, expectedNames: ["2️⃣"])], + "6️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self ) @@ -154,7 +201,14 @@ final class testNameLookup: XCTestCase { print(7️⃣a, 8️⃣b) } """, - references: ["2️⃣": [], "4️⃣": ["1️⃣"], "5️⃣": ["1️⃣"], "6️⃣": ["3️⃣"], "7️⃣": [], "8️⃣": []], + references: [ + "2️⃣": [], + "4️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "6️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["3️⃣"])], + "7️⃣": [], + "8️⃣": [], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self ) @@ -179,7 +233,20 @@ final class testNameLookup: XCTestCase { print(0️⃣a) } """, - references: ["3️⃣": ["2️⃣", "1️⃣"], "5️⃣": ["4️⃣", "1️⃣"], "6️⃣": ["1️⃣"], "7️⃣": ["1️⃣"], "9️⃣": ["8️⃣"], "0️⃣": []], + references: [ + "3️⃣": [ + .fromScope(IfExprSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"]), + ], + "5️⃣": [ + .fromScope(IfExprSyntax.self, expectedNames: ["4️⃣"]), + .fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"]), + ], + "6️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "7️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "9️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["8️⃣"])], + "0️⃣": [], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self ) @@ -206,7 +273,12 @@ final class testNameLookup: XCTestCase { 0️⃣protocol d {} } """, - references: ["5️⃣": ["1️⃣", "4️⃣"], "6️⃣": ["2️⃣", "3️⃣"], "7️⃣": ["9️⃣"], "8️⃣": ["0️⃣"]], + references: [ + "5️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "4️⃣"])], + "6️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["2️⃣", "3️⃣"])], + "7️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["9️⃣"])], + "8️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["0️⃣"])], + ], expectedResultTypes: .distinct([ "1️⃣": IdentifierPatternSyntax.self, "2️⃣": ClassDeclSyntax.self, @@ -217,13 +289,13 @@ final class testNameLookup: XCTestCase { ]) ) } - + func testLookupInDeclaration() { assertLexicalNameLookup( source: """ class foo { let 1️⃣a = 2️⃣a - + func foo() { let 3️⃣a = 4️⃣a @@ -231,14 +303,66 @@ final class testNameLookup: XCTestCase { let (a, b) = 8️⃣a } } - + let 9️⃣a = 0️⃣a } """, - references: ["2️⃣": ["1️⃣", "9️⃣"], "0️⃣": ["1️⃣", "9️⃣"], "4️⃣": ["1️⃣", "9️⃣"], "6️⃣": ["3️⃣", "1️⃣", "9️⃣"], "8️⃣": ["5️⃣", "3️⃣", "1️⃣", "9️⃣"]], + references: [ + "2️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "9️⃣"])], + "0️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "9️⃣"])], + "4️⃣": [.fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "9️⃣"])], + "6️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "9️⃣"]), + ], + "8️⃣": [ + .fromScope(IfExprSyntax.self, expectedNames: ["5️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "9️⃣"]), + ], + ], expectedResultTypes: .all( IdentifierPatternSyntax.self ) ) } + + func testNameLookupForNilParameter() { + assertLexicalNameLookup( + source: """ + class foo { + let 1️⃣a = 0 + let 2️⃣b = 0 + + 3️⃣func foo() { + let 4️⃣a = 0 + let 5️⃣c = 0 + + if let 6️⃣a = 7️⃣x { + let (8️⃣a, 9️⃣b) = (0, 0) + + 0️⃣x + } + } + } + """, + references: [ + "7️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), + .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + ], + "0️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["8️⃣", "9️⃣"]), + .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), + .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + ], + ], + expectedResultTypes: .all( + IdentifierPatternSyntax.self, + except: ["3️⃣": FunctionDeclSyntax.self] + ), + useNilAsTheParameter: true + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/SimpleQueryTests.swift b/Tests/SwiftLexicalLookupTest/SimpleQueryTests.swift index a72269e9bbb..6ef7497a7d8 100644 --- a/Tests/SwiftLexicalLookupTest/SimpleQueryTests.swift +++ b/Tests/SwiftLexicalLookupTest/SimpleQueryTests.swift @@ -28,7 +28,7 @@ final class testSimpleQueries: XCTestCase { 4️⃣break } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in argument.lookupLabeledStmts() }, expected: ["3️⃣": ["2️⃣", "1️⃣"], "4️⃣": ["1️⃣"]] @@ -42,7 +42,7 @@ final class testSimpleQueries: XCTestCase { 1️⃣break } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in argument.lookupLabeledStmts() }, expected: ["1️⃣": []] @@ -63,7 +63,7 @@ final class testSimpleQueries: XCTestCase { 4️⃣break } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in argument.lookupLabeledStmts() }, expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]] @@ -82,7 +82,7 @@ final class testSimpleQueries: XCTestCase { 4️⃣break } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in argument.lookupLabeledStmts() }, expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]] @@ -101,7 +101,7 @@ final class testSimpleQueries: XCTestCase { 4️⃣break } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in argument.lookupLabeledStmts() }, expected: ["3️⃣": ["2️⃣"], "4️⃣": ["1️⃣"]] @@ -124,7 +124,7 @@ final class testSimpleQueries: XCTestCase { 6️⃣fallthrough } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in guard let fallthroughStmt = argument.ancestorOrSelf(mapping: { $0.as(FallThroughStmtSyntax.self) }) else { return [] } @@ -151,7 +151,7 @@ final class testSimpleQueries: XCTestCase { throw 7️⃣f() } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in return [argument.lookupCatchNode()] }, expected: ["3️⃣": ["2️⃣"], "5️⃣": ["4️⃣"], "6️⃣": ["1️⃣"], "7️⃣": ["8️⃣"]] @@ -174,7 +174,7 @@ final class testSimpleQueries: XCTestCase { } } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in [argument.lookupCatchNode()] }, expected: ["4️⃣": ["3️⃣"], "5️⃣": ["2️⃣"], "7️⃣": ["6️⃣"], "8️⃣": ["1️⃣"]] @@ -191,7 +191,7 @@ final class testSimpleQueries: XCTestCase { print(error) } """, - methodUnderTest: { argument in + methodUnderTest: { _, argument in [argument.lookupCatchNode()] }, expected: ["2️⃣": ["1️⃣"], "3️⃣": ["1️⃣"], "4️⃣": ["1️⃣"], "6️⃣": ["5️⃣"], "7️⃣": ["5️⃣"], "8️⃣": ["5️⃣"]] From 85be4f13bff36385e118ec164bb7c57f241a511c Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:55:27 +0200 Subject: [PATCH 04/10] Simplify result type assertion. --- Tests/SwiftLexicalLookupTest/Assertions.swift | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 2bd9f025737..ad0c858e159 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -63,13 +63,6 @@ enum ResultExpectation { expectedNames } } - - static func == (lhs: ResultExpectation, rhs: LookupResult) -> Bool { - switch (lhs, rhs) { - case (.fromScope, .fromScope): - return true - } - } } /// `methodUnderTest` is called with the token at every position marker in the keys of `expected`. @@ -164,24 +157,17 @@ func assertLexicalNameLookup( let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text) guard let expectedValues = references[marker] else { - XCTFail("For marker \(marker), couldn't find expectation") + XCTFail("For marker \(marker), couldn't find result expectation") return [] } for (actual, expected) in zip(result, expectedValues) { - XCTAssert( - expected == actual, - "For marker \(marker), expected actual result \(actual) doesn't match expected \(expected)" - ) - - switch actual { - case .fromScope(let scope, withNames: _): - if case .fromScope(let expectedType, expectedNames: _) = expected { - XCTAssert( - scope.syntaxNodeType == expectedType, - "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" - ) - } + switch (actual, expected) { + case (.fromScope(let scope, withNames: _), .fromScope(let expectedType, expectedNames: _)): + XCTAssert( + scope.syntaxNodeType == expectedType, + "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" + ) } } From 1cb4a9f62b96c23d05a20b57e2428671617d8c50 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Wed, 10 Jul 2024 15:56:37 +0200 Subject: [PATCH 05/10] Add file and guard scopes. Add closure capture and if case support. Add a way to customize lookup behavior through `LookupConfig`/ --- .../FileScopeHandlingConfig.swift | 25 +++ .../IdentifiableSyntax.swift | 10 +- Sources/SwiftLexicalLookup/LookupConfig.swift | 16 ++ Sources/SwiftLexicalLookup/LookupName.swift | 10 + Sources/SwiftLexicalLookup/LookupResult.swift | 7 + .../ScopeImplementations.swift | 79 ++++++- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 29 ++- Tests/SwiftLexicalLookupTest/Assertions.swift | 15 +- .../NameLookupTests.swift | 198 +++++++++++++++++- 9 files changed, 368 insertions(+), 21 deletions(-) create mode 100644 Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift create mode 100644 Sources/SwiftLexicalLookup/LookupConfig.swift diff --git a/Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift b/Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift new file mode 100644 index 00000000000..f9b6b2e77c4 --- /dev/null +++ b/Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift @@ -0,0 +1,25 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Specifies how names should be introduced at the file scope. +@_spi(Experimental) public enum FileScopeNameIntroductionStrategy: LookupConfig { + /// Default behavior. Names introduced sequentially like in member block + /// scope up to the first non-declaration after and including which, + /// the declarations are treated like in code block scope. + case memberBlockUpToLastDecl + /// File scope behaves like member block scope. + case memberBlock + /// File scope behaves like code block scope. + case codeBlock +} diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index 750e3ef4220..797bf594b9e 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -20,13 +20,19 @@ public protocol IdentifiableSyntax: SyntaxProtocol { extension IdentifierPatternSyntax: IdentifiableSyntax {} extension ClosureParameterSyntax: IdentifiableSyntax { - @_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { secondName ?? firstName } } extension ClosureShorthandParameterSyntax: IdentifiableSyntax { - @_spi(Experimental) public var identifier: SwiftSyntax.TokenSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { name } } + +extension ClosureCaptureSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { + expression.as(DeclReferenceExprSyntax.self)!.baseName + } +} diff --git a/Sources/SwiftLexicalLookup/LookupConfig.swift b/Sources/SwiftLexicalLookup/LookupConfig.swift new file mode 100644 index 00000000000..c1b62b459cb --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupConfig.swift @@ -0,0 +1,16 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import Foundation + +/// Used to customize lookup behavior. +@_spi(Experimental) public protocol LookupConfig {} diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 89d0f71eeb8..9d7459a4402 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -85,6 +85,16 @@ import SwiftSyntax getNames(from: patternExpr.pattern, accessibleAfter: accessibleAfter) case .optionalBindingCondition(let optionalBinding): getNames(from: optionalBinding.pattern, accessibleAfter: accessibleAfter) + case .matchingPatternCondition(let matchingPatternCondition): + getNames(from: matchingPatternCondition.pattern, accessibleAfter: accessibleAfter) + case .functionCallExpr(let functionCallExpr): + functionCallExpr.arguments.flatMap { argument in + getNames(from: argument.expression, accessibleAfter: accessibleAfter) + } + case .guardStmt(let guardStmt): + guardStmt.conditions.flatMap { cond in + getNames(from: cond.condition, accessibleAfter: cond.endPosition) + } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 1aa552f549d..409888bada1 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -16,12 +16,17 @@ import SwiftSyntax @_spi(Experimental) public enum LookupResult { /// Scope and the names that matched lookup. case fromScope(ScopeSyntax, withNames: [LookupName]) + /// File scope, names that matched lookup and name introduction + /// strategy used for the lookup. + case fromFileScope(SourceFileSyntax, withNames: [LookupName], nameIntroductionStrategy: FileScopeNameIntroductionStrategy) /// Associated scope. @_spi(Experimental) public var scope: ScopeSyntax? { switch self { case .fromScope(let scopeSyntax, _): scopeSyntax + case .fromFileScope(let fileScopeSyntax, withNames: _, nameIntroductionStrategy: _): + fileScopeSyntax } } @@ -30,6 +35,8 @@ import SwiftSyntax switch self { case .fromScope(_, let names): names + case .fromFileScope(_, withNames: let names, nameIntroductionStrategy: _): + names } } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index fabda38ce44..248fed372ad 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -25,7 +25,50 @@ extension SyntaxProtocol { @_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { public var introducedNames: [LookupName] { - [] + introducedNames(using: .memberBlockUpToLastDecl) + } + + public func introducedNames(using nameIntroductionStrategy: FileScopeNameIntroductionStrategy) -> [LookupName] { + switch nameIntroductionStrategy { + case .memberBlockUpToLastDecl: + var encounteredNonDeclaration = false + + return statements.flatMap { codeBlockItem in + let item = codeBlockItem.item + + if encounteredNonDeclaration { + return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition) + } else { + if item.is(DeclSyntax.self) || item.is(VariableDeclSyntax.self) { + return LookupName.getNames(from: item) + } else { + encounteredNonDeclaration = true + return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition) + } + } + } + case .codeBlock: + return statements.flatMap { codeBlockItem in + LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) + } + case .memberBlock: + return statements.flatMap { codeBlockItem in + LookupName.getNames(from: codeBlockItem.item) + } + } + } + + public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { + let nameIntroductionStrategy = config.first { + $0 is FileScopeNameIntroductionStrategy + } as? FileScopeNameIntroductionStrategy ?? .memberBlockUpToLastDecl + + let names = introducedNames(using: nameIntroductionStrategy) + .filter { introducedName in + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + } + + return [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)] } } @@ -45,7 +88,17 @@ extension SyntaxProtocol { @_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax { public var introducedNames: [LookupName] { - signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in + let captureNames = signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in + if let captureList = child.as(ClosureCaptureListSyntax.self) { + captureList.children(viewMode: .sourceAccurate).flatMap { capture in + LookupName.getNames(from: capture) + } + } else { + LookupName.getNames(from: child) + } + } ?? [] + + let parameterNames = signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in if let parameterList = parameter.as(ClosureParameterListSyntax.self) { parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in LookupName.getNames(from: parameter) @@ -54,6 +107,8 @@ extension SyntaxProtocol { LookupName.getNames(from: parameter) } } ?? [] + + return captureNames + parameterNames } } @@ -91,11 +146,11 @@ extension SyntaxProtocol { } } - public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] { + public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - parentScope?.lookup(for: name, at: syntax) ?? [] + lookupInParent(for: name, at: syntax, with: config) } else { - defaultLookupImplementation(for: name, at: syntax) + defaultLookupImplementation(for: name, at: syntax, with: config) } } } @@ -107,3 +162,17 @@ extension SyntaxProtocol { } } } + +@_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax { + public var introducedNames: [LookupName] { + [] + } + + public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { + if body.position <= syntax.position && body.endPosition >= syntax.position { + lookupInParent(for: name, at: self, with: config) + } else { + defaultLookupImplementation(for: name, at: syntax, with: config) + } + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 412ad01338e..15eb3c840ad 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -14,6 +14,7 @@ import SwiftSyntax extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. + /// Optional configuration can be passed as `with` to customize the lookup behavior. /// /// - Returns: An array of `LookupResult` for name `for` at this syntax node, /// ordered by visibility. If set to `nil`, returns all available names ordered by visibility. @@ -42,8 +43,8 @@ extension SyntaxProtocol { /// declaration, followed by the first function name, and then the second function name, /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. - @_spi(Experimental) public func lookup(for name: String?) -> [LookupResult] { - scope?.lookup(for: name, at: self) ?? [] + @_spi(Experimental) public func lookup(for name: String?, with config: [LookupConfig] = []) -> [LookupResult] { + scope?.lookup(for: name, at: self, with: config) ?? [] } } @@ -54,7 +55,7 @@ extension SyntaxProtocol { var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] + func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { @@ -65,16 +66,17 @@ extension SyntaxProtocol { /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func lookup(for name: String?, at syntax: SyntaxProtocol) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax) + public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { + defaultLookupImplementation(for: name, at: syntax, with: config) } /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func defaultLookupImplementation( + func defaultLookupImplementation( for name: String?, - at syntax: SyntaxProtocol + at syntax: SyntaxProtocol, + with config: [LookupConfig] ) -> [LookupResult] { let filteredNames = introducedNames @@ -83,9 +85,18 @@ extension SyntaxProtocol { } if filteredNames.isEmpty { - return parentScope?.lookup(for: name, at: syntax) ?? [] + return lookupInParent(for: name, at: syntax, with: config) } else { - return [.fromScope(self, withNames: filteredNames)] + (parentScope?.lookup(for: name, at: syntax) ?? []) + return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) } } + + /// Looks up in parent scope. + func lookupInParent( + for name: String?, + at syntax: SyntaxProtocol, + with config: [LookupConfig] + ) -> [LookupResult] { + parentScope?.lookup(for: name, at: syntax, with: config) ?? [] + } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index ad0c858e159..55b4b52bf76 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -56,11 +56,14 @@ enum MarkerExpectation { /// Used to define enum ResultExpectation { case fromScope(ScopeSyntax.Type, expectedNames: [String]) + case fromFileScope(expectedNames: [String], nameIntroductionStrategy: FileScopeNameIntroductionStrategy) var expectedNames: [String] { switch self { case .fromScope(_, let expectedNames): expectedNames + case .fromFileScope(expectedNames: let expectedNames, nameIntroductionStrategy: _): + expectedNames } } } @@ -149,12 +152,13 @@ func assertLexicalNameLookup( source: String, references: [String: [ResultExpectation]], expectedResultTypes: MarkerExpectation = .none, - useNilAsTheParameter: Bool = false + useNilAsTheParameter: Bool = false, + config: [LookupConfig] = [] ) { assertLexicalScopeQuery( source: source, methodUnderTest: { marker, argument in - let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text) + let result = argument.lookup(for: useNilAsTheParameter ? nil : argument.text, with: config) guard let expectedValues = references[marker] else { XCTFail("For marker \(marker), couldn't find result expectation") @@ -168,6 +172,13 @@ func assertLexicalNameLookup( scope.syntaxNodeType == expectedType, "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" ) + case (.fromFileScope(_, withNames: _, nameIntroductionStrategy: let nameIntroductionStrategy), .fromFileScope(expectedNames: _, nameIntroductionStrategy: let expectedNameIntroductionStrategy)): + XCTAssert( + nameIntroductionStrategy == expectedNameIntroductionStrategy, + "For marker \(marker), actual file scope name introduction strategy \(nameIntroductionStrategy) doesn't match expected \(expectedNameIntroductionStrategy)" + ) + default: + XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)") } } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index e7d2dac28f5..f0196bcc927 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -11,7 +11,7 @@ //===----------------------------------------------------------------------===// import Foundation -@_spi(Testing) import SwiftLexicalLookup +@_spi(Experimental) import SwiftLexicalLookup import SwiftSyntax import XCTest @@ -134,6 +134,33 @@ final class testNameLookup: XCTestCase { ) ) } + + func testClosureCaptureLookup() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let x = { [2️⃣weak self, 3️⃣a, 4️⃣unowned b] in + print(5️⃣self, 6️⃣a) + } + let b = 0 + } + """, + references: [ + "5️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["2️⃣"])], + "6️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + ], + expectedResultTypes: .all( + ClosureCaptureSyntax.self, + except: [ + "1️⃣": IdentifierPatternSyntax.self, + ] + ) + ) + } func testParameterLookupClosure() { assertLexicalNameLookup( @@ -326,11 +353,36 @@ final class testNameLookup: XCTestCase { ) ) } + + func testIfCaseLookup() { + assertLexicalNameLookup( + source: """ + if case .x(let 1️⃣a, let 2️⃣b) = f { + print(3️⃣a, 4️⃣b) + } else if case .y(let 5️⃣a) = f { + print(6️⃣a, 7️⃣b) + } else if case .z = f { + print(8️⃣a, 9️⃣b) + } + """, + references: [ + "3️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "4️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["2️⃣"])], + "6️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["5️⃣"])], + "7️⃣": [], + "8️⃣": [], + "9️⃣": [], + ], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } func testNameLookupForNilParameter() { assertLexicalNameLookup( source: """ - class foo { + 🔟class foo { let 1️⃣a = 0 let 2️⃣b = 0 @@ -350,19 +402,159 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl) ], "0️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["8️⃣", "9️⃣"]), .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl) ], ], expectedResultTypes: .all( IdentifierPatternSyntax.self, - except: ["3️⃣": FunctionDeclSyntax.self] + except: ["3️⃣": FunctionDeclSyntax.self, + "🔟": ClassDeclSyntax.self] ), useNilAsTheParameter: true ) } + + func testGuardLookup() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 0 + + guard let 2️⃣a, let 3️⃣b = c else { + print(4️⃣a, 5️⃣b) + return + } + + print(6️⃣a, 7️⃣b) + } + """, + references: [ + "4️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [], + "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], + "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + ], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } + + func testGuardLookupInConditions() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 0 + guard let 2️⃣a = 3️⃣a, let 4️⃣a = 5️⃣a, let a = 6️⃣a else { return } + } + """, + references: [ + "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], + "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "4️⃣"])], + ], + expectedResultTypes: .all( + IdentifierPatternSyntax.self + ) + ) + } + + func testSimpleFileScope() { + assertLexicalNameLookup( + source: """ + 1️⃣class a {} + + 2️⃣class b { + let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d + } + + let 8️⃣a = 0 + + 7️⃣class c {} + + if a == 0 {} + + 9️⃣class d {} + + let x = 0️⃣d + """, + references: [ + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + "6️⃣": [], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + ], + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]) + ) + } + + func testFileScopeAsMember() { + assertLexicalNameLookup( + source: """ + 1️⃣class a {} + + 2️⃣class b { + let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d + } + + let 8️⃣a = 0 + + 7️⃣class c {} + + if a == 0 {} + + 9️⃣class d {} + + let x = 0️⃣d + """, + references: [ + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"], nameIntroductionStrategy: .memberBlock)], + "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"], nameIntroductionStrategy: .memberBlock)], + "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"], nameIntroductionStrategy: .memberBlock)], + "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlock)], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlock)], + ], + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), + config: [FileScopeNameIntroductionStrategy.memberBlock] + ) + } + + func testFileScopeAsCodeBlock() { + assertLexicalNameLookup( + source: """ + 1️⃣class a {} + + 2️⃣class b { + let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d + } + + let 8️⃣a = 0 + + 7️⃣class c {} + + if a == 0 {} + + 9️⃣class d {} + + let x = 0️⃣d + """, + references: [ + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣"], nameIntroductionStrategy: .codeBlock)], + "4️⃣": [], + "5️⃣": [], + "6️⃣": [], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .codeBlock)], + ], + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), + config: [FileScopeNameIntroductionStrategy.codeBlock] + ) + } } From 7fdb145273999f374c1b63b6030e463c919a548f Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:23:14 +0200 Subject: [PATCH 06/10] Introduce `LookupConfigDictionary` for retrieving lookup configurations. --- .../FileScopeHandlingConfig.swift | 0 .../{ => Configurations}/LookupConfig.swift | 6 +++ .../LookupConfigDictionary.swift | 37 +++++++++++++++++++ .../ScopeImplementations.swift | 18 ++++----- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 20 +++++----- 5 files changed, 61 insertions(+), 20 deletions(-) rename Sources/SwiftLexicalLookup/{ => Configurations}/FileScopeHandlingConfig.swift (100%) rename Sources/SwiftLexicalLookup/{ => Configurations}/LookupConfig.swift (86%) create mode 100644 Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift diff --git a/Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift similarity index 100% rename from Sources/SwiftLexicalLookup/FileScopeHandlingConfig.swift rename to Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift diff --git a/Sources/SwiftLexicalLookup/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift similarity index 86% rename from Sources/SwiftLexicalLookup/LookupConfig.swift rename to Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index c1b62b459cb..e84e8b22920 100644 --- a/Sources/SwiftLexicalLookup/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -14,3 +14,9 @@ import Foundation /// Used to customize lookup behavior. @_spi(Experimental) public protocol LookupConfig {} + +extension LookupConfig { + var identifier: ObjectIdentifier { + ObjectIdentifier(Self.self) + } +} diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift new file mode 100644 index 00000000000..0165031e63e --- /dev/null +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Stores and provides easy access for lookup configuration. +@_spi(Experimental) public struct LookupConfigDictionary { + private var dictionary: [ObjectIdentifier: LookupConfig] + + /// Creates a new lookup configuration dictionary + /// from a given array of configurations. + init(from configArray: [LookupConfig]) { + dictionary = [:] + + for config in configArray { + dictionary[config.identifier] = config + } + } + + subscript(key: T.Type) -> T? { + get { + return dictionary[ObjectIdentifier(key)] as? T + } + set { + dictionary[ObjectIdentifier(key)] = newValue + } + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 248fed372ad..1d0b72eac87 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -58,10 +58,8 @@ extension SyntaxProtocol { } } - public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { - let nameIntroductionStrategy = config.first { - $0 is FileScopeNameIntroductionStrategy - } as? FileScopeNameIntroductionStrategy ?? .memberBlockUpToLastDecl + public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + let nameIntroductionStrategy = configDict[FileScopeNameIntroductionStrategy.self] ?? .memberBlockUpToLastDecl let names = introducedNames(using: nameIntroductionStrategy) .filter { introducedName in @@ -146,11 +144,11 @@ extension SyntaxProtocol { } } - public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { + public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: config) + lookupInParent(for: name, at: syntax, with: configDict) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: configDict) } } } @@ -168,11 +166,11 @@ extension SyntaxProtocol { [] } - public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { + public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - lookupInParent(for: name, at: self, with: config) + lookupInParent(for: name, at: self, with: configDict) } else { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: name, at: syntax, with: configDict) } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 15eb3c840ad..8c20b7689e5 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -43,8 +43,8 @@ extension SyntaxProtocol { /// declaration, followed by the first function name, and then the second function name, /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. - @_spi(Experimental) public func lookup(for name: String?, with config: [LookupConfig] = []) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config) ?? [] + @_spi(Experimental) public func lookup(for name: String?, with configArr: [LookupConfig] = []) -> [LookupResult] { + scope?.lookup(for: name, at: self, with: LookupConfigDictionary(from: configArr)) ?? [] } } @@ -55,7 +55,7 @@ extension SyntaxProtocol { var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] + func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { @@ -66,8 +66,8 @@ extension SyntaxProtocol { /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func lookup(for name: String?, at syntax: SyntaxProtocol, with config: [LookupConfig]) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config) + public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + defaultLookupImplementation(for: name, at: syntax, with: configDict) } /// Returns `LookupResult` of all names introduced in this scope that `name` @@ -76,7 +76,7 @@ extension SyntaxProtocol { func defaultLookupImplementation( for name: String?, at syntax: SyntaxProtocol, - with config: [LookupConfig] + with configDict: LookupConfigDictionary ) -> [LookupResult] { let filteredNames = introducedNames @@ -85,9 +85,9 @@ extension SyntaxProtocol { } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config) + return lookupInParent(for: name, at: syntax, with: configDict) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) + return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: configDict) } } @@ -95,8 +95,8 @@ extension SyntaxProtocol { func lookupInParent( for name: String?, at syntax: SyntaxProtocol, - with config: [LookupConfig] + with configDict: LookupConfigDictionary ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config) ?? [] + parentScope?.lookup(for: name, at: syntax, with: configDict) ?? [] } } From 5519ccbe0ce06dde2ccbd3fd81ba607379349652 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:47:11 +0200 Subject: [PATCH 07/10] Format. --- .../LookupConfigDictionary.swift | 6 +- Sources/SwiftLexicalLookup/LookupResult.swift | 6 +- .../ScopeImplementations.swift | 68 +++++++++++-------- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 8 ++- Tests/SwiftLexicalLookupTest/Assertions.swift | 5 +- .../NameLookupTests.swift | 58 ++++++++-------- 6 files changed, 89 insertions(+), 62 deletions(-) diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift index 0165031e63e..804b3b67656 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift @@ -15,17 +15,17 @@ import SwiftSyntax /// Stores and provides easy access for lookup configuration. @_spi(Experimental) public struct LookupConfigDictionary { private var dictionary: [ObjectIdentifier: LookupConfig] - + /// Creates a new lookup configuration dictionary /// from a given array of configurations. init(from configArray: [LookupConfig]) { dictionary = [:] - + for config in configArray { dictionary[config.identifier] = config } } - + subscript(key: T.Type) -> T? { get { return dictionary[ObjectIdentifier(key)] as? T diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 409888bada1..3a20c98e7e5 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -18,7 +18,11 @@ import SwiftSyntax case fromScope(ScopeSyntax, withNames: [LookupName]) /// File scope, names that matched lookup and name introduction /// strategy used for the lookup. - case fromFileScope(SourceFileSyntax, withNames: [LookupName], nameIntroductionStrategy: FileScopeNameIntroductionStrategy) + case fromFileScope( + SourceFileSyntax, + withNames: [LookupName], + nameIntroductionStrategy: FileScopeNameIntroductionStrategy + ) /// Associated scope. @_spi(Experimental) public var scope: ScopeSyntax? { diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 1d0b72eac87..6eb2e0032d7 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -27,15 +27,15 @@ extension SyntaxProtocol { public var introducedNames: [LookupName] { introducedNames(using: .memberBlockUpToLastDecl) } - + public func introducedNames(using nameIntroductionStrategy: FileScopeNameIntroductionStrategy) -> [LookupName] { switch nameIntroductionStrategy { case .memberBlockUpToLastDecl: var encounteredNonDeclaration = false - + return statements.flatMap { codeBlockItem in let item = codeBlockItem.item - + if encounteredNonDeclaration { return LookupName.getNames(from: item, accessibleAfter: codeBlockItem.endPosition) } else { @@ -57,15 +57,19 @@ extension SyntaxProtocol { } } } - - public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { let nameIntroductionStrategy = configDict[FileScopeNameIntroductionStrategy.self] ?? .memberBlockUpToLastDecl - + let names = introducedNames(using: nameIntroductionStrategy) .filter { introducedName in introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } - + return [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)] } } @@ -86,26 +90,28 @@ extension SyntaxProtocol { @_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax { public var introducedNames: [LookupName] { - let captureNames = signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in - if let captureList = child.as(ClosureCaptureListSyntax.self) { - captureList.children(viewMode: .sourceAccurate).flatMap { capture in - LookupName.getNames(from: capture) + let captureNames = + signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in + if let captureList = child.as(ClosureCaptureListSyntax.self) { + captureList.children(viewMode: .sourceAccurate).flatMap { capture in + LookupName.getNames(from: capture) + } + } else { + LookupName.getNames(from: child) } - } else { - LookupName.getNames(from: child) - } - } ?? [] - - let parameterNames = signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in - if let parameterList = parameter.as(ClosureParameterListSyntax.self) { - parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in + } ?? [] + + let parameterNames = + signature?.parameterClause?.children(viewMode: .sourceAccurate).flatMap { parameter in + if let parameterList = parameter.as(ClosureParameterListSyntax.self) { + parameterList.children(viewMode: .sourceAccurate).flatMap { parameter in + LookupName.getNames(from: parameter) + } + } else { LookupName.getNames(from: parameter) } - } else { - LookupName.getNames(from: parameter) - } - } ?? [] - + } ?? [] + return captureNames + parameterNames } } @@ -144,7 +150,11 @@ extension SyntaxProtocol { } } - public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { lookupInParent(for: name, at: syntax, with: configDict) } else { @@ -165,8 +175,12 @@ extension SyntaxProtocol { public var introducedNames: [LookupName] { [] } - - public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { lookupInParent(for: name, at: self, with: configDict) } else { diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index 8c20b7689e5..ffa505e0511 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -66,7 +66,11 @@ extension SyntaxProtocol { /// Returns `LookupResult` of all names introduced in this scope that `name` /// refers to and is accessible at given syntax node then passes lookup to the parent. /// If `name` set to `nil`, returns all available names at the given node. - public func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] { + public func lookup( + for name: String?, + at syntax: SyntaxProtocol, + with configDict: LookupConfigDictionary + ) -> [LookupResult] { defaultLookupImplementation(for: name, at: syntax, with: configDict) } @@ -90,7 +94,7 @@ extension SyntaxProtocol { return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: configDict) } } - + /// Looks up in parent scope. func lookupInParent( for name: String?, diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 55b4b52bf76..c46c111f036 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -172,7 +172,10 @@ func assertLexicalNameLookup( scope.syntaxNodeType == expectedType, "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" ) - case (.fromFileScope(_, withNames: _, nameIntroductionStrategy: let nameIntroductionStrategy), .fromFileScope(expectedNames: _, nameIntroductionStrategy: let expectedNameIntroductionStrategy)): + case ( + .fromFileScope(_, withNames: _, nameIntroductionStrategy: let nameIntroductionStrategy), + .fromFileScope(expectedNames: _, nameIntroductionStrategy: let expectedNameIntroductionStrategy) + ): XCTAssert( nameIntroductionStrategy == expectedNameIntroductionStrategy, "For marker \(marker), actual file scope name introduction strategy \(nameIntroductionStrategy) doesn't match expected \(expectedNameIntroductionStrategy)" diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index f0196bcc927..366ca895224 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -134,7 +134,7 @@ final class testNameLookup: XCTestCase { ) ) } - + func testClosureCaptureLookup() { assertLexicalNameLookup( source: """ @@ -156,7 +156,7 @@ final class testNameLookup: XCTestCase { expectedResultTypes: .all( ClosureCaptureSyntax.self, except: [ - "1️⃣": IdentifierPatternSyntax.self, + "1️⃣": IdentifierPatternSyntax.self ] ) ) @@ -353,7 +353,7 @@ final class testNameLookup: XCTestCase { ) ) } - + func testIfCaseLookup() { assertLexicalNameLookup( source: """ @@ -402,25 +402,27 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), - .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl) + .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl), ], "0️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["8️⃣", "9️⃣"]), .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), - .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl) + .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl), ], ], expectedResultTypes: .all( IdentifierPatternSyntax.self, - except: ["3️⃣": FunctionDeclSyntax.self, - "🔟": ClassDeclSyntax.self] + except: [ + "3️⃣": FunctionDeclSyntax.self, + "🔟": ClassDeclSyntax.self, + ] ), useNilAsTheParameter: true ) } - + func testGuardLookup() { assertLexicalNameLookup( source: """ @@ -431,7 +433,7 @@ final class testNameLookup: XCTestCase { print(4️⃣a, 5️⃣b) return } - + print(6️⃣a, 7️⃣b) } """, @@ -446,7 +448,7 @@ final class testNameLookup: XCTestCase { ) ) } - + func testGuardLookupInConditions() { assertLexicalNameLookup( source: """ @@ -465,24 +467,24 @@ final class testNameLookup: XCTestCase { ) ) } - + func testSimpleFileScope() { assertLexicalNameLookup( source: """ 1️⃣class a {} - + 2️⃣class b { let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d } let 8️⃣a = 0 - + 7️⃣class c {} - + if a == 0 {} - + 9️⃣class d {} - + let x = 0️⃣d """, references: [ @@ -495,24 +497,24 @@ final class testNameLookup: XCTestCase { expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]) ) } - + func testFileScopeAsMember() { assertLexicalNameLookup( source: """ 1️⃣class a {} - + 2️⃣class b { let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d } let 8️⃣a = 0 - + 7️⃣class c {} - + if a == 0 {} - + 9️⃣class d {} - + let x = 0️⃣d """, references: [ @@ -526,24 +528,24 @@ final class testNameLookup: XCTestCase { config: [FileScopeNameIntroductionStrategy.memberBlock] ) } - + func testFileScopeAsCodeBlock() { assertLexicalNameLookup( source: """ 1️⃣class a {} - + 2️⃣class b { let x = 3️⃣a + 4️⃣b + 5️⃣c + 6️⃣d } let 8️⃣a = 0 - + 7️⃣class c {} - + if a == 0 {} - + 9️⃣class d {} - + let x = 0️⃣d """, references: [ From 5af6d9122ceaaf974cb37b0ddea97ab7abe9dda2 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:00:25 +0200 Subject: [PATCH 08/10] Simlify config handling. Add documentation. --- ...> FileScopeNameIntroductionStrategy.swift} | 2 +- .../Configurations/LookupConfig.swift | 11 +- .../LookupConfigDictionary.swift | 37 ----- .../IdentifiableSyntax.swift | 5 + Sources/SwiftLexicalLookup/LookupName.swift | 4 +- Sources/SwiftLexicalLookup/LookupResult.swift | 15 +-- .../ScopeImplementations.swift | 127 ++++++++++++++++-- Sources/SwiftLexicalLookup/ScopeSyntax.swift | 23 ++-- Tests/SwiftLexicalLookupTest/Assertions.swift | 16 +-- .../NameLookupTests.swift | 30 ++--- 10 files changed, 163 insertions(+), 107 deletions(-) rename Sources/SwiftLexicalLookup/Configurations/{FileScopeHandlingConfig.swift => FileScopeNameIntroductionStrategy.swift} (92%) delete mode 100644 Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift diff --git a/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift similarity index 92% rename from Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift rename to Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift index f9b6b2e77c4..ca0c6642d8d 100644 --- a/Sources/SwiftLexicalLookup/Configurations/FileScopeHandlingConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift @@ -13,7 +13,7 @@ import SwiftSyntax /// Specifies how names should be introduced at the file scope. -@_spi(Experimental) public enum FileScopeNameIntroductionStrategy: LookupConfig { +@_spi(Experimental) public enum FileScopeHandlingConfig { /// Default behavior. Names introduced sequentially like in member block /// scope up to the first non-declaration after and including which, /// the declarations are treated like in code block scope. diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift index e84e8b22920..d96093037ca 100644 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -12,11 +12,12 @@ import Foundation -/// Used to customize lookup behavior. -@_spi(Experimental) public protocol LookupConfig {} +@_spi(Experimental) public struct LookupConfig { + /// Specifies behaviour of file scope. + /// `memberBlockUpToLastDecl` by default. + public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl -extension LookupConfig { - var identifier: ObjectIdentifier { - ObjectIdentifier(Self.self) + public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) { + self.fileScopeHandling = fileScopeHandling } } diff --git a/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift deleted file mode 100644 index 804b3b67656..00000000000 --- a/Sources/SwiftLexicalLookup/Configurations/LookupConfigDictionary.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Swift.org open source project -// -// Copyright (c) 2014 - 2024 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 -// -//===----------------------------------------------------------------------===// - -import SwiftSyntax - -/// Stores and provides easy access for lookup configuration. -@_spi(Experimental) public struct LookupConfigDictionary { - private var dictionary: [ObjectIdentifier: LookupConfig] - - /// Creates a new lookup configuration dictionary - /// from a given array of configurations. - init(from configArray: [LookupConfig]) { - dictionary = [:] - - for config in configArray { - dictionary[config.identifier] = config - } - } - - subscript(key: T.Type) -> T? { - get { - return dictionary[ObjectIdentifier(key)] as? T - } - set { - dictionary[ObjectIdentifier(key)] = newValue - } - } -} diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index 797bf594b9e..67e6de6b4e9 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -33,6 +33,11 @@ extension ClosureShorthandParameterSyntax: IdentifiableSyntax { extension ClosureCaptureSyntax: IdentifiableSyntax { @_spi(Experimental) public var identifier: TokenSyntax { + /* Doesn't work with closures like: + _ = { [y=1+2] in + print(y) + } + */ expression.as(DeclReferenceExprSyntax.self)!.baseName } } diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 9d7459a4402..ff86e4d8eef 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -44,9 +44,7 @@ import SwiftSyntax /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { switch self { - case .identifier(_, let absolutePosition): - absolutePosition - case .declaration(_, let absolutePosition): + case .identifier(_, let absolutePosition), .declaration(_, let absolutePosition): absolutePosition } } diff --git a/Sources/SwiftLexicalLookup/LookupResult.swift b/Sources/SwiftLexicalLookup/LookupResult.swift index 3a20c98e7e5..4e5cc04f97b 100644 --- a/Sources/SwiftLexicalLookup/LookupResult.swift +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -16,20 +16,15 @@ import SwiftSyntax @_spi(Experimental) public enum LookupResult { /// Scope and the names that matched lookup. case fromScope(ScopeSyntax, withNames: [LookupName]) - /// File scope, names that matched lookup and name introduction - /// strategy used for the lookup. - case fromFileScope( - SourceFileSyntax, - withNames: [LookupName], - nameIntroductionStrategy: FileScopeNameIntroductionStrategy - ) + /// File scope and names that matched lookup. + case fromFileScope(SourceFileSyntax, withNames: [LookupName]) /// Associated scope. @_spi(Experimental) public var scope: ScopeSyntax? { switch self { case .fromScope(let scopeSyntax, _): scopeSyntax - case .fromFileScope(let fileScopeSyntax, withNames: _, nameIntroductionStrategy: _): + case .fromFileScope(let fileScopeSyntax, _): fileScopeSyntax } } @@ -37,9 +32,7 @@ import SwiftSyntax /// Names that matched lookup. @_spi(Experimental) public var names: [LookupName] { switch self { - case .fromScope(_, let names): - names - case .fromFileScope(_, withNames: let names, nameIntroductionStrategy: _): + case .fromScope(_, let names), .fromFileScope(_, let names): names } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 6eb2e0032d7..06eb4da0a0e 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -24,12 +24,34 @@ extension SyntaxProtocol { } @_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { + /// All names introduced in the file scope + /// according to the default strategy: `memberBlockUpToLastDecl`. public var introducedNames: [LookupName] { introducedNames(using: .memberBlockUpToLastDecl) } - public func introducedNames(using nameIntroductionStrategy: FileScopeNameIntroductionStrategy) -> [LookupName] { - switch nameIntroductionStrategy { + /// All names introduced in the file scope + /// using the provided configuration. + /// + /// Example usage: + /// ```swift + /// class a {} + /// class b { + /// // <-- + /// } + /// let c = 0 + /// class d {} + /// if true {} + /// class e {} + /// let f = 0 + /// ``` + /// During lookup, according to different configurations, + /// names available at the marked place are: + /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlock` - a, b, c, d, e, f + /// - for `codeBlock` - a + public func introducedNames(using fileScopeHandling: FileScopeHandlingConfig) -> [LookupName] { + switch fileScopeHandling { case .memberBlockUpToLastDecl: var encounteredNonDeclaration = false @@ -58,23 +80,43 @@ extension SyntaxProtocol { } } + /// Returns names matching lookup using provided file + /// scope handling configuration (by default: `memberBlockUpToLastDecl`). + /// + /// Example usage: + /// ```swift + /// class a {} + /// class b { + /// // <-- + /// } + /// let c = 0 + /// class d {} + /// if true {} + /// class e {} + /// let f = 0 + /// ``` + /// According to different configurations, + /// names available at the marked place are: + /// - for `fileScopeNameIntroductionStrategy` - a, b, c, d + /// - for `memberBlock` - a, b, c, d, e, f + /// - for `codeBlock` - a public func lookup( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { - let nameIntroductionStrategy = configDict[FileScopeNameIntroductionStrategy.self] ?? .memberBlockUpToLastDecl - - let names = introducedNames(using: nameIntroductionStrategy) + let names = introducedNames(using: config.fileScopeHandling) .filter { introducedName in introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) } - return [.fromFileScope(self, withNames: names, nameIntroductionStrategy: nameIntroductionStrategy)] + return [.fromFileScope(self, withNames: names)] } } @_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax { + /// Names introduced in the code block scope + /// accessible after their declaration. public var introducedNames: [LookupName] { statements.flatMap { codeBlockItem in LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) @@ -83,12 +125,24 @@ extension SyntaxProtocol { } @_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { + /// Names introduced in the `for` body. public var introducedNames: [LookupName] { LookupName.getNames(from: pattern) } } @_spi(Experimental) extension ClosureExprSyntax: ScopeSyntax { + /// All names introduced by the closure signature. + /// Could be closure captures or (shorthand) parameters. + /// + /// Example: + /// ```swift + /// let x = { [weak self, a] b, _ in + /// // <-- + /// } + /// ``` + /// During lookup, names available at the marked place are: + /// `self`, a, b. public var introducedNames: [LookupName] { let captureNames = signature?.capture?.children(viewMode: .sourceAccurate).flatMap { child in @@ -117,6 +171,7 @@ extension SyntaxProtocol { } @_spi(Experimental) extension WhileStmtSyntax: ScopeSyntax { + /// Names introduced by the `while` loop by its conditions. public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition) @@ -125,11 +180,28 @@ extension SyntaxProtocol { } @_spi(Experimental) extension IfExprSyntax: ScopeSyntax { + /// Parent scope, omitting ancestor `if` statements if part of their `else if` clause. public var parentScope: ScopeSyntax? { getParent(for: self.parent, previousIfElse: self.elseKeyword == nil) } - /// Finds the parent scope, omitting parent `if` statements if part of their `else if` clause. + /// Finds parent scope, omitting ancestor `if` statements if part of their `else if` clause. + /// + /// Example: + /// ```swift + /// func foo() { + /// if let a = x { + /// // <-- + /// } else if let b { + /// // <-- + /// } else if y == 1 { + /// // <-- + /// } + /// } + /// ``` + /// For each of the marked scopes, resulting parent + /// is the enclosing code block scope associated with + /// the function body. private func getParent(for syntax: Syntax?, previousIfElse: Bool) -> ScopeSyntax? { guard let syntax else { return nil } @@ -144,26 +216,40 @@ extension SyntaxProtocol { } } + /// Names introduced by the `if` optional binding conditions. public var introducedNames: [LookupName] { conditions.flatMap { element in LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) } } + /// Returns names matching lookup. + /// Lookup triggered from inside of `else` + /// clause is immediately forwarded to parent scope. + /// + /// Example: + /// ```swift + /// if let a = x { + /// // <-- a is visible here + /// } else { + /// // <-- a is not visible here + /// } + /// ``` public func lookup( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - lookupInParent(for: name, at: syntax, with: configDict) + lookupInParent(for: name, at: syntax, with: config) } else { - defaultLookupImplementation(for: name, at: syntax, with: configDict) + defaultLookupImplementation(for: name, at: syntax, with: config) } } } @_spi(Experimental) extension MemberBlockSyntax: ScopeSyntax { + /// All names introduced by members of this member scope. public var introducedNames: [LookupName] { members.flatMap { member in LookupName.getNames(from: member.decl) @@ -172,19 +258,32 @@ extension SyntaxProtocol { } @_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax { + /// Guard doesn't introduce any names to its children. + /// It's always empty. public var introducedNames: [LookupName] { [] } + /// Returns names matching lookup. + /// Lookup triggered from inside of `else` + /// clause is immediately forwarded to parent scope. + /// + /// Example: + /// ```swift + /// guard let a = x else { + /// return // a is not visible here + /// } + /// // a is visible here + /// ``` public func lookup( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { if body.position <= syntax.position && body.endPosition >= syntax.position { - lookupInParent(for: name, at: self, with: configDict) + lookupInParent(for: name, at: self, with: config) } else { - defaultLookupImplementation(for: name, at: syntax, with: configDict) + defaultLookupImplementation(for: name, at: syntax, with: config) } } } diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index ffa505e0511..55bebd9ef78 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -43,8 +43,11 @@ extension SyntaxProtocol { /// declaration, followed by the first function name, and then the second function name, /// in this exact order. The constant declaration within the function body is omitted /// due to the ordering rules that prioritize visibility within the function body. - @_spi(Experimental) public func lookup(for name: String?, with configArr: [LookupConfig] = []) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: LookupConfigDictionary(from: configArr)) ?? [] + @_spi(Experimental) public func lookup( + for name: String?, + with config: LookupConfig = LookupConfig() + ) -> [LookupResult] { + scope?.lookup(for: name, at: self, with: config) ?? [] } } @@ -55,7 +58,7 @@ extension SyntaxProtocol { var introducedNames: [LookupName] { get } /// Finds all declarations `name` refers to. `at` specifies the node lookup was triggered with. /// If `name` set to `nil`, returns all available names at the given node. - func lookup(for name: String?, at syntax: SyntaxProtocol, with configDict: LookupConfigDictionary) -> [LookupResult] + func lookup(for name: String?, at syntax: SyntaxProtocol, with config: LookupConfig) -> [LookupResult] } @_spi(Experimental) extension ScopeSyntax { @@ -69,9 +72,9 @@ extension SyntaxProtocol { public func lookup( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: configDict) + defaultLookupImplementation(for: name, at: syntax, with: config) } /// Returns `LookupResult` of all names introduced in this scope that `name` @@ -80,7 +83,7 @@ extension SyntaxProtocol { func defaultLookupImplementation( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { let filteredNames = introducedNames @@ -89,9 +92,9 @@ extension SyntaxProtocol { } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: configDict) + return lookupInParent(for: name, at: syntax, with: config) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: configDict) + return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) } } @@ -99,8 +102,8 @@ extension SyntaxProtocol { func lookupInParent( for name: String?, at syntax: SyntaxProtocol, - with configDict: LookupConfigDictionary + with config: LookupConfig ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: configDict) ?? [] + parentScope?.lookup(for: name, at: syntax, with: config) ?? [] } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index c46c111f036..76106fb1453 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -56,13 +56,13 @@ enum MarkerExpectation { /// Used to define enum ResultExpectation { case fromScope(ScopeSyntax.Type, expectedNames: [String]) - case fromFileScope(expectedNames: [String], nameIntroductionStrategy: FileScopeNameIntroductionStrategy) + case fromFileScope(expectedNames: [String]) var expectedNames: [String] { switch self { case .fromScope(_, let expectedNames): expectedNames - case .fromFileScope(expectedNames: let expectedNames, nameIntroductionStrategy: _): + case .fromFileScope(expectedNames: let expectedNames): expectedNames } } @@ -153,7 +153,7 @@ func assertLexicalNameLookup( references: [String: [ResultExpectation]], expectedResultTypes: MarkerExpectation = .none, useNilAsTheParameter: Bool = false, - config: [LookupConfig] = [] + config: LookupConfig = LookupConfig() ) { assertLexicalScopeQuery( source: source, @@ -172,14 +172,8 @@ func assertLexicalNameLookup( scope.syntaxNodeType == expectedType, "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" ) - case ( - .fromFileScope(_, withNames: _, nameIntroductionStrategy: let nameIntroductionStrategy), - .fromFileScope(expectedNames: _, nameIntroductionStrategy: let expectedNameIntroductionStrategy) - ): - XCTAssert( - nameIntroductionStrategy == expectedNameIntroductionStrategy, - "For marker \(marker), actual file scope name introduction strategy \(nameIntroductionStrategy) doesn't match expected \(expectedNameIntroductionStrategy)" - ) + case (.fromFileScope, .fromFileScope): + break default: XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)") } diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 366ca895224..fdeb061f34d 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -402,14 +402,14 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), - .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl), + .fromFileScope(expectedNames: ["🔟"]), ], "0️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["8️⃣", "9️⃣"]), .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), - .fromFileScope(expectedNames: ["🔟"], nameIntroductionStrategy: .memberBlockUpToLastDecl), + .fromFileScope(expectedNames: ["🔟"]), ], ], expectedResultTypes: .all( @@ -488,11 +488,11 @@ final class testNameLookup: XCTestCase { let x = 0️⃣d """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], - "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], - "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], + "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], + "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], "6️⃣": [], - "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlockUpToLastDecl)], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]) ) @@ -518,14 +518,14 @@ final class testNameLookup: XCTestCase { let x = 0️⃣d """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"], nameIntroductionStrategy: .memberBlock)], - "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"], nameIntroductionStrategy: .memberBlock)], - "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"], nameIntroductionStrategy: .memberBlock)], - "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlock)], - "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .memberBlock)], + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣", "8️⃣"])], + "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], + "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], + "6️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), - config: [FileScopeNameIntroductionStrategy.memberBlock] + config: LookupConfig(fileScopeHandling: .memberBlock) ) } @@ -549,14 +549,14 @@ final class testNameLookup: XCTestCase { let x = 0️⃣d """, references: [ - "3️⃣": [.fromFileScope(expectedNames: ["1️⃣"], nameIntroductionStrategy: .codeBlock)], + "3️⃣": [.fromFileScope(expectedNames: ["1️⃣"])], "4️⃣": [], "5️⃣": [], "6️⃣": [], - "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"], nameIntroductionStrategy: .codeBlock)], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], ], expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), - config: [FileScopeNameIntroductionStrategy.codeBlock] + config: LookupConfig(fileScopeHandling: .codeBlock) ) } } From a0ff1be4d5314e459bb22edf1808d3cee500cb80 Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:52:36 +0200 Subject: [PATCH 09/10] Use `Identifier` for name comparison. --- Sources/SwiftLexicalLookup/LookupName.swift | 9 +++++---- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 5 ++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index ff86e4d8eef..da7338a46f5 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -31,12 +31,12 @@ import SwiftSyntax } /// Introduced name. - @_spi(Experimental) public var name: String { + @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): - syntax.identifier.text + Identifier(syntax.identifier) case .declaration(let syntax, _): - syntax.name.text + Identifier(syntax.name) } } @@ -57,7 +57,8 @@ import SwiftSyntax /// Checks if this name refers to the looked up phrase. func refersTo(_ lookedUpName: String) -> Bool { - name == lookedUpName + guard let name = identifier?.name else { return false } + return name == lookedUpName } /// Extracts names introduced by the given `from` structure. diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index fdeb061f34d..8afbb8e29c8 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -140,14 +140,13 @@ final class testNameLookup: XCTestCase { source: """ func foo() { let 1️⃣a = 1 - let x = { [2️⃣weak self, 3️⃣a, 4️⃣unowned b] in - print(5️⃣self, 6️⃣a) + let x = { [3️⃣a, 4️⃣unowned b] in + print(6️⃣a) } let b = 0 } """, references: [ - "5️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["2️⃣"])], "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), From caae6772d766e7ba95464d9565ad3dc3af5202ff Mon Sep 17 00:00:00 2001 From: Jakub Florek <63607600+MAJKFL@users.noreply.github.com> Date: Mon, 22 Jul 2024 16:35:27 +0200 Subject: [PATCH 10/10] Remove trailing comma that made format check fail. --- Tests/SwiftLexicalLookupTest/NameLookupTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 8afbb8e29c8..a04c35382e7 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -150,7 +150,7 @@ final class testNameLookup: XCTestCase { "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), - ], + ] ], expectedResultTypes: .all( ClosureCaptureSyntax.self,