diff --git a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift index 004e3f36b4d..033dad3244b 100644 --- a/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift +++ b/Sources/SwiftLexicalLookup/IdentifiableSyntax.swift @@ -41,3 +41,9 @@ import SwiftSyntax expression.as(DeclReferenceExprSyntax.self)!.baseName } } + +@_spi(Experimental) extension AccessorParametersSyntax: IdentifiableSyntax { + @_spi(Experimental) public var identifier: TokenSyntax { + name + } +} diff --git a/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift new file mode 100644 index 00000000000..5d58aa7a87b --- /dev/null +++ b/Sources/SwiftLexicalLookup/IntroducingToSequentialParentScopeSyntax.swift @@ -0,0 +1,26 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +protocol IntroducingToSequentialParentScopeSyntax: ScopeSyntax { + /// Returns all names introduced to parent. + var namesIntroducedToSequentialParent: [LookupName] { get } + + /// Returns results matching lookup that should be + /// interleaved with results of the sequential parent. + func lookupFromSequentialParent( + for identifier: Identifier?, + at origin: AbsolutePosition, + with config: LookupConfig + ) -> [LookupResult] +} diff --git a/Sources/SwiftLexicalLookup/LookupName.swift b/Sources/SwiftLexicalLookup/LookupName.swift index 868cfd1f0d6..aac72f6c24f 100644 --- a/Sources/SwiftLexicalLookup/LookupName.swift +++ b/Sources/SwiftLexicalLookup/LookupName.swift @@ -12,6 +12,95 @@ import SwiftSyntax +/// An entity that is implicitly declared based on the syntactic structure of the program. +@_spi(Experimental) public enum ImplicitDecl { + /// `self` keyword representing object instance. + /// Could be associated with type declaration, extension, + /// or closure captures. + case `self`(DeclSyntaxProtocol) + /// `Self` keyword representing object type. + /// Could be associated with type declaration or extension. + case `Self`(DeclSyntaxProtocol) + /// `error` value caught by a `catch` + /// block that does not specify a catch pattern. + case error(CatchClauseSyntax) + /// `newValue` available by default inside `set` and `willSet`. + case newValue(AccessorDeclSyntax) + /// `oldValue` available by default inside `didSet`. + case oldValue(AccessorDeclSyntax) + + /// Syntax associated with this name. + @_spi(Experimental) public var syntax: SyntaxProtocol { + switch self { + case .self(let syntax): + return syntax + case .Self(let syntax): + return syntax + case .error(let syntax): + return syntax + case .newValue(let syntax): + return syntax + case .oldValue(let syntax): + return syntax + } + } + + /// The name of the implicit declaration. + private var name: String { + switch self { + case .self: + return "self" + case .Self: + return "Self" + case .error: + return "error" + case .newValue: + return "newValue" + case .oldValue: + return "oldValue" + } + } + + /// Identifier used for name comparison. + /// + /// Note that `self` and `Self` are treated as identifiers for name lookup purposes + /// and that a variable named `self` can shadow the `self` keyword. For example. + /// ```swift + /// class Foo { + /// func test() { + /// let `Self` = "abc" + /// print(Self.self) + /// + /// let `self` = "def" + /// print(self) + /// } + /// } + /// + /// Foo().test() + /// ``` + /// prints: + /// ``` + /// abc + /// def + /// ``` + /// `self` and `Self` identifers override implicit `self` and `Self` introduced by + /// the `Foo` class declaration. + var identifier: Identifier { + switch self { + case .self: + return Identifier("self") + case .Self: + return Identifier("Self") + case .error: + return Identifier("error") + case .newValue: + return Identifier("newValue") + case .oldValue: + return Identifier("oldValue") + } + } +} + @_spi(Experimental) public enum LookupName { /// Identifier associated with the name. /// Could be an identifier of a variable, function or closure parameter and more. @@ -19,6 +108,12 @@ import SwiftSyntax /// Declaration associated with the name. /// Could be class, struct, actor, protocol, function and more. case declaration(NamedDeclSyntax) + /// Name introduced implicitly by certain syntax nodes. + case implicit(ImplicitDecl) + /// Explicit `self` keyword. + case `self`(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) + /// Explicit `Self` keyword. + case `Self`(IdentifiableSyntax, accessibleAfter: AbsolutePosition?) /// Syntax associated with this name. @_spi(Experimental) public var syntax: SyntaxProtocol { @@ -27,16 +122,26 @@ import SwiftSyntax return syntax case .declaration(let syntax): return syntax + case .implicit(let implicitName): + return implicitName.syntax + case .self(let syntax, _), .Self(let syntax, _): + return syntax } } - /// Introduced name. + /// Identifier used for name comparison. @_spi(Experimental) public var identifier: Identifier? { switch self { case .identifier(let syntax, _): return Identifier(syntax.identifier) case .declaration(let syntax): return Identifier(syntax.name) + case .implicit(let kind): + return kind.identifier + case .self: + return Identifier("self") + case .Self: + return Identifier("Self") } } @@ -44,7 +149,9 @@ import SwiftSyntax /// If set to `nil`, the name is available at any point in scope. var accessibleAfter: AbsolutePosition? { switch self { - case .identifier(_, let absolutePosition): + case .identifier(_, let absolutePosition), + .self(_, let absolutePosition), + .Self(_, let absolutePosition): return absolutePosition default: return nil @@ -52,15 +159,15 @@ import SwiftSyntax } /// Checks if this name was introduced before the syntax used for lookup. - func isAccessible(at lookedUpSyntax: SyntaxProtocol) -> Bool { + func isAccessible(at origin: AbsolutePosition) -> Bool { guard let accessibleAfter else { return true } - return accessibleAfter <= lookedUpSyntax.position + return accessibleAfter <= origin } /// 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 + func refersTo(_ lookedUpIdentifier: Identifier) -> Bool { + guard let identifier else { return false } + return identifier == lookedUpIdentifier } /// Extracts names introduced by the given `syntax` structure. @@ -105,10 +212,6 @@ import SwiftSyntax return functionCallExpr.arguments.flatMap { argument in getNames(from: argument.expression, accessibleAfter: accessibleAfter) } - case .guardStmt(let guardStmt): - return guardStmt.conditions.flatMap { cond in - getNames(from: cond.condition, accessibleAfter: cond.endPosition) - } default: if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax { return handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter) @@ -121,12 +224,21 @@ import SwiftSyntax } /// Extracts name introduced by `IdentifiableSyntax` node. - private static func handle(identifiable: IdentifiableSyntax, accessibleAfter: AbsolutePosition? = nil) -> [LookupName] - { - if identifiable.identifier.tokenKind != .wildcard { - return [.identifier(identifiable, accessibleAfter: accessibleAfter)] - } else { - return [] + private static func handle( + identifiable: IdentifiableSyntax, + accessibleAfter: AbsolutePosition? = nil + ) -> [LookupName] { + switch identifiable.identifier.tokenKind { + case .keyword(.self): + return [.self(identifiable, accessibleAfter: accessibleAfter)] + case .keyword(.Self): + return [.Self(identifiable, accessibleAfter: accessibleAfter)] + default: + if identifiable.identifier.tokenKind != .wildcard { + return [.identifier(identifiable, accessibleAfter: accessibleAfter)] + } else { + return [] + } } } diff --git a/Sources/SwiftLexicalLookup/ScopeImplementations.swift b/Sources/SwiftLexicalLookup/ScopeImplementations.swift index 9671ce176a0..432f99b900f 100644 --- a/Sources/SwiftLexicalLookup/ScopeImplementations.swift +++ b/Sources/SwiftLexicalLookup/ScopeImplementations.swift @@ -23,7 +23,7 @@ import SwiftSyntax } } -@_spi(Experimental) extension SourceFileSyntax: ScopeSyntax { +@_spi(Experimental) extension SourceFileSyntax: SequentialScopeSyntax { /// All names introduced in the file scope /// according to the default strategy: `memberBlockUpToLastDecl`. @_spi(Experimental) public var introducedNames: [LookupName] { @@ -97,20 +97,54 @@ import SwiftSyntax /// - for `memberBlock` - a, b, c, d, e, f /// - for `codeBlock` - a @_spi(Experimental) public func lookup( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - let names = introducedNames(using: config.fileScopeHandling) - .filter { introducedName in - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + switch config.fileScopeHandling { + case .memberBlock: + let names = introducedNames(using: .memberBlock) + .filter { lookupName in + checkName(identifier, refersTo: lookupName, at: origin) + } + + return names.isEmpty ? [] : [.fromFileScope(self, withNames: names)] + case .memberBlockUpToLastDecl: + var members: [LookupName] = [] + var sequentialItems: [CodeBlockItemSyntax] = [] + var encounteredNonDeclaration = false + + for codeBlockItem in statements { + let item = codeBlockItem.item + + if encounteredNonDeclaration { + sequentialItems.append(codeBlockItem) + } else { + if item.is(DeclSyntax.self) { + let foundNames = LookupName.getNames(from: item) + + members.append(contentsOf: foundNames.filter { checkName(identifier, refersTo: $0, at: origin) }) + } else { + encounteredNonDeclaration = true + sequentialItems.append(codeBlockItem) + } + } } - return [.fromFileScope(self, withNames: names)] + let sequentialNames = sequentialLookup( + in: sequentialItems, + for: identifier, + at: origin, + with: config, + createResultsForThisScopeWith: { .fromFileScope(self, withNames: $0) } + ) + + return (members.isEmpty ? [] : [.fromFileScope(self, withNames: members)]) + sequentialNames + } } } -@_spi(Experimental) extension CodeBlockSyntax: ScopeSyntax { +@_spi(Experimental) extension CodeBlockSyntax: SequentialScopeSyntax { /// Names introduced in the code block scope /// accessible after their declaration. @_spi(Experimental) public var introducedNames: [LookupName] { @@ -118,6 +152,20 @@ import SwiftSyntax LookupName.getNames(from: codeBlockItem.item, accessibleAfter: codeBlockItem.endPosition) } } + + @_spi(Experimental) public func lookup( + for identifier: Identifier?, + at origin: AbsolutePosition, + with config: LookupConfig + ) -> [LookupResult] { + sequentialLookup( + in: statements, + for: identifier, + at: origin, + with: config, + createResultsForThisScopeWith: { .fromScope(self, withNames: $0) } + ) + } } @_spi(Experimental) extension ForStmtSyntax: ScopeSyntax { @@ -226,14 +274,14 @@ import SwiftSyntax /// } /// ``` @_spi(Experimental) public func lookup( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - if let elseBody, elseBody.position <= syntax.position, elseBody.endPosition >= syntax.position { - return lookupInParent(for: name, at: syntax, with: config) + if let elseBody, elseBody.position <= origin, elseBody.endPosition >= origin { + return lookupInParent(for: identifier, at: origin, with: config) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config) + return defaultLookupImplementation(for: identifier, at: origin, with: config) } } } @@ -247,16 +295,21 @@ import SwiftSyntax } } -@_spi(Experimental) extension GuardStmtSyntax: ScopeSyntax { - /// Guard doesn't introduce any names to its children. - /// It's always empty. +@_spi(Experimental) extension GuardStmtSyntax: IntroducingToSequentialParentScopeSyntax { + var namesIntroducedToSequentialParent: [LookupName] { + conditions.flatMap { element in + LookupName.getNames(from: element.condition, accessibleAfter: element.endPosition) + } + } + @_spi(Experimental) public var introducedNames: [LookupName] { [] } - /// Returns names matching lookup. - /// Lookup triggered from inside of `else` - /// clause is immediately forwarded to parent scope. + /// Returns results matching lookup that should be + /// interleaved with sequential parent's results. + /// Lookup triggered from within of the `else` body + /// returns no names. /// /// Example: /// ```swift @@ -265,15 +318,49 @@ import SwiftSyntax /// } /// // a is visible here /// ``` - @_spi(Experimental) public func lookup( - for name: String?, - at syntax: SyntaxProtocol, + func lookupFromSequentialParent( + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - if body.position <= syntax.position && body.endPosition >= syntax.position { - return lookupInParent(for: name, at: self, with: config) + guard body.position > origin || body.endPosition < origin + else { return [] } + + let names = namesIntroducedToSequentialParent.filter { introducedName in + checkName(identifier, refersTo: introducedName, at: origin) + } + + return names.isEmpty ? [] : [.fromScope(self, withNames: names)] + } +} + +@_spi(Experimental) extension ActorDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ClassDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension StructDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension EnumDeclSyntax: TypeScopeSyntax {} +@_spi(Experimental) extension ExtensionDeclSyntax: TypeScopeSyntax {} + +@_spi(Experimental) extension AccessorDeclSyntax: ScopeSyntax { + /// Implicit and/or explicit names introduced within the accessor. + @_spi(Experimental) public var introducedNames: [LookupName] { + if let parameters { + return LookupName.getNames(from: parameters) } else { - return defaultLookupImplementation(for: name, at: syntax, with: config) + switch accessorSpecifier.tokenKind { + case .keyword(.set), .keyword(.willSet): + return [.implicit(.newValue(self))] + case .keyword(.didSet): + return [.implicit(.oldValue(self))] + default: + return [] + } } } } + +@_spi(Experimental) extension CatchClauseSyntax: ScopeSyntax { + /// Implicit `error` when there are no catch items. + public var introducedNames: [LookupName] { + return catchItems.isEmpty ? [.implicit(.error(self))] : [] + } +} diff --git a/Sources/SwiftLexicalLookup/ScopeSyntax.swift b/Sources/SwiftLexicalLookup/ScopeSyntax.swift index a1d426ea84b..af9a247a40c 100644 --- a/Sources/SwiftLexicalLookup/ScopeSyntax.swift +++ b/Sources/SwiftLexicalLookup/ScopeSyntax.swift @@ -16,8 +16,8 @@ extension SyntaxProtocol { /// Returns all names that `for` refers to at this syntax node. /// Optional configuration can be passed as `config` to customize the lookup behavior. /// - /// - Returns: An array of `LookupResult` for `name` at this syntax node, - /// ordered by visibility. If `name` is set to `nil`, returns all available names ordered by visibility. + /// - Returns: An array of `LookupResult` for `identifier` at this syntax node, + /// ordered by visibility. If `identifier` is 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. @@ -44,10 +44,10 @@ extension SyntaxProtocol { /// 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?, + for identifier: Identifier?, with config: LookupConfig = LookupConfig() ) -> [LookupResult] { - scope?.lookup(for: name, at: self, with: config) ?? [] + scope?.lookup(for: identifier, at: self.position, with: config) ?? [] } } @@ -56,11 +56,12 @@ extension SyntaxProtocol { 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. `syntax` specifies the node lookup was triggered with. - /// If `name` set to `nil`, returns all available names at the given node. + /// Finds all declarations `identifier` refers to. `syntax` specifies the node lookup was triggered with. + /// If `identifier` set to `nil`, returns all available names at the given node. + /// `state` represents lookup state passed between lookup methods. func lookup( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] } @@ -70,48 +71,50 @@ extension SyntaxProtocol { self.parent?.scope } - /// Returns `LookupResult` of all names introduced in this scope that `name` + /// Returns `LookupResult` of all names introduced in this scope that `identifier` /// 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. + /// If `identifier` set to `nil`, returns all available names at the given node. + /// `state` represents lookup state passed between lookup methods. @_spi(Experimental) public func lookup( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - defaultLookupImplementation(for: name, at: syntax, with: config) + defaultLookupImplementation(for: identifier, at: origin, with: config) } - /// Returns `LookupResult` of all names introduced in this scope that `name` + /// Returns `LookupResult` of all names introduced in this scope that `identifier` /// 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. + /// If `identifier` set to `nil`, returns all available names at the given node. func defaultLookupImplementation( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { let filteredNames = introducedNames .filter { introducedName in - checkName(name, refersTo: introducedName, at: syntax) + checkName(identifier, refersTo: introducedName, at: origin) } if filteredNames.isEmpty { - return lookupInParent(for: name, at: syntax, with: config) + return lookupInParent(for: identifier, at: origin, with: config) } else { - return [.fromScope(self, withNames: filteredNames)] + lookupInParent(for: name, at: syntax, with: config) + return [.fromScope(self, withNames: filteredNames)] + + lookupInParent(for: identifier, at: origin, with: config) } } /// Looks up in parent scope. func lookupInParent( - for name: String?, - at syntax: SyntaxProtocol, + for identifier: Identifier?, + at origin: AbsolutePosition, with config: LookupConfig ) -> [LookupResult] { - parentScope?.lookup(for: name, at: syntax, with: config) ?? [] + parentScope?.lookup(for: identifier, at: origin, with: config) ?? [] } - func checkName(_ name: String?, refersTo introducedName: LookupName, at syntax: SyntaxProtocol) -> Bool { - introducedName.isAccessible(at: syntax) && (name == nil || introducedName.refersTo(name!)) + func checkName(_ name: Identifier?, refersTo introducedName: LookupName, at origin: AbsolutePosition) -> Bool { + introducedName.isAccessible(at: origin) && (name == nil || introducedName.refersTo(name!)) } } diff --git a/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift new file mode 100644 index 00000000000..21f6de167cd --- /dev/null +++ b/Sources/SwiftLexicalLookup/SequentialScopeSyntax.swift @@ -0,0 +1,106 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +/// Scope that, in addition to names introduced by itself, +/// also handles names introduced by +/// `IntroducingToSequentialParentScopeSyntax` children scopes. +protocol SequentialScopeSyntax: ScopeSyntax {} + +extension SequentialScopeSyntax { + /// Returns names introduced by `codeBlockItems` + /// and included `IntroducingToSequentialParentScopeSyntax` children + /// scopes that match the lookup. + /// + /// Example: + /// ```swift + /// func foo() { + /// let a = 1 + /// guard let a = x else { return } + /// let a = a // <-- 1 + /// guard let a = y else { return } + /// a // <-- 2 + /// } + /// ``` + /// For the first `a` reference, sequential lookup returns + /// two results: from `guard` scope and from code block scope + /// in this exact order. For the second `a` reference, + /// sequential lookup yields results from four scopes starting + /// from the bottom: `guard`, code block, `guard` and + /// code block scope in this exact order. + func sequentialLookup( + in codeBlockItems: some Collection, + for identifier: Identifier?, + at origin: AbsolutePosition, + with config: LookupConfig, + createResultsForThisScopeWith getResults: ([LookupName]) -> (LookupResult) + ) -> [LookupResult] { + var results: [LookupResult] = [] + var currentChunk: [LookupName] = [] + var itemsWithoutNamedDecl: [CodeBlockItemSyntax] = [] + + for codeBlockItem in codeBlockItems { + if Syntax(codeBlockItem.item).isProtocol(NamedDeclSyntax.self) { + currentChunk += LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + checkName(identifier, refersTo: introducedName, at: origin) + } + } else { + itemsWithoutNamedDecl.append(codeBlockItem) + } + } + + for codeBlockItem in itemsWithoutNamedDecl { + guard codeBlockItem.position < origin else { break } + + if let introducingToParentScope = Syntax(codeBlockItem.item).asProtocol(SyntaxProtocol.self) + as? IntroducingToSequentialParentScopeSyntax + { + // Get results from encountered scope. + let introducedResults = introducingToParentScope.lookupFromSequentialParent( + for: identifier, + at: origin, + with: config + ) + + // Skip, if no results were found. + guard !introducedResults.isEmpty else { continue } + + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + results.append(getResults(currentChunk)) + currentChunk = [] + } + + results += introducedResults + } else { + // Extract new names from encountered node. + currentChunk += LookupName.getNames( + from: codeBlockItem.item, + accessibleAfter: codeBlockItem.endPosition + ).filter { introducedName in + checkName(identifier, refersTo: introducedName, at: origin) + } + } + } + + // If there are some names collected, create a new result for this scope. + if !currentChunk.isEmpty { + results.append(getResults(currentChunk)) + } + + return results.reversed() + lookupInParent(for: identifier, at: origin, with: config) + } +} diff --git a/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift b/Sources/SwiftLexicalLookup/TypeScopeSyntax.swift new file mode 100644 index 00000000000..5f344405792 --- /dev/null +++ b/Sources/SwiftLexicalLookup/TypeScopeSyntax.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 + +@_spi(Experimental) public protocol TypeScopeSyntax: ScopeSyntax, DeclSyntaxProtocol {} + +extension TypeScopeSyntax { + @_spi(Experimental) public var implicitInstanceAndTypeNames: [LookupName] { + [.implicit(.self(self)), .implicit(.Self(self))] + } + + @_spi(Experimental) public var introducedNames: [LookupName] { + implicitInstanceAndTypeNames + } +} diff --git a/Sources/SwiftSyntax/Identifier.swift b/Sources/SwiftSyntax/Identifier.swift index 0864e754744..97a588395e3 100644 --- a/Sources/SwiftSyntax/Identifier.swift +++ b/Sources/SwiftSyntax/Identifier.swift @@ -12,15 +12,14 @@ /// A canonicalized representation of an identifier that strips away backticks. public struct Identifier: Equatable, Hashable, Sendable { - /// The sanitized `text` of a token. + /// The sanitized name of the identifier. public var name: String { String(syntaxText: raw.name) } @_spi(RawSyntax) public let raw: RawIdentifier - - private let arena: SyntaxArenaRef + let arena: SyntaxArenaRef? public init?(_ token: TokenSyntax) { guard case .identifier = token.tokenKind else { @@ -28,7 +27,23 @@ public struct Identifier: Equatable, Hashable, Sendable { } self.raw = RawIdentifier(token.tokenView) - self.arena = token.tokenView.raw.arenaReference + self.arena = token.raw.arenaReference + } + + public init(_ staticString: StaticString) { + self.raw = RawIdentifier(staticString) + self.arena = nil + } + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name + } +} + +extension Identifier { + @_spi(Testing) public init(anyToken token: TokenSyntax) { + self.raw = RawIdentifier(token.tokenView.rawText) + self.arena = token.raw.arenaReference } } @@ -47,4 +62,12 @@ public struct RawIdentifier: Equatable, Hashable, Sendable { self.name = raw.rawText } } + + fileprivate init(_ staticString: StaticString) { + self.init(SyntaxText(staticString)) + } + + fileprivate init(_ syntaxText: SyntaxText) { + name = syntaxText + } } diff --git a/Tests/SwiftLexicalLookupTest/Assertions.swift b/Tests/SwiftLexicalLookupTest/Assertions.swift index 56161b8e43a..eb671c5ccce 100644 --- a/Tests/SwiftLexicalLookupTest/Assertions.swift +++ b/Tests/SwiftLexicalLookupTest/Assertions.swift @@ -12,61 +12,10 @@ @_spi(Experimental) import SwiftLexicalLookup import SwiftParser -import SwiftSyntax +@_spi(Testing) import SwiftSyntax import XCTest import _SwiftSyntaxTestSupport -/// 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 result assertion. -enum ResultExpectation { - case fromScope(ScopeSyntax.Type, expectedNames: [String]) - case fromFileScope(expectedNames: [String]) - - var expectedNames: [String] { - switch self { - case .fromScope(_, let expectedNames): - return expectedNames - case .fromFileScope(expectedNames: let expectedNames): - return 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`. @@ -119,7 +68,6 @@ func assertLexicalScopeQuery( // Assert validity of the output for (actual, (expectedMarker, expectedPosition)) in zip(result, zip(expectedMarkers, expectedPositions)) { guard let actual, let expectedPosition else { continue } - XCTAssert( actual.positionAfterSkippingLeadingTrivia == expectedPosition, "For marker \(marker), actual result: \(actual) doesn't match expected value: \(sourceFileSyntax.token(at: expectedPosition)?.description ?? "nil")" @@ -145,26 +93,16 @@ func assertLexicalNameLookup( assertLexicalScopeQuery( source: source, methodUnderTest: { marker, tokenAtMarker in - let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : tokenAtMarker.text, with: config) + let lookupIdentifier = Identifier(tokenAtMarker) ?? Identifier(anyToken: tokenAtMarker) + + let result = tokenAtMarker.lookup(for: useNilAsTheParameter ? nil : lookupIdentifier, 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)") - } - } + ResultExpectation.assertResult(marker: marker, result: result, expectedValues: expectedValues) return result.flatMap { lookUpResult in lookUpResult.names.map { lookupName in @@ -174,7 +112,9 @@ func assertLexicalNameLookup( }, expected: references.mapValues { expectations in expectations.flatMap { expectation in - expectation.expectedNames + expectation.expectedNames.map { expectedName in + expectedName.marker + } } }, expectedResultTypes: expectedResultTypes diff --git a/Tests/SwiftLexicalLookupTest/ExpectedName.swift b/Tests/SwiftLexicalLookupTest/ExpectedName.swift new file mode 100644 index 00000000000..8db81175767 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ExpectedName.swift @@ -0,0 +1,100 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup name assertion. +protocol ExpectedName { + var marker: String { get } +} + +extension String: ExpectedName { + var marker: String { + self + } +} + +enum ImplicitNameExpectation { + case `self`(String) + case `Self`(String) + case error(String) + case newValue(String) + case oldValue(String) + + func assertExpectation(marker: String, for name: ImplicitDecl) { + switch (name, self) { + case (.self, .self): break + case (.Self, .Self): break + case (.error, .error): break + case (.newValue, .newValue): break + case (.oldValue, .oldValue): break + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + var marker: String { + switch self { + case .self(let marker), + .Self(let marker), + .error(let marker), + .newValue(let marker), + .oldValue(let marker): + return marker + } + } +} + +/// Can be used to optionally assert +/// exact lookup name kind. +enum NameExpectation: ExpectedName { + case identifier(String) + case declaration(String) + case implicit(ImplicitNameExpectation) + case `self`(String) + case `Self`(String) + + var marker: String { + switch self { + case .identifier(let marker), + .declaration(let marker), + .self(let marker), + .Self(let marker): + return marker + case .implicit(let implicitName): + return implicitName.marker + } + } + + private func assertExpectation(marker: String, for name: LookupName) { + switch (name, self) { + case (.identifier, .identifier): break + case (.declaration, .declaration): break + case (.self, .self): break + case (.Self, .Self): break + case (.implicit(let implicitName), .implicit(let implicitNameExpectation)): + implicitNameExpectation.assertExpectation(marker: marker, for: implicitName) + default: + XCTFail("For marker \(marker), actual name kind \(name) doesn't match expected \(self)") + } + } + + static func assertNames(marker: String, acutalNames: [LookupName], expectedNames: [ExpectedName]) { + for (actualName, expectedName) in zip(acutalNames, expectedNames) { + guard let nameExpectation = expectedName as? NameExpectation else { continue } + + nameExpectation.assertExpectation(marker: marker, for: actualName) + } + } +} diff --git a/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift new file mode 100644 index 00000000000..a39e981a543 --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/MarkerExpectation.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// 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. + 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)" + ) + } +} diff --git a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift index 4b232686c5a..121610a1ed9 100644 --- a/Tests/SwiftLexicalLookupTest/NameLookupTests.swift +++ b/Tests/SwiftLexicalLookupTest/NameLookupTests.swift @@ -140,24 +140,33 @@ final class testNameLookup: XCTestCase { func testClosureCaptureLookup() { assertLexicalNameLookup( source: """ - func foo() { - let 1️⃣a = 1 - let x = { [3️⃣a, 4️⃣unowned b] in - print(6️⃣a) + 7️⃣class a { + func foo() { + let 1️⃣a = 1 + let x = { [2️⃣weak self, 3️⃣a, 4️⃣unowned b] in + print(5️⃣self, 6️⃣a, 8️⃣b) + } + let b = 0 } - let b = 0 } """, references: [ + "5️⃣": [ + .fromScope(ClosureExprSyntax.self, expectedNames: [NameExpectation.`self`("2️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("7️⃣"))]), + ], "6️⃣": [ .fromScope(ClosureExprSyntax.self, expectedNames: ["3️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), - ] + .fromFileScope(expectedNames: ["7️⃣"]), + ], + "8️⃣": [.fromScope(ClosureExprSyntax.self, expectedNames: ["4️⃣"])], ], expectedResultTypes: .all( ClosureCaptureSyntax.self, except: [ - "1️⃣": IdentifierPatternSyntax.self + "1️⃣": IdentifierPatternSyntax.self, + "7️⃣": ClassDeclSyntax.self, ] ) ) @@ -403,6 +412,10 @@ final class testNameLookup: XCTestCase { "7️⃣": [ .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] + ), .fromFileScope(expectedNames: ["🔟"]), ], "0️⃣": [ @@ -410,6 +423,10 @@ final class testNameLookup: XCTestCase { .fromScope(IfExprSyntax.self, expectedNames: ["6️⃣"]), .fromScope(CodeBlockSyntax.self, expectedNames: ["4️⃣", "5️⃣"]), .fromScope(MemberBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "3️⃣"]), + .fromScope( + ClassDeclSyntax.self, + expectedNames: [NameExpectation.implicit(.self("🔟")), NameExpectation.implicit(.Self("🔟"))] + ), .fromFileScope(expectedNames: ["🔟"]), ], ], @@ -441,8 +458,11 @@ final class testNameLookup: XCTestCase { references: [ "4️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], "5️⃣": [], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "7️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["3️⃣"])], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "7️⃣": [.fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"])], ], expectedResultTypes: .all( IdentifierPatternSyntax.self @@ -460,8 +480,14 @@ final class testNameLookup: XCTestCase { """, references: [ "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"])], - "5️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣"])], - "6️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "2️⃣", "4️⃣"])], + "5️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], + "6️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣", "4️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣"]), + ], ], expectedResultTypes: .all( IdentifierPatternSyntax.self @@ -553,4 +579,255 @@ final class testNameLookup: XCTestCase { expectedResultTypes: .all(ClassDeclSyntax.self) ) } + + func testGuardOnFileScope() { + assertLexicalNameLookup( + source: """ + let 1️⃣a = 0 + + class c {} + + guard let 2️⃣a else { fatalError() } + + 3️⃣class a {} + + let x = 4️⃣a + """, + references: [ + "4️⃣": [ + .fromFileScope(expectedNames: ["1️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["2️⃣"]), + .fromFileScope(expectedNames: ["3️⃣"]), + ] + ], + expectedResultTypes: .all(IdentifierPatternSyntax.self, except: ["3️⃣": ClassDeclSyntax.self]) + ) + } + + func testImplicitSelf() { + assertLexicalNameLookup( + source: """ + 1️⃣extension a { + 2️⃣struct b { + func foo() { + let x: 3️⃣Self = 4️⃣self + } + } + + func bar() { + let x: 5️⃣Self = 6️⃣self + } + } + """, + references: [ + "3️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))]), + ], + "4️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("2️⃣"))]), + .fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "5️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))])], + "6️⃣": [.fromScope(ExtensionDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))])], + ] + ) + } + + func testAccessorImplicitNames() { + assertLexicalNameLookup( + source: """ + var a: Int { + get { y } + 1️⃣set { + y = 2️⃣newValue + } + } + + var b: Int { + get { y } + set3️⃣(newValue) { + y = 4️⃣newValue + } + } + + var c = 0 { + 5️⃣willSet { + 6️⃣newValue + } + 7️⃣didSet { + 8️⃣oldValue + } + } + """, + references: [ + "2️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("1️⃣"))])], + "4️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.identifier("3️⃣")])], + "6️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.newValue("5️⃣"))])], + "8️⃣": [.fromScope(AccessorDeclSyntax.self, expectedNames: [NameExpectation.implicit(.oldValue("7️⃣"))])], + ] + ) + } + + func testBacktickCompatibility() { + assertLexicalNameLookup( + source: """ + 1️⃣struct Foo { + func test() { + let 2️⃣`self` = 1 + print(3️⃣self) + print(4️⃣`self`) + } + } + + 5️⃣struct Bar { + func test() { + print(6️⃣self) + let 7️⃣`self` = 1 + print(8️⃣`self`) + } + } + """, + references: [ + "3️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "4️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + "6️⃣": [ + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]) + ], + "8️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("7️⃣")]), + .fromScope(StructDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("5️⃣"))]), + ], + ] + ) + } + + func testImplicitSelfOverride() { + assertLexicalNameLookup( + source: """ + 1️⃣class Foo { + func test() { + let 2️⃣`Self` = "abc" + print(3️⃣Self.self) + + let 4️⃣`self` = "def" + print(5️⃣self) + } + } + """, + references: [ + "3️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("2️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.Self("1️⃣"))]), + ], + "5️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("4️⃣")]), + .fromScope(ClassDeclSyntax.self, expectedNames: [NameExpectation.implicit(.self("1️⃣"))]), + ], + ] + ) + } + + func testImplicitErrorInCatchClause() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣error = 0 + + do { + try x.bar() + 2️⃣error + } catch SomeError { + 3️⃣error + } 4️⃣catch { + 5️⃣error + } + } + """, + references: [ + "2️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")])], + "3️⃣": [.fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")])], + "5️⃣": [ + .fromScope(CatchClauseSyntax.self, expectedNames: [NameExpectation.implicit(.error("4️⃣"))]), + .fromScope(CodeBlockSyntax.self, expectedNames: [NameExpectation.identifier("1️⃣")]), + ], + ] + ) + } + + func testTypeDeclAvaialabilityInSequentialScope() { + let declExpectation: [ResultExpectation] = [ + .fromScope( + CodeBlockSyntax.self, + expectedNames: [ + NameExpectation.declaration("2️⃣"), + NameExpectation.declaration("5️⃣"), + NameExpectation.declaration("8️⃣"), + ] + ) + ] + + assertLexicalNameLookup( + source: """ + func foo() { + 1️⃣a + 2️⃣class a {} + 3️⃣a + guard let x else { return } + 4️⃣a + 5️⃣actor a {} + 6️⃣a + guard let x else { return } + 7️⃣a + 8️⃣struct a {} + 9️⃣a + } + """, + references: [ + "1️⃣": declExpectation, + "3️⃣": declExpectation, + "4️⃣": declExpectation, + "6️⃣": declExpectation, + "7️⃣": declExpectation, + "9️⃣": declExpectation, + ] + ) + } + + func testNonMatchingGuardScopeDoesntPartitionResult() { + assertLexicalNameLookup( + source: """ + func foo() { + let 1️⃣a = 1 + let 2️⃣b = 2 + + guard let 3️⃣b = a else { return } + + let 4️⃣a = 3 + let 5️⃣b = 4 + + guard let 6️⃣a = b else { return } + + print(7️⃣a, 8️⃣b) + } + """, + references: [ + "7️⃣": [ + .fromScope(GuardStmtSyntax.self, expectedNames: ["6️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["1️⃣", "4️⃣"]), + ], + "8️⃣": [ + .fromScope(CodeBlockSyntax.self, expectedNames: ["5️⃣"]), + .fromScope(GuardStmtSyntax.self, expectedNames: ["3️⃣"]), + .fromScope(CodeBlockSyntax.self, expectedNames: ["2️⃣"]), + ], + ] + ) + } } diff --git a/Tests/SwiftLexicalLookupTest/ResultExpectation.swift b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift new file mode 100644 index 00000000000..b4a8c62f6db --- /dev/null +++ b/Tests/SwiftLexicalLookupTest/ResultExpectation.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(Experimental) import SwiftLexicalLookup +import SwiftSyntax +import XCTest + +/// Used to define lookup result assertion. +enum ResultExpectation { + case fromScope(ScopeSyntax.Type, expectedNames: [ExpectedName]) + case fromFileScope(expectedNames: [ExpectedName]) + + var expectedNames: [ExpectedName] { + switch self { + case .fromScope(_, let expectedNames): + return expectedNames + case .fromFileScope(expectedNames: let expectedNames): + return expectedNames + } + } + + var debugDescription: String { + switch self { + case .fromScope: + return "fromScope" + case .fromFileScope: + return "fromFileScope" + } + } + + static func assertResult(marker: String, result: [LookupResult], expectedValues: [ResultExpectation]) { + XCTAssert( + result.count == expectedValues.count, + "For marker \(marker), actual result count \(result.count) doesn't match expected \(expectedValues.count)" + ) + + for (actual, expected) in zip(result, expectedValues) { + switch (actual, expected) { + case ( + .fromScope(let scope, withNames: let actualNames), + .fromScope(let expectedType, expectedNames: let expectedNames) + ): + XCTAssert( + scope.syntaxNodeType == expectedType, + "For marker \(marker), scope result type of \(scope.syntaxNodeType) doesn't match expected \(expectedType)" + ) + + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + case (.fromFileScope(_, let actualNames), .fromFileScope(let expectedNames)): + NameExpectation.assertNames(marker: marker, acutalNames: actualNames, expectedNames: expectedNames) + default: + XCTFail( + "For marker \(marker), actual result kind \(actual.debugDescription) doesn't match expected \(expected.debugDescription)" + ) + } + } + } +} + +extension LookupResult { + var debugDescription: String { + switch self { + case .fromScope: + return "fromScope" + case .fromFileScope: + return "fromFileScope" + } + } +}