How to recursively iterate over Swift Syntax with SwiftSyntax library?

Viewed 163

I would like to iterate in my code over the Swift AST like this, finding the struct keyword.

private func recursion(node: Syntax) -> String {
    for child in node.children {
        if let tokenKind = (child as? TokenSyntax)?.tokenKind, tokenKind == .structKeyword {
       // if type(of: child) == StructDeclSyntax.self {
            print ("yeah")
        }

        recursion(node: child)
    }
}

let input = """
        public struct cmd_deleteEdge<E: VEdge> : EdgeCommand {
            public var keyEquivalent = KeyEquivalent.none
            public let title = "Delete Edge"
            public let id = "deleteEdge"
            public let toolTip = "Delete selected Edge"
            public let icon = Icon.delete
            
            //receivers
            public let edge: E
            
            public init(edge: E) {
                self.edge = edge
            }
            
            public func execute() throws -> ActionResult {
                edge.deleteYourself()
                return .success("deleted edge")
            }
        }
"""

public func convert(structText: String) throws -> String  {
    let sourceFile = try SyntaxParser.parse(source: structText)
    let result = recursion(node: Syntax(sourceFile))
    return result
}


try convert(structText: input)

It just simply doesn't work, I never reach the "Yeah" (which means I cannot do anything useful during the recursion).

I find this library very confusing. Would anyone have a good UML diagram explaining how it really works?

Before you tell me, yes I know I could use a Visitor, but I want to understand how it works by myself.

2 Answers

You can use SyntaxProtocol for iterating all items in AST and then use its _syntaxNode public property to make a target syntax, e.g.:

import SwiftSyntax
import SwiftSyntaxParser

func recursion(node: SyntaxProtocol) {
    if let decl = StructDeclSyntax(node._syntaxNode) {
        print(decl.identifier)
    }
    node.children.forEach { recursion(node: $0) }
}

let code = """
struct A {}

class Some {
    struct B {}
}

func foo() {
    struct C {}
}
"""

let sourceFile = try SyntaxParser.parse(source: code)
recursion(node: sourceFile)

Outputs:

A 
B 
C

NOTE: It is not recommended to retrieve _syntaxNode property directly and you can use Syntax(fromProtocol: node) instead.

SyntaxVisitor

But the best approach is using Visitor pattern with SyntaxVisitor class to avoid recursion issues for large and complex files:

class Visitor: SyntaxVisitor {
    var structs = [StructDeclSyntax]()
    
    init(source: String) throws {
        super.init()
        let sourceFile = try SyntaxParser.parse(source: source)
        walk(sourceFile)
    }
    
    // MARK: - SyntaxVisitor
    
    override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
        structs.append(node)
        return .skipChildren
    }
}

let visitor = try Visitor(source: code)
visitor.structs.forEach {
    print($0.identifier)
}

I found it after trial & error and reviewing of the API.

private func recursion(node: Syntax) -> String {
    for child in node.children {
        if let token = TokenSyntax(child),  token.tokenKind == .structKeyword {
            print ("yeah")
        }
        recursion(node: child)
    }
    return node.description
}

This approach to identify the kind of the token works, and the print statement will be reached. Again, I do wonder how the class diagram for SwiftSyntax would look like.

Related