diff --git a/Package.swift b/Package.swift index 783272c..29598e8 100644 --- a/Package.swift +++ b/Package.swift @@ -14,11 +14,13 @@ var useLocalDependencies: Bool { hasEnvironmentVariable("SWIFTCI_USE_LOCAL_DEPS" var dependencies: [Package.Dependency] { if useLocalDependencies { return [ - .package(path: "../swift-lmdb") + .package(path: "../swift-lmdb"), + .package(path: "../swift-argument-parser"), ] } else { return [ - .package(url: "https://github.com/swiftlang/swift-lmdb.git", branch: "main") + .package(url: "https://github.com/swiftlang/swift-lmdb.git", branch: "main"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), ] } } @@ -47,6 +49,11 @@ let package = Package( name: "IndexStore", targets: ["IndexStore"] ), + // --- ADDED: The Executable Product --- + .executable( + name: "index-dump", + targets: ["index-dump"] + ), ], dependencies: dependencies, targets: [ @@ -76,7 +83,7 @@ let package = Package( ), // C API of libIndexStore that can be dlopen'ed from the IndexStore target - .target(name: "IndexStoreCAPI",), + .target(name: "IndexStoreCAPI", ), // MARK: Swift interface @@ -93,7 +100,6 @@ let package = Package( ), // MARK: Swift Test Infrastructure - // The Test Index Build System (tibs) library. .target( name: "ISDBTibs", @@ -168,7 +174,7 @@ let package = Package( exclude: ["CMakeLists.txt"] ), - // Copy of a subset of llvm's ADT and Support libraries. + // Copy of a subset of llvm's ADT and support libraries. .target( name: "IndexStoreDB_LLVMSupport", dependencies: [], @@ -199,6 +205,24 @@ let package = Package( "Windows/Watchdog.inc", ] ), + + // MARK: Command Line Tools + + .executableTarget( + name: "index-dump", + dependencies: [ + "IndexStore", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InternalImportsByDefault"), + .enableUpcomingFeature("MemberImportVisibility"), + .enableUpcomingFeature("InferIsolatedConformances"), + .enableUpcomingFeature("NonisolatedNonsendingByDefault"), + .swiftLanguageMode(.v6), + ] + ), ], swiftLanguageModes: [.v5], cxxLanguageStandard: .cxx17 diff --git a/Sources/IndexStore/IndexStoreDescription.swift b/Sources/IndexStore/IndexStoreDescription.swift new file mode 100644 index 0000000..5f0a71b --- /dev/null +++ b/Sources/IndexStore/IndexStoreDescription.swift @@ -0,0 +1,74 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +extension IndexStoreUnit: CustomStringConvertible { + public var description: String { + var result = """ + Module: \(moduleName.string) + Has Main File: \(hasMainFile) + Main File: \(mainFile.string) + Output File: \(outputFile.string) + Target: \(target.string) + Sysroot: \(sysrootPath.string) + Working Directory: \(workingDirectory.string) + Is System: \(isSystemUnit) + Is Module: \(isModuleUnit) + Is Debug: \(isDebugCompilation) + Provider Identifier: \(providerIdentifier.string) + Provider Version: \(providerVersion.string) + Mod Date: \(modificationDate) + + DEPENDENCIES START + \(dependencies.map { dep in + "\(String(describing: dep.kind).capitalized) | \(dep.name.string)" + }.joined(separator: "\n")) + DEPENDENCIES END + """ + + return result + } +} + +extension IndexStoreRecord: CustomStringConvertible { + public var description: String { + let symbolLines = symbols.map { symbol in + "\(symbol.kind) | \(symbol.name.string) | USR: \(symbol.usr.string)" + } + + let occurrencesLines = occurrences.map { occurrence in + var result = + [ + """ + \(occurrence.position.line):\(occurrence.position.column) \ + | \(occurrence.symbol.kind) \ + | USR: \(occurrence.symbol.usr.string) \ + | Roles: \(occurrence.roles) + """ + ] + + occurrence.relations.map { relation in + " Relation | \(relation.symbol.usr.string) | Roles: \(relation.roles)" + } + return result + } + + return """ + SYMBOLS START + \(symbolLines.joined(separator: "\n")) + SYMBOLS END + OCCURRENCES START + \(occurrencesLines.joined(separator: "\n")) + OCCURRENCES END + """ + } +} diff --git a/Sources/index-dump/LibIndexStoreProvider.swift b/Sources/index-dump/LibIndexStoreProvider.swift new file mode 100644 index 0000000..4fefbd7 --- /dev/null +++ b/Sources/index-dump/LibIndexStoreProvider.swift @@ -0,0 +1,76 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Foundation + +#if os(Windows) +import WinSDK +#endif + +/// Provides utilities to discover and locate libIndexStore in the system. +private enum LibIndexStoreProvider { + /// Find a tool using xcrun/which/where (copied logic from TibsToolchain.findTool) + static func findTool(name: String) -> URL? { + #if os(macOS) + let cmd = ["/usr/bin/xcrun", "--find", name] + #elseif os(Windows) + var buf = [WCHAR](repeating: 0, count: Int(MAX_PATH)) + GetWindowsDirectoryW(&buf, UINT(MAX_PATH)) + var wherePath = String(decodingCString: &buf, as: UTF16.self) + wherePath = (wherePath as NSString).appendingPathComponent("system32") + wherePath = (wherePath as NSString).appendingPathComponent("where.exe") + let cmd = [wherePath, name] + #else + let cmd = ["/usr/bin/which", name] + #endif + + let process = Process() + process.executableURL = URL(fileURLWithPath: cmd[0]) + process.arguments = Array(cmd.dropFirst()) + let pipe = Pipe() + process.standardOutput = pipe + + try? process.run() + process.waitUntilExit() + guard process.terminationStatus == 0 else { return nil } + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + var path = String(decoding: data, as: UTF8.self) + #if os(Windows) + path = String((path.split { $0.isNewline })[0]) + #endif + return URL(fileURLWithPath: path.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + /// Infer libIndexStore dylib path from the default toolchain + static func inferLibPath() -> URL? { + guard let swiftURL = findTool(name: "swift") else { + return nil + } + + let toolchainURL = swiftURL.deletingLastPathComponent().deletingLastPathComponent() + + #if os(macOS) + let libName = "libIndexStore.dylib" + #elseif os(Windows) + let libName = "IndexStore.dll" + #else + let libName = "libIndexStore.so" + #endif + + let libURL = toolchainURL.appending(components: "lib", libName) + guard FileManager.default.fileExists(atPath: libURL.path) else { + return nil + } + return libURL + } +} diff --git a/Sources/index-dump/main.swift b/Sources/index-dump/main.swift new file mode 100644 index 0000000..791d3f4 --- /dev/null +++ b/Sources/index-dump/main.swift @@ -0,0 +1,95 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import ArgumentParser +import Foundation +import IndexStore + +#if os(Windows) +import WinSDK +#endif + +@main +struct IndexDump: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Dumps the content of unit or record files from an IndexStore." + ) + + @Argument(help: "Name of the unit/record or a direct path to the file") + var nameOrPath: String + + @Option(help: "Path to libIndexStore. Inferred if omitted.") + var libIndexStore: String? + + @Option(help: "Path to the index store directory. Inferred if omitted.") + var indexStore: String? + + @Option(help: "Explicitly set mode (unit/record).") + var mode: Mode? + + enum Mode: String, ExpressibleByArgument { + case unit, record + } + + func run() async throws { + let libURL = try explicitOrInferredLibPath() + let storeURL = try explicitOrInferredStorePath() + + let lib = try await IndexStoreLibrary.at(dylibPath: libURL) + let store = try lib.indexStore(at: storeURL) + + let determinedMode = try mode ?? inferMode(from: nameOrPath) + let cleanName = URL(fileURLWithPath: nameOrPath).lastPathComponent + + switch determinedMode { + case .unit: + let unit = try store.unit(named: cleanName) + print(unit) + case .record: + let record = try store.record(named: cleanName) + print(record) + } + } + + // MARK: - Mode Inference + + private func inferMode(from path: String) throws -> Mode { + let components = URL(fileURLWithPath: path).pathComponents + if components.contains("units") { return .unit } + if components.contains("records") { return .record } + throw ValidationError("Could not infer mode from path. Please specify --mode explicitly.") + } + + private func explicitOrInferredStorePath() throws -> URL { + if let explicit = indexStore { return URL(fileURLWithPath: explicit) } + + var url = URL(fileURLWithPath: nameOrPath) + if url.pathComponents.contains("v5") { + while url.lastPathComponent != "v5" && url.pathComponents.count > 1 { + url = url.deletingLastPathComponent() + } + return url.deletingLastPathComponent() + } + throw ValidationError("Could not infer store path. Please specify --index-store.") + } + + private func explicitOrInferredLibPath() throws -> URL { + if let explicit = libIndexStore { return URL(fileURLWithPath: explicit) } + + guard let libURL = LibIndexStoreProvider.inferLibPath() else { + throw ValidationError( + "Could not find 'swift' to infer toolchain path. Please specify --lib-index-store explicitly." + ) + } + return libURL + } +} diff --git a/Tests/IndexStoreTests/IndexStoreTests.swift b/Tests/IndexStoreTests/IndexStoreTests.swift index 2342927..b45bcd1 100644 --- a/Tests/IndexStoreTests/IndexStoreTests.swift +++ b/Tests/IndexStoreTests/IndexStoreTests.swift @@ -1,3 +1,15 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + import Foundation import ISDBTibs import IndexStore @@ -204,10 +216,86 @@ struct IndexStoreTests { guard occurrence.symbol.name.string == "bar()" else { return .continue } - #expect(occurrence.symbol.roles == [.definition, .reference, .call, .calledBy, .containedBy]) + #expect( + occurrence.symbol.roles == [.definition, .reference, .call, .calledBy, .containedBy] + ) #expect(occurrence.symbol.relatedRoles == [.calledBy, .containedBy]) return .continue } } } + + @Test func unitDescription() async throws { + let project = TestProject(swiftFiles: [ + "test.swift": """ + func testFunc() {} + """ + ]) + try await project.withIndexStore { indexStore in + let unitName = try #require(indexStore.unitNames(sorted: false).map { $0.string }.only) + let unit = try indexStore.unit(named: unitName) + + let description = unit.description + + #expect( + description == """ + Module: test + Has Main File: true + Has Main File: false + Main File: + Output File: + Target: test + Sysroot: + Working Directory: + Is System: false + Is Module: false + Is Debug: false + Provider Identifier: + Provider Version: + Mod Date: 0 + + DEPENDENCIES START + Record | test + DEPENDENCIES END + """ + ) + + } + } + + @Test func recordDescription() async throws { + let project = TestProject(swiftFiles: [ + "test.swift": """ + struct Foo { + func testFunc() {} + } + """ + ]) + try await project.withIndexStore { indexStore in + let unitName = try #require(indexStore.unitNames(sorted: false).map { $0.string }.only) + let unit = try indexStore.unit(named: unitName) + + let recordNames = unit.dependencies.compactMap { dep in + dep.kind == .record ? dep.name.string : nil + } + let recordName = try #require(recordNames.only) + let record = try indexStore.record(named: recordName) + + let description = record.description + + #expect( + description == """ + SYMBOLS START + struct | Foo | USR: s:4test3FooV + instance-method | testFunc | USR: s:4test3FooV8testFuncyyF + SYMBOLS END + OCCURRENCES START + 5:8 | struct | USR: s:4test3FooV | Roles: [definition] + 6:8 | instance-method | USR: s:4test3FooV8testFuncyyF | Roles: [definition] + Relation | s:4test3FooV | Roles: [childOf] + OCCURRENCES END + """ + ) + } + } }