Skip to content

[SwiftLexicalLookup] Add ASTScope related fixes and lookInMembers result kind. #2852

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

Merged
merged 8 commits into from
Oct 8, 2024
Merged
5 changes: 4 additions & 1 deletion Sources/SwiftLexicalLookup/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ add_swift_syntax_library(SwiftLexicalLookup
Configurations/FileScopeHandlingConfig.swift
Configurations/LookupConfig.swift

Scopes/CanInterleaveResultsLaterScopeSyntax.swift
Scopes/FunctionScopeSyntax.swift
Scopes/GenericParameterScopeSyntax.swift
Scopes/IntroducingToSequentialParentScopeSyntax.swift
Scopes/LookInMembersScopeSyntax.swift
Scopes/NominalTypeDeclSyntax.swift
Scopes/ScopeImplementations.swift
Scopes/ScopeSyntax.swift
Scopes/SequentialScopeSyntax.swift
Scopes/TypeScopeSyntax.swift
Scopes/WithGenericParametersScopeSyntax.swift
)

Expand Down
49 changes: 48 additions & 1 deletion Sources/SwiftLexicalLookup/Configurations/LookupConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,61 @@
@_spi(Experimental) public struct LookupConfig {
/// Specifies behavior of file scope.
@_spi(Experimental) public var fileScopeHandling: FileScopeHandlingConfig
/// Specifies whether lookup should finish in the closest sequential scope.
///
/// ### Example
/// ```swift
/// class X {
/// let a = 42
///
/// func (a: Int) {
/// let a = 123
///
/// a // <-- lookup here
/// }
/// }
/// ```
/// When looking up at the specified position with `finishInSequentialScope`
/// set to `false`, lookup will return declaration from inside function body,
/// 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
/// Specifies whether to include results generated in file and member block scopes.
///
/// ### Example
/// ```swift
/// class X {
/// let a = 42
///
/// func (a: Int) {
/// let a = 123
///
/// a // <-- lookup here
/// }
/// }
/// ```
/// When looking up at the specified position with `includeMembers`
/// set to `true`, lookup will return declaration from inside function body,
/// function parameter and the `a` declaration from `class X` member block.
/// If `includeMembers` would be set to `false`, the latter name would be omitted.
@_spi(Experimental) public var includeMembers: Bool

/// Creates a new lookup configuration.
///
/// - `fileScopeHandling` - specifies behavior of file scope.
/// `memberBlockUpToLastDecl` by default.
/// - `finishInSequentialScope` - specifies whether lookup should finish
/// in the closest sequential scope. `false` by default.
/// - `includeMembers` - specifies whether to include results generated
/// in file and member block scopes. `true` by default.
@_spi(Experimental) public init(
fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl
fileScopeHandling: FileScopeHandlingConfig = .memberBlockUpToLastDecl,
finishInSequentialScope: Bool = false,
includeMembers: Bool = true
) {
self.fileScopeHandling = fileScopeHandling
self.finishInSequentialScope = finishInSequentialScope
self.includeMembers = includeMembers
}
}
77 changes: 68 additions & 9 deletions Sources/SwiftLexicalLookup/LookupName.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import SwiftSyntax
@_spi(Experimental) public enum ImplicitDecl {
/// `self` keyword representing object instance.
/// Could be associated with type declaration, extension,
/// or closure captures.
/// or closure captures. Introduced at function edge.
case `self`(DeclSyntaxProtocol)
/// `Self` keyword representing object type.
/// Could be associated with type declaration or extension.
case `Self`(DeclSyntaxProtocol)
case `Self`(ProtocolDeclSyntax)
/// `error` value caught by a `catch`
/// block that does not specify a catch pattern.
case error(CatchClauseSyntax)
Expand Down Expand Up @@ -135,6 +135,45 @@ import SwiftSyntax
}
}

/// Position of this name.
///
/// For some syntax nodes, their position doesn't reflect
/// the position at which a particular name was introduced at.
/// 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 {
switch self {
case .identifier(let syntax, _):
return syntax.identifier.positionAfterSkippingLeadingTrivia
case .declaration(let syntax):
return syntax.name.position
case .implicit(let implicitName):
switch implicitName {
case .self(let declSyntax):
switch Syntax(declSyntax).as(SyntaxEnum.self) {
case .functionDecl(let functionDecl):
return functionDecl.name.position
case .initializerDecl(let initializerDecl):
return initializerDecl.initKeyword.positionAfterSkippingLeadingTrivia
case .subscriptDecl(let subscriptDecl):
return subscriptDecl.accessorBlock?.position ?? subscriptDecl.endPosition
case .variableDecl(let variableDecl):
return variableDecl.bindings.first?.accessorBlock?.positionAfterSkippingLeadingTrivia
?? variableDecl.endPosition
default:
return declSyntax.positionAfterSkippingLeadingTrivia
}
case .Self(let protocolDecl):
return protocolDecl.name.positionAfterSkippingLeadingTrivia
case .error(let catchClause):
return catchClause.body.position
default:
return implicitName.syntax.positionAfterSkippingLeadingTrivia
}
}
}

/// 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? {
Expand Down Expand Up @@ -166,7 +205,7 @@ import SwiftSyntax
) -> [LookupName] {
switch Syntax(syntax).as(SyntaxEnum.self) {
case .variableDecl(let variableDecl):
return variableDecl.bindings.flatMap { binding in
return variableDecl.bindings.reversed().flatMap { binding in
getNames(
from: binding.pattern,
accessibleAfter: accessibleAfter != nil ? binding.endPositionBeforeTrailingTrivia : nil
Expand Down Expand Up @@ -194,6 +233,8 @@ import SwiftSyntax
return functionCallExpr.arguments.flatMap { argument in
getNames(from: argument.expression, accessibleAfter: accessibleAfter)
}
case .optionalChainingExpr(let optionalChainingExpr):
return getNames(from: optionalChainingExpr.expression, accessibleAfter: accessibleAfter)
default:
if let namedDecl = Syntax(syntax).asProtocol(SyntaxProtocol.self) as? NamedDeclSyntax {
return handle(namedDecl: namedDecl, accessibleAfter: accessibleAfter)
Expand All @@ -210,12 +251,7 @@ import SwiftSyntax
identifiable: IdentifiableSyntax,
accessibleAfter: AbsolutePosition? = nil
) -> [LookupName] {
switch identifiable.identifier.tokenKind {
case .wildcard:
return []
default:
return [.identifier(identifiable, accessibleAfter: accessibleAfter)]
}
[.identifier(identifiable, accessibleAfter: accessibleAfter)]
}

/// Extracts name introduced by `NamedDeclSyntax` node.
Expand All @@ -225,4 +261,27 @@ import SwiftSyntax
) -> [LookupName] {
[.declaration(namedDecl)]
}

/// Debug description of this lookup name.
@_spi(Experimental) 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)"

switch self {
case .identifier:
let str = "identifier: \(strName)"

if let accessibleAfter {
let location = sourceLocationConverter.location(for: accessibleAfter)
return str + " after: \(location.line):\(location.column)"
} else {
return str
}
case .declaration:
return "declaration: \(strName)"
case .implicit:
return "implicit: \(strName)"
}
}
}
52 changes: 51 additions & 1 deletion Sources/SwiftLexicalLookup/LookupResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@ import SwiftSyntax
case fromScope(ScopeSyntax, withNames: [LookupName])
/// File scope and names that matched lookup.
case fromFileScope(SourceFileSyntax, withNames: [LookupName])
/// Indicates where to perform member lookup.
case lookInMembers(LookInMembersScopeSyntax)

/// Associated scope.
@_spi(Experimental) public var scope: ScopeSyntax? {
@_spi(Experimental) public var scope: ScopeSyntax {
switch self {
case .fromScope(let scopeSyntax, _):
return scopeSyntax
case .fromFileScope(let fileScopeSyntax, _):
return fileScopeSyntax
case .lookInMembers(let lookInMemb):
return lookInMemb
}
}

Expand All @@ -34,6 +38,8 @@ import SwiftSyntax
switch self {
case .fromScope(_, let names), .fromFileScope(_, let names):
return names
case .lookInMembers(_):
return []
}
}

Expand All @@ -46,4 +52,48 @@ import SwiftSyntax
return .fromScope(scope, withNames: names)
}
}

/// Debug description of this lookup name.
@_spi(Experimental) public var debugDescription: String {
var description =
resultKindDebugName + ": " + scope.scopeDebugDescription

switch self {
case .lookInMembers:
break
default:
if !names.isEmpty {
description += "\n"
}
}

for (index, name) in names.enumerated() {
if index + 1 == names.count {
description += "`-" + name.debugDescription
} else {
description += "|-" + name.debugDescription + "\n"
}
}

return description
}

/// Debug name of this result kind.
private var resultKindDebugName: String {
switch self {
case .fromScope:
return "fromScope"
case .fromFileScope:
return "fromFileScope"
case .lookInMembers:
return "lookInMembers"
}
}
}

@_spi(Experimental) extension [LookupResult] {
/// Debug description this array of lookup results.
@_spi(Experimental) public var debugDescription: String {
return self.map(\.debugDescription).joined(separator: "\n")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@

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
}
protocol CanInterleaveResultsLaterScopeSyntax: ScopeSyntax {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a comment explaining what it means to interleave results later?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! I added documentation comments to the definition of lookupWithInterleavedResults function and it's concrete implementations.

func lookupWithInterleavedResults(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig,
resultsToInterleave: [LookupResult]
) -> [LookupResult]
}
52 changes: 52 additions & 0 deletions Sources/SwiftLexicalLookup/Scopes/FunctionScopeSyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//===----------------------------------------------------------------------===//
//
// 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 FunctionScopeSyntax: DeclSyntaxProtocol, WithGenericParametersScopeSyntax {
var signature: FunctionSignatureSyntax { get }
}

extension FunctionScopeSyntax {
/// Function parameters introduced by this function's signature.
@_spi(Experimental) public var introducedNames: [LookupName] {
signature.parameterClause.parameters.flatMap { parameter in
LookupName.getNames(from: parameter)
} + (parentScope?.is(MemberBlockSyntax.self) ?? false ? [.implicit(.self(self))] : [])
}

/// Lookup results from this function scope.
/// Routes to generic parameter clause scope if exists.
@_spi(Experimental) public func lookup(
_ identifier: Identifier?,
at lookUpPosition: AbsolutePosition,
with config: LookupConfig
) -> [LookupResult] {
var thisScopeResults: [LookupResult] = []

if !signature.range.contains(lookUpPosition) {
thisScopeResults = defaultLookupImplementation(
identifier,
at: position,
with: config,
propagateToParent: false
)
}

return thisScopeResults
+ lookupThroughGenericParameterScope(
identifier,
at: lookUpPosition,
with: config
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SwiftSyntax
/// futher lookup to its `WithGenericParametersScopeSyntax`
/// parent scope's parent scope (i.e. on return, bypasses names
/// introduced by its parent).
@_spi(Experimental) public protocol GenericParameterScopeSyntax: ScopeSyntax {}
protocol GenericParameterScopeSyntax: ScopeSyntax {}

@_spi(Experimental) extension GenericParameterScopeSyntax {
/// Returns names matching lookup and bypasses
Expand Down Expand Up @@ -83,7 +83,7 @@ import SwiftSyntax
if let parentScope = Syntax(parentScope).asProtocol(SyntaxProtocol.self)
as? WithGenericParametersScopeSyntax
{
return parentScope.lookupInParent(identifier, at: lookUpPosition, with: config)
return parentScope.returningLookupFromGenericParameterScope(identifier, at: lookUpPosition, with: config)
} else {
return lookupInParent(identifier, at: lookUpPosition, with: config)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//===----------------------------------------------------------------------===//
//
// 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 LookInMembersScopeSyntax: ScopeSyntax {
/// Position used for member lookup.
var lookupMembersPosition: AbsolutePosition { get }
}
Loading