1.7K Views
October 02, 25
スライド概要
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせて Swiftコードベースの理解を深めるツールを 開発しよう! Ockey 1
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! product|Palcy Ockey 2025年4月にピクシブに新卒入社 好きなSwift • KeyPath • enum • Argument Label 2
Jump to Definition 3 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね 「どこで定義されているか」だけでなく、「どこで参照されているか」 を調べる機能もある • Show Callers… • Find → Find Selected Symbol in Workspace • Find → Find Call Hierarchy 4
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! Xcodeって便利ですよね もっと便利にしたい • 型に対して使いたい • 依存関係のリストと静的構造を同時に見たい • 辿ってきた依存関係を遡りたい SwiftSyntaxなどが公開されているので、好みのツールを 実装できる 5
6
https://github.com/Ockey12/swift-kraken 7 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! 実装したツール
Swiftで書かれたコードから、宣言と依存関係を抽出したい 8 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! ツールを実装するために
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! テーマ 1. SwiftSyntax 2. IndexStore 3. SwiftSyntaxとIndexStoreを組み合わせる 9
10 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Swiftで書かれたコードに忠実な構文木を扱うライブラリ Macrosの登場で使われるようになってきた https://github.com/swiftlang/swift-syntax 11
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 構文木を見てみる 簡単な5行のコードから構文木を生成する struct SomeType { func method() { let constant: Int = 0 } } 12
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
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
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
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax DeclSyntax 宣言を表すノードの型が決まっている 対象 DeclSyntax struct class enum actor protocol extension func let/var StructDeclSyntax ClassDeclSyntax EnumDeclSyntax ActorDeclSyntax ProtocolDeclSyntax ExtensionDeclSyntax FunctionDeclSyntax VariableDeclSyntax 17
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax 宣言抽出の流れ 1. SwiftソースコードをString型の定数に格納しておく 2. Parser構造体でソースコードから構文木を生成する 3. Visitorクラスで構文木を走査する ① visitでインスタンスを生成してスタックにpushする ② visitPostでスタックからpopして親/出力用配列に格納する 19
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Parser swift-syntaxのSwiftParserにParser構造体がある parseメソッドにString型のSwiftソースコードを渡すと、 構文木の最上位であるSourceFileSyntaxを返す import SwiftParser let sourceFileSyntax = Parser.parse(source: sourceCode) 20
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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")
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntax Visitorによる走査 静的構造を抽出できた struct SomeStruct { enum NestedEnum { static func nestedMethod() {} } } func notNestedMethod() {} 40 SomeStruct ├─NestedEnum │ ╰─nestedMethod ╰─notNestedMethod
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 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore ソースコード中のシンボル情報を保管するもの Xcodeの定義ジャンプなどで使われている(はず) UnitとRecordに分かれている • Unit: コンパイルごとの入力ファイルやオプションを保持するもの • Record: シンボルの出現位置、識別子、役割などをまとめたもの 43
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore ソースコード中のシンボル情報を保管するもの Xcodeの定義ジャンプなどで使われている(はず) UnitとRecordに分かれている • Unit: コンパイルごとの入力ファイルやオプションを保持するもの • Record: シンボルの出現位置、識別子、役割などをまとめたもの USR 44 • definition • reference
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する • 2行10列目で • USR”XXX”のシンボルが • definitionとして 出現している 45
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する • 7行20列目で • USR”XXX”のシンボルが • referenceとして 出現している 46
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 「calledMethodは定数zeroから参照 されている」とはわからない… 「誰に参照されているか」はスコープ 単位でしかわからない さらに、関数内の定数/変数の definitionなシンボルはない… 47
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 「calledMethodはcallerMethod から参照されている」という依存関係 しかわからない 48
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する 依存関係の構築に足りないこと • 参照する側が誰なのか IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference • 関数内の定数/変数の出現 「誰がどこで参照されているか」はわかる 「そこで誰が参照しているのか」がわからない 49
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference referenceの出現位置を定義範囲に含む、最も深い階層の宣言に よって参照されている 50
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! IndexStore IndexStoreから依存関係を構築する IndexStoreからわかること • シンボルの識別子(USR) • シンボルの出現位置 • 出現の役割 • definition • reference 出現位置と定義範囲を元に、参照する側を特定できそう SwiftSyntaxならできる 51
52 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを 組み合わせる
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 54
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 55
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
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる IndexStoreからシンボルの出現情報を抽出 IndexStoreのRecordはバイナリ形式 Swiftを使ってIndexStoreを自力で読み取るのは大変 ライブラリを頼る 58
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる IndexStoreからシンボルの出現情報を抽出 SwiftIndexStore https://github.com/kateinoigakukun/swift-indexstore 59
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
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
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 63
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
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
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
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
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
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
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 71
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
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
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
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
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
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
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
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
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
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
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる 1. IndexStoreからシンボルの出現情報を抽出する 2. SwiftSyntaxのVisitorで構文木を走査する 3. 依存関係を構築する 4. USRを元に宣言と依存関係を取り出せるようにする 82
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする ツールを使用する流れ 1. ユーザーがファイルを選択する 2. ツールがファイル内の宣言をリスト表示する 3. ユーザーが依存関係を調べたい宣言を選択する 4. ツールが依存関係を表示する 83
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする ツールを使用する流れ 1. ユーザーがファイルを選択する 2. ツールがファイル内の宣言をリスト表示する 3. ユーザーが依存関係を調べたい宣言を選択する 4. ツールが依存関係を表示する definitionのUSRがわかる 依存関係は既に取り出せるので、USRで宣言も 取り出せるようにする 84
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする • 宣言のインスタンスはSingle Source of Truthを守りたい • USRから宣言のインスタンスに、高速にアクセスしたい USRをKey、プロジェクトのディレクトリに対応するインスタンス から宣言までのKeyPathをValueとするDictionaryを作る 85
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! SwiftSyntaxとIndexStoreを組み合わせる USRを元に宣言と依存関係を取り出せるようにする KeyPathの基礎からDictionaryの生成まで解説しています https://www.docswell.com/s/Ockey/K37YJM-pixiv-App-Night-KeyPath/1 86
87 SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! まとめ
SwiftSyntaxとIndexStoreを組み合わせてSwiftコードベースの理解を深めるツールを開発しよう! まとめ • SwiftSyntaxを使って階層構造を維持したまま宣言を抽出できる • IndexStoreを使ってシンボルの出現を抽出できる • 2つを合わせることで依存関係を構築できる • DictionaryとKeyPathが便利 88