-
Notifications
You must be signed in to change notification settings - Fork 440
[SwiftLexicalScopes][GSoC] Add simple scope queries and initial name lookup API structure #2696
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
Changes from 7 commits
9f2fab0
d4fb767
211cb6f
a00c922
db7fd99
74ddcc4
b85dced
8dddaa3
0dc27c9
0059f2f
08297ae
b4a9b3f
67e2608
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
ahoppen marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
import SwiftSyntax | ||
|
||
extension SyntaxProtocol { | ||
/// Given syntax node position, returns all available labeled statements. | ||
@_spi(Compiler) @_spi(Testing) public func lookupLabeledStmts() -> [LabeledStmtSyntax] { | ||
guard let scope else { return [] } | ||
return scope.lookupLabeledStmts(at: self) | ||
} | ||
|
||
/// Given syntax node position, returns the current switch case and it's fallthrough destination. | ||
@_spi(Compiler) @_spi(Testing) public func lookupFallthroughSourceAndDest() | ||
-> (source: SwitchCaseSyntax?, destination: SwitchCaseSyntax?) | ||
{ | ||
guard let scope else { return (nil, nil) } | ||
return scope.lookupFallthroughSourceAndDestination(at: self) | ||
} | ||
|
||
/// Given syntax node position, returns the closest ancestor catch node. | ||
@_spi(Compiler) @_spi(Testing) public func lookupCatchNode() -> Syntax? { | ||
guard let scope else { return nil } | ||
return scope.lookupCatchNode(at: Syntax(self)) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
import SwiftSyntax | ||
|
||
extension SyntaxProtocol { | ||
/// Scope at the syntax node. Could be inherited from parent or introduced at the node. | ||
var scope: Scope? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This property is effectively always returning a |
||
switch self.syntaxNodeType { | ||
case is SourceFileSyntax.Type: | ||
FileScope(syntax: self) | ||
default: | ||
parent?.scope | ||
} | ||
} | ||
} | ||
|
||
/// Provide common functionality for specialized scope implementatations. | ||
protocol Scope { | ||
MAJKFL marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// The parent of this scope. | ||
var parent: Scope? { get } | ||
|
||
/// Syntax node that introduces this protocol. | ||
var sourceSyntax: SyntaxProtocol { get } | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It was a leftover from the original API structure I wanted to implement. I moved now all the query methods to |
||
|
||
extension Scope { | ||
/// Recursively walks up syntax tree and finds the closest scope other than this scope. | ||
func getParentScope(forSyntax syntax: SyntaxProtocol?) -> Scope? { | ||
if let lookedUpScope = syntax?.scope, lookedUpScope.sourceSyntax.id == syntax?.id { | ||
return getParentScope(forSyntax: sourceSyntax.parent) | ||
} else { | ||
return syntax?.scope | ||
} | ||
} | ||
|
||
// MARK: - lookupLabeledStmts | ||
|
||
/// Given syntax node position, returns all available labeled statements. | ||
func lookupLabeledStmts(at syntax: SyntaxProtocol) -> [LabeledStmtSyntax] { | ||
return walkParentTreeUpToFunctionBoundary( | ||
at: syntax.parent, | ||
collect: LabeledStmtSyntax.self | ||
) | ||
} | ||
|
||
// MARK: - lookupFallthroughSourceAndDest | ||
|
||
/// Given syntax node position, returns the current switch case and it's fallthrough destination. | ||
func lookupFallthroughSourceAndDestination(at syntax: SyntaxProtocol) -> (SwitchCaseSyntax?, SwitchCaseSyntax?) { | ||
guard | ||
let originalSwitchCase = walkParentTreeUpToFunctionBoundary( | ||
at: Syntax(syntax), | ||
collect: SwitchCaseSyntax.self | ||
) | ||
else { | ||
return (nil, nil) | ||
} | ||
|
||
let nextSwitchCase = lookupNextSwitchCase(at: originalSwitchCase) | ||
|
||
return (originalSwitchCase, nextSwitchCase) | ||
} | ||
|
||
/// Given a switch case, returns the case that follows according to the parent. | ||
private func lookupNextSwitchCase(at switchCaseSyntax: SwitchCaseSyntax) -> SwitchCaseSyntax? { | ||
guard let switchCaseListSyntax = switchCaseSyntax.parent?.as(SwitchCaseListSyntax.self) else { return nil } | ||
|
||
var visitedOriginalCase = false | ||
|
||
for child in switchCaseListSyntax.children(viewMode: .sourceAccurate) { | ||
if let thisCase = child.as(SwitchCaseSyntax.self) { | ||
if thisCase.id == switchCaseSyntax.id { | ||
visitedOriginalCase = true | ||
} else if visitedOriginalCase { | ||
return thisCase | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this currently doesn’t handle There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it would be possible to e.g. evaluate the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @DougGregor has a PR up to evaluate Let’s wait until Doug is back to discuss how we want to approach this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had in fact not considered how to handle So, it seems like we will need something like #1816 to have any chance of making |
||
} | ||
|
||
return nil | ||
} | ||
|
||
// MARK: - lookupCatchNode | ||
|
||
/// Given syntax node position, returns the closest ancestor catch node. | ||
func lookupCatchNode(at syntax: Syntax) -> Syntax? { | ||
return lookupCatchNodeHelper(at: syntax, traversedCatchClause: false) | ||
} | ||
|
||
/// Given syntax node location, finds where an error could be caught. If set to `true`, `traverseCatchClause`lookup will skip the next do statement. | ||
private func lookupCatchNodeHelper(at syntax: Syntax?, traversedCatchClause: Bool) -> Syntax? { | ||
MAJKFL marked this conversation as resolved.
Show resolved
Hide resolved
|
||
guard let syntax else { return nil } | ||
|
||
switch syntax.as(SyntaxEnum.self) { | ||
case .doStmt: | ||
if traversedCatchClause { | ||
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: false) | ||
} else { | ||
return syntax | ||
} | ||
case .catchClause: | ||
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: true) | ||
case .tryExpr(let tryExpr): | ||
if tryExpr.questionOrExclamationMark != nil { | ||
return syntax | ||
} else { | ||
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause) | ||
} | ||
case .functionDecl, .accessorDecl, .initializerDecl: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also deinits and closures. |
||
return syntax | ||
default: | ||
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause) | ||
} | ||
} | ||
|
||
/// Callect the first syntax node matching the collection type up to a function boundary. | ||
func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>( | ||
at syntax: Syntax?, | ||
collect: T.Type | ||
) -> T? { | ||
walkParentTreeUpToFunctionBoundary(at: syntax, collect: collect, stopWithFirstMatch: true).first | ||
} | ||
|
||
/// Callect syntax nodes matching the collection type up to a function boundary. | ||
func walkParentTreeUpToFunctionBoundary<T: SyntaxProtocol>( | ||
at syntax: Syntax?, | ||
collect: T.Type, | ||
stopWithFirstMatch: Bool = false | ||
) -> [T] { | ||
walkParentTree( | ||
upTo: [ | ||
MemberBlockSyntax.self, | ||
FunctionDeclSyntax.self, | ||
InitializerDeclSyntax.self, | ||
DeinitializerDeclSyntax.self, | ||
AccessorDeclSyntax.self, | ||
ClosureExprSyntax.self, | ||
], | ||
at: syntax, | ||
collect: collect, | ||
stopWithFirstMatch: stopWithFirstMatch | ||
) | ||
} | ||
|
||
/// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes. | ||
func walkParentTree<T: SyntaxProtocol>( | ||
upTo stopAt: [SyntaxProtocol.Type], | ||
at syntax: Syntax?, | ||
collect: T.Type, | ||
stopWithFirstMatch: Bool = false | ||
) -> [T] { | ||
guard let syntax, !stopAt.contains(where: { syntax.is($0) }) else { return [] } | ||
if let matchedSyntax = syntax.as(T.self) { | ||
if stopWithFirstMatch { | ||
return [matchedSyntax] | ||
} else { | ||
return [matchedSyntax] | ||
+ walkParentTree( | ||
upTo: stopAt, | ||
at: syntax.parent, | ||
collect: collect, | ||
stopWithFirstMatch: stopWithFirstMatch | ||
) | ||
} | ||
} else { | ||
return walkParentTree( | ||
upTo: stopAt, | ||
at: syntax.parent, | ||
collect: collect, | ||
stopWithFirstMatch: stopWithFirstMatch | ||
) | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// 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 | ||
import SwiftSyntax | ||
|
||
class FileScope: Scope { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
var parent: Scope? = nil | ||
|
||
var sourceSyntax: SyntaxProtocol | ||
|
||
init(syntax: SyntaxProtocol) { | ||
self.sourceSyntax = syntax | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should talk about the name here a little more. I think I suggested
SwiftLexicalScopes
initially, but given that the "scope" part is actually getting somewhat downplayed... should we call thisSwiftLexicalLookup
? All of these operations are, essentially, lexical lookups for various kinds of things---lookups for declarations, labeled statements, where afallthrough
ends up---and most will have "lookup" in their names.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I also like the idea. I changed it to
SwiftLexicalLookup
. It’s more broad and not limiting the library to just scope lookup could pave the way for including a broader range of APIs, like thebreak
andcontinue
lookup you’ve mentioned yesterday that is not necessarily part ofASTScope
as far as I remember. Maybe it would be a good idea to use this library in the long run to consolidate this sort of logic in one codebase?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think this is the right place to consolidate all of the lexical lookup logic over time.