Files
CodeMapper/Sources/CodeMapper/main.swift
T
2026-05-28 22:24:48 -04:00

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()