Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC][SwiftLexicalLookup] Make unqualified name lookup entry-point public #2952

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Release Notes/602.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## New APIs

- `SwiftLexicalLookup` - A new Swift unqualified lookup library
- Description: The library provides a new Swift unqualified lookup implementation detached from the compiler and accessible to outside clients. The query is stateless and can be directly run on swift-syntax syntax tree, with any syntax node functioning as an entry point. It produces an enum-based data structure as a result that partitions collected names based on the lexical scope of introduction.
- Pull Request: https://github.com/swiftlang/swift-syntax/pull/2952
- Notes: The library follows the behavior of the compiler implementation with some minor differences, such as a different way of handling dollar identifiers `$x` and generic parameters inside extensions. Furthermore, in the future, once the compiler adopts `SwiftLexicalLookup` and it becomes the canonical implementation, results produced by the query will be guaranteed to be correct.

## API Behavior Changes

## Deprecations
Expand Down
32 changes: 16 additions & 16 deletions Sources/SwiftLexicalLookup/IdentifiableSyntax.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,50 @@
import SwiftSyntax

/// Syntax node that can be refered to with an identifier.
@_spi(Experimental) public protocol IdentifiableSyntax: SyntaxProtocol {
protocol IdentifiableSyntax: SyntaxProtocol {
var identifier: TokenSyntax { get }
}

@_spi(Experimental) extension IdentifierPatternSyntax: IdentifiableSyntax {}
extension IdentifierPatternSyntax: IdentifiableSyntax {}

@_spi(Experimental) extension ClosureParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension ClosureParameterSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
secondName ?? firstName
}
}

@_spi(Experimental) extension FunctionParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension FunctionParameterSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
secondName ?? firstName
}
}

@_spi(Experimental) extension ClosureShorthandParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension ClosureShorthandParameterSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
name
}
}

@_spi(Experimental) extension ClosureCaptureSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension ClosureCaptureSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
name
}
}

@_spi(Experimental) extension AccessorParametersSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension AccessorParametersSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
name
}
}

@_spi(Experimental) extension GenericParameterSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension GenericParameterSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
name
}
}

@_spi(Experimental) extension PrimaryAssociatedTypeSyntax: IdentifiableSyntax {
@_spi(Experimental) public var identifier: TokenSyntax {
extension PrimaryAssociatedTypeSyntax: IdentifiableSyntax {
var identifier: TokenSyntax {
name
}
}
8 changes: 4 additions & 4 deletions Sources/SwiftLexicalLookup/LookupConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import SwiftIfConfig

@_spi(Experimental) public struct LookupConfig {
public struct LookupConfig {
/// Specifies whether lookup should finish in the closest sequential scope.
///
/// ### Example
Expand All @@ -32,14 +32,14 @@ import SwiftIfConfig
/// function parameter and the `a` declaration from `class X` member block.
/// If `finishInSequentialScope` would be set to `false`, the only name
/// returned by lookup would be the `a` declaration from inside function body.
@_spi(Experimental) public var finishInSequentialScope: Bool
@_spi(Experimental) public var configuredRegions: ConfiguredRegions?
public var finishInSequentialScope: Bool
public var configuredRegions: ConfiguredRegions?

/// Creates a new lookup configuration.
///
/// - `finishInSequentialScope` - specifies whether lookup should finish
/// in the closest sequential scope. `false` by default.
@_spi(Experimental) public init(
public init(
finishInSequentialScope: Bool = false,
configuredRegions: ConfiguredRegions? = nil
) {
Expand Down
72 changes: 30 additions & 42 deletions Sources/SwiftLexicalLookup/LookupName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@
import SwiftSyntax

/// An entity that is implicitly declared based on the syntactic structure of the program.
@_spi(Experimental) public enum ImplicitDecl {
public enum ImplicitDecl {
/// `self` keyword representing object instance.
/// Could be associated with type declaration, extension,
/// or closure captures. Introduced at function edge.
case `self`(DeclSyntaxProtocol)
/// Introduced at member boundary.
/// Associated syntax node could be: `FunctionDeclSyntax`,
/// `AccessorDeclSyntax`, `SubscriptDeclSyntax`,
/// `DeinitializerDeclSyntax`, or `InitializerDeclSyntax`.
case `self`(DeclSyntax)
/// `Self` keyword representing object type.
/// Could be associated with type declaration or extension.
case `Self`(DeclSyntaxProtocol)
/// `error` value caught by a `catch`
/// Associated syntax node could be: `ExtensionDeclSyntax`,
/// or `ProtocolDeclSyntax`.
case `Self`(DeclSyntax)
/// `error` available by default inside `catch`
/// block that does not specify a catch pattern.
case error(CatchClauseSyntax)
/// `newValue` available by default inside `set` and `willSet`.
Expand All @@ -30,7 +33,7 @@ import SwiftSyntax
case oldValue(AccessorDeclSyntax)

/// Syntax associated with this name.
@_spi(Experimental) public var syntax: SyntaxProtocol {
public var syntax: SyntaxProtocol {
switch self {
case .self(let syntax):
return syntax
Expand All @@ -46,7 +49,7 @@ import SwiftSyntax
}

/// The name of the implicit declaration.
private var name: StaticString {
public var name: StaticString {
switch self {
case .self:
return "self"
Expand Down Expand Up @@ -85,12 +88,12 @@ import SwiftSyntax
/// ```
/// `self` and `Self` identifers override implicit `self` and `Self` introduced by
/// the `Foo` class declaration.
var identifier: Identifier {
public var identifier: Identifier {
Identifier(canonicalName: name)
}

/// Position of this implicit name.
@_spi(Experimental) public var position: AbsolutePosition {
public var position: AbsolutePosition {
switch self {
case .self(let declSyntax):
switch Syntax(declSyntax).as(SyntaxEnum.self) {
Expand Down Expand Up @@ -128,17 +131,15 @@ import SwiftSyntax
}
}

@_spi(Experimental) public enum LookupName {
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?)
case identifier(Syntax, accessibleAfter: AbsolutePosition?)
/// Declaration associated with the name.
/// Could be class, struct, actor, protocol, function and more.
case declaration(NamedDeclSyntax)
case declaration(Syntax)
/// Name introduced implicitly by certain syntax nodes.
case implicit(ImplicitDecl)
/// Dollar identifier introduced by a closure without parameters.
case dollarIdentifier(ClosureExprSyntax, strRepresentation: String)
/// Represents equivalent names grouped together.
/// - Important: The array should be non-empty.
///
Expand All @@ -154,32 +155,28 @@ import SwiftSyntax
case equivalentNames([LookupName])

/// Syntax associated with this name.
@_spi(Experimental) public var syntax: SyntaxProtocol {
public var syntax: SyntaxProtocol {
switch self {
case .identifier(let syntax, _):
return syntax
case .declaration(let syntax):
return syntax
case .implicit(let implicitName):
return implicitName.syntax
case .dollarIdentifier(let closureExpr, _):
return closureExpr
case .equivalentNames(let names):
return names.first!.syntax
}
}

/// Identifier used for name comparison.
@_spi(Experimental) public var identifier: Identifier? {
public var identifier: Identifier {
switch self {
case .identifier(let syntax, _):
return Identifier(syntax.identifier)
return Identifier((syntax.asProtocol(SyntaxProtocol.self) as! IdentifiableSyntax).identifier)!
case .declaration(let syntax):
return Identifier(syntax.name)
return Identifier((syntax.asProtocol(SyntaxProtocol.self) as! NamedDeclSyntax).name)!
case .implicit(let kind):
return kind.identifier
case .dollarIdentifier(_, strRepresentation: _):
return nil
case .equivalentNames(let names):
return names.first!.identifier
}
Expand All @@ -192,16 +189,15 @@ import SwiftSyntax
/// Such cases are function parameters (as they can
/// contain two identifiers) and function declarations (where name
/// is precided by access modifiers and `func` keyword).
@_spi(Experimental) public var position: AbsolutePosition {
public var position: AbsolutePosition {
switch self {
case .identifier(let syntax, _):
return syntax.identifier.positionAfterSkippingLeadingTrivia
return (syntax.asProtocol(SyntaxProtocol.self) as! IdentifiableSyntax).identifier
.positionAfterSkippingLeadingTrivia
case .declaration(let syntax):
return syntax.name.positionAfterSkippingLeadingTrivia
return (syntax.asProtocol(SyntaxProtocol.self) as! NamedDeclSyntax).name.positionAfterSkippingLeadingTrivia
case .implicit(let implicitName):
return implicitName.position
case .dollarIdentifier(let closureExpr, _):
return closureExpr.positionAfterSkippingLeadingTrivia
case .equivalentNames(let names):
return names.first!.position
}
Expand All @@ -226,13 +222,7 @@ import SwiftSyntax

func refersTo(_ otherIdentifier: Identifier?) -> Bool {
guard let otherIdentifier else { return true }

switch self {
case .dollarIdentifier(_, let strRepresentation):
return strRepresentation == otherIdentifier.name
default:
return identifier == otherIdentifier
}
return identifier == otherIdentifier
}

/// Extracts names introduced by the given `syntax` structure.
Expand Down Expand Up @@ -305,22 +295,22 @@ import SwiftSyntax
return []
}

return [.identifier(identifiable, accessibleAfter: accessibleAfter)]
return [.identifier(Syntax(identifiable), accessibleAfter: accessibleAfter)]
}

/// Extracts name introduced by `NamedDeclSyntax` node.
private static func handle(
namedDecl: NamedDeclSyntax,
accessibleAfter: AbsolutePosition? = nil
) -> [LookupName] {
[.declaration(namedDecl)]
[.declaration(Syntax(namedDecl))]
}

/// Debug description of this lookup name.
@_spi(Experimental) public var debugDescription: String {
public var debugDescription: String {
let sourceLocationConverter = SourceLocationConverter(fileName: "", tree: syntax.root)
let location = sourceLocationConverter.location(for: position)
let strName = (identifier?.name ?? "NO-NAME") + " at: \(location.line):\(location.column)"
let strName = identifier.name + " at: \(location.line):\(location.column)"

switch self {
case .identifier:
Expand All @@ -336,8 +326,6 @@ import SwiftSyntax
return "declaration: \(strName)"
case .implicit:
return "implicit: \(strName)"
case .dollarIdentifier(_, strRepresentation: let str):
return "dollarIdentifier: \(str)"
case .equivalentNames(let names):
return "Composite name: [ \(names.map(\.debugDescription).joined(separator: ", ")) ]"
}
Expand Down
39 changes: 12 additions & 27 deletions Sources/SwiftLexicalLookup/LookupResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
import SwiftSyntax

/// Represents result from a specific scope.
@_spi(Experimental) public enum LookupResult {
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])
case fromScope(Syntax, withNames: [LookupName])
/// Indicates where to perform member lookup.
case lookInMembers(LookInMembersScopeSyntax)
case lookInMembers(Syntax)
/// Indicates to lookup generic parameters of extended type.
///
/// ### Example
Expand Down Expand Up @@ -52,12 +50,10 @@ import SwiftSyntax
case mightIntroduceDollarIdentifiers(ClosureExprSyntax)

/// Associated scope.
@_spi(Experimental) public var scope: ScopeSyntax {
public var scope: SyntaxProtocol {
switch self {
case .fromScope(let scopeSyntax, _):
return scopeSyntax
case .fromFileScope(let fileScopeSyntax, _):
return fileScopeSyntax
case .lookInMembers(let lookInMemb):
return lookInMemb
case .lookInGenericParametersOfExtendedType(let extensionDecl):
Expand All @@ -68,9 +64,9 @@ import SwiftSyntax
}

/// Names that matched lookup.
@_spi(Experimental) public var names: [LookupName] {
public var names: [LookupName] {
switch self {
case .fromScope(_, let names), .fromFileScope(_, let names):
case .fromScope(_, let names):
return names
case .lookInMembers(_),
.lookInGenericParametersOfExtendedType(_),
Expand All @@ -79,28 +75,19 @@ import SwiftSyntax
}
}

/// Returns result specific for the particular `scope` kind with provided `names`.
static func getResult(for scope: ScopeSyntax, withNames names: [LookupName]) -> LookupResult {
switch Syntax(scope).as(SyntaxEnum.self) {
case .sourceFile(let sourceFileSyntax):
return .fromFileScope(sourceFileSyntax, withNames: names)
default:
return .fromScope(scope, withNames: names)
}
}

/// Returns result specific for the particular `scope` kind with provided `names`
/// as an array with one element. If names are empty, returns an empty array.
static func getResultArray(for scope: ScopeSyntax, withNames names: [LookupName]) -> [LookupResult] {
guard !names.isEmpty else { return [] }

return [getResult(for: scope, withNames: names)]
return [.fromScope(Syntax(scope), withNames: names)]
}

/// Debug description of this lookup name.
@_spi(Experimental) public var debugDescription: String {
public var debugDescription: String {
var description =
resultKindDebugName + ": " + scope.scopeDebugDescription
resultKindDebugName + ": "
+ ((Syntax(scope).asProtocol(SyntaxProtocol.self) as? ScopeSyntax)?.scopeDebugDescription ?? "NOT-A-SCOPE")

switch self {
case .lookInMembers:
Expand All @@ -127,8 +114,6 @@ import SwiftSyntax
switch self {
case .fromScope:
return "fromScope"
case .fromFileScope:
return "fromFileScope"
case .lookInMembers:
return "lookInMembers"
case .lookInGenericParametersOfExtendedType(_):
Expand All @@ -139,9 +124,9 @@ import SwiftSyntax
}
}

@_spi(Experimental) extension [LookupResult] {
extension [LookupResult] {
/// Debug description this array of lookup results.
@_spi(Experimental) public var debugDescription: String {
public var debugDescription: String {
return self.map(\.debugDescription).joined(separator: "\n")
}
}
Loading