Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ brew install swiftfindrefs
## ⚙️ Command line options

### Common options (available for all subcommands)
- `-p, --projectName` helps the tool infer the right DerivedData folder when you do not pass `derivedDataPath`.
- `-d, --derivedDataPath` points directly to a DerivedData (or IndexStoreDB) directory and skips discovery.
- `-p, --projectName` helps the tool infer the right DerivedData folder when `dataStorePath` is not provided.
- `-d, --dataStorePath` points directly to a DataStore directory. When provided, this takes priority over `--projectName`.
- `-v, --verbose` enables verbose output for diagnostic purposes (flag, no value required).

### Search subcommand
Expand Down
24 changes: 15 additions & 9 deletions Sources/SwiftFindRefs/CompositionRoot/RemoveCompositionRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation

struct RemoveCompositionRoot {
let projectName: String?
let derivedDataPath: String?
let dataStorePath: String?
let excludeCompilationConditionals: Bool
let print: (String) -> Void
let vPrint: (String) -> Void
Expand All @@ -12,14 +12,20 @@ struct RemoveCompositionRoot {
let removerFactory: (String) -> UnnecessaryTestableRemoving

func run() async throws {
let derivedDataPaths = try derivedDataLocator.locateDerivedData(
projectName: projectName,
derivedDataPath: derivedDataPath
)
vPrint("DerivedData path: \(derivedDataPaths.derivedDataURL.path)")
vPrint("IndexStoreDB path: \(derivedDataPaths.indexStoreDBURL.path)")
let indexStorePath = derivedDataPaths.indexStoreDBURL.deletingLastPathComponent().path
let remover = removerFactory(indexStorePath)
var pathToDataStore: String
if let dataStorePath {
guard fileSystem.fileExists(atPath: dataStorePath) else { throw DataStorePathValidationError.invalidPath(dataStorePath) }
pathToDataStore = dataStorePath
} else {
let derivedDataPaths = try derivedDataLocator.locateDerivedData(
projectName: projectName
)
pathToDataStore = derivedDataPaths.dataStoreURL.path()
vPrint("Discovering DataStore path based on projectName: \(String(describing: projectName))")
}
vPrint("Using DataStore path: \(pathToDataStore)")

let remover = removerFactory(pathToDataStore)
let updatedFiles = try await remover.run()
print("✅ Updated \(updatedFiles.count) files")
vPrint("Updated files:")
Expand Down
23 changes: 14 additions & 9 deletions Sources/SwiftFindRefs/CompositionRoot/SearchCompositionRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

struct SearchCompositionRoot {
let projectName: String?
let derivedDataPath: String?
let dataStorePath: String?
let symbolName: String
let symbolType: String?
let print: (String) -> Void
Expand All @@ -11,14 +11,19 @@ struct SearchCompositionRoot {
let derivedDataLocator: DerivedDataLocatorProtocol

func run() async throws {
let derivedDataPaths = try derivedDataLocator.locateDerivedData(
projectName: projectName,
derivedDataPath: derivedDataPath
)
vPrint("DerivedData path: \(derivedDataPaths.derivedDataURL.path)")
vPrint("IndexStoreDB path: \(derivedDataPaths.indexStoreDBURL.path)")
let indexStorePath = derivedDataPaths.indexStoreDBURL.deletingLastPathComponent().path
let indexStoreFinder = IndexStoreFinder(indexStorePath: indexStorePath)
var pathToDataStore: String
if let dataStorePath {
guard fileSystem.fileExists(atPath: dataStorePath) else { throw DataStorePathValidationError.invalidPath(dataStorePath) }
pathToDataStore = dataStorePath

} else {
let derivedDataPaths = try derivedDataLocator.locateDerivedData(projectName: projectName)
pathToDataStore = derivedDataPaths.dataStoreURL.path()
vPrint("Discovering DataStore path based on projectName: \(String(describing: projectName))")
}
vPrint("Using DataStore path: \(pathToDataStore)")

let indexStoreFinder = IndexStoreFinder(indexStorePath: pathToDataStore)
print("🔍 Searching for references to symbol '\(symbolName)' of type '\(symbolType ?? "any")'")
let references = try await indexStoreFinder.fileReferences(of: symbolName, symbolType: symbolType)
print("✅ Found \(references.count) references:\n\(references.joined(separator: "\n"))")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Errors thrown while validating the DataPath for a project.
enum DataStorePathValidationError: LocalizedError {
case invalidPath(String)

/// Human-readable description surfaced to end users.
var errorDescription: String? {
switch self {
case .invalidPath(let path):
"❌ The provided DataStore path does not exist: \(path)."
}
}
}
12 changes: 2 additions & 10 deletions Sources/SwiftFindRefs/DerivedData/DerivedDataLocator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,12 @@ struct DerivedDataLocator: DerivedDataLocatorProtocol {
/// Finds the DerivedData directory for the given inputs and indicates whether helper paths should be appended.
/// - Parameters:
/// - projectName: The name of the Xcode project whose DerivedData should be inferred when no explicit path is supplied.
/// - derivedDataPath: An optional explicit DerivedData path which, when present, takes precedence over project resolution.
/// - dataStorePath: An optional explicit DataStore path which, when present, takes precedence over project resolution.
/// - Returns: A ``DerivedDataPaths`` value containing the resolved URL and whether additional IndexStore paths should be appended.
/// - Throws: ``DerivedDataLocatorError`` when inputs are missing, invalid, or when the DerivedData directory cannot be found.
func locateDerivedData(
projectName: String?,
derivedDataPath: String?
projectName: String?
) throws -> DerivedDataPaths {
if let derivedDataPath, !derivedDataPath.isEmpty {
guard fileSystem.fileExists(atPath: derivedDataPath) else {
throw DerivedDataLocatorError.invalidPath(derivedDataPath)
}
return DerivedDataPaths(derivedDataURL: URL(fileURLWithPath: derivedDataPath), shouldAppendExtraPaths: false)
}

guard let projectName, !projectName.isEmpty else {
throw DerivedDataLocatorError.missingInputs
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,22 @@ import Foundation

/// Errors thrown while locating the DerivedData directory for a project.
enum DerivedDataLocatorError: LocalizedError {
/// No usable `projectName` or `derivedDataPath` was provided.
/// No usable `projectName` or `dataStorePath` was provided.
case missingInputs
/// The default DerivedData root directory does not exist at the expected path.
case derivedDataRootMissing(String)
/// A DerivedData entry matching the provided project name was not found.
case projectNotFound(String)
/// The explicit DerivedData path provided by the caller could not be resolved on disk.
case invalidPath(String)

/// Human-readable description surfaced to end users.
var errorDescription: String? {
switch self {
case .missingInputs:
"❌ Either projectName or derivedDataPath must be provided."
"❌ Either projectName or dataStorePath must be provided."
case .derivedDataRootMissing(let path):
"❌ DerivedData root directory was not found at \(path)."
case .projectNotFound(let name):
"❌ No DerivedData entry matching project \(name) was found."
case .invalidPath(let path):
"❌ The provided DerivedData path does not exist: \(path)."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ protocol DerivedDataLocatorProtocol {
/// Locates the DerivedData directory either from an explicit path or by inferring it from the project name.
/// - Parameters:
/// - projectName: The name of the Xcode project whose DerivedData should be searched.
/// - derivedDataPath: An optional explicit DerivedData path that takes precedence when provided.
/// - dataStorePath: An optional explicit DataStore path that takes precedence when provided.
/// - Returns: Paths describing the resolved DerivedData location and whether helper paths should be appended.
/// - Throws: ``DerivedDataLocatorError`` when the provided inputs are invalid or the DerivedData directory cannot be found.
func locateDerivedData(
projectName: String?,
derivedDataPath: String?
projectName: String?
) throws -> DerivedDataPaths
}
3 changes: 1 addition & 2 deletions Sources/SwiftFindRefs/DerivedData/DerivedDataPaths.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ struct DerivedDataPaths {
let shouldAppendExtraPaths: Bool

/// Points to the IndexStore database, automatically appending the Xcode-specific subpath when needed.
var indexStoreDBURL: URL {
var dataStoreURL: URL {
shouldAppendExtraPaths ? derivedDataURL
.appendingPathComponent("Index.noindex", isDirectory: true)
.appendingPathComponent("DataStore", isDirectory: true)
.appendingPathComponent("IndexStoreDB", isDirectory: true)
: derivedDataURL
}
}
12 changes: 6 additions & 6 deletions Sources/SwiftFindRefs/SwiftFindRefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ extension SwiftFindRefs {
@Option(name: [.short, .customLong("projectName")], help: "The name of the Xcode project to help CLI find the Derived Data Index Store Path")
var projectName: String?

@Option(name: [.short, .customLong("derivedDataPath")], help: "The Derived Data path where Xcode stores build data")
var derivedDataPath: String?
@Option(name: [.short, .customLong("dataStorePath")], help: "The DataStore path inside DerivedData where Xcode stores index data")
var dataStorePath: String?

/// Flag to enable verbose output for diagnostic purposes.
@Flag(name: .shortAndLong, help: "Enable verbose output.")
var verbose: Bool = false

func validate() throws {
guard projectName?.isEmpty == false || derivedDataPath?.isEmpty == false else {
throw ValidationError("Provide either --projectName or --derivedDataPath.")
guard projectName?.isEmpty == false || dataStorePath?.isEmpty == false else {
throw ValidationError("Provide either --projectName or --dataStorePath.")
}
}
}
Expand All @@ -48,7 +48,7 @@ extension SwiftFindRefs {
let derivedDataLocator = DerivedDataLocator(fileSystem: fileSystem)
let compositionRoot = SearchCompositionRoot(
projectName: common.projectName,
derivedDataPath: common.derivedDataPath,
dataStorePath: common.dataStorePath,
symbolName: name,
symbolType: type,
print: { print($0) },
Expand Down Expand Up @@ -79,7 +79,7 @@ extension SwiftFindRefs {
let derivedDataLocator = DerivedDataLocator(fileSystem: fileSystem)
let compositionRoot = RemoveCompositionRoot(
projectName: common.projectName,
derivedDataPath: common.derivedDataPath,
dataStorePath: common.dataStorePath,
excludeCompilationConditionals: excludeCompilationConditionals,
print: { print($0) },
vPrint: { if common.verbose { print($0) } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ struct RemoveCompositionRootTests {
let fileSystem = MockFileSystem(fileExistsResults: [invalidPath: false])
let sut = makeRemoveSUT(
projectName: "Project",
derivedDataPath: invalidPath,
dataStorePath: invalidPath,
excludeCompilationConditionals: false,
fileSystem: fileSystem,
removerFactory: { _ in MockRemover(result: []) }
)

// When
let error = await #expect(throws: DerivedDataLocatorError.self) {
let error = await #expect(throws: DataStorePathValidationError.self) {
try await sut.run()
}

Expand All @@ -32,12 +32,12 @@ struct RemoveCompositionRootTests {
@Test("test run prints updated files count when nothing updated")
func test_run_WhenNoUpdates_PrintsUpdatedCount() async throws {
// Given
let derivedDataPath = "/mock/DerivedData/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [derivedDataPath: true])
let dataStorePath = "/mock/DerivedData/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [dataStorePath: true])
var printMessages: [String] = []
let sut = makeRemoveSUT(
projectName: "Project",
derivedDataPath: derivedDataPath,
dataStorePath: dataStorePath,
excludeCompilationConditionals: false,
fileSystem: fileSystem,
print: { printMessages.append($0) },
Expand All @@ -54,15 +54,15 @@ struct RemoveCompositionRootTests {
@Test("test run prints updated files when remover returns results")
func test_run_WhenUpdatesExist_PrintsUpdatedFiles() async throws {
// Given
let derivedDataPath = "/mock/DerivedData/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [derivedDataPath: true])
let dataStorePath = "/mock/DerivedData/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [dataStorePath: true])
var printMessages: [String] = []
var vPrintMessages: [String] = []
let updatedFiles = ["/mock/FileA.swift", "/mock/FileB.swift"]
var receivedIndexStorePath: String?
let sut = makeRemoveSUT(
projectName: "Project",
derivedDataPath: derivedDataPath,
dataStorePath: dataStorePath,
excludeCompilationConditionals: false,
fileSystem: fileSystem,
print: { printMessages.append($0) },
Expand All @@ -77,7 +77,7 @@ struct RemoveCompositionRootTests {
try await sut.run()

// Then
#expect(receivedIndexStorePath == "/mock/DerivedData")
#expect(receivedIndexStorePath == "/mock/DerivedData/IndexStoreDB")
#expect(printMessages.contains("✅ Updated 2 files"))
#expect(vPrintMessages.contains("Updated files:"))
#expect(vPrintMessages.contains("/mock/FileA.swift"))
Expand All @@ -86,7 +86,7 @@ struct RemoveCompositionRootTests {

private func makeRemoveSUT(
projectName: String?,
derivedDataPath: String?,
dataStorePath: String?,
excludeCompilationConditionals: Bool,
fileSystem: MockFileSystem,
print: @escaping (String) -> Void = { _ in },
Expand All @@ -95,7 +95,7 @@ struct RemoveCompositionRootTests {
) -> RemoveCompositionRoot {
RemoveCompositionRoot(
projectName: projectName,
derivedDataPath: derivedDataPath,
dataStorePath: dataStorePath,
excludeCompilationConditionals: excludeCompilationConditionals,
print: print,
vPrint: vPrint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ struct SearchCompositionRootTests {
let fileSystem = MockFileSystem()
let sut = makeSearchSUT(
projectName: nil,
derivedDataPath: nil,
dataStorePath: nil,
symbolType: "class",
fileSystem: fileSystem
)
Expand All @@ -36,13 +36,13 @@ struct SearchCompositionRootTests {
let fileSystem = MockFileSystem(fileExistsResults: [invalidPath: false])
let sut = makeSearchSUT(
projectName: "Project",
derivedDataPath: invalidPath,
dataStorePath: invalidPath,
symbolType: "class",
fileSystem: fileSystem
)

// When
let error = await #expect(throws: DerivedDataLocatorError.self) {
let error = await #expect(throws: DataStorePathValidationError.self) {
try await sut.run()
}

Expand All @@ -57,13 +57,13 @@ struct SearchCompositionRootTests {
@Test("test run with nil symbol type logs fallback before index store failure")
func test_run_WithNilSymbolType_logsFallbackBeforeIndexStoreFailure() async throws {
// Given
let derivedDataPath = "/tmp/nonexistent/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [derivedDataPath: true])
let dataStorePath = "/tmp/nonexistent/IndexStoreDB"
let fileSystem = MockFileSystem(fileExistsResults: [dataStorePath: true])
var standardMessages: [String] = []
var verboseMessages: [String] = []
let sut = makeSearchSUT(
projectName: "Project",
derivedDataPath: derivedDataPath,
dataStorePath: dataStorePath,
symbolType: nil,
fileSystem: fileSystem,
print: { standardMessages.append($0) },
Expand All @@ -76,8 +76,7 @@ struct SearchCompositionRootTests {
}

// Then
#expect(verboseMessages.contains("DerivedData path: \(derivedDataPath)"))
#expect(verboseMessages.contains("IndexStoreDB path: \(derivedDataPath)"))
#expect(verboseMessages.contains("Using DataStore path: \(dataStorePath)"))
let searchMessage = try #require(standardMessages.first { $0.contains("Searching for references") })
#expect(searchMessage.contains("symbol 'MySymbol'"))
#expect(searchMessage.contains("of type 'any'"))
Expand All @@ -87,7 +86,7 @@ struct SearchCompositionRootTests {

private func makeSearchSUT(
projectName: String?,
derivedDataPath: String?,
dataStorePath: String?,
symbolName: String = "MySymbol",
symbolType: String? = "class",
fileSystem: MockFileSystem,
Expand All @@ -96,7 +95,7 @@ struct SearchCompositionRootTests {
) -> SearchCompositionRoot {
SearchCompositionRoot(
projectName: projectName,
derivedDataPath: derivedDataPath,
dataStorePath: dataStorePath,
symbolName: symbolName,
symbolType: symbolType,
print: print,
Expand All @@ -106,3 +105,4 @@ struct SearchCompositionRootTests {
)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ struct DerivedDataLocatorErrorTests {
let description = try #require(sut.errorDescription)

// Then
#expect(description == "❌ Either projectName or derivedDataPath must be provided.")
#expect(description == "❌ Either projectName or dataStorePath must be provided.")
}

@Test("test errorDescription with missing derived data root includes provided path")
Expand Down Expand Up @@ -41,17 +41,4 @@ struct DerivedDataLocatorErrorTests {
// Then
#expect(description == "❌ No DerivedData entry matching project \(projectName) was found.")
}

@Test("test errorDescription with invalid path includes the invalid value")
func test_errorDescription_WithInvalidPath_returnsDetailedMessage() throws {
// Given
let invalidPath = "/invalid/path"
let sut: DerivedDataLocatorError = .invalidPath(invalidPath)

// When
let description = try #require(sut.errorDescription)

// Then
#expect(description == "❌ The provided DerivedData path does not exist: \(invalidPath).")
}
}
Loading