Skip to content

[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

Merged
merged 13 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let package = Package(
.library(name: "SwiftSyntaxMacros", targets: ["SwiftSyntaxMacros"]),
.library(name: "SwiftSyntaxMacroExpansion", targets: ["SwiftSyntaxMacroExpansion"]),
.library(name: "SwiftSyntaxMacrosTestSupport", targets: ["SwiftSyntaxMacrosTestSupport"]),
.library(name: "SwiftLexicalLookup", targets: ["SwiftLexicalLookup"]),
.library(
name: "SwiftSyntaxMacrosGenericTestSupport",
targets: ["SwiftSyntaxMacrosGenericTestSupport"]
Expand Down Expand Up @@ -243,6 +244,18 @@ let package = Package(
]
),

// MARK: SwiftLexicalLookup

.target(
name: "SwiftLexicalLookup",
dependencies: ["SwiftSyntax"]
),

.testTarget(
name: "SwiftLexicalLookupTest",
dependencies: ["_SwiftSyntaxTestSupport", "SwiftLexicalLookup"]
),

// MARK: SwiftSyntaxMacrosGenericTestSupport

.target(
Expand Down
14 changes: 0 additions & 14 deletions Sources/SwiftBasicFormat/BasicFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -676,17 +676,3 @@ fileprivate extension TokenSyntax {
}
}
}

fileprivate extension SyntaxProtocol {
/// Returns this node or the first ancestor that satisfies `condition`.
func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
var walk: Syntax? = Syntax(self)
while let unwrappedParent = walk {
if let mapped = map(unwrappedParent) {
return mapped
}
walk = unwrappedParent.parent
}
return nil
}
}
176 changes: 176 additions & 0 deletions Sources/SwiftLexicalLookup/SimpleLookupQueries.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
//===----------------------------------------------------------------------===//
//
// 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] {
return 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?)
{
return lookupFallthroughSourceAndDestination(at: self)
}

/// Given syntax node position, returns the closest ancestor catch node.
@_spi(Compiler) @_spi(Testing) public func lookupCatchNode() -> Syntax? {
return lookupCatchNodeHelper(at: Syntax(self), traversedCatchClause: false)
}

// MARK: - lookupLabeledStmts

/// Given syntax node position, returns all available labeled statements.
private 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.
private 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
}
}
}

return nil
}

// MARK: - lookupCatchNode

/// 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? {
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, .deinitializerDecl, .closureExpr:
return syntax
case .exprList(let exprList):
if let tryExpr = exprList.first?.as(TryExprSyntax.self), tryExpr.questionOrExclamationMark != nil {
return Syntax(tryExpr)
}
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
default:
return lookupCatchNodeHelper(at: syntax.parent, traversedCatchClause: traversedCatchClause)
}
}

// MARK: - walkParentTree helper methods

/// Callect the first syntax node matching the collection type up to a function boundary.
private 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.
private 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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Callect syntax nodes matching the collection type up until encountering one of the specified syntax nodes.
/// Collect syntax nodes matching the collection type up until encountering one of the specified syntax nodes.

private func 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
)
}
}
}
12 changes: 0 additions & 12 deletions Sources/SwiftParserDiagnostics/SyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,6 @@ extension SyntaxProtocol {
}
}

/// Returns this node or the first ancestor that satisfies `condition`.
func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
var walk: Syntax? = Syntax(self)
while let unwrappedParent = walk {
if let mapped = map(unwrappedParent) {
return mapped
}
walk = unwrappedParent.parent
}
return nil
}

/// Returns `true` if the next token's leading trivia should be made leading trivia
/// of this mode, when it is switched from being missing to present.
var shouldBeInsertedAfterNextTokenTrivia: Bool {
Expand Down
14 changes: 13 additions & 1 deletion Sources/SwiftSyntax/SyntaxProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ extension SyntaxProtocol {
}
}

// MARK: Children / parent
// MARK: Children / parent / ancestor

extension SyntaxProtocol {
/// A sequence over the children of this node.
Expand Down Expand Up @@ -238,6 +238,18 @@ extension SyntaxProtocol {
public var previousToken: TokenSyntax? {
return self.previousToken(viewMode: .sourceAccurate)
}

/// Returns this node or the first ancestor that satisfies `condition`.
public func ancestorOrSelf<T>(mapping map: (Syntax) -> T?) -> T? {
var walk: Syntax? = Syntax(self)
while let unwrappedParent = walk {
if let mapped = map(unwrappedParent) {
return mapped
}
walk = unwrappedParent.parent
}
return nil
}
}

// MARK: Accessing tokens
Expand Down
83 changes: 83 additions & 0 deletions Tests/SwiftLexicalLookupTest/Assertions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import Foundation
@_spi(Testing) import SwiftLexicalLookup
import SwiftParser
import SwiftSyntax
import XCTest
import _SwiftSyntaxTestSupport

/// Parse `source` and check if the method passed as `methodUnderTest` produces the same results as indicated in `expected`.
///
/// The `methodUnderTest` provides test inputs taken from the `expected` dictionary. The closure should return result produced by the tested method as an array with the same ordering.
///
/// - Parameters:
/// - methodUnderTest: Closure with the tested method. Provides test argument from `expected` to the tested function. Should return method result as an array.
/// - expected: A dictionary with parameter markers as keys and expected results as marker arrays ordered as returned by the test method.
func assertLexicalScopeQuery(
source: String,
methodUnderTest: (SyntaxProtocol) -> ([SyntaxProtocol?]),
expected: [String: [String?]]
) {
// Extract markers
let (markerDict, textWithoutMarkers) = extractMarkers(source)

// Parse the test source
var parser = Parser(textWithoutMarkers)
let sourceFileSyntax = SourceFileSyntax.parse(from: &parser)

// Iterate through the expected results
for (marker, expectedMarkers) in expected {
// Extract a test argument
guard let position = markerDict[marker],
let testArgument = sourceFileSyntax.token(at: AbsolutePosition(utf8Offset: position))
else {
XCTFail("Could not find token at location \(marker)")
continue
}

// Execute the tested method
let result = methodUnderTest(testArgument)

// Extract the expected results for the test argument
let expectedValues: [SyntaxProtocol?] = expectedMarkers.map { expectedMarker in
guard let expectedMarker else { return nil }

guard let expectedPosition = markerDict[expectedMarker],
let expectedToken = sourceFileSyntax.token(at: AbsolutePosition(utf8Offset: expectedPosition))
else {
XCTFail("Could not find token at location \(marker)")
return nil
}

return expectedToken
}

// Compare number of actual results to the number of expected results
if result.count != expectedValues.count {
XCTFail(
"For marker \(marker), actual number of elements: \(result.count) doesn't match the expected: \(expectedValues.count)"
)
}

// Assert validity of the output
for (actual, expected) in zip(result, expectedValues) {
if actual == nil && expected == nil { continue }

XCTAssert(
actual?.firstToken(viewMode: .sourceAccurate)?.id == expected?.id,
"For marker \(marker), actual result: \(actual?.firstToken(viewMode: .sourceAccurate) ?? "nil") doesn't match expected value: \(expected?.firstToken(viewMode: .sourceAccurate) ?? "nil")"
)
}
}
}
Loading