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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
324F1EC02B314E030001AC0C /* SafeDIConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324F1EBF2B314E030001AC0C /* SafeDIConfiguration.swift */; };
324F1ECF2B314E030001AC0C /* NameEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324F1ECE2B314E030001AC0C /* NameEntryView.swift */; };
324F1ED22B3150480001AC0C /* NoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324F1ED12B3150480001AC0C /* NoteView.swift */; };
32756FE62B24C042006BDD24 /* ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32756FE52B24C042006BDD24 /* ExampleApp.swift */; };
Expand Down Expand Up @@ -48,6 +49,7 @@
/* Begin PBXFileReference section */
324F1ECA2B314D8D0001AC0C /* UserService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserService.swift; sourceTree = "<group>"; };
324F1ECC2B314DB20001AC0C /* StringStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringStorage.swift; sourceTree = "<group>"; };
324F1EBF2B314E030001AC0C /* SafeDIConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDIConfiguration.swift; sourceTree = "<group>"; };
324F1ECE2B314E030001AC0C /* NameEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NameEntryView.swift; sourceTree = "<group>"; };
324F1ED12B3150480001AC0C /* NoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteView.swift; sourceTree = "<group>"; };
32756FE22B24C042006BDD24 /* ExampleMultiProjectIntegration.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ExampleMultiProjectIntegration.app; sourceTree = BUILT_PRODUCTS_DIR; };
Expand Down Expand Up @@ -112,6 +114,7 @@
32756FE42B24C042006BDD24 /* ExampleMultiProjectIntegration */ = {
isa = PBXGroup;
children = (
324F1EBF2B314E030001AC0C /* SafeDIConfiguration.swift */,
324F1EDA2B315AB20001AC0C /* Views */,
32756FE92B24C044006BDD24 /* Assets.xcassets */,
32756FEB2B24C044006BDD24 /* ExampleMultiProjectIntegration.entitlements */,
Expand Down Expand Up @@ -267,6 +270,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
324F1EC02B314E030001AC0C /* SafeDIConfiguration.swift in Sources */,
324F1ECF2B314E030001AC0C /* NameEntryView.swift in Sources */,
324F1ED22B3150480001AC0C /* NoteView.swift in Sources */,
32756FE62B24C042006BDD24 /* ExampleApp.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SafeDI

@SafeDIConfiguration
enum ExampleSafeDIConfiguration {
/// The names of modules to import in the generated dependency tree.
/// This list is in addition to the import statements found in files that declare @Instantiable types.
static let additionalImportedModules: [StaticString] = []

/// Directories containing Swift files to include, relative to the executing directory.
/// This property only applies to SafeDI repos that utilize the SPM plugin via an Xcode project.
static let additionalDirectoriesToInclude: [StaticString] = ["Subproject"]
}
8 changes: 6 additions & 2 deletions Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
encoding: .utf8
)

// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let includeCSV = context.safediFolder.appending(components: "configuration", "include.csv")
let includeArguments: [String] = if FileManager.default.fileExists(atPath: includeCSV.path(percentEncoded: false)) {
[
Expand All @@ -64,6 +65,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
} else {
[]
}
// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let additionalImportedModulesCSV = context.safediFolder.appending(components: "configuration", "additionalImportedModules.csv")
let additionalImportedModulesArguments: [String] = if FileManager.default.fileExists(atPath: additionalImportedModulesCSV.path(percentEncoded: false)) {
[
Expand Down Expand Up @@ -149,8 +151,8 @@ extension Target {
// to inspect target dependencies. As a result, this Xcode plugin
// only works if it is running on a single-module project, or if
// all `@Instantiable`-decorated types are in the target module,
// or if a .safedi/configuration/include.csv directs the plugin
// to search additional modules for Swift files.
// or if a @SafeDIConfiguration type's `additionalDirectoriesToInclude`
// directs the plugin to search additional modules for Swift files.
// https://github.com/apple/swift-package-manager/issues/6003
let inputSwiftFiles = target
.inputFiles
Expand All @@ -172,6 +174,7 @@ extension Target {
encoding: .utf8
)

// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let includeCSV = context.safediFolder.appending(components: "configuration", "include.csv")
let includeArguments: [String] = if FileManager.default.fileExists(atPath: includeCSV.path(percentEncoded: false)) {
[
Expand All @@ -181,6 +184,7 @@ extension Target {
} else {
[]
}
// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let additionalImportedModulesCSV = context.safediFolder.appending(components: "configuration", "additionalImportedModules.csv")
let additionalImportedModulesArguments: [String] = if FileManager.default.fileExists(atPath: additionalImportedModulesCSV.path(percentEncoded: false)) {
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
encoding: .utf8
)

// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let includeCSV = context.safediFolder.appending(components: "configuration", "include.csv")
let includeArguments: [String] = if FileManager.default.fileExists(atPath: includeCSV.path(percentEncoded: false)) {
[
Expand All @@ -64,6 +65,7 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
} else {
[]
}
// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let additionalImportedModulesCSV = context.safediFolder.appending(components: "configuration", "additionalImportedModules.csv")
let additionalImportedModulesArguments: [String] = if FileManager.default.fileExists(atPath: additionalImportedModulesCSV.path(percentEncoded: false)) {
[
Expand Down Expand Up @@ -159,8 +161,8 @@ extension Target {
// to inspect target dependencies. As a result, this Xcode plugin
// only works if it is running on a single-module project, or if
// all `@Instantiable`-decorated types are in the target module,
// or if a .safedi/configuration/include.csv directs the plugin
// to search additional modules for Swift files.
// or if a @SafeDIConfiguration type's `additionalDirectoriesToInclude`
// directs the plugin to search additional modules for Swift files.
// https://github.com/apple/swift-package-manager/issues/6003
let inputSwiftFiles = target
.inputFiles
Expand All @@ -182,6 +184,7 @@ extension Target {
encoding: .utf8
)

// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let includeCSV = context.safediFolder.appending(components: "configuration", "include.csv")
let includeArguments: [String] = if FileManager.default.fileExists(atPath: includeCSV.path(percentEncoded: false)) {
[
Expand All @@ -191,6 +194,7 @@ extension Target {
} else {
[]
}
// TODO: Delete CSV support in version 2.0. Use @SafeDIConfiguration instead.
let additionalImportedModulesCSV = context.safediFolder.appending(components: "configuration", "additionalImportedModules.csv")
let additionalImportedModulesArguments: [String] = if FileManager.default.fileExists(atPath: additionalImportedModulesCSV.path(percentEncoded: false)) {
[
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,24 @@ SafeDI provides a code generation plugin named `SafeDIGenerator`. This plugin wo

If your first-party code comprises a single module in an `.xcodeproj`, once your Xcode project depends on the SafeDI package you can integrate the Swift Package Plugin simply by going to your target’s `Build Phases`, expanding the `Run Build Tool Plug-ins` drop-down, and adding the `SafeDIGenerator` as a build tool plug-in. You can see this integration in practice in the [ExampleProjectIntegration](Examples/ExampleProjectIntegration) project.

If your Xcode project comprises multiple modules, follow the above steps, and then create a `.safedi/configuration/include.csv` file containing a comma-separated list of folders outside of your root module that SafeDI will scan for Swift source files. The `.safedi/` folder must be placed in the same folder as your `*.xcodeproj`, and the paths must be relative to the same folder. You can see [an example of this customization](Examples/ExampleMultiProjectIntegration/.safedi/configuration/include.csv) in the [ExampleMultiProjectIntegration](Examples/ExampleMultiProjectIntegration) project. To ensure that generated SafeDI code includes imports to all of your required modules, you may create a `.safedi/configuration/additionalImportedModules.csv` with a comma-separated list of module names to import.
If your Xcode project comprises multiple modules, follow the above steps, and then create a `@SafeDIConfiguration`-decorated enum in your root module to configure SafeDI:

```swift
import SafeDI

@SafeDIConfiguration
enum MySafeDIConfiguration {
/// Directories containing Swift files to include, relative to the executing directory.
/// This property only applies to SafeDI repos that utilize the SPM plugin via an Xcode project.
static let additionalDirectoriesToInclude: [StaticString] = ["Subproject"]

/// The names of modules to import in the generated dependency tree.
/// This list is in addition to the import statements found in files that declare @Instantiable types.
static let additionalImportedModules: [StaticString] = []
}
```

The `additionalDirectoriesToInclude` property specifies folders outside of your root module that SafeDI will scan for Swift source files. Paths must be relative to the project directory. You can see [an example of this configuration](Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/SafeDIConfiguration.swift) in the [ExampleMultiProjectIntegration](Examples/ExampleMultiProjectIntegration) project.

##### Swift package

Expand All @@ -116,7 +133,17 @@ If your first-party code is entirely contained in a Swift Package with one or mo

You can see this integration in practice in the [Example Package Integration](Examples/Example Package Integration) package.

Unlike the `SafeDIGenerator` Xcode project plugin, the `SafeDIGenerator` Swift package plugin finds source files in dependent modules without additional configuration steps. If you find that SafeDI’s generated dependency tree is missing required imports, you may create a `.safedi/configuration/additionalImportedModules.csv` with a comma-separated list of module names to import. The `.safedi/` folder must be placed in the same folder as your `Package.swift` file.
Unlike the `SafeDIGenerator` Xcode project plugin, the `SafeDIGenerator` Swift package plugin finds source files in dependent modules without additional configuration steps. If you find that SafeDI’s generated dependency tree is missing required imports, you may create a `@SafeDIConfiguration`-decorated enum in your root module with the additional module names:

```swift
import SafeDI

@SafeDIConfiguration
enum MySafeDIConfiguration {
static let additionalImportedModules: [StaticString] = ["MyModule"]
static let additionalDirectoriesToInclude: [StaticString] = []
}
```

##### Unlocking faster builds with Swift Package Manager plugins

Expand Down
44 changes: 44 additions & 0 deletions Sources/SafeDI/Decorators/SafeDIConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

/// Marks an enum as providing SafeDI configuration.
///
/// An enum decorated with `@SafeDIConfiguration` provides build-time configuration for SafeDI's code generation plugin.
/// The decorated enum must declare two static properties:
///
/// - `additionalImportedModules`: Module names to import in the generated dependency tree, in addition to the import statements found in files that declare `@Instantiable` types.
/// - `additionalDirectoriesToInclude`: Directories containing Swift files to include, relative to the executing directory. This property only applies to SafeDI repos that utilize the SPM plugin via an Xcode project.
///
/// Both properties must be of type `[StaticString]` and initialized with array literals containing only string literals.
///
/// Example:
///
/// @SafeDIConfiguration
/// enum MyConfiguration {
/// /// The names of modules to import in the generated dependency tree.
/// /// This list is in addition to the import statements found in files that declare @Instantiable types.
/// static let additionalImportedModules: [StaticString] = ["MyModule", "OtherModule"]
///
/// /// Directories containing Swift files to include, relative to the executing directory.
/// /// This property only applies to SafeDI repos that utilize the SPM plugin via an Xcode project.
/// static let additionalDirectoriesToInclude: [StaticString] = ["Sources/OtherModule"]
/// }
@attached(peer)
public macro SafeDIConfiguration() = #externalMacro(module: "SafeDIMacros", type: "SafeDIConfigurationMacro")
78 changes: 78 additions & 0 deletions Sources/SafeDICore/Errors/FixableSafeDIConfigurationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import SwiftDiagnostics

public enum FixableSafeDIConfigurationError: DiagnosticError {
case missingAdditionalImportedModulesProperty
case missingAdditionalDirectoriesToIncludeProperty

public var description: String {
switch self {
case .missingAdditionalImportedModulesProperty:
"@\(SafeDIConfigurationVisitor.macroName)-decorated type must have a `static let additionalImportedModules: [StaticString]` property"
case .missingAdditionalDirectoriesToIncludeProperty:
"@\(SafeDIConfigurationVisitor.macroName)-decorated type must have a `static let additionalDirectoriesToInclude: [StaticString]` property"
}
}

public var diagnostic: DiagnosticMessage {
SafeDIConfigurationDiagnosticMessage(error: self)
}

public var fixIt: FixItMessage {
SafeDIConfigurationFixItMessage(error: self)
}

// MARK: - SafeDIConfigurationDiagnosticMessage

private struct SafeDIConfigurationDiagnosticMessage: DiagnosticMessage {
init(error: FixableSafeDIConfigurationError) {
diagnosticID = MessageID(domain: "\(Self.self)", id: error.description)
severity = switch error {
case .missingAdditionalImportedModulesProperty,
.missingAdditionalDirectoriesToIncludeProperty:
.error
}
message = error.description
}

let diagnosticID: MessageID
let severity: DiagnosticSeverity
let message: String
}

// MARK: - SafeDIConfigurationFixItMessage

private struct SafeDIConfigurationFixItMessage: FixItMessage {
init(error: FixableSafeDIConfigurationError) {
message = switch error {
case .missingAdditionalImportedModulesProperty:
"Add `static let additionalImportedModules: [StaticString]` property"
case .missingAdditionalDirectoriesToIncludeProperty:
"Add `static let additionalDirectoriesToInclude: [StaticString]` property"
}
fixItID = MessageID(domain: "\(Self.self)", id: error.description)
}

let message: String
let fixItID: MessageID
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ extension AttributeListSyntax.Element {
attributeIfNameEquals(Dependency.Source.forwardedRawValue)
}

var safeDIConfigurationMacro: AttributeSyntax? {
attributeIfNameEquals(SafeDIConfigurationVisitor.macroName)
}

private func attributeIfNameEquals(_ expectedName: String) -> AttributeSyntax? {
if case let .attribute(attribute) = self,
let identifier = IdentifierTypeSyntax(attribute.attributeName),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ extension AttributeListSyntax {
return AttributeSyntax(attribute)
}

public var safeDIConfigurationMacro: AttributeSyntax? {
guard let attribute = first(where: { element in
element.safeDIConfigurationMacro != nil
}) else {
return nil
}
return AttributeSyntax(attribute)
}

public var dependencySources: [(source: Dependency.Source, node: AttributeListSyntax.Element)] {
compactMap {
switch $0 {
Expand Down
Loading
Loading