SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

1.7K Views

October 02, 25

スライド概要

profile-image

Swiftを書いています

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせて Swiftコードベースの理解を深めるツールを 開発しよう! Ockey 1

2.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! product|Palcy Ockey 2025年4月にピクシブに新卒入社 好きなSwift • KeyPath • enum • Argument Label 2

3.

Jump to Definition 3 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね

4.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね 「どこで定義されているか」だけでなく、「どこで参照されているか」 を調べる機能もある • Show Callers… • Find → Find Selected Symbol in Workspace • Find → Find Call Hierarchy 4

5.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね もっと便利にしたい • 型に対して使いたい • 依存関係のリストと静的構造を同時に見たい • 辿ってきた依存関係を遡りたい SwiftSyntaxなどが公開されているので、好みのツールを 実装できる 5

7.

https://github.com/Ockey12/swift-kraken 7 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! 実装したツール

8.

Swiftで書かれたコードから、宣言と依存関係を抽出したい 8 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! ツールを実装するために

9.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! テーマ 1. SwiftSyntax 2. IndexStore 3. SwiftSyntaxとIndexStoreを組み合わせる 9

10.

10 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax

11.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Swiftで書かれたコードに忠実な構文木を扱うライブラリ Macrosの登場で使われるようになってきた https://github.com/swiftlang/swift-syntax 11

12.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる 簡単な5行のコードから構文木を生成する struct SomeType { func method() { let constant: Int = 0 } } 12

13.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる 簡単な5行のコードから構文木を生成する struct SomeType { func method() { let constant: Int = 0 } } 1.リポジトリからclone 2.対象のSwiftファイルを作成 3.swift-syntaxディレクトリへ移動 4.print-treeコマンドを実行 swift run --package-path SwiftParserCLI swift-parser-cli print-tree file.swift 13

14.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる 生成される構文木 struct SomeType { func method() { let constant: Int = 0 } } 14 SourceFileSyntax ├─statements: CodeBlockItemListSyntax │ ╰─[0]: CodeBlockItemSyntax │ ╰─item: StructDeclSyntax │ ├─attributes: AttributeListSyntax │ ├─modifiers: DeclModifierListSyntax │ ├─structKeyword: keyword(SwiftSyntax.Keyword.struct) │ ├─name: identifier("SomeType") │ ╰─memberBlock: MemberBlockSyntax │ ├─leftBrace: leftBrace │ ├─members: MemberBlockItemListSyntax │ │ ╰─[0]: MemberBlockItemSyntax │ │ ╰─decl: FunctionDeclSyntax │ │ ├─attributes: AttributeListSyntax │ │ ├─modifiers: DeclModifierListSyntax │ │ ├─funcKeyword: keyword(SwiftSyntax.Keyword.func) │ │ ├─name: identifier("method") │ │ ├─signature: FunctionSignatureSyntax │ │ │ ╰─parameterClause: FunctionParameterClauseSyntax │ │ │ ├─leftParen: leftParen │ │ │ ├─parameters: FunctionParameterListSyntax │ │ │ ╰─rightParen: rightParen │ │ ╰─body: CodeBlockSyntax │ │ ├─leftBrace: leftBrace │ │ ├─statements: CodeBlockItemListSyntax │ │ │ ╰─[0]: CodeBlockItemSyntax │ │ │ ╰─item: VariableDeclSyntax │ │ │ ├─attributes: AttributeListSyntax │ │ │ ├─modifiers: DeclModifierListSyntax │ │ │ ├─bindingSpecifier: keyword(SwiftSyntax.Keyword.let) │ │ │ ╰─bindings: PatternBindingListSyntax │ │ │ ╰─[0]: PatternBindingSyntax │ │ │ ├─pattern: IdentifierPatternSyntax │ │ │ │ ╰─identifier: identifier("constant") │ │ │ ├─typeAnnotation: TypeAnnotationSyntax │ │ │ │ ├─colon: colon │ │ │ │ ╰─type: IdentifierTypeSyntax │ │ │ │ ╰─name: identifier("Int") │ │ │ ╰─initializer: InitializerClauseSyntax │ │ │ ├─equal: equal │ │ │ ╰─value: IntegerLiteralExprSyntax │ │ │ ╰─literal: integerLiteral("0") │ │ ╰─rightBrace: rightBrace │ ╰─rightBrace: rightBrace ╰─endOfFileToken: endOfFile

15.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる 宣言の抽出に必要なノード struct SomeType { func method() { let constant: Int = 0 } } SourceFileSyntax ├─statements: CodeBlockItemListSyntax │ ╰─[0]: CodeBlockItemSyntax │ ╰─item: StructDeclSyntax │ ├─name: identifier("SomeType") │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ├─name: identifier("method") │ ╰─body: CodeBlockSyntax │ ╰─statements: CodeBlockItemListSyntax │ ╰─[0]: CodeBlockItemSyntax │ ╰─item: VariableDeclSyntax │ ╰─bindings: PatternBindingListSyntax │ ╰─[0]: PatternBindingSyntax │ ╰─pattern: IdentifierPatternSyntax │ ╰─identifier: identifier("constant") ╰─endOfFileToken: endOfFile 15

16.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる DeclSyntaxが宣言を表し、nameプロパティが名前を持つ struct SomeType { func method() { let constant: Int = 0 } } SourceFileSyntax ├─statements: CodeBlockItemListSyntax │ ╰─[0]: CodeBlockItemSyntax │ ╰─item: StructDeclSyntax │ ├─name: identifier("SomeType") │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ├─name: identifier("method") │ ╰─body: CodeBlockSyntax │ ╰─statements: CodeBlockItemListSyntax │ ╰─[0]: CodeBlockItemSyntax │ ╰─item: VariableDeclSyntax │ ╰─bindings: PatternBindingListSyntax │ ╰─[0]: PatternBindingSyntax │ ╰─pattern: IdentifierPatternSyntax │ ╰─identifier: identifier("constant") ╰─endOfFileToken: endOfFile 16

17.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax DeclSyntax 宣言を表すノードの型が決まっている 対象 DeclSyntax struct class enum actor protocol extension func let/var StructDeclSyntax ClassDeclSyntax EnumDeclSyntax ActorDeclSyntax ProtocolDeclSyntax ExtensionDeclSyntax FunctionDeclSyntax VariableDeclSyntax 17

18.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax DeclSyntax DeclSyntaxを見つけたら対応するインスタンスを生成することで、 コードから宣言を抽出できる 対象 DeclSyntax struct class enum actor protocol extension func let/var StructDeclSyntax ClassDeclSyntax EnumDeclSyntax ActorDeclSyntax ProtocolDeclSyntax ExtensionDeclSyntax FunctionDeclSyntax VariableDeclSyntax 18

19.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 宣言抽出の流れ 1. SwiftソースコードをString型の定数に格納しておく 2. Parser構造体でソースコードから構文木を生成する 3. Visitorクラスで構文木を走査する ① visitでインスタンスを生成してスタックにpushする ② visitPostでスタックからpopして親/出力用配列に格納する 19

20.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Parser swift-syntaxのSwiftParserにParser構造体がある parseメソッドにString型のSwiftソースコードを渡すと、 構文木の最上位であるSourceFileSyntaxを返す import SwiftParser let sourceFileSyntax = Parser.parse(source: sourceCode) 20

21.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitor swift-syntaxのSwiftSyntaxにSyntaxVisitorクラスがある SourceFileSyntaxから始まる構文木をwalkメソッドで走査(深さ 優先探索)する import SwiftSyntax final class DeclarationVisitor: SyntaxVisitor {} let visitor = DeclarationVisitor(viewMode: .fixedUp) visitor.walk(sourceFileSyntax) 21

22.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitor SyntaxVisitorクラスを継承して、ノードに到達した際の処理を overrideする import SwiftSyntax final class DeclarationVisitor: SyntaxVisitor { override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { .visitChildren } } override func visitPost(_ node: StructDeclSyntax) {} 22

23.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitor visitは親から対象ノードに到達すると呼び出される visitPostはすべての子ノードの探索を終えると呼び出される import SwiftSyntax final class DeclarationVisitor: SyntaxVisitor { override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind { .visitChildren } } override func visitPost(_ node: StructDeclSyntax) {} 23

24.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorで宣言を抽出する ↓のコードを例に抽出する SourceFileSyntax ╰─statements: CodeBlockItemListSyntax ╰─[0]: CodeBlockItemSyntax ╰─item: StructDeclSyntax ├─name: identifier(“SomeStruct") ╰─memberBlock: MemberBlockSyntax ╰─members: MemberBlockItemListSyntax struct SomeStruct { ├─[0]: MemberBlockItemSyntax │ ╰─decl: EnumDeclSyntax enum NestedEnum { │ ├─name: identifier(“NestedEnum") static func nestedMethod() {} │ ╰─memberBlock: MemberBlockSyntax } │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax func notNestedMethod() {} │ ╰─name: identifier("nestedMethod") } ╰─[1]: MemberBlockItemSyntax ╰─decl: FunctionDeclSyntax ╰─name: identifier("notNestedMethod") 24

25.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

宣言を表す型を実装する
struct Declaration: Identifiable, Sendable {
let id: UUID
let name: String
let kind: Kind
let sourceLocationRange: ClosedRange<Location>

}

struct Location: Equatable, Hashable, Sendable {
let fullPath: String
let line: Int
let column: Int
}

var nestingEnums: [Self] = []
var functions: [Self] = []
. . .

extension Location: Comparable {
static func < (lhs: Location, rhs: Location) -> Bool {
if lhs.fullPath != rhs.fullPath {
return lhs.fullPath < rhs.fullPath
}
if lhs.line != rhs.line {
return lhs.line < rhs.line
}
return lhs.column < rhs.column
}
}

extension Declaration {
enum Kind: Sendable {
case `struct`
case `class`
case `enum`
case `actor`
case `extension`
case variable
case function
case `case`
}
}

25

26.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorで宣言を抽出する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
init(sourceLocationConverter: SourceLocationConverter) {
self.sourceLocationConverter = sourceLocationConverter
super.init(viewMode: .fixedUp)
}

}

// override func visit(...) -> SyntaxVisitorContinueKind {}
// override func visitPost(...) {}

func extractDeclarations() throws {
let fileURL = URL(string: “ockey/SomeStruct.swift")!
let sourceCode = try String(contentsOf: fileURL, encoding: .utf8)
let sourceFileSyntax = Parser.parse(source: sourceCode)
let sourceLocationConverter = SourceLocationConverter(fileName: fileURL.path(), tree: sourceFileSyntax)
let visitor = DeclarationVisitor(sourceLocationConverter: sourceLocationConverter)
visitor.walk(sourceFileSyntax)
}

26

27.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorで宣言を抽出する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter

visitで生成したインスタンスを
保持しておくスタック

init(sourceLocationConverter: SourceLocationConverter) {
self.sourceLocationConverter = sourceLocationConverter
super.init(viewMode: .fixedUp)
}

}

// override func visit(...) -> SyntaxVisitorContinueKind {}
// override func visitPost(...) {}

func extractDeclarations() throws {
let fileURL = URL(string: “ockey/SomeStruct.swift")!
let sourceCode = try String(contentsOf: fileURL, encoding: .utf8)
let sourceFileSyntax = Parser.parse(source: sourceCode)
let sourceLocationConverter = SourceLocationConverter(fileName: fileURL.path(), tree: sourceFileSyntax)
let visitor = DeclarationVisitor(sourceLocationConverter: sourceLocationConverter)
visitor.walk(sourceFileSyntax)
}

27

28.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorで宣言を抽出する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter

出力用の配列

init(sourceLocationConverter: SourceLocationConverter) {
self.sourceLocationConverter = sourceLocationConverter
super.init(viewMode: .fixedUp)
}

}

// override func visit(...) -> SyntaxVisitorContinueKind {}
// override func visitPost(...) {}

func extractDeclarations() throws {
let fileURL = URL(string: “ockey/SomeStruct.swift")!
let sourceCode = try String(contentsOf: fileURL, encoding: .utf8)
let sourceFileSyntax = Parser.parse(source: sourceCode)
let sourceLocationConverter = SourceLocationConverter(fileName: fileURL.path(), tree: sourceFileSyntax)
let visitor = DeclarationVisitor(sourceLocationConverter: sourceLocationConverter)
visitor.walk(sourceFileSyntax)
}

28

29.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorで宣言を抽出する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
init(sourceLocationConverter: SourceLocationConverter) {
self.sourceLocationConverter = sourceLocationConverter
super.init(viewMode: .fixedUp)
}

}

構文木からノードの位置を算出する
(import SwiftSyntax)

// override func visit(...) -> SyntaxVisitorContinueKind {}
// override func visitPost(...) {}

func extractDeclarations() throws {
let fileURL = URL(string: “ockey/SomeStruct.swift")!
let sourceCode = try String(contentsOf: fileURL, encoding: .utf8)
let sourceFileSyntax = Parser.parse(source: sourceCode)
let sourceLocationConverter = SourceLocationConverter(fileName: fileURL.path(), tree: sourceFileSyntax)
let visitor = DeclarationVisitor(sourceLocationConverter: sourceLocationConverter)
visitor.walk(sourceFileSyntax)
}

29

30.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorによる走査

declarationsStack
[SomeStruct]

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
SourceFileSyntax
line: start.line,
╰─statements: CodeBlockItemListSyntax
column: start.column
╰─[0]: CodeBlockItemSyntax
)
╰─item: StructDeclSyntax ①
... Location(
├─name: identifier(“SomeStruct")
╰─memberBlock: MemberBlockSyntax
fullPath: end.file,
╰─members: MemberBlockItemListSyntax
line: end.line,
├─[0]: MemberBlockItemSyntax
column: end.column
│ ╰─decl: EnumDeclSyntax
)
│
├─name: identifier(“NestedEnum")
let declaration = Declaration(
│
╰─memberBlock: MemberBlockSyntax
id: UUID(),
│
╰─members: MemberBlockItemListSyntax
name: node.name.text,
│
╰─[0]: MemberBlockItemSyntax
│
╰─decl: FunctionDeclSyntax
kind: .struct,
│
╰─name: identifier("nestedMethod")
sourceLocationRange: sourceLocationRange
╰─[1]: MemberBlockItemSyntax
)
╰─decl:
FunctionDeclSyntax
declarationsStack.append(declaration)
╰─name: identifier("notNestedMethod")
return .visitChildren
}

30

31.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorによる走査

declarationsStack
[SomeStruct]

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
line: start.line,
宣言の範囲を抽出
column: start.column
)
... Location(
fullPath: end.file,
line: end.line,
column: end.column
)
let declaration = Declaration(
id: UUID(),
name: node.name.text,
宣言に対応するインスタンスを生成
kind: .struct,
sourceLocationRange: sourceLocationRange
)
declarationsStack.append(declaration)
スタックにpush
return .visitChildren
}
子ノードの探索を続ける

31

32.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorによる走査

declarationsStack
[SomeStruct, NestedEnum]

override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
SourceFileSyntax
line: start.line,
╰─statements: CodeBlockItemListSyntax
column: start.column
╰─[0]: CodeBlockItemSyntax
)
╰─item: StructDeclSyntax
... Location(
├─name: identifier(“SomeStruct")
╰─memberBlock: MemberBlockSyntax
fullPath: end.file,
╰─members: MemberBlockItemListSyntax
line: end.line,
├─[0]: MemberBlockItemSyntax
column: end.column
②
│ ╰─decl: EnumDeclSyntax
)
│
├─name: identifier(“NestedEnum")
let declaration = Declaration(
│
╰─memberBlock: MemberBlockSyntax
id: UUID(),
│
╰─members: MemberBlockItemListSyntax
name: node.name.text,
│
╰─[0]: MemberBlockItemSyntax
│
╰─decl: FunctionDeclSyntax
kind: .enum,
│
╰─name: identifier("nestedMethod")
sourceLocationRange: sourceLocationRange
╰─[1]: MemberBlockItemSyntax
)
╰─decl:
FunctionDeclSyntax
declarationsStack.append(declaration)
╰─name: identifier("notNestedMethod")
return .visitChildren
}

32

33.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorによる走査

declarationsStack
[SomeStruct, NestedEnum, nestedMethod]

override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
SourceFileSyntax
line: start.line,
╰─statements: CodeBlockItemListSyntax
column: start.column
╰─[0]: CodeBlockItemSyntax
)
╰─item: StructDeclSyntax
... Location(
├─name: identifier(“SomeStruct")
╰─memberBlock: MemberBlockSyntax
fullPath: end.file,
╰─members: MemberBlockItemListSyntax
line: end.line,
├─[0]: MemberBlockItemSyntax
column: end.column
│ ╰─decl: EnumDeclSyntax
)
│
├─name: identifier(“NestedEnum")
let declaration = Declaration(
│
╰─memberBlock: MemberBlockSyntax
id: UUID(),
│
╰─members: MemberBlockItemListSyntax
name: node.name.text,
│
╰─[0]: MemberBlockItemSyntax
│
╰─decl: FunctionDeclSyntax ③
kind: .function,
│
╰─name: identifier("nestedMethod")
sourceLocationRange: sourceLocationRange
╰─[1]: MemberBlockItemSyntax
)
╰─decl:
FunctionDeclSyntax
declarationsStack.append(declaration)
╰─name: identifier("notNestedMethod")
return .visitChildren
}

33

34.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 declarationsStack [SomeStruct, NestedEnum] override func visitPost(_ node: FunctionDeclSyntax) { guard let functionDeclaration = declarationsStack.popLast() else { return } if declarationsStack.isEmpty { topDeclarations .append(functionDeclaration) return } } SourceFileSyntax ╰─statements: CodeBlockItemListSyntax ╰─[0]: CodeBlockItemSyntax ╰─item: StructDeclSyntax ├─name: identifier(“SomeStruct") ╰─memberBlock: MemberBlockSyntax ╰─members: MemberBlockItemListSyntax if let lastIndex = declarationsStack.indices.last { ├─[0]: MemberBlockItemSyntax declarationsStack[lastIndex] │ ╰─decl: EnumDeclSyntax .functions.append(functionDeclaration) │ ├─name: identifier(“NestedEnum") } │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ╰─name: identifier("nestedMethod") ╰─[1]: MemberBlockItemSyntax ╰─decl: FunctionDeclSyntax ╰─name: identifier("notNestedMethod") ④ 34

35.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 declarationsStack [SomeStruct, NestedEnum] override func visitPost(_ node: FunctionDeclSyntax) { guard let functionDeclaration = declarationsStack.popLast() else { return スタックの末尾からpop } if declarationsStack.isEmpty { topDeclarations .append(functionDeclaration) return } スタックが空なら親要素が存在しない ので出力用配列にappend if let lastIndex = declarationsStack.indices.last { declarationsStack[lastIndex] .functions.append(functionDeclaration) } } スタックが空でないなら末尾が親要素 なのでプロパティにappend declarationsStack [SomeStruct, NestedEnum, nestedMethod] nestedMethod 35

36.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 declarationsStack [SomeStruct] override func visitPost(_ node: EnumDeclSyntax) { guard let enumDeclaration = declarationsStack.popLast() else { return } if declarationsStack.isEmpty { topDeclarations SourceFileSyntax .append(enumDeclaration) ╰─statements: CodeBlockItemListSyntax return ╰─[0]: CodeBlockItemSyntax } ╰─item: StructDeclSyntax if let lastIndex = declarationsStack.indices.last { ├─name: identifier(“SomeStruct") ╰─memberBlock: MemberBlockSyntax declarationsStack[lastIndex] ╰─members: MemberBlockItemListSyntax .nestingEnums.append(enumDeclaration) ├─[0]: MemberBlockItemSyntax } ⑤ │ ╰─decl: EnumDeclSyntax } │ ├─name: identifier(“NestedEnum") │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ╰─name: identifier("nestedMethod") ╰─[1]: MemberBlockItemSyntax ╰─decl: FunctionDeclSyntax ╰─name: identifier("notNestedMethod") 36

37.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntax

Visitorによる走査

declarationsStack
[SomeStruct, notNestedMethod]

override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
SourceFileSyntax
line: start.line,
╰─statements: CodeBlockItemListSyntax
column: start.column
╰─[0]: CodeBlockItemSyntax
)
╰─item: StructDeclSyntax
... Location(
├─name: identifier(“SomeStruct")
╰─memberBlock: MemberBlockSyntax
fullPath: end.file,
╰─members: MemberBlockItemListSyntax
line: end.line,
├─[0]: MemberBlockItemSyntax
column: end.column
│ ╰─decl: EnumDeclSyntax
)
│
├─name: identifier(“NestedEnum")
let declaration = Declaration(
│
╰─memberBlock: MemberBlockSyntax
id: UUID(),
│
╰─members: MemberBlockItemListSyntax
name: node.name.text,
│
╰─[0]: MemberBlockItemSyntax
│
╰─decl: FunctionDeclSyntax
kind: .function,
│
╰─name: identifier("nestedMethod")
sourceLocationRange: sourceLocationRange
╰─[1]: MemberBlockItemSyntax
)
⑥
╰─decl:
FunctionDeclSyntax
declarationsStack.append(declaration)
╰─name: identifier("notNestedMethod")
return .visitChildren
}

37

38.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 declarationsStack [SomeStruct] override func visitPost(_ node: FunctionDeclSyntax) { guard let functionDeclaration = declarationsStack.popLast() else { return } if declarationsStack.isEmpty { topDeclarations .append(functionDeclaration) return } } SourceFileSyntax ╰─statements: CodeBlockItemListSyntax ╰─[0]: CodeBlockItemSyntax ╰─item: StructDeclSyntax ├─name: identifier(“SomeStruct") ╰─memberBlock: MemberBlockSyntax ╰─members: MemberBlockItemListSyntax if let lastIndex = declarationsStack.indices.last { ├─[0]: MemberBlockItemSyntax declarationsStack[lastIndex] │ ╰─decl: EnumDeclSyntax .functions.append(functionDeclaration) │ ├─name: identifier(“NestedEnum") } │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ╰─name: identifier("nestedMethod") ╰─[1]: MemberBlockItemSyntax ╰─decl: FunctionDeclSyntax ╰─name: identifier("notNestedMethod") ⑦ 38

39.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax declarationsStack [] Visitorによる走査 topDeclarations [SomeStruct] override func visitPost(_ node: StructDeclSyntax) { guard let structDeclaration = declarationsStack.popLast() else { return } if declarationsStack.isEmpty { topDeclarations SourceFileSyntax .append(structDeclaration) ╰─statements: CodeBlockItemListSyntax return ╰─[0]: CodeBlockItemSyntax } ╰─item: StructDeclSyntax ⑧ if let lastIndex = declarationsStack.indices.last { ├─name: identifier(“SomeStruct") ╰─memberBlock: MemberBlockSyntax declarationsStack[lastIndex] ╰─members: MemberBlockItemListSyntax .nestingStructs.append(structDeclaration) ├─[0]: MemberBlockItemSyntax } │ ╰─decl: EnumDeclSyntax } declarationsStackが空なので親が存在せず、 topDeclarationsにappend 39 │ ├─name: identifier(“NestedEnum") │ ╰─memberBlock: MemberBlockSyntax │ ╰─members: MemberBlockItemListSyntax │ ╰─[0]: MemberBlockItemSyntax │ ╰─decl: FunctionDeclSyntax │ ╰─name: identifier("nestedMethod") ╰─[1]: MemberBlockItemSyntax ╰─decl: FunctionDeclSyntax ╰─name: identifier("notNestedMethod")

40.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 静的構造を抽出できた struct SomeStruct { enum NestedEnum { static func nestedMethod() {} } } func notNestedMethod() {} 40 SomeStruct ├─NestedEnum │ ╰─nestedMethod ╰─notNestedMethod

41.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 依存関係は… 依存関係も抽出するぞ!といきたいところだけど… let a: SomeType = SomeType(value: 0) let b: SomeType = .init(value: 1) let c = SomeType(value: 2) 構文木から依存関係を構築するのはつらい let d = { SomeType(value: 3) }() 定数dが型としてSomeTypeに依存していることは わかりにくい func returnSomeType() -> SomeType { .init(value: 4) } 41

42.

42 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore

43.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore ソースコード中のシンボル情報を保管するもの Xcodeの定義ジャンプなどで使われている(はず) UnitとRecordに分かれている • Unit: コンパイルごとの入力ファイルやオプションを保持するもの • Record: シンボルの出現位置、識別子、役割などをまとめたもの 43

44.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore ソースコード中のシンボル情報を保管するもの Xcodeの定義ジャンプなどで使われている(はず) UnitとRecordに分かれている • Unit: コンパイルごとの入力ファイルやオプションを保持するもの • Record: シンボルの出現位置、識別子、役割などをまとめたもの USR 44 • definition • reference

45.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する • 2行10列目で • USR”XXX”のシンボルが • definitionとして 出現している 45

46.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する • 7行20列目で • USR”XXX”のシンボルが • referenceとして 出現している 46

47.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 「calledMethodは定数zeroから参照 されている」とはわからない… 「誰に参照されているか」はスコープ 単位でしかわからない さらに、関数内の定数/変数の definitionなシンボルはない… 47

48.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 「calledMethodはcallerMethod から参照されている」という依存関係 しかわからない 48

49.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 依存関係の構築に足りないこと • 参照する側が誰なのか IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference • 関数内の定数/変数の出現 「誰がどこで参照されているか」はわかる 「そこで誰が参照しているのか」がわからない 49

50.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference referenceの出現位置を定義範囲に含む、最も深い階層の宣言に よって参照されている 50

51.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference 出現位置と定義範囲を元に、参照する側を特定できそう SwiftSyntaxならできる 51

52.

52 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを 組み合わせる

53.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる
SwiftSyntaxで名前の開始位置を抽出できる
これを元にSwiftSyntaxとIndexSoreを組み合わせる
final class DeclarationVisitor: SyntaxVisitor {
private let sourceLocationConverter: SourceLocationConverter

}

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nameStartLocation = node.name.startLocation(converter: sourceLocationConverter)
let symbolLocation = Location(
fullPath: nameStartLocation.file,
line: nameStartLocation.line,
column: nameStartLocation.column
)
return .visitChildren
}

53

54.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 54

55.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 55

56.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

IndexStoreからシンボルの出現情報を抽出
IndexStoreから抽出するデータの型
struct IndexStoreResponse: Equatable, Sendable {
let definitionUSRs: [Location: [USR]]
let referenceOccurrences: [String: [Occurrence]]
}

definitionのシンボルについて、

• Key: 出現位置
• Value: definitionのUSR
を保持しておく

Visitorで走査する際に使う
struct Location: Equatable, Hashable, Sendable, Comparable {
let fullPath: String
let line: Int
let column: Int
static func < (lhs: Location, rhs: Location) -> Bool {. . .}
}
struct USR: Hashable, Sendable {
let value: String
}
struct Occurrence: Equatable, Hashable, Sendable {
let usr: USR
let location: Location
}

56

57.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

IndexStoreからシンボルの出現情報を抽出
IndexStoreから抽出するデータの型
struct IndexStoreResponse: Equatable, Sendable {
let definitionUSRs: [Location: [USR]]
let referenceOccurrences: [String: [Occurrence]]
}

referenceのシンボルについて、

• Key: 出現したファイルのパス
• Value: USRと出現位置
を保持しておく
参照する側を特定する際に使う

struct Location: Equatable, Hashable, Sendable, Comparable {
let fullPath: String
let line: Int
let column: Int
static func < (lhs: Location, rhs: Location) -> Bool {. . .}
}
struct USR: Hashable, Sendable {
let value: String
}
struct Occurrence: Equatable, Hashable, Sendable {
let usr: USR
let location: Location
}

57

58.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる IndexStoreからシンボルの出現情報を抽出 IndexStoreのRecordはバイナリ形式 Swiftを使ってIndexStoreを自力で読み取るのは大変 ライブラリを頼る 58

59.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる IndexStoreからシンボルの出現情報を抽出 SwiftIndexStore https://github.com/kateinoigakukun/swift-indexstore 59

60.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

IndexStoreからシンボルの出現情報を抽出
func extract(from indexStoreURL: URL, projectRootURL: URL) async throws -> IndexStoreResponse {
let indexStore = try IndexStore.open(store: indexStoreURL, lib: .open())
var definitionUSRs: [Location: Set[USR]] = [:]
var referenceOccurrences: [String: [Occurrence]] = [:]
try indexStore.forEachUnits { unit in
try indexStore.forEachRecordDependencies(for: unit) { dependency in
guard case let .record(record) = dependency,
let recordPath = record.filePath,
recordPath.starts(with: projectRootURL.path()) else {
return true
}
try indexStore.forEachOccurrences(for: record) { occurrence in
guard let occurrenceUSR = occurrence.symbol.usr,
let fullPath = occurrence.location.path else {
return true
}
let location = Location(
fullPath: fullPath,
line: Int(occurrence.location.line),
column: Int(occurrence.location.column),
)
if occurrence.roles.contains(.definition) {
definitionUSRs[location, default: []].append(USR(occurrenceUSR))
return true
}
if occurrence.roles.contains(.reference),
let referencedUSR = occurrence.symbol.usr {
referenceOccurrences[fullPath, default: []].append(Occurrence(usr: USR(referencedUSR), location: location))
}
return true
} // try indexStore.forEachOccurrences(for: record)
return true
} // try indexStore.forEachRecordDependencies(for: unit)
return true
} // try indexStore.forEachUnits

}

return IndexStoreResponse(
definitionUSRs: definitionUSRs,
referenceOccurrences: referenceOccurrences,
)

60

61.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

IndexStoreからシンボルの出現情報を抽出
func extract(from indexStoreURL: URL, projectRootURL: URL) async throws -> IndexStoreResponse {
let indexStore = try IndexStore.open(store: indexStoreURL, lib: .open())
var definitionUSRs: [Location: Set[USR]] = [:]
var referenceOccurrences: [String: [Occurrence]] = [:]
try indexStore.forEachUnits { unit in
try indexStore.forEachRecordDependencies(for: unit) { dependency in
guard case let .record(record) = dependency,
let recordPath = record.filePath,
recordPath.starts(with: projectRootURL.path()) else {
return true
}
try indexStore.forEachOccurrences(for: record) { occurrence in
guard let occurrenceUSR = occurrence.symbol.usr,
let fullPath = occurrence.location.path else {
return true
}
let location = Location(
fullPath: fullPath,
line: Int(occurrence.location.line),
column: Int(occurrence.location.column),
)

シンボルの出現位置
構文木の名前の開始位置と一致する

if occurrence.roles.contains(.definition) {
definitionUSRs[location, default: []].append(USR(occurrenceUSR))
return true
}
if occurrence.roles.contains(.reference),
let referencedUSR = occurrence.symbol.usr {
referenceOccurrences[fullPath, default: []].append(Occurrence(usr: USR(referencedUSR), location: location))
}
return true
} // try indexStore.forEachOccurrences(for: record)
return true
} // try indexStore.forEachRecordDependencies(for: unit)
return true
} // try indexStore.forEachUnits

}

return IndexStoreResponse(
definitionUSRs: definitionUSRs,
referenceOccurrences: referenceOccurrences,
)

61

62.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

IndexStoreからシンボルの出現情報を抽出
func extract(from indexStoreURL: URL, projectRootURL: URL) async throws -> IndexStoreResponse {
let indexStore = try IndexStore.open(store: indexStoreURL, lib: .open())
var definitionUSRs: [Location: Set[USR]] = [:]
var referenceOccurrences: [String: [Occurrence]] = [:]
try indexStore.forEachUnits { unit in
try indexStore.forEachRecordDependencies(for: unit) { dependency in
guard case let .record(record) = dependency,
let recordPath = record.filePath,
recordPath.starts(with: projectRootURL.path()) else {
return true
}
try indexStore.forEachOccurrences(for: record) { occurrence in
guard let occurrenceUSR = occurrence.symbol.usr,
let fullPath = occurrence.location.path else {
return true
}
let location = Location(
fullPath: fullPath,
line: Int(occurrence.location.line),
column: Int(occurrence.location.column),
)

definitionとreferenceで分岐する

if occurrence.roles.contains(.definition) {
definitionUSRs[location, default: []].append(USR(occurrenceUSR))
return true
}
if occurrence.roles.contains(.reference),
let referencedUSR = occurrence.symbol.usr {
referenceOccurrences[fullPath, default: []].append(Occurrence(usr: USR(referencedUSR), location: location))
}
return true
} // try indexStore.forEachOccurrences(for: record)
return true
} // try indexStore.forEachRecordDependencies(for: unit)
return true
} // try indexStore.forEachUnits

}

return IndexStoreResponse(
definitionUSRs: definitionUSRs,
referenceOccurrences: referenceOccurrences,
)

62

63.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 63

64.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる SwiftSyntaxのVisitorで構文木を走査する struct Declaration: Identifiable, Sendable { let id: UUID let name: String let kind: Kind let sourceLocationRange: ClosedRange<Location> var definitionUSRs: [USR] definitionのUSRを保持するプロパティを var sortedChildren: [Self] = [] } var nestingEnums: [Self] = [] var functions: [Self] = [] var nestingStructs: [Self] = [] 追加する 64

65.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる SwiftSyntaxのVisitorで構文木を走査する struct Declaration: Identifiable, Sendable { let id: UUID let name: String let kind: Kind let sourceLocationRange: ClosedRange<Location> var definitionUSRs: [USR] 直下の子要素を定義位置でソートしたプロパティを var sortedChildren: [Self] = [] 追加する } var nestingEnums: [Self] = [] var functions: [Self] = [] var nestingStructs: [Self] = [] 65

66.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

SwiftSyntaxのVisitorで構文木を走査する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
private let indexStoreResponse: IndexStoreResponse
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nameStartLocation = node.name.startLocation(converter: sourceLocationConverter)
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
line: start.line,
column: start.column
)
... Location(
fullPath: end.file,
line: end.line,
column: end.column
)
let symbolLocation = Location(
fullPath: nameStartLocation.file,
line: nameStartLocation.line,
column: nameStartLocation.column
)
let declaration = Declaration(
id: UUID(),
name: node.name.text,
kind: .struct,
sourceLocationRange: sourceLocationRange,
definitionUSRs: indexStoreResponse.definitionUSRs[symbolLocation] ?? [USR(UUID().uuidString)]
)
declarationsStack.append(declaration)

}

}

return .visitChildren

66

67.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

SwiftSyntaxのVisitorで構文木を走査する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
private let indexStoreResponse: IndexStoreResponse

IndexStoreから抽出したデータを受け取る

override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nameStartLocation = node.name.startLocation(converter: sourceLocationConverter)
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
line: start.line,
column: start.column
)
... Location(
fullPath: end.file,
line: end.line,
column: end.column
)
let symbolLocation = Location(
fullPath: nameStartLocation.file,
line: nameStartLocation.line,
column: nameStartLocation.column
)
let declaration = Declaration(
id: UUID(),
name: node.name.text,
kind: .struct,
sourceLocationRange: sourceLocationRange,
definitionUSRs: indexStoreResponse.definitionUSRs[symbolLocation] ?? [USR(UUID().uuidString)]
)
declarationsStack.append(declaration)

}

}

return .visitChildren

67

68.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

SwiftSyntaxのVisitorで構文木を走査する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
private let indexStoreResponse: IndexStoreResponse
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nameStartLocation = node.name.startLocation(converter: sourceLocationConverter)
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
line: start.line,
column: start.column
)
... Location(
fullPath: end.file,
line: end.line,
column: end.column
)
let symbolLocation = Location(
fullPath: nameStartLocation.file,
line: nameStartLocation.line,
column: nameStartLocation.column
)
let declaration = Declaration(
id: UUID(),
name: node.name.text,
kind: .struct,
sourceLocationRange: sourceLocationRange,
definitionUSRs: indexStoreResponse.definitionUSRs[symbolLocation] ?? [USR(UUID().uuidString)]
)
declarationsStack.append(declaration)

名前の開始位置が、definitionなシンボルの出現位置と
同じことを利用する

DictionaryにKeyとして渡してUSRを取り出し、宣言の
情報と一緒に保持させる

}

}

return .visitChildren

68

69.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

SwiftSyntaxのVisitorで構文木を走査する
final class DeclarationVisitor: SyntaxVisitor {
private var declarationsStack: [Declaration] = []
private(set) var topDeclarations: [Declaration] = []
private let sourceLocationConverter: SourceLocationConverter
private let indexStoreResponse: IndexStoreResponse
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
let nameStartLocation = node.name.startLocation(converter: sourceLocationConverter)
let nodeRange = node.sourceRange(converter: sourceLocationConverter)
let start = nodeRange.start
let end = nodeRange.end
let sourceLocationRange = Location(
fullPath: start.file,
line: start.line,
column: start.column
)
... Location(
fullPath: end.file,
line: end.line,
column: end.column
)
let symbolLocation = Location(
fullPath: nameStartLocation.file,
line: nameStartLocation.line,
column: nameStartLocation.column
)
let declaration = Declaration(
id: UUID(),
name: node.name.text,
kind: .struct,
sourceLocationRange: sourceLocationRange,
definitionUSRs: indexStoreResponse.definitionUSRs[symbolLocation] ?? [USR(UUID().uuidString)]
)
declarationsStack.append(declaration)

関数の中で宣言されている定数/変数などはdefinitionの
USRが存在しないので、一意なStringを代わりに渡す
これは後で依存関係をDictionaryとして構築する際にKey
になる

}

}

return .visitChildren

69

70.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる SwiftSyntaxのVisitorで構文木を走査する override func visitPost(_ node: StructDeclSyntax) { guard var structDeclaration = declarationsStack.popLast() else { return } // 子要素の配列を合成して定義位置でソートしたものをstructDeclaration.sortedChildrenに格納する if declarationsStack.isEmpty { topDeclarations .append(structDeclaration) return } if let lastIndex = declarationsStack.indices.last { declarationsStack[lastIndex] .nestingStructs.append(structDeclaration) } } 70

71.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 71

72.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 依存関係を構築する 依存関係を保持する型 public struct DependenciesStore: Equatable, Sendable { public var referrerUSRs: [USR: [USR]] Keyが参照される側、Valueが参照する側 public var referencedUSRs: [USR: [USR]] mutating func merge(with other: Self) { referrerUSRs.merge(other.referrerUSRs) { first, second in first + second } referencedUSRs.merge(other.referencedUSRs) { first, second in first + second } } } 72

73.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 依存関係を構築する 依存関係を保持する型 public struct DependenciesStore: Equatable, Sendable { public var referrerUSRs: [USR: [USR]] Keyが参照する側、Valueが参照される側 public var referencedUSRs: [USR: [USR]] mutating func merge(with other: Self) { referrerUSRs.merge(other.referrerUSRs) { first, second in first + second } referencedUSRs.merge(other.referencedUSRs) { first, second in first + second } } } 73

74.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
import Algorithms
enum DependenciesStoreGenerator {
static func generateWithDeclaration(_ declaration: Declaration, occurrence: Occurrence) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let child = declaration.sortedChildren.declaration(containing: occurrence.location) else {
declaration.definitionUSRs.forEach { referrerUSR in
store.referrerUSRs[occurrence.usr, default: []].append(referrerUSR)
store.referencedUSRs[referrerUSR, default: []].append(occurrence.usr)
}
return store
}

}

}

return generateWithDeclaration(child, occurrence: occurrence)

private extension [AbstractDeclaration] {
func declaration(containing location: Location) -> Element? {
guard !isEmpty else {
return nil
}

referenceとして出現したシンボル
ごとに、これを誰が参照しているのか
特定する

let index = partitioningIndex { $0.sourceLocationRange.contains(location) }

}

}

return index == endIndex ? nil : self[index]

74

75.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
import Algorithms
enum DependenciesStoreGenerator {
static func generateWithDeclaration(_ declaration: Declaration, occurrence: Occurrence) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let child = declaration.sortedChildren.declaration(containing: occurrence.location) else {
declaration.definitionUSRs.forEach { referrerUSR in
store.referrerUSRs[occurrence.usr, default: []].append(referrerUSR)
store.referencedUSRs[referrerUSR, default: []].append(occurrence.usr)
}
return store
}

出現位置を定義範囲に含む子要素を探す

}

}

return generateWithDeclaration(child, occurrence: occurrence)

private extension [AbstractDeclaration] {
func declaration(containing location: Location) -> Element? {
guard !isEmpty else {
return nil
}
let index = partitioningIndex { $0.sourceLocationRange.contains(location) }

}

}

return index == endIndex ? nil : self[index]

75

76.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
import Algorithms
enum DependenciesStoreGenerator {
static func generateWithDeclaration(_ declaration: Declaration, occurrence: Occurrence) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let child = declaration.sortedChildren.declaration(containing: occurrence.location) else {
declaration.definitionUSRs.forEach { referrerUSR in
store.referrerUSRs[occurrence.usr, default: []].append(referrerUSR)
store.referencedUSRs[referrerUSR, default: []].append(occurrence.usr)
}
return store
}

子要素はソート済みなのでSwift

}

}

return generateWithDeclaration(child, occurrence: occurrence)

Algorithmsで二分探索すると速い

private extension [AbstractDeclaration] {
func declaration(containing location: Location) -> Element? {
guard !isEmpty else {
return nil
}
let index = partitioningIndex { $0.sourceLocationRange.contains(location) }

}

}

return index == endIndex ? nil : self[index]

76

77.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
import Algorithms
enum DependenciesStoreGenerator {
static func generateWithDeclaration(_ declaration: Declaration, occurrence: Occurrence) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let child = declaration.sortedChildren.declaration(containing: occurrence.location) else {
declaration.definitionUSRs.forEach { referrerUSR in
store.referrerUSRs[occurrence.usr, default: []].append(referrerUSR)
store.referencedUSRs[referrerUSR, default: []].append(occurrence.usr)
}
return store
}

}

}

return generateWithDeclaration(child, occurrence: occurrence)

子要素が見つかれば、さらにその子要素
について再帰的に探す

private extension [AbstractDeclaration] {
func declaration(containing location: Location) -> Element? {
guard !isEmpty else {
return nil
}
let index = partitioningIndex { $0.sourceLocationRange.contains(location) }

}

}

return index == endIndex ? nil : self[index]

77

78.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
import Algorithms
enum DependenciesStoreGenerator {
static func generateWithDeclaration(_ declaration: Declaration, occurrence: Occurrence) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let child = declaration.sortedChildren.declaration(containing: occurrence.location) else {
declaration.definitionUSRs.forEach { referrerUSR in
store.referrerUSRs[occurrence.usr, default: []].append(referrerUSR)
store.referencedUSRs[referrerUSR, default: []].append(occurrence.usr)
}
return store
}

}

}

return generateWithDeclaration(child, occurrence: occurrence)

private extension [AbstractDeclaration] {
func declaration(containing location: Location) -> Element? {
guard !isEmpty else {
return nil
}

子要素が見つからなければ、現在注目し
ている宣言がシンボルを参照している
参照する側とされる側のDictionaryに
USRを追加して返す

let index = partitioningIndex { $0.sourceLocationRange.contains(location) }

}

}

return index == endIndex ? nil : self[index]

78

79.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
enum DependenciesStoreGenerator {
static func generateWithFile(_ file: File, indexStoreResponse: IndexStoreResponse) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let occurrences = indexStoreResponse.referenceOccurrences[file.fullPath] else {
return store
}
for occurrence in occurrences {
guard let topDeclaration = file.topDeclarations.first(where: {
$0.sourceLocationRange.contains(occurrence.location)
}) else {
continue
}
}

}

}

ファイル内のreferenceに対して
依存関係を構築する

store.merge(with: generateWithDeclaration(topDeclaration, occurrence: occurrence))

return store

public struct File: Identifiable, Equatable, Hashable, Sendable {
public var id: String {
fullPath
}
public let fullPath: String

}

public let sourceCode: String
public internal(set) var topDeclarations: IdentifiedArrayOf<AbstractDeclaration>

79

80.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
enum DependenciesStoreGenerator {
static func generateWithFile(_ file: File, indexStoreResponse: IndexStoreResponse) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let occurrences = indexStoreResponse.referenceOccurrences[file.fullPath] else {
return store
}
for occurrence in occurrences {
guard let topDeclaration = file.topDeclarations.first(where: {
$0.sourceLocationRange.contains(occurrence.location)
}) else {
continue
}
}

}

}

ファイルのパスをKeyとして
referenceのシンボルを取り出
せるようにしていた

store.merge(with: generateWithDeclaration(topDeclaration, occurrence: occurrence))

return store

public struct File: Identifiable, Equatable, Hashable, Sendable {
public var id: String {
fullPath
}
public let fullPath: String

}

public let sourceCode: String
public internal(set) var topDeclarations: IdentifiedArrayOf<AbstractDeclaration>

80

81.
[beta]
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう!

SwiftSyntaxとIndexStoreを組み合わせる

依存関係を構築する
enum DependenciesStoreGenerator {
static func generateWithFile(_ file: File, indexStoreResponse: IndexStoreResponse) -> DependenciesStore {
var store = DependenciesStore(referrerUSRs: [:], referencedUSRs: [:])
guard let occurrences = indexStoreResponse.referenceOccurrences[file.fullPath] else {
return store
}
for occurrence in occurrences {
guard let topDeclaration = file.topDeclarations.first(where: {
$0.sourceLocationRange.contains(occurrence.location)
}) else {
continue
}
}

}

}

store.merge(with: generateWithDeclaration(topDeclaration, occurrence: occurrence))

return store

public struct File: Identifiable, Equatable, Hashable, Sendable {
public var id: String {
fullPath
}

シンボルごとに依存関係を構築して、
Dictionaryをマージして返す

public let fullPath: String

}

public let sourceCode: String
public internal(set) var topDeclarations: IdentifiedArrayOf<AbstractDeclaration>

81

82.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 82

83.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする ツールを使用する流れ 1. ユーザーがファイルを選択する 2. ツールがファイル内の宣言をリスト表示する 3. ユーザーが依存関係を調べたい宣言を選択する 4. ツールが依存関係を表示する 83

84.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする ツールを使用する流れ 1. ユーザーがファイルを選択する 2. ツールがファイル内の宣言をリスト表示する 3. ユーザーが依存関係を調べたい宣言を選択する 4. ツールが依存関係を表示する definitionのUSRがわかる 依存関係は既に取り出せるので、USRで宣言も 取り出せるようにする 84

85.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする • 宣言のインスタンスはSingle Source of Truthを守りたい • USRから宣言のインスタンスに、高速にアクセスしたい USRをKey、プロジェクトのディレクトリに対応するインスタンス から宣言までのKeyPathをValueとするDictionaryを作る 85

86.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする KeyPathの基礎からDictionaryの生成まで解説しています https://www.docswell.com/s/Ockey/K37YJM-pixiv-App-Night-KeyPath/1 86

87.

87 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! まとめ

88.

SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! まとめ • SwiftSyntaxを使って階層構造を維持したまま宣言を抽出できる • IndexStoreを使ってシンボルの出現を抽出できる • 2つを合わせることで依存関係を構築できる • DictionaryとKeyPathが便利 88