SwiftSyntax rewriter code insertion

Hi Swift Community,

I would like to get from:

var body: some View {
    VStack {
        Button(action: {
        call2()
        })
        {
            Text("some text")
        }
    }
}

to:

var body: some View {
    VStack {
        Button(action: {
        call1()
        call2()
        })
        {
            Text("some text")
        }
    }
}

By overriding func visit(_ node: IdentifierExprSyntax) & friends, it's easy to find the target nodes but I'm not really fond of the idea of building a state machine that tracks "Button", "action", "colon", " leftBrace" and then replace that node.
Is there a better way to do this sort of code insertion?

Thank's in advance for the responses :slight_smile:

It looks to me like the problem you are trying to solve is solved for
C programs by applying a "semantic patch," http://coccinelle.lip6.fr.
Maybe someone, someday will be inspired to write a semantic-patching
tool for Swift.

Dave

Can you override func visit(_ node: ExprListSyntax) -> Syntax, iterate its entries checking for a FunctionCallExprSyntax that matches call2(), and then if it is not preceded by a FunctionCallExprSyntax matching call1(), return a copy (of the ExprListSyntax) inserting(syntax:at:) with a call1() node of your own?

I guess it depends how unique the real names of call1() and call2() are. The less unique, the more of the context you would need to verify.

Actually call1() and call2() can be anything, what I wanted to do is find the action block (if present) and then insert some code. The only way I was able to do this (until now) was to override visit(_ node: ClosureExprSyntax) -> ExprSyntax and then look up the parent chain if they match a certain pattern.

I think I understand better know. Button(action: { is really what you are looking for, and you want to insert a call1() after it no matter what?

Yes, that is what you have to do. Your only real choices are whether to catch narrow nodes and check their ancestors, or to catch wide nodes and check their descendants. In this case the narrowest node is ExprListSyntax—the part inside the braces. And the widest relevant node is FunctionCallExprSyntax—referring to all of Button(...). Note that you can never go any narrower than you plan to replace.

Checking descendants is probably easier because nodes have named, typed child properties whereas checking parents requires foreknowledge of the type in order to downcast.

But which method is faster depends on which node type is likely to be more common; if speed is important, aim to be able short circuit early most of the time.

Thanks for the suggestions I will also try the top down approach. I was also a bit weary about all the down casting that I had to do in the bottom-up version.

One question still, when going top down starting in func visit(_ node: FunctionCallExprSyntax) and identifying all the right children, how do I replace the statements in the closure?

With the bottom up approach I'm already in the right context to return a new statements collection.

I may as well just demonstrate the whole thing.

It looks like you are using SwiftSyntax from very recent snapshot, which is necessary to parse the new language features in recent snapshots. I slightly adjusted your source example below so that it is compatible with the latest stable release. To use it with newer snapshots, you will likely need to tweak it in slightly, but exactly which changes you need depend on exactly which snapshot. I’ll leave that for you. The following uses Swift 5.0.1 and SwiftSyntax 0.50000.0.

import Foundation
import SwiftSyntax

public class Rewriter : SyntaxRewriter {

    public override func visit(_ token: TokenSyntax) -> Syntax {
        // A helpful way of learning how the tree fits together:
        if token.text == "call2" {
            var ancestor: Syntax? = token
            while ancestor != nil {
                print(type(of: ancestor!))
                print(ancestor!)
                ancestor = ancestor?.parent
            }
        }
        return token
    }

    public override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
        var modified = node

        // Here we make sure this actually is a node we want to mess with.
        if let called = modified.calledExpression as? IdentifierExprSyntax,
            called.identifier.text == "Button",
            let firstArgument = modified.argumentList.first,
            firstArgument.label?.text == "action",
            let closure = firstArgument.expression as? ClosureExprSyntax {

            // Here we build our addition.
            // Side note: Looking for the factory’s “make...()” functions with autocomplete
            // helps learn what all the children of a particular node are.
            let newCall = SyntaxFactory.makeFunctionCallExpr(
                calledExpression: SyntaxFactory.makeIdentifierExpr(
                    identifier: SyntaxFactory.makeIdentifier("call1")
                        .withLeadingTrivia([.newlines(1), .spaces(12)]),
                    declNameArguments: nil),
                leftParen: SyntaxFactory.makeLeftParenToken(),
                argumentList: SyntaxFactory.makeFunctionCallArgumentList([]),
                rightParen: SyntaxFactory.makeRightParenToken(),
                trailingClosure: nil)
            let newStatement = SyntaxFactory.makeCodeBlockItem(
                item: newCall,
                semicolon: nil,
                errorTokens: nil)

            // Here we slot it in by replacing the current node’s children.
            let modifiedClosure = closure.withStatements(closure.statements.prepending(newStatement))
            let modifiedFirstArgument = firstArgument.withExpression(modifiedClosure)
            modified = modified.withArgumentList(
                modified.argumentList.removingFirst().prepending(modifiedFirstArgument))
        }

        // Here we continue rewriting child nodes in case something is nested.
        // (Which is not always necessary.)
        modified = modified.withCalledExpression(Rewriter().visit(modified.calledExpression) as? ExprSyntax)
        modified = modified.withLeftParen(modified.leftParen.map({ Rewriter().visit($0) }) as? TokenSyntax)
        modified = modified.withArgumentList(Rewriter().visit(modified.argumentList) as? FunctionCallArgumentListSyntax)
        modified = modified.withRightParen(modified.rightParen.map({ Rewriter().visit($0) }) as? TokenSyntax)
        modified = modified.withTrailingClosure(modified.trailingClosure.map({ Rewriter().visit($0) }) as? ClosureExprSyntax)
        return modified
    }
}

/// This is a helper function to transform source as String instances.
public func rewrite(_ source: String) throws -> String {
    // The “master” branch of SwiftSyntax can parse source strings directly,
    // but the current release still requires a file.
    let temporary = URL(fileURLWithPath: NSTemporaryDirectory())
        .appendingPathComponent(UUID().uuidString + ".swift")
    defer { try? FileManager.default.removeItem(at: temporary) }

    let directory = temporary.deletingLastPathComponent()
    try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
    try source.data(using: .utf8)?.write(to: temporary, options: [.atomic])

    let sourceSyntax = try SyntaxTreeParser.parse(temporary)
    let rewrittenSyntax = Rewriter().visit(sourceSyntax)

    var rewritten = ""
    rewrittenSyntax.write(to: &rewritten)
    return rewritten
}
import XCTest
import Rewriter // The module containing the above code.

final class RewriterTests: XCTestCase {

    func testRewriter() {
        XCTAssertEqual(try rewrite("""
        var body: View {
            return VStack {
                return (Button(action: {
                    call2()
                }),
                {
                    return Text("some text")
                })
            }
        }
        """), """
        var body: View {
            return VStack {
                return (Button(action: {
                    call1()
                    call2()
                }),
                {
                    return Text("some text")
                })
            }
        }
        """)
    }
}
2 Likes

Thank you very much for the example. A lot of thing are a lot more clear now. :slight_smile: