diff --git a/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.swift new file mode 100644 index 00000000000..ca0c6642d8d --- /dev/null +++ b/Sources/SwiftLexicalLookup/Configurations/FileScopeNameIntroductionStrategy.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 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. + 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/Configurations/LookupConfig.swift b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift new file mode 100644 index 00000000000..d96093037ca --- /dev/null +++ b/Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift @@ -0,0 +1,23 @@ +//===----------------------------------------------------------------------===// +// +// 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(Experimental) public struct LookupConfig { + /// Specifies behaviour of file scope. + /// `memberBlockUpToLastDecl` by default. + public var fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl + + public init(fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl) { + self.fileScopeHandling = fileScopeHandling + } +} diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift new file mode 100644 index 00000000000..67e6de6b4e9 --- /dev/null +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// 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: TokenSyntax { + secondName ?? firstName + } +} + +extension ClosureShorthandParameterSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { + name + } +} + +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 new file mode 100644 index 00000000000..da7338a46f5 --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -0,0 +1,122 @@ +//===----------------------------------------------------------------------===// +// +// 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(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) + /// Declaration associated with the name. + /// Could be class, struct, actor, protocol, function and more + case declaration(NamedDeclSyntax, accessibleAfter: AbsolutePosition?) + + /// 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 identifier: Identifier? { + switch self { + case .identifier(let syntax, _): + Identifier(syntax.identifier) + case .declaration(let syntax, _): + Identifier(syntax.name) + } + } + + /// 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), .declaration(_, let absolutePosition): + absolutePosition + } + } + + /// Checks if this name was introduced before the syntax used for lookup. + 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. + func refersTo(_ lookedUpName: String) -> Bool { + guard let name = identifier?.name else { return false } + return name == lookedUpName + } + + /// Extracts names introduced by the given `from` structure. + 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, accessibleAfter: accessibleAfter) + } + case .tuplePattern(let tuplePattern): + tuplePattern.elements.flatMap { tupleElement in + getNames(from: tupleElement.pattern, accessibleAfter: accessibleAfter) + } + case .valueBindingPattern(let valueBindingPattern): + getNames(from: valueBindingPattern.pattern, accessibleAfter: accessibleAfter) + case .expressionPattern(let expressionPattern): + getNames(from: expressionPattern.expression, accessibleAfter: accessibleAfter) + case .sequenceExpr(let sequenceExpr): + sequenceExpr.elements.flatMap { expression in + getNames(from: expression, accessibleAfter: accessibleAfter) + } + case .patternExpr(let patternExpr): + 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) + } else if let identifiable = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? IdentifiableSyntax { + handle(identifiable: identifiable, accessibleAfter: accessibleAfter) + } else { + [] + } + } + } + + /// 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 `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..4e5cc04f97b --- /dev/null +++ b/Sources/SwiftLexicalLookup/LookupResult.swift @@ -0,0 +1,39 @@ +//===----------------------------------------------------------------------===// +// +// 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]) + /// 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, _): + fileScopeSyntax + } + } + + /// Names that matched lookup. + @_spi(Experimental) public var names: [LookupName] { + switch self { + case .fromScope(_, let names), .fromFileScope(_, let names): + names + } + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift new file mode 100644 index 00000000000..06eb4da0a0e --- /dev/null +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -0,0 +1,289 @@ +//===----------------------------------------------------------------------===// +// +// 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? { + if let scopeSyntax = Syntax(self).asProtocol(SyntaxProtocol.self) as? ScopeSyntax { + scopeSyntax + } else { + self.parent?.scope + } + } +} + +@_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) + } + + /// 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 + + 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) + } + } + } + + /// 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 config: LookupConfig + ) -> [LookupResult] { + let names = introducedNames(using: config.fileScopeHandling) + .filter { introducedName in + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + } + + 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) + } + } +} + +@_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 + 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) + } + } else { + LookupName.getNames(from: parameter) + } + } ?? [] + + return captureNames + parameterNames + } +} + +@_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) + } + } +} + +@_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 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 } + + 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) + } + } + + /// 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 config: LookupConfig + ) -> [LookupResult] { + if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { + lookupInParent(for: name, at: syntax, with: config) + } else { + 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) + } + } +} + +@_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 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 new file mode 100644 index 00000000000..55bebd9ef78 --- /dev/null +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -0,0 +1,109 @@ +//===----------------------------------------------------------------------===// +// +// 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. + /// 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. + /// 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: + /// ```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?, + with config: LookupConfig = LookupConfig() + ) -> [LookupResult] { + scope?.lookup(for: name, at: self, with: config) ?? [] + } +} + +@_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. + /// 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] +} + +@_spi(Experimental) extension ScopeSyntax { + public var parentScope: ScopeSyntax? { + self.parent?.scope + } + + /// 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) + } + + /// 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. + func defaultLookupImplementation( + for name: String?, + at syntax: SyntaxProtocol, + with config: LookupConfig + ) -> [LookupResult] { + let filteredNames = + introducedNames + .filter { introducedName in + introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + } + + if filteredNames.isEmpty { + return lookupInParent(for: name, at: syntax, with: config) + } else { + 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/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..76106fb1453 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -17,11 +17,65 @@ 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)" + ) + } +} + +/// Used to define +enum ResultExpectation { + case fromScope(ScopeSyntax.Type, expectedNames: [String]) + case fromFileScope(expectedNames: [String]) + + var expectedNames: [String] { + switch self { + case .fromScope(_, let expectedNames): + expectedNames + case .fromFileScope(expectedNames: let expectedNames): + expectedNames + } + } +} + +/// `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?]] + methodUnderTest: (String, TokenSyntax) -> ([SyntaxProtocol?]), + expected: [String: [String?]], + expectedResultTypes: MarkerExpectation = .none ) { // Extract markers let (markerDict, textWithoutMarkers) = extractMarkers(source) @@ -41,15 +95,15 @@ func assertLexicalScopeQuery( } // Execute the tested method - let result = methodUnderTest(testArgument) + let result = methodUnderTest(marker, 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 +111,85 @@ 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: [ResultExpectation]], + expectedResultTypes: MarkerExpectation = .none, + useNilAsTheParameter: Bool = false, + config: LookupConfig = LookupConfig() +) { + assertLexicalScopeQuery( + source: source, + methodUnderTest: { marker, argument in + 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") + return [] + } + + for (actual, expected) in zip(result, expectedValues) { + 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)" + ) + case (.fromFileScope, .fromFileScope): + break + default: + XCTFail("For marker \(marker), result actual result kind \(actual) doesn't match expected \(expected)") + } + } + + return result.flatMap { lookUpResult in + lookUpResult.names.map { lookupName in + lookupName.syntax + } + } + }, + expected: references.mapValues { expectations in + expectations.flatMap { expectation in + expectation.expectedNames + } + }, + expectedResultTypes: expectedResultTypes + ) +} diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift new file mode 100644 index 00000000000..a04c35382e7 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -0,0 +1,561 @@ +//===----------------------------------------------------------------------===// +// +// 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(Experimental) 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️⃣": [.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) + ) + } + + 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️⃣": [.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) + ) + } + + 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️⃣": [.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) + ) + } + + func testLookupForCaseLetLoop() { + assertLexicalNameLookup( + source: """ + for case let 1️⃣a as T in arr { + 2️⃣a.foo() + } + """, + references: ["2️⃣": [.fromScope(ForStmtSyntax.self, expectedNames: ["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️⃣": [ + .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: [ + "4️⃣": ClosureShorthandParameterSyntax.self, + "5️⃣": ClosureShorthandParameterSyntax.self, + ] + ) + ) + } + + func testClosureCaptureLookup() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let x = { [3️⃣a, 4️⃣unowned b] in + print(6️⃣a) + } + let b = 0 + } + """, + references: [ + "6️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ] + ], + expectedResultTypes: .all( + ClosureCaptureSyntax.self, + except: [ + "1️⃣": IdentifierPatternSyntax.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️⃣": [.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: [ + "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️⃣": [.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 + ) + ) + } + + 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️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "5️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["1️⃣"])], + "6️⃣": [.fromScope(IfExprSyntax.self, expectedNames: ["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️⃣": [ + .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 + ) + ) + } + + 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️⃣": [.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, + "3️⃣": StructDeclSyntax.self, + "4️⃣": FunctionDeclSyntax.self, + "9️⃣": ActorDeclSyntax.self, + "0️⃣": ProtocolDeclSyntax.self, + ]) + ) + } + + 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️⃣": [.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 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 { + 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️⃣"]), + .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: ["🔟"]), + ], + ], + expectedResultTypes: .all( + IdentifierPatternSyntax.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️⃣"])], + "4️⃣": [.fromFileScope(expectedNames: ["2️⃣"])], + "5️⃣": [.fromFileScope(expectedNames: ["7️⃣"])], + "6️⃣": [], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], + ], + 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️⃣"])], + "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: LookupConfig(fileScopeHandling: .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️⃣"])], + "4️⃣": [], + "5️⃣": [], + "6️⃣": [], + "0️⃣": [.fromFileScope(expectedNames: ["9️⃣"])], + ], + expectedResultTypes: .all(ClassDeclSyntax.self, except: ["8️⃣": IdentifierPatternSyntax.self]), + config: LookupConfig(fileScopeHandling: .codeBlock) + ) + } +} 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️⃣"]]