Skip to content

Commit bf1694c

Browse files
committed
classify contents within files by suing swift-syntax library
1 parent 4ce5708 commit bf1694c

File tree

2 files changed

+169
-2
lines changed

2 files changed

+169
-2
lines changed

Core/Sources/Service/MultiFileContext/MultiFileContextManager.swift

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import Foundation
22
import Workspace
33
import XcodeInspector
4+
import SwiftSyntax
5+
import SwiftParser
46

57
class MultiFileContextManager {
6-
let workspaceProvider: WorkspaceProvider
8+
private let workspaceProvider: WorkspaceProvider
9+
10+
// private static let classificationKeywords: [String] = ["class", "struct", "enum", "actor", "protocol", "func", "var", "let"]
11+
// private static let classificationKeywordsWithSpecialCases: [String] = ["extension", "typealias"]
712

813
init(workspaceProvider: WorkspaceProvider) {
914
self.workspaceProvider = workspaceProvider
@@ -41,9 +46,162 @@ class MultiFileContextManager {
4146
}
4247
}
4348
}
49+
50+
func classifyContentWithinFile() async -> [String: SymbolContent] {
51+
let fileContents = await readFileContents()
52+
var result: [String: SymbolContent] = [:]
53+
54+
for file in fileContents {
55+
guard let fileURL = URL(string: file.fileURL) else { continue }
56+
do {
57+
let sourceFile = Parser.parse(source: file.content)
58+
let converter = SourceLocationConverter(fileName: file.fileURL, tree: sourceFile)
59+
let collector = DeclarationCollector(sourceLocationConverter: converter, sourceText: file.content)
60+
collector.walk(sourceFile)
61+
result[file.fileName] = file.mapToSymbolContent(symbols: collector.symbols)
62+
} catch {
63+
print("SwiftSyntax parse failed for \(file.fileURL):", error)
64+
}
65+
}
66+
67+
return result
68+
}
69+
4470
}
4571

4672
struct FileContent {
4773
let fileURL: String
4874
let content: String
75+
76+
var fileName: String {
77+
let fileNameWithExtension = String(fileURL.split(separator: "/").last ?? "")
78+
let fileName: String = fileNameWithExtension.replacingOccurrences(of: ".swift", with: "")
79+
return fileName
80+
}
81+
}
82+
83+
extension FileContent {
84+
func mapToSymbolContent(symbols: [SymbolInfo]) -> SymbolContent {
85+
SymbolContent(fileURL: fileURL, content: content, symbols: symbols)
86+
}
87+
}
88+
89+
struct SymbolContent {
90+
let fileURL: String
91+
let content: String
92+
let symbols: [SymbolInfo]
93+
}
94+
95+
enum ClassificationKeywords: String {
96+
case classWord = "class"
97+
case structWord = "struct"
98+
case enumWord = "enum"
99+
case actorWord = "actor"
100+
case protocolWord = "protocol"
101+
case funcWord = "func"
102+
case varWord = "var"
103+
case letWord = "let"
104+
case extensionWord = "extension"
105+
case typealiasWord = "typealias"
106+
}
107+
108+
struct SymbolInfo {
109+
let name: String
110+
let kind: String
111+
let startLine: Int
112+
let endLine: Int
113+
let content: String
114+
}
115+
116+
import SwiftSyntax
117+
//import SwiftSyntaxParser
118+
119+
class DeclarationCollector: SyntaxVisitor {
120+
var symbols: [SymbolInfo] = []
121+
let sourceLocationConverter: SourceLocationConverter
122+
let sourceText: String
123+
124+
init(sourceLocationConverter: SourceLocationConverter, sourceText: String) {
125+
self.sourceLocationConverter = sourceLocationConverter
126+
self.sourceText = sourceText
127+
super.init(viewMode: .all)
128+
}
129+
130+
// override func visit(_ node: ImportDeclSyntax) -> SyntaxVisitorContinueKind {
131+
// recordSymbol(name: node.name.text, kind: "import", node: node)
132+
// return .skipChildren
133+
// }
134+
135+
override func visit(_ node: ClassDeclSyntax) -> SyntaxVisitorContinueKind {
136+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.classWord.rawValue, node: node)
137+
return .skipChildren
138+
}
139+
140+
override func visit(_ node: StructDeclSyntax) -> SyntaxVisitorContinueKind {
141+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.structWord.rawValue, node: node)
142+
return .skipChildren
143+
}
144+
145+
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorContinueKind {
146+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.enumWord.rawValue, node: node)
147+
return .skipChildren
148+
}
149+
150+
override func visit(_ node: ProtocolDeclSyntax) -> SyntaxVisitorContinueKind {
151+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.protocolWord.rawValue, node: node)
152+
return .skipChildren
153+
}
154+
155+
override func visit(_ node: ActorDeclSyntax) -> SyntaxVisitorContinueKind {
156+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.actorWord.rawValue, node: node)
157+
return .skipChildren
158+
}
159+
160+
override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
161+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.funcWord.rawValue, node: node)
162+
return .skipChildren
163+
}
164+
165+
override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind {
166+
guard let binding = node.bindings.first,
167+
let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else {
168+
return .skipChildren
169+
}
170+
171+
let keyword = node.bindingSpecifier.text // "let" or "var"
172+
recordSymbol(name: pattern.identifier.text, kind: keyword, node: node)
173+
return .skipChildren
174+
}
175+
176+
override func visit(_ node: ExtensionDeclSyntax) -> SyntaxVisitorContinueKind {
177+
let name = node.extendedType.trimmedDescription
178+
recordSymbol(name: name, kind: ClassificationKeywords.extensionWord.rawValue, node: node)
179+
return .skipChildren
180+
}
181+
182+
override func visit(_ node: TypeAliasDeclSyntax) -> SyntaxVisitorContinueKind {
183+
recordSymbol(name: node.name.text, kind: ClassificationKeywords.typealiasWord.rawValue, node: node)
184+
return .skipChildren
185+
}
186+
187+
private func recordSymbol(name: String, kind: String, node: SyntaxProtocol) {
188+
let startLoc = sourceLocationConverter.location(for: node.positionAfterSkippingLeadingTrivia)
189+
let endLoc = sourceLocationConverter.location(for: node.endPositionBeforeTrailingTrivia)
190+
let startLineIndex = startLoc.line - 1
191+
let endLineIndex = endLoc.line - 1
192+
193+
let lines = sourceText.split(separator: "\n", omittingEmptySubsequences: false)
194+
195+
let contentLines = lines[startLineIndex...min(endLineIndex, lines.count - 1)]
196+
let content = contentLines.joined(separator: "\n")
197+
198+
let symbol = SymbolInfo(
199+
name: name,
200+
kind: kind,
201+
startLine: startLoc.line,
202+
endLine: endLoc.line,
203+
content: content
204+
)
205+
symbols.append(symbol)
206+
}
49207
}

Core/Tests/ServiceTests/MultiFileContextManagerTests.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,22 @@ class MultiFileContextManagerTests: XCTestCase {
2424

2525
XCTAssertNotEqual(files.count, 0)
2626
}
27+
28+
func testClassifyingCode() async {
29+
let sut = sut
30+
let classifiedFiles = await sut.classifyContentWithinFile()
31+
// symbols
32+
XCTAssertNotEqual(classifiedFiles.count, 0)
33+
}
2734
}
2835

2936

3037
class WorkspaceProviderMock: WorkspaceProvider {
3138
func workspace() async throws -> Workspace? {
3239
// let workspaceURL = URL(fileURLWithPath: "/Users/christopherknapp/repos/CopilotForXcode-Fork/Copilot for Xcode.xcworkspace")
33-
let workspaceURL = URL(fileURLWithPath: "/Users/christopherknapp/repos/clean-architecture-swiftui-fork")
40+
// let workspaceURL = URL(fileURLWithPath: "/Users/christopherknapp/repos/clean-architecture-swiftui-fork")
41+
// let workspaceURL = URL(fileURLWithPath: "/Users/christopherknapp/repos/ios-minttv")
42+
let workspaceURL = URL(fileURLWithPath: "/Users/christopherknapp/repos/ios-minttv/Pod/Classes/Twitch/TwitchEndpoint.swift")
3443
return Workspace(workspaceURL: workspaceURL)
3544
}
3645
}

0 commit comments

Comments
 (0)