Reheat
Link to my portfolio.
Link to my portfolio.

Swift用のLinterを自作している話

この記事は琉球大学(知能情報)アドベントカレンダー Advent Calendar 2024の記事です。

作ったもの

OSSとしてGitHubで公開してます。

きっかけ

SwiftのLinterは、SwiftLintほぼ一択なのが現状です。SwiftLintに独自のルールを追加したい場合、Swift Custom RulesRegex 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つです。

  1. ソースコードのオブジェクト化
  2. ASTの探索

ソースコードのオブジェクト化

ソースコードはAST(抽象構文木)というものに変換できます。Swift Syntaxは、ソースコードを元にして、ASTを表現したオブジェクトを生成できます。HTMLを元にDOMを生成するのと同じです。どのような構造のオブジェクトが生成されるのかはSwift AST Explorerを使うと分かりやすいと思います。

ASTの探索

SyntaxVisitorvisit() を用いることで、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: 出力したいメッセージ で警告を出せる感じですね。 warningerror に変えると赤いエラー表示として出力されます。

まとめ

かなりざっくりとした説明になってしまいましたが、Linterの基盤となる機能の解説でした。今後、余裕ができればconfigファイルの読み込みや、ルールの抽象化、 disable:next などによる部分的な無効化・有効化などについても解説できればと思います。

それでは、よきSwiftライフを。