Swift用のLinterを自作している話
この記事は琉球大学(知能情報)アドベントカレンダー Advent Calendar 2024の記事です。
作ったもの
OSSとしてGitHubで公開してます。
きっかけ
SwiftのLinterは、SwiftLintほぼ一択なのが現状です。SwiftLintに独自のルールを追加したい場合、Swift Custom RulesとRegex Custom Rulesの2種類の方法があります。
Swift Custom Rules
Swift Syntaxを用いてルールを作れます。Swiftで表現できるため複雑なルールを実現できますが、Bazelでビルドする必要があります。
Regex Custom Rules
正規表現を用いてルールを作れます。 .swiftlint.yml
に書き加えるだけで手軽にルールを追加できますが、任意の正規表現にマッチした箇所に警告を出す、という仕組みなので複雑なルールを実現するのは難しいです。
ある程度複雑なルールを実現するにはSwift Custom Rules一択ですが、Bazelでビルドする必要がある都合上、環境構築がやや面倒です。個人用や社内独自のコーディングルール用にカスタマイズすることを考えると、ルールはサクッと追加できるのが望ましいため、いっそのこと拡張しやすいようなLinter自体を自作することにしました。
Linterを作る
Linterと呼べるようなものを作るには、ざっくり以下のような機能が必要です。
- CLIの引数として外部から値を設定できる
- ソースコードの構文解析
- エディタ上で任意の行に警告を表示する
これらを実現するためにはどうすれば良いかをざっくり解説します。
CLIの引数として外部から値を設定
ArgumentParserを用いることで簡単に引数を受け取るCLIを作ることができます。
ArgumentParserはプロパティラッパーを使って機能を実現しています。引数にしたいプロパティに対して@Argument
, @Flag
, @Option
といったプロパティラッパーを付与することでコマンドとしてビルドした際に引数として受け取れる値になります。
import ArgumentParser
@main
struct FooCommand: ParsableCommand {
@Flag(help: "Include a counter with each repetition.")
var includeCounter = false
@Option(name: .shortAndLong, help: "The number of times to repeat 'phrase'.")
var count: Int? = nil
@Argument(help: "The phrase to repeat.")
var phrase: String
}
ソースコードの構文解析
Linterとして動作するためには、コマンド実行時にLint対象のソースファイルを構文解析する必要があります。
これはSwiftSyntaxを用いることで簡単に実現できます。本記事ではかなり簡単に解説するので、SwiftSyntaxについての詳しい解説を聞きたい人は以下の資料・動画などを見ると良いかなと思います。
https://fortee.jp/iosdc-japan-2023/proposal/da53c5df-a243-46b1-a3e2-c35dd9fdda11
SwiftSyntaxで何ができるかを簡単にまとめると、以下の2つです。
- ソースコードのオブジェクト化
- ASTの探索
ソースコードのオブジェクト化
ソースコードはAST(抽象構文木)というものに変換できます。Swift Syntaxは、ソースコードを元にして、ASTを表現したオブジェクトを生成できます。HTMLを元にDOMを生成するのと同じです。どのような構造のオブジェクトが生成されるのかはSwift AST Explorerを使うと分かりやすいと思います。
ASTの探索
SyntaxVisitor
の visit()
を用いることで、ASTの各ノードを順番に探索することができます。引数の node
の型で任意の種類のノードのみ探索できます。
final class MySyntaxVisitor: SyntaxVisitor {
override func visit(_ node: 探索対象のノード型) -> SyntaxVisitorContinueKind {
// 引数のnode型のノードに到達した場合にしたい処理を書く
// 子ノードの探索をする場合
return .visitChildren
// 子ノードの探索をしない場合
// return .skipChildren
}
}
// 探索開始
MySyntaxVisitor(viewMode: .all).walk(探索対象のSourceFileSyntax)
SyntaxVisitor
の代わりにSyntaxRewriterを使うことで、探索しているASTの書き換えを行うことができます。
final class MySyntaxRewriter: SyntaxRewriter {
override func visit(_ node: 書き換え対象のノード型) -> 書き換え対象のノード型 {
// ~nodeを加工する~
return super.visit(加工後のnode)
}
}
// 書き換え
let rewrittenAST = MySyntaxRewriter().visit(packageSwift)
エディタ上で任意の行に警告を表示する
Linterというツールの性質上、エディタ上にルールに違反している箇所を表示できると便利です。Xcode上で特定の行に警告を表示するのは意外と簡単で、以下のフォーマットでprintするようにするだけです。
// 警告
print("{/path/to/your/file}:{line}:{column}: warning: foo warning message")
// エラー
print("{/path/to/your/file}:{line}:{column}: error: foo warning message")
ファイルパス:行:文字: warning: 出力したいメッセージ
で警告を出せる感じですね。 warning
を error
に変えると赤いエラー表示として出力されます。
まとめ
かなりざっくりとした説明になってしまいましたが、Linterの基盤となる機能の解説でした。今後、余裕ができればconfigファイルの読み込みや、ルールの抽象化、 disable:next
などによる部分的な無効化・有効化などについても解説できればと思います。
それでは、よきSwiftライフを。