📝

[入門]SwiftSyntaxでコードを生成する[SwiftSyntaxBuilder]

2023/08/27に公開

[入門]SwiftSyntaxでコードを生成する

SwiftSyntaxでコードを生成する方法を紹介していきます。

レベル別に進めていきます。レベルが高くなるほど安全にコードを生成できるようになっています。

SwiftSyntaxBuilderのテストであるSwiftSyntaxBuilderTestを参考に記事を作成しました。

Swift AST Explorerを使用することで、ソースの構造を確認することができます。

注意事項

SwiftSyntaxはSwiftの進化についていくために、アップデートごとに破壊的変更が入ります。
この記事ではSwift 5.8基準である508.0.1のバージョンを基準に進めていきます。

生成したいコード

以下のコードをSwiftSyntax(SwiftSyntaxBuilder)を使って、コードを組み上げていきます。
レベル1, 2, 3に分けて進めていきます。

import Foundation

class SampleModel {
    private let title: String = "Hello"
}

レベル1

上のようなコードを出力したい場合、下のようにコードの要素を組んでいく必要があります。
レベル1ではコードを組むのではなく、DeclSyntaxで直接指定しているような形になっています。
これでもいいのですが、レベルを上げて抽象度を上げていきます。

最後に人が見やすいようにformatted()しています。

import SwiftSyntax
import SwiftSyntaxBuilder

let source = SourceFileSyntax {
  DeclSyntax("import Foundation")
      
  ClassDeclSyntax(identifier: "SampleModel") {
    DeclSyntax(#"private let title: String = "Hello""#)
  }
}

print(source.formatted())

レベル2

importの部分をImportDeclSyntaxに置き換えます。

import SwiftSyntax
import SwiftSyntaxBuilder

let source = SourceFileSyntax {
  ImportDeclSyntax(
    path: AccessPathSyntax([AccessPathComponentSyntax(name: "Foundation")])
  )
      
  ClassDeclSyntax(identifier: "SampleModel") {
    DeclSyntax(#"private let title: String = "Hello""#)
  }
}
    
print(source.formatted())

レベル3

titleを宣言している部分をVariableDeclSyntaxに置き換えます。

private let title String "Hello"の5つの要素を個別に設定します。

private let title: String = "Hello"
import SwiftSyntax
import SwiftSyntaxBuilder

let source = SourceFileSyntax {
  ImportDeclSyntax(
    path: AccessPathSyntax([AccessPathComponentSyntax(name: "Foundation")])
  )
      
  ClassDeclSyntax(identifier: "SampleModel") {
    VariableDeclSyntax(
      modifiers: [DeclModifierSyntax(name: .private)],
      letOrVarKeyword: .let
    ) {
      PatternBindingSyntax(
        pattern: PatternSyntax("title"),
        typeAnnotation: TypeAnnotationSyntax(
          type: TypeSyntax("String")
        ),
        initializer: InitializerClauseSyntax(
          value: StringLiteralExprSyntax(content: "Hello")
        )
      )
    }
  }
}
   
print(source.formatted())

サンプル1

Builder.swift
let source = SourceFileSyntax {
  ImportDeclSyntax(
    path: AccessPathSyntax([AccessPathComponentSyntax(name: "Foundation")])
  )
      
  StructDeclSyntax(
    modifiers: [DeclModifierSyntax(name: .public)],
    identifier: "SampleModel"
  ) {
    VariableDeclSyntax(
      modifiers: [DeclModifierSyntax(name: .private)],
      .let,
      name: "title",
      type: TypeAnnotationSyntax(type: TypeSyntax("String")),
      initializer: InitializerClauseSyntax(
        value:StringLiteralExprSyntax(content: "Hello")
      )
    )
  }
}
    
print(source.formatted())
Output.swift
public struct SampleModel {
    private let title: String = "Hello"
}

サンプル2

Builder.swift
let source = SourceFileSyntax {
  FunctionDeclSyntax(
    identifier: TokenSyntax.identifier("area"),
    signature: FunctionSignatureSyntax(
      input: ParameterClauseSyntax(
        parameterList: FunctionParameterListSyntax {
          FunctionParameterSyntax(
            firstName: TokenSyntax.identifier("width"),
            colon: .colonToken(),
            type: TypeSyntax("Int")
          )
          FunctionParameterSyntax(
            firstName: TokenSyntax.identifier("height"),
            colon: .colonToken(),
            type: TypeSyntax("Int")
          )
        }
      ),
      output: ReturnClauseSyntax(
        returnType: SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"))
      )
    )
  ) {
    ReturnStmtSyntax(
      expression: SequenceExprSyntax {
        ExprListSyntax {
          IdentifierExprSyntax(identifier: .identifier("width"))
          BinaryOperatorExprSyntax(operatorToken: TokenSyntax.spacedBinaryOperator("*"))
          IdentifierExprSyntax(identifier: .identifier("height"))
        }
      }
    )
  }
}

print(source.formatted())
Output.swift
func area(width: Int, height: Int) -> Int {
    return width * height
}

サンプル3

structにprotocolを準拠させる

Builder.swift
let source = SourceFileSyntax {
  StructDeclSyntax(
    modifiers: [DeclModifierSyntax(name: .public)],
    identifier: "SampleModel",
    inheritanceClause: TypeInheritanceClauseSyntax {
      for typeName in  ["Codable", "Hashable", "Sendable"] {
        InheritedTypeSyntax(typeName: TypeSyntax(stringLiteral: typeName))
      }
    }
  )
}

print(source.formatted())
Output.swift
public struct SampleModel: Codable, Hashable, Sendable {
}

SwiftSyntaxの難易度について

SwiftSyntax全体を理解するのは難しいですが、コード生成に関しては成果物がわかりやすいので、比較的とっつきやすいと思います。

ですが、多くの部分が抽象化(protocol)されており、間違ったコードでも期待したものが出来上がるので、本当にこれでいいのかな感が強いです。

SeleneというパッケージでSwiftSyntaxBuilderを取り入れているので、参考になればと思います!

Discussion