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
13 changes: 11 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ let package = Package(
"SafeDITool",
],
),
// A lightweight library containing root scanning and output file naming logic.
// Used by SafeDIRootScanner (executable), SafeDITool, and plugins (via symlinks).
.target(
name: "SafeDIRootScannerCore",
swiftSettings: [
.swiftLanguageMode(.v6),
],
),
// A lightweight executable that performs lexical root discovery without SwiftSyntax.
// SPM plugins run this in-process via symlinked sources.
// This target exists as a standalone executable for non-SPM build systems (e.g. Buck, Bazel)
Expand All @@ -134,6 +142,7 @@ let package = Package(
name: "SafeDIRootScanner",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SafeDIRootScannerCore",
],
swiftSettings: [
.swiftLanguageMode(.v6),
Expand All @@ -142,9 +151,9 @@ let package = Package(
.testTarget(
name: "SafeDIRootScannerTests",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SafeDICore",
"SafeDIRootScanner",
"SafeDIRootScannerCore",
],
swiftSettings: [
.swiftLanguageMode(.v6),
Expand All @@ -165,7 +174,7 @@ let package = Package(
name: "SafeDIToolTests",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"SafeDIRootScanner",
"SafeDIRootScannerCore",
"SafeDITool",
],
swiftSettings: [
Expand Down
2 changes: 1 addition & 1 deletion Plugins/SafeDIGenerator/RelativePath.swift
2 changes: 1 addition & 1 deletion Plugins/SafeDIGenerator/RootScanner.swift
30 changes: 22 additions & 8 deletions Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,21 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
to: inputSourcesFile,
)

// Discover additional directories from the current module's config only.
let additionalSwiftFiles = discoverAdditionalDirectorySwiftFiles(
in: targetSwiftFiles,
relativeTo: packageRoot,
)

let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json")
let outputFiles = try runRootScanner(
let scanResult = try runRootScanner(
inputSourcesFile: inputSourcesFile,
projectRoot: packageRoot,
outputDirectory: outputDirectory,
manifestFile: manifestFile,
additionalSwiftFiles: additionalSwiftFiles,
)
guard !outputFiles.isEmpty else {
guard !scanResult.outputFiles.isEmpty else {
return []
}

Expand Down Expand Up @@ -96,8 +103,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
executable: toolLocation,
arguments: arguments,
environment: [:],
inputFiles: allSwiftFiles,
outputFiles: outputFiles,
inputFiles: allSwiftFiles + scanResult.additionalInputFiles,
outputFiles: scanResult.outputFiles,
),
]
}
Expand Down Expand Up @@ -163,14 +170,21 @@ extension Target {
to: inputSourcesFile,
)

// Discover additional directories from the current target's config only.
let additionalSwiftFiles = discoverAdditionalDirectorySwiftFiles(
in: inputSwiftFiles,
relativeTo: projectRoot,
)

let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json")
let outputFiles = try runRootScanner(
let scanResult = try runRootScanner(
inputSourcesFile: inputSourcesFile,
projectRoot: projectRoot,
outputDirectory: outputDirectory,
manifestFile: manifestFile,
additionalSwiftFiles: additionalSwiftFiles,
)
guard !outputFiles.isEmpty else {
guard !scanResult.outputFiles.isEmpty else {
return []
}

Expand Down Expand Up @@ -200,8 +214,8 @@ extension Target {
executable: toolLocation,
arguments: arguments,
environment: [:],
inputFiles: inputSwiftFiles,
outputFiles: outputFiles,
inputFiles: inputSwiftFiles + scanResult.additionalInputFiles,
outputFiles: scanResult.outputFiles,
),
]
}
Expand Down
2 changes: 1 addition & 1 deletion Plugins/SafeDIPrebuiltGenerator/RelativePath.swift
2 changes: 1 addition & 1 deletion Plugins/SafeDIPrebuiltGenerator/RootScanner.swift
28 changes: 20 additions & 8 deletions Plugins/SafeDIPrebuiltGenerator/SafeDIGenerateDependencyTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,20 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
to: inputSourcesFile,
)

let additionalSwiftFiles = discoverAdditionalDirectorySwiftFiles(
in: targetSwiftFiles,
relativeTo: packageRoot,
)

let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json")
let outputFiles = try runRootScanner(
let scanResult = try runRootScanner(
inputSourcesFile: inputSourcesFile,
projectRoot: packageRoot,
outputDirectory: outputDirectory,
manifestFile: manifestFile,
additionalSwiftFiles: additionalSwiftFiles,
)
guard !outputFiles.isEmpty else {
guard !scanResult.outputFiles.isEmpty else {
return []
}

Expand Down Expand Up @@ -106,8 +112,8 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
executable: toolLocation,
arguments: arguments,
environment: [:],
inputFiles: allSwiftFiles,
outputFiles: outputFiles,
inputFiles: allSwiftFiles + scanResult.additionalInputFiles,
outputFiles: scanResult.outputFiles,
),
]
}
Expand Down Expand Up @@ -173,14 +179,20 @@ extension Target {
to: inputSourcesFile,
)

let additionalSwiftFiles = discoverAdditionalDirectorySwiftFiles(
in: inputSwiftFiles,
relativeTo: projectRoot,
)

let manifestFile = context.pluginWorkDirectoryURL.appending(path: "SafeDIManifest.json")
let outputFiles = try runRootScanner(
let scanResult = try runRootScanner(
inputSourcesFile: inputSourcesFile,
projectRoot: projectRoot,
outputDirectory: outputDirectory,
manifestFile: manifestFile,
additionalSwiftFiles: additionalSwiftFiles,
)
guard !outputFiles.isEmpty else {
guard !scanResult.outputFiles.isEmpty else {
return []
}

Expand Down Expand Up @@ -208,8 +220,8 @@ extension Target {
executable: toolLocation,
arguments: arguments,
environment: [:],
inputFiles: inputSwiftFiles,
outputFiles: outputFiles,
inputFiles: inputSwiftFiles + scanResult.additionalInputFiles,
outputFiles: scanResult.outputFiles,
),
]
}
Expand Down
60 changes: 57 additions & 3 deletions Plugins/SharedRootScanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,72 @@ func writeInputSwiftFilesCSV(
)
}

struct RootScannerResult {
/// Output files that the build command will generate.
var outputFiles: [URL]
/// Swift files discovered from additionalDirectoriesToInclude that should
/// be declared as build inputs so edits there trigger rebuilds.
var additionalInputFiles: [URL]
}

/// Discovers `additionalDirectoriesToInclude` from the first `@SafeDIConfiguration`
/// found in the given Swift files. Only the current module's own files should be passed
/// here — not dependency source files — to match `SafeDITool`'s `configurations.first`
/// behavior.
func discoverAdditionalDirectorySwiftFiles(
in moduleSwiftFiles: [URL],
relativeTo projectRoot: URL,
) -> [URL] {
for swiftFile in moduleSwiftFiles {
guard let content = try? String(contentsOf: swiftFile, encoding: .utf8) else { continue }
let directories = RootScanner.extractAdditionalDirectoriesToInclude(in: content)
guard !directories.isEmpty else { continue }

// Use only the first configuration found, matching SafeDITool's behavior.
var additionalSwiftFiles = [URL]()
let directoryBaseURL = projectRoot.hasDirectoryPath
? projectRoot
: projectRoot.appendingPathComponent("", isDirectory: true)
for directory in directories {
let directoryURL = URL(fileURLWithPath: directory, relativeTo: directoryBaseURL)
guard let enumerator = FileManager.default.enumerator(
at: directoryURL,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles],
) else { continue }
for case let fileURL as URL in enumerator where fileURL.pathExtension == "swift" {
additionalSwiftFiles.append(fileURL)
}
}
return additionalSwiftFiles
}
return []
}

func runRootScanner(
inputSourcesFile: URL,
projectRoot: URL,
outputDirectory: URL,
manifestFile: URL,
) throws -> [URL] {
additionalSwiftFiles: [URL] = [],
) throws -> RootScannerResult {
let inputFilePaths = try RootScanner.inputFilePaths(from: inputSourcesFile)

let directoryBaseURL = projectRoot.hasDirectoryPath
? projectRoot
: projectRoot.appendingPathComponent("", isDirectory: true)
let targetSwiftFiles = inputFilePaths.map {
URL(fileURLWithPath: $0, relativeTo: directoryBaseURL).standardizedFileURL
}
let allSwiftFiles = targetSwiftFiles + additionalSwiftFiles
let result = try RootScanner().scan(
inputFilePaths: inputFilePaths,
swiftFiles: allSwiftFiles,
relativeTo: projectRoot,
outputDirectory: outputDirectory,
)
try result.writeManifest(to: manifestFile)
return result.outputFiles
return RootScannerResult(
outputFiles: result.outputFiles,
additionalInputFiles: additionalSwiftFiles,
)
}
1 change: 1 addition & 0 deletions Sources/SafeDIRootScanner/SafeDIRootScannerCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import ArgumentParser
import Foundation
import SafeDIRootScannerCore

@main
struct SafeDIRootScannerCommand: ParsableCommand {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import Foundation

/// Compute a path string relative to a base directory, for use in the CSV and manifest.
/// Falls back to the absolute path if the URL is not under the base directory.
func relativePath(for url: URL, relativeTo base: URL) -> String {
public func relativePath(for url: URL, relativeTo base: URL) -> String {
let urlPath = url.standardizedFileURL.path
let standardizedBasePath = base.standardizedFileURL.path
let basePath = standardizedBasePath.hasSuffix("/")
Expand Down
Loading
Loading