swift-syntaxを用いて、簡単なコマンドラインツールを作ってみる
本記事は、SwiftWednesday Advent Calendar 2023 23日目の記事です。
本記事では、swift-syntaxを用いて、簡単なコマンドラインツールを作成する方法について解説します。
swift-syntaxとは
The swift-syntax package is a set of libraries that work on a source-accurate tree representation of Swift source code, called the SwiftSyntax tree. The SwiftSyntax tree forms the backbone of Swift’s macro system – the macro expansion nodes are represented as SwiftSyntax nodes and a macro generates a SwiftSyntax tree to be inserted into the source file.
https://github.com/apple/swift-syntax
swift-syntaxは、Swiftのソースコードの構文解析や、Swiftのソースコードの生成、変換を行うことができるライブラリ群です。
Swiftのソースコードを解析し抽象構文木を構築します。
realm/SwiftLintやapple/swift-formatなどで利用されています。
抽象構文木(AST : Abstract Syntax Tree)とは
抽象構文木(以下、AST)とは、プログラムの構文の各要素をノードとする木構造のデータ構造です。これはプログラムの構造を抽象的に表現したものであり、コンパイラや静的解析器などで利用されます。
例えば、struct Person
をASTに変換すると、以下のようになります。
struct Person {
var name = "hoge"
}
構造体の宣言、変数の宣言など、それぞれの要素がノードとして表現されます。全体の構造体はStructDecl
というノードで表現され、その中にMemberDeclBlock
というノードが含まれます。このMemberDeclBlock
は、構造体の中にあるメンバの宣言を表しています。
また、name
という変数は、VariableDecl
というノードで表現されます。このノードは変数宣言を表しています。
実装
実際に、swift-syntaxを用いて、コマンドラインツールを作成してみます。
今回は、関数の呼び出し引数にSwift
という文字列が含まれているかどうかを検出するコマンドラインツールを作成します。
はじめに、以下のコマンドで、find-swift
という名前のPackageを作成します。
❯ swift package init --type=executable --name=find-swift
Creating executable package: find-swift
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/main.swift
次に、swift-syntaxを追加します。
import PackageDescription
let package = Package(
name: "no-print",
platforms: [
.macOS(.v10_15)
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax", from: "509.0.2")
],
targets: [
.executableTarget(
name: "no-print",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftParser", package: "swift-syntax")
],
path: "Sources"),
]
)
swift-syntaxの追加が完了したら、Sources/main.swift
で処理の実装を始めます。
はじめに、swift-syntaxライブラリから、SwiftSyntax
とSwiftParser
をimportします。
次に、今回解析を行う対象となる適当なソースコードを文字列で定義していきます。
import SwiftSyntax
import SwiftParser
let source = """
test(100, true, "Swift", false)
"""
func main() throws {
let sourceFile = Parser.parse(source: source)
let visitor = FindSwiftSyntaxVisitor(viewMode: .fixedUp) // FindSwiftSyntaxVisitorについては、後ほど定義します。
visitor.walk(sourceFile) // 構文木を走査する
}
実装を進めていく上で、Swift AST Explorerを活用しましょう。
Swift AST Explorerとは、SwiftソースコードのASTを視覚化するためのツールです。
各ノードがソースコードのどの部分に対応しているのかを確認したり、各ノードの構成を表示することができます。
Swift AST Explorerを使って、test(100, true, "Swift", false)
のコードを見てみましょう。
すると、以下のような構造になっています。
今回検出したい関数呼び出しのノードは、FunctionCallExprSyntax
であることがわかります。
ここまでの情報をもとに、コードディングを行います。
新しくFindSwiftSyntaxVisitor
クラスを定義し、SyntaxVisitor
クラスを継承するようにします。
class FindSwiftSyntaxVisitor: SyntaxVisitor {
}
SyntaxVisitorクラスは、構文木を走査するためのクラスであり、Visitorパターンで実装されています。
Visitorパターンとは、GoFのデザインパターンの一つです。
Visitorパターンには、オブジェクトの構造とその操作を分離することで、既存のコードを変更せずに新しい動作を追加することができるという特徴があります。
関数呼び出しは、FunctionCallExpr
ノードであるため、型がFunctionCallExprSyntax
であるvisit
関数をオーバーライドします。
SyntaxVisitorがVisitorパターンで実装されているため、ASTの特定のノードに対して独自の動作を簡単に追加することができます。
class FindSwiftSyntaxVisitor: SyntaxVisitor {
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
}
}
次に、関数呼び出しの引数部分に注目します。Swift AST Explorerで見てみると、メンバ変数arguments
で、関数の引数リストLabeledExprList
にアクセスできることがわかります。
次に、LabeledExprList
の要素である、LabeledExpr
に注目します。
LabeledExprSyntax
のメンバ変数expression
で、StringLiteralExprSyntax
にアクセスできることがわかります。
ここで、関数呼び出しの他の引数である 100, true, false について見てみると、IntegerLiterarlExpr
、BooleanLiteralExpr
となっていることがわかります。
今回は、関数呼び出しの引数が文字列である時のみ処理を行いたいため、ノードの型を変換する必要があります。
ノードの型を変換する際には、swift-syntaxで用意されている型変換関数を使用するようにします。
class FindSwiftSyntaxVisitor: SyntaxVisitor {
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
let arguments = node.arguments
arguments.forEach {
let stringLiteral = $0.expression.as(StringLiteralExprSyntax.self) // 型変換する
}
return super.visit(node)
}
}
最後に、segments.trimmedDescription
によって、文字列を取得し、その文字列が"Swift"であるがどうかのチェックを行うことで、検出をすることができます。
以下が、最終的なコードになります。
import SwiftSyntax
import SwiftParser
let source = """
test(100, true, "Swift", false)
"""
class FindSwiftSyntaxVisitor: SyntaxVisitor {
override func visit(_ node: FunctionCallExprSyntax) -> SyntaxVisitorContinueKind {
let arguments = node.arguments
arguments.forEach {
if let stringLiteral = $0.expression.as(StringLiteralExprSyntax.self) {
if stringLiteral.segments.trimmedDescription == "Swift" {
print("found!")
}
}
}
return super.visit(node)
}
}
func main() throws {
let sourceFile = Parser.parse(source: source)
let visitor = FindSwiftSyntaxVisitor(viewMode: .fixedUp)
visitor.walk(sourceFile)
}
try main()
おわりに
本記事では、swift-syntaxを用いて、簡単なコマンドラインツールを作成する方法を解説しました。
今回は、SyntaxRewriter
については触れませんでしたが、SyntaxRewriter
によってコードの生成や変換も可能になります。
実際にswift-syntaxを使用して簡単なコマンドラインツールを作成することで、少しずつですがswift-syntaxについて理解できるようになりました。
この記事が、swift-syntaxに入門する際の手助けとなれば幸いです。
Discussion