Swift-Syntaxを触る

Swiftの構文を用いてJSONオブジェクトの定義を作ってみたくなったので、Swift-Syntaxを使ってどうやってSwiftの構文を解析できるかを基礎知識を少しすっ飛ばしながら触ってみる。

書き換えは今回行わず、どのように解析結果を取り出せるかの実験になる見込み

スタートはこんな感じから。

Parserの中で必要なメソッドをOverrideしていく

import SwiftSyntax
import Foundation

class Parser: SyntaxRewriter {
   
}

let file = CommandLine.arguments[1]
let url = URL(fileURLWithPath: file)
let sourceFile = try SyntaxParser.parse(url)
let result = Parser().visit(sourceFile)
print("End")
print(result)

最初はこのぐらいのシンプルなものを流してみる

struct Message {

  var body: String?

}

ひとまず、全てが流れてくる TokenStyntaxが引数のメソッドをoverrideする。

class Parser: SyntaxRewriter {
  
  override func visit(_ token: TokenSyntax) -> Syntax {
    print(token)
    return token
  }
}

とりあえずprintすると結果は次のようになる。

struct 
Message 
{


  var 
body
: 
String
?


}

kindとかをprintすれば入力したコードの隅々まで把握できる情報が取れるはず。

ただ、今回はここの深堀りはスキップして、struct一覧を取れないかという方向に進みたい。

SyntaxRewriterにはTokenSyntax以外にもたくさんのメソッドがあり、その中に

💡

func visit(_ node: StructDeclSyntax) -> DeclSyntax

というものがある。

正しい表現には自信がないが、Structのシンタックスが見つかったときに、そこの情報が詰まったオブジェクトが渡されるメソッドとなる。

実際にoverrideしてbreakpointを貼ってみる。

少しだけ解析するコードに変更を与える

/// Hello
/// JSON
struct Message {

  var body: String?

}

override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
    print(node.identifier.text)
    print(node.structKeyword.leadingTrivia.first)
    return node
  }

すると、breakpoint止まる

po node

SwiftSyntax.StructDeclSyntax
  - attributes : nil
  - modifiers : nil
  ▿ structKeyword : SwiftSyntax.TokenSyntax
    - text : "struct"
    ▿ leadingTrivia : Trivia
      ▿ pieces : 5 elements
0 : TriviaPiece
          - newlines : 1
1 : TriviaPiece
          - docLineComment : "/// Hello"
2 : TriviaPiece
          - newlines : 1
3 : TriviaPiece
          - docLineComment : "/// JSON"
4 : TriviaPiece
          - newlines : 1
    ▿ trailingTrivia : Trivia
      ▿ pieces : 1 element
0 : TriviaPiece
          - spaces : 1
    - tokenKind : SwiftSyntax.TokenKind.structKeyword
  ▿ identifier : SwiftSyntax.TokenSyntax
    - text : "Message"
    ▿ leadingTrivia : Trivia
      - pieces : 0 elements
    ▿ trailingTrivia : Trivia
      ▿ pieces : 1 element
0 : TriviaPiece
          - spaces : 1
    ▿ tokenKind : TokenKind
      - identifier : "Message"
  - genericParameterClause : nil
  - inheritanceClause : nil
  - genericWhereClause : nil
  ▿ members : SwiftSyntax.MemberDeclBlockSyntax
    ▿ leftBrace : SwiftSyntax.TokenSyntax
      - text : "{"
      ▿ leadingTrivia : Trivia
        - pieces : 0 elements
      ▿ trailingTrivia : Trivia
        - pieces : 0 elements
      - tokenKind : SwiftSyntax.TokenKind.leftBrace
    ▿ members : SwiftSyntax.MemberDeclListSyntax
0 : SwiftSyntax.MemberDeclListItemSyntax
        ▿ decl : SwiftSyntax.VariableDeclSyntax
          - attributes : nil
          - modifiers : nil
          ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
            - text : "var"
            ▿ leadingTrivia : Trivia
              ▿ pieces : 2 elements
0 : TriviaPiece
                  - newlines : 2
1 : TriviaPiece
                  - spaces : 2
            ▿ trailingTrivia : Trivia
              ▿ pieces : 1 element
0 : TriviaPiece
                  - spaces : 1
            - tokenKind : SwiftSyntax.TokenKind.varKeyword
          ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
              ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
                ▿ identifier : SwiftSyntax.TokenSyntax
                  - text : "body"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ tokenKind : TokenKind
                    - identifier : "body"
              ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
                ▿ some : SwiftSyntax.TypeAnnotationSyntax
                  ▿ colon : SwiftSyntax.TokenSyntax
                    - text : ":"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      ▿ pieces : 1 element
0 : TriviaPiece
                          - spaces : 1
                    - tokenKind : SwiftSyntax.TokenKind.colon
                  ▿ type : SwiftSyntax.OptionalTypeSyntax
                    ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                      ▿ name : SwiftSyntax.TokenSyntax
                        - text : "String"
                        ▿ leadingTrivia : Trivia
                          - pieces : 0 elements
                        ▿ trailingTrivia : Trivia
                          - pieces : 0 elements
                        ▿ tokenKind : TokenKind
                          - identifier : "String"
                      - genericArgumentClause : nil
                    ▿ questionMark : SwiftSyntax.TokenSyntax
                      - text : "?"
                      ▿ leadingTrivia : Trivia
                        - pieces : 0 elements
                      ▿ trailingTrivia : Trivia
                        - pieces : 0 elements
                      - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
              - initializer : nil
              - accessor : nil
              - trailingComma : nil
        - semicolon : nil
    ▿ rightBrace : SwiftSyntax.TokenSyntax
      - text : "}"
      ▿ leadingTrivia : Trivia
        ▿ pieces : 1 element
0 : TriviaPiece
            - newlines : 2
      ▿ trailingTrivia : Trivia
        - pieces : 0 elements
      - tokenKind : SwiftSyntax.TokenKind.rightBrace

ずらっとstruct Messageにまつわる情報が取れる。

コメントを取得してみる

let comment = {
      
  node.structKeyword.leadingTrivia.flatMap { t -> String? in
    guard case .docLineComment(let comment) = t else { return nil }
    return comment
  }
  .joined(separator: "\n")
  
}()

/// Hello
/// JSON

次はメンバの取得

node.membersでメンバにアクセスできる様子

let members: MemberDeclBlockSyntax = node.members

とりあえずprint

(lldb) po node.members
SwiftSyntax.MemberDeclBlockSyntax
  ▿ leftBrace : SwiftSyntax.TokenSyntax
    - text : "{"
    ▿ leadingTrivia : Trivia
      - pieces : 0 elements
    ▿ trailingTrivia : Trivia
      - pieces : 0 elements
    - tokenKind : SwiftSyntax.TokenKind.leftBrace
  ▿ members : SwiftSyntax.MemberDeclListSyntax
0 : SwiftSyntax.MemberDeclListItemSyntax
      ▿ decl : SwiftSyntax.VariableDeclSyntax
        - attributes : nil
        - modifiers : nil
        ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
          - text : "var"
          ▿ leadingTrivia : Trivia
            ▿ pieces : 2 elements
0 : TriviaPiece
                - newlines : 2
1 : TriviaPiece
                - spaces : 2
          ▿ trailingTrivia : Trivia
            ▿ pieces : 1 element
0 : TriviaPiece
                - spaces : 1
          - tokenKind : SwiftSyntax.TokenKind.varKeyword
        ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
            ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
              ▿ identifier : SwiftSyntax.TokenSyntax
                - text : "body"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  - pieces : 0 elements
                ▿ tokenKind : TokenKind
                  - identifier : "body"
            ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
              ▿ some : SwiftSyntax.TypeAnnotationSyntax
                ▿ colon : SwiftSyntax.TokenSyntax
                  - text : ":"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    ▿ pieces : 1 element
0 : TriviaPiece
                        - spaces : 1
                  - tokenKind : SwiftSyntax.TokenKind.colon
                ▿ type : SwiftSyntax.OptionalTypeSyntax
                  ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                    ▿ name : SwiftSyntax.TokenSyntax
                      - text : "String"
                      ▿ leadingTrivia : Trivia
                        - pieces : 0 elements
                      ▿ trailingTrivia : Trivia
                        - pieces : 0 elements
                      ▿ tokenKind : TokenKind
                        - identifier : "String"
                    - genericArgumentClause : nil
                  ▿ questionMark : SwiftSyntax.TokenSyntax
                    - text : "?"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
            - initializer : nil
            - accessor : nil
            - trailingComma : nil
      - semicolon : nil
  ▿ rightBrace : SwiftSyntax.TokenSyntax
    - text : "}"
    ▿ leadingTrivia : Trivia
      ▿ pieces : 1 element
0 : TriviaPiece
          - newlines : 2
    ▿ trailingTrivia : Trivia
      - pieces : 0 elements
    - tokenKind : SwiftSyntax.TokenKind.rightBrace

どうやら、このなかにさらにmembersが存在する様子

(lldb) po node.members.members
SwiftSyntax.MemberDeclListSyntax
0 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      - modifiers : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "var"
        ▿ leadingTrivia : Trivia
          ▿ pieces : 2 elements
0 : TriviaPiece
              - newlines : 2
1 : TriviaPiece
              - spaces : 2
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.varKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "body"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                - pieces : 0 elements
              ▿ tokenKind : TokenKind
                - identifier : "body"
          ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
            ▿ some : SwiftSyntax.TypeAnnotationSyntax
              ▿ colon : SwiftSyntax.TokenSyntax
                - text : ":"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.colon
              ▿ type : SwiftSyntax.OptionalTypeSyntax
                ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                  ▿ name : SwiftSyntax.TokenSyntax
                    - text : "String"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ tokenKind : TokenKind
                      - identifier : "String"
                  - genericArgumentClause : nil
                ▿ questionMark : SwiftSyntax.TokenSyntax
                  - text : "?"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
          - initializer : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil

bodyやString? の情報について取得したい

MemberDeclListSyntaxはCollectionの実装を持っているのでmapなどで回せる

.firstTokenは目的と違うものが取れるので一旦無視

po node.members.members.map { $0 }

先程のprintと同じものが出る。

1 element
0 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax

ここだけ見るとdeclというプロパティがあるので、アクセスしてみる

image

declはabstractionです。

VariableDeclSyntaxにキャストする必要がありそう

ひとまずこれでフィルター

po node.members.members.compactMap { $0.decl as? VariableDeclSyntax }

(lldb) po node.members.members.compactMap { $0.decl as? VariableDeclSyntax }
1 element
0 : SwiftSyntax.VariableDeclSyntax
    - attributes : nil
    - modifiers : nil
    ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
      - text : "var"
      ▿ leadingTrivia : Trivia
        ▿ pieces : 2 elements
0 : TriviaPiece
            - newlines : 2
1 : TriviaPiece
            - spaces : 2
      ▿ trailingTrivia : Trivia
        ▿ pieces : 1 element
0 : TriviaPiece
            - spaces : 1
      - tokenKind : SwiftSyntax.TokenKind.varKeyword
    ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
        ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
          ▿ identifier : SwiftSyntax.TokenSyntax
            - text : "body"
            ▿ leadingTrivia : Trivia
              - pieces : 0 elements
            ▿ trailingTrivia : Trivia
              - pieces : 0 elements
            ▿ tokenKind : TokenKind
              - identifier : "body"
        ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
          ▿ some : SwiftSyntax.TypeAnnotationSyntax
            ▿ colon : SwiftSyntax.TokenSyntax
              - text : ":"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                ▿ pieces : 1 element
0 : TriviaPiece
                    - spaces : 1
              - tokenKind : SwiftSyntax.TokenKind.colon
            ▿ type : SwiftSyntax.OptionalTypeSyntax
              ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                ▿ name : SwiftSyntax.TokenSyntax
                  - text : "String"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ tokenKind : TokenKind
                    - identifier : "String"
                - genericArgumentClause : nil
              ▿ questionMark : SwiftSyntax.TokenSyntax
                - text : "?"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  - pieces : 0 elements
                - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
        - initializer : nil
        - accessor : nil
        - trailingComma : nil

これを眺めていると、今回ほしい情報は

letOrVarKeywordとbindingsに入っている様子

bindingがこれまたcollectionで大変になってきたなーという印象

メンバが一つだと感覚が掴みづらいと思ったので、メンバを増やす

/// Hello
/// JSON
struct Message {

  var body: String?
  var updatedAt: Date

}

(lldb) po node.members.members
SwiftSyntax.MemberDeclListSyntax
0 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      - modifiers : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "var"
        ▿ leadingTrivia : Trivia
          ▿ pieces : 2 elements
0 : TriviaPiece
              - newlines : 2
1 : TriviaPiece
              - spaces : 2
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.varKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "body"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                - pieces : 0 elements
              ▿ tokenKind : TokenKind
                - identifier : "body"
          ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
            ▿ some : SwiftSyntax.TypeAnnotationSyntax
              ▿ colon : SwiftSyntax.TokenSyntax
                - text : ":"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.colon
              ▿ type : SwiftSyntax.OptionalTypeSyntax
                ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                  ▿ name : SwiftSyntax.TokenSyntax
                    - text : "String"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ tokenKind : TokenKind
                      - identifier : "String"
                  - genericArgumentClause : nil
                ▿ questionMark : SwiftSyntax.TokenSyntax
                  - text : "?"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
          - initializer : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil
1 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      - modifiers : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "var"
        ▿ leadingTrivia : Trivia
          ▿ pieces : 2 elements
0 : TriviaPiece
              - newlines : 1
1 : TriviaPiece
              - spaces : 2
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.varKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "updatedAt"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                - pieces : 0 elements
              ▿ tokenKind : TokenKind
                - identifier : "updatedAt"
          ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
            ▿ some : SwiftSyntax.TypeAnnotationSyntax
              ▿ colon : SwiftSyntax.TokenSyntax
                - text : ":"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.colon
              ▿ type : SwiftSyntax.SimpleTypeIdentifierSyntax
                ▿ name : SwiftSyntax.TokenSyntax
                  - text : "Date"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ tokenKind : TokenKind
                    - identifier : "Date"
                - genericArgumentClause : nil
          - initializer : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil

bindingの要素はどちらも1つの様子

一応、static letはどうなるのか見ておく

/// Hello
/// JSON
struct Message {

  static let `default` = Date()

  var body: String?
  var updatedAt: Date

}

ちなみに、コンパイルは通らないコード

(lldb) po node.members.members
SwiftSyntax.MemberDeclListSyntax
0 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      ▿ modifiers : Optional<ModifierListSyntax>
        ▿ some : SwiftSyntax.ModifierListSyntax
0 : SwiftSyntax.DeclModifierSyntax
            ▿ name : SwiftSyntax.TokenSyntax
              - text : "static"
              ▿ leadingTrivia : Trivia
                ▿ pieces : 2 elements
0 : TriviaPiece
                    - newlines : 2
1 : TriviaPiece
                    - spaces : 2
              ▿ trailingTrivia : Trivia
                ▿ pieces : 1 element
0 : TriviaPiece
                    - spaces : 1
              - tokenKind : SwiftSyntax.TokenKind.staticKeyword
            - detailLeftParen : nil
            - detail : nil
            - detailRightParen : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "let"
        ▿ leadingTrivia : Trivia
          - pieces : 0 elements
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.letKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "default"
              ▿ leadingTrivia : Trivia
                ▿ pieces : 1 element
0 : TriviaPiece
                    - backticks : 1
              ▿ trailingTrivia : Trivia
                ▿ pieces : 2 elements
0 : TriviaPiece
                    - backticks : 1
1 : TriviaPiece
                    - spaces : 1
              ▿ tokenKind : TokenKind
                - identifier : "default"
          - typeAnnotation : nil
          ▿ initializer : Optional<InitializerClauseSyntax>
            ▿ some : SwiftSyntax.InitializerClauseSyntax
equal : SwiftSyntax.TokenSyntax
                - text : "="
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.equal
              ▿ value : SwiftSyntax.FunctionCallExprSyntax
                ▿ calledExpression : SwiftSyntax.IdentifierExprSyntax
                  ▿ identifier : SwiftSyntax.TokenSyntax
                    - text : "Date"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ tokenKind : TokenKind
                      - identifier : "Date"
                  - declNameArguments : nil
                ▿ leftParen : Optional<TokenSyntax>
                  ▿ some : SwiftSyntax.TokenSyntax
                    - text : "("
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    - tokenKind : SwiftSyntax.TokenKind.leftParen
                - argumentList : SwiftSyntax.FunctionCallArgumentListSyntax
                ▿ rightParen : Optional<TokenSyntax>
                  ▿ some : SwiftSyntax.TokenSyntax
                    - text : ")"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    - tokenKind : SwiftSyntax.TokenKind.rightParen
                - trailingClosure : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil
1 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      - modifiers : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "var"
        ▿ leadingTrivia : Trivia
          ▿ pieces : 2 elements
0 : TriviaPiece
              - newlines : 2
1 : TriviaPiece
              - spaces : 2
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.varKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "body"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                - pieces : 0 elements
              ▿ tokenKind : TokenKind
                - identifier : "body"
          ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
            ▿ some : SwiftSyntax.TypeAnnotationSyntax
              ▿ colon : SwiftSyntax.TokenSyntax
                - text : ":"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.colon
              ▿ type : SwiftSyntax.SimpleTypeIdentifierSyntax
                ▿ wrappedType : SwiftSyntax.SimpleTypeIdentifierSyntax
                  ▿ name : SwiftSyntax.TokenSyntax
                    - text : "String"
                    ▿ leadingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ trailingTrivia : Trivia
                      - pieces : 0 elements
                    ▿ tokenKind : TokenKind
                      - identifier : "String"
                  - genericArgumentClause : nil
                ▿ questionMark : SwiftSyntax.TokenSyntax
                  - text : "?"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  - tokenKind : SwiftSyntax.TokenKind.postfixQuestionMark
          - initializer : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil
2 : SwiftSyntax.MemberDeclListItemSyntax
    ▿ decl : SwiftSyntax.VariableDeclSyntax
      - attributes : nil
      - modifiers : nil
      ▿ letOrVarKeyword : SwiftSyntax.TokenSyntax
        - text : "var"
        ▿ leadingTrivia : Trivia
          ▿ pieces : 2 elements
0 : TriviaPiece
              - newlines : 1
1 : TriviaPiece
              - spaces : 2
        ▿ trailingTrivia : Trivia
          ▿ pieces : 1 element
0 : TriviaPiece
              - spaces : 1
        - tokenKind : SwiftSyntax.TokenKind.varKeyword
      ▿ bindings : SwiftSyntax.PatternBindingListSyntax
0 : SwiftSyntax.PatternBindingSyntax
          ▿ pattern : SwiftSyntax.IdentifierPatternSyntax
            ▿ identifier : SwiftSyntax.TokenSyntax
              - text : "updatedAt"
              ▿ leadingTrivia : Trivia
                - pieces : 0 elements
              ▿ trailingTrivia : Trivia
                - pieces : 0 elements
              ▿ tokenKind : TokenKind
                - identifier : "updatedAt"
          ▿ typeAnnotation : Optional<TypeAnnotationSyntax>
            ▿ some : SwiftSyntax.TypeAnnotationSyntax
              ▿ colon : SwiftSyntax.TokenSyntax
                - text : ":"
                ▿ leadingTrivia : Trivia
                  - pieces : 0 elements
                ▿ trailingTrivia : Trivia
                  ▿ pieces : 1 element
0 : TriviaPiece
                      - spaces : 1
                - tokenKind : SwiftSyntax.TokenKind.colon
              ▿ type : SwiftSyntax.SimpleTypeIdentifierSyntax
                ▿ name : SwiftSyntax.TokenSyntax
                  - text : "Date"
                  ▿ leadingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ trailingTrivia : Trivia
                    - pieces : 0 elements
                  ▿ tokenKind : TokenKind
                    - identifier : "Date"
                - genericArgumentClause : nil
          - initializer : nil
          - accessor : nil
          - trailingComma : nil
    - semicolon : nil

staticに関する記述はmodifierとして入るみたい。

▿ modifiers : Optional<ModifierListSyntax>
    ▿ some : SwiftSyntax.ModifierListSyntax
0 : SwiftSyntax.DeclModifierSyntax
        ▿ name : SwiftSyntax.TokenSyntax
          - text : "static"
          ▿ leadingTrivia : Trivia
            ▿ pieces : 2 elements
0 : TriviaPiece
                - newlines : 2
1 : TriviaPiece
                - spaces : 2
          ▿ trailingTrivia : Trivia
            ▿ pieces : 1 element
0 : TriviaPiece
                - spaces : 1
          - tokenKind : SwiftSyntax.TokenKind.staticKeyword
        - detailLeftParen : nil
        - detail : nil
        - detailRightParen : nil

non-optionalの場合とoptionalでstynaxのstructが異なる。

LLDBのprintでは同じ表示だったけどなー

SimpleTypeIdentifierSyntax

OptionalTypeSyntax

この辺の分岐がポイントかな

import SwiftSyntax
import Foundation

struct Object {
  var name: String
  var comment: String
  var members: [Member] = []
}

struct Member {
  
  var name: String
  var type: String
  var isRequired: Bool
}

class Parser: SyntaxRewriter {
  
  var parsedObjects: [Object] = []
  
  override func visit(_ token: TokenSyntax) -> Syntax {
    print(token)
    return token
  }
  
  override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
    
    let comment = {
      
      node.structKeyword.leadingTrivia.flatMap { t -> String? in
        guard case .docLineComment(let comment) = t else { return nil }
        return comment.replacingOccurrences(of: "/// ", with: "")
      }
      .joined(separator: "\n")
      
    }()
    
    var obj = Object(
      name: node.identifier.text,
      comment: comment
    )
    
    let member = node.members.members
      .compactMap { $0.decl as? VariableDeclSyntax }
      .filter {
        $0.modifiers == nil
    }
    .flatMap {
      $0.bindings.compactMap { binding -> Member? in
        
        let name = (binding.pattern as! IdentifierPatternSyntax).identifier.text
                        
        switch binding.typeAnnotation?.type {
        case let b as SimpleTypeIdentifierSyntax:
          return Member(
            name: name,
            type: b.name.text,
            isRequired: true
          )
        case let b as OptionalTypeSyntax:
          return Member(
            name: name,
            type: (b.wrappedType as! SimpleTypeIdentifierSyntax).name.text,
            isRequired: false
          )          
        default:
          assertionFailure("unhandled")
          return nil
        }

      }
    }
    
    obj.members = member
    
    parsedObjects.append(obj)
              
    print(obj)
    print(member)
    
    return node
  }

  override func visit(_ node: CodeBlockSyntax) -> Syntax {
    print(node)

    return node
  }
  
}

let file = CommandLine.arguments[1]
let url = URL(fileURLWithPath: file)
let sourceFile = try SyntaxParser.parse(url)
let parser = Parser()
let result = parser.visit(sourceFile)


var lines: [String] = []
for obj in parser.parsedObjects {
  lines.append("## \(obj.name)")
  lines.append("")
  lines.append(obj.comment)
  lines.append("")
  lines.append("### Properties")
  
  for member in obj.members {
    lines.append("- \(member.name), type: \(member.type) required: \(member.isRequired)")
  }
}

print(lines.joined(separator: "\n"))


print("===End===")
//print(result)

入力

/// Hello
/// JSON
struct Message {

  let body: String?
  let updatedAt: Date

}

## Message

Hello
JSON

### Properties
- body, type: String required: false
- updatedAt, type: Date required: true