f0bd10ef66
Automated-By: Claude Sonnet 4.6
116 lines
3.9 KiB
Swift
116 lines
3.9 KiB
Swift
import Foundation
|
|
import ArgumentParser
|
|
|
|
let buildSignature = "CODEMAPPER-SIGNATURE-izackp"
|
|
|
|
struct CodeMapper: ParsableCommand {
|
|
static let configuration = CommandConfiguration(
|
|
commandName: "CodeMapper",
|
|
abstract: "Generate LLM-friendly architectural maps from Swift source files."
|
|
)
|
|
|
|
|
|
@Option(name: .long, help: "Root of the Swift package to analyze.")
|
|
var sources: String
|
|
|
|
@Option(name: .long, help: "Only process this target name.")
|
|
var filter: String?
|
|
|
|
@Option(name: .long, help: "Path to sourcekit-lsp binary.")
|
|
var lspPath: String = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/sourcekit-lsp"
|
|
|
|
@Option(name: .long, help: "Only write .map files for paths under this folder or matching this file.")
|
|
var path: String?
|
|
|
|
@Flag(name: .long, help: "Omit >> and << call graph lines (faster, fewer tokens).")
|
|
var noCalls: Bool = false
|
|
|
|
@Flag(name: .long, help: "Show only outgoing >> calls, omit << incoming callers.")
|
|
var outgoingOnly: Bool = false
|
|
|
|
@Flag(name: .long, help: "Print map output to stdout instead of writing .map files.")
|
|
var stdout: Bool = false
|
|
|
|
mutating func run() throws {
|
|
let packageRoot = (sources as NSString).standardizingPath
|
|
|
|
log("CodeMapper starting...")
|
|
log("Package root: \(packageRoot)")
|
|
log("LSP: \(lspPath)")
|
|
|
|
let symbolTable = SymbolTable()
|
|
|
|
let walker = SourceWalker(packageRoot: packageRoot, filter: filter)
|
|
let files = walker.discoverFiles(symbolTable: symbolTable)
|
|
|
|
guard !files.isEmpty else {
|
|
log("No Swift files found.")
|
|
return
|
|
}
|
|
log("Found \(files.count) Swift files.")
|
|
|
|
let lsp = try LSPClient(lspPath: lspPath, projectRoot: packageRoot)
|
|
log("Initializing LSP...")
|
|
try lsp.initialize()
|
|
log("LSP ready.")
|
|
|
|
let extractor = SymbolExtractor(lsp: lsp, symbolTable: symbolTable)
|
|
|
|
log("Pass 1: extracting symbols...")
|
|
for (i, (filePath, targetName)) in files.enumerated() {
|
|
if (i + 1) % 20 == 0 { log(" \(i + 1)/\(files.count)") }
|
|
do {
|
|
try extractor.process(filePath: filePath, targetName: targetName)
|
|
} catch {
|
|
fputs("Warning: symbol extraction failed for \(filePath): \(error)\n", stderr)
|
|
}
|
|
}
|
|
log(" \(files.count)/\(files.count) done.")
|
|
|
|
if !noCalls {
|
|
let callBuilder = CallGraphBuilder(lsp: lsp, symbolTable: symbolTable)
|
|
log("Pass 1b: building call graph (may be slow on first run)...")
|
|
for (i, (filePath, _)) in files.enumerated() {
|
|
if (i + 1) % 10 == 0 { log(" \(i + 1)/\(files.count)") }
|
|
do {
|
|
try callBuilder.process(filePath: filePath)
|
|
} catch {
|
|
fputs("Warning: call graph failed for \(filePath): \(error)\n", stderr)
|
|
}
|
|
}
|
|
log(" \(files.count)/\(files.count) done.")
|
|
}
|
|
|
|
log("Pass 2: building reverse index...")
|
|
symbolTable.buildReverseIndex()
|
|
symbolTable.buildImplementorMap()
|
|
|
|
let resolvedPath = path.map { ($0 as NSString).standardizingPath }
|
|
let writer = OutputWriter(symbolTable: symbolTable, packageRoot: packageRoot, includeCalls: !noCalls, outgoingOnly: outgoingOnly, pathFilter: resolvedPath)
|
|
|
|
if stdout {
|
|
try writer.printAll()
|
|
} else {
|
|
log("Writing .map files...")
|
|
try writer.writeAll()
|
|
log("Done. Wrote \(symbolTable.fileTargets.count) .swift.map files.")
|
|
}
|
|
|
|
try lsp.shutdownGracefully()
|
|
}
|
|
|
|
private func log(_ msg: String) {
|
|
fputs(msg + "\n", stderr)
|
|
}
|
|
}
|
|
|
|
// Ignore SIGPIPE so LSP subprocess death returns an error instead of killing this process
|
|
signal(SIGPIPE, SIG_IGN)
|
|
|
|
if CommandLine.arguments.contains("--signature") {
|
|
print(buildSignature)
|
|
exit(0)
|
|
}
|
|
|
|
CodeMapper.main()
|