Repository navigation
2.1.0
What's new
Conditional compilation that matches your build
SafeDI now understands #if. Earlier releases scanned every clause of every #if block, so #if DEBUG … #else … #endif around two declarations of the same type was reported as a duplicate, and code behind an inactive condition leaked into generated code.
- SafeDITool scans only the clauses that are active in your build.
- In Xcode builds and
swift buildon the Swift Build backend, SafeDI reads the conditions straight from the build settings, so it sees exactly what the compiler sees, per configuration. - Every generated file asserts the conditions it was generated under with
#if … #error. If the compiler ever disagrees, the build stops with a message naming the condition instead of compiling the wrong graph. - Mock-only declarations under
#if DEBUG(or yourmockConditionalCompilation) keep working in every integration, including an import they need under its own#if DEBUG. - A root or a
generateMock: truetype behind a condition gets its generated file in every configuration, without code where the condition is inactive, so Debug-only and Release-only roots both build.
See Conditional compilation in the Manual.
Errors you can find and act on
- Every SafeDI error is now a compiler-format diagnostic located at your declaration:
File.swift:12:5: error: …, with notes pointing at the related declarations. They are easy to find in a command-line build log and show up inline in Xcode. - Many errors carry fix-its: a near-miss
@Receivedlabel or type, a missingonlyIfAvailable: true, a stale entry inadditionalMocksToGenerate. - A type mismatch on a renamed dependency (
@Received(fulfilledByDependencyNamed:ofType:)) is reported at your@Receiveddeclaration instead of inside generated code. - The diagnostics are written to be acted on without opening the generated file, which makes them work well for agentic development as well as for people.
Bazel
- New
safedi_swift_librarymacro declares a module's two targets (SafeDI generation and the Swift library) in one call. safedi_compilereportsNDEBUGinoptbuilds, so SafeDI's view ofDEBUGmatches the compiler's.
Smaller things
- Put your
#SafeDIConfigurationin a file namedSafeDIConfiguration.swiftand SafeDI reads it first, which keeps scanning to a single pass. A configuration elsewhere still works; SafeDI warns only when that placement actually slows the scan. - Xcode 27 and Swift 6.4 are supported, including SwiftPM's Swift Build backend.
onlyIfAvailabledependencies are ordered correctly among siblings: a receiver whose dependency can only be built after it getsnilinstead of generated code that does not compile.- A dependency that one type requires and another renames only-if-available is now promoted correctly in mocks.
- Mock overrides nested under a
@Sendablebuilder compile in Swift 6. - Generated output and
.safediordering no longer depend on the order files finish parsing.
Performance
Measured with the published 2.0.0 and 2.1.0 tools on a 15-module, 3,000-file production app:
generateis about 30% faster.scantakes the same time, even though it now evaluates#ifblocks.- Cycle validation no longer re-walks received dependencies. Deep received chains that took exponential time in 2.0.0 validate in linear time.
- Generated mocks compile faster. Mock bodies now use
if letbuilder calls, which cut compiler time for mock code by about a quarter on a synthetic module. - The scan cache's freshness check uses
statand no longer re-resolves paths that are already resolved.
Upgrading from 2.0.0
Most projects need no changes. Two things can require an edit:
1. Production declarations behind a bare #if condition
If a production declaration (not a mock) sits behind a bare condition such as #if DEBUG or #if STAGING, SafeDI now scans it according to the conditions your build reports, and the generated code asserts that choice.
- Xcode projects, Xcode builds of a package, and
swift buildon the Swift Build backend: SafeDI reads the build settings. Nothing to do. swift buildon SwiftPM's native backend (the default through Swift 6.2, the toolchain in Xcode 26): SwiftPM does not report its implicitDEBUGto plugins. Add.define("DEBUG", .when(configuration: .debug))to the target'sswiftSettings, or move the declaration out of the block. If you do neither, the build fails with a message naming the condition; it never silently compiles the wrong graph.- Bazel: pass your defines to
safedi_compile(or usesafedi_swift_library, which does it for you). - Tuist: update the SafeDI Tuist plugin tag together with the package version; the 2.0.0 plugin script does not forward conditions to the 2.1.0 tool.
Full details: Upgrading to 2.1.0 or later.
2. A hand-written mock next to a generated mock()
When SafeDI generates a mock() for a type (generateMock: true, or the type is listed in additionalMocksToGenerate) and that type also has a hand-written method named mock in a separate mockOnly declaration, generation now fails and asks for customMockName. The two methods made MyService.mock() ambiguous.
Before:
@Instantiable(generateMock: true)
public struct MyService: Instantiable {
public init(engine: Engine) { self.engine = engine }
@Instantiated let engine: Engine
}
@Instantiable(mockOnly: true)
extension MyService {
public static func mock(engine: Engine = Engine()) -> MyService { MyService(engine: engine) }
}After:
@Instantiable(mockOnly: true, customMockName: "preview")
extension MyService {
public static func preview(engine: Engine = Engine()) -> MyService { MyService(engine: engine) }
}A production type without a generated mock may still pair with a mockOnly method named mock.
After upgrading
Do one clean build. .safedi module-info files written by 2.0.0 are not readable by 2.1.0, and a build system that does not rerun generation when only the tool changes (Tuist's script phase, for example) can otherwise hand a stale file to a downstream module.
If you used a 2.1.0 beta
#SafeDIConfiguration(activeCompilationConditions:) existed in 2.1.0-beta-1 through beta-3 and was removed before this release. Delete the argument: SafeDI now reads the conditions from your build.
Known limitations
- Very large SwiftPM packages built with the Swift Build backend can exceed SwiftPM's plugin-input limit (roughly 1,000–4,500 input files in one target's plugin invocation). This is a SwiftPM limit; see the Manual.
- On SwiftPM's native backend, SafeDI cannot see the build configuration, so a production declaration behind
#if DEBUGneeds theswiftSettingsdefine described above.
Full changelog
- Docs: rewrite prebuild-script migration guide around --combined-output by @dfed in #296
- Locate build artifacts under the swiftbuild .build layout by @dfed in #300
- Document sharing one instance across unrelated protocols by @dfed in #299
- Add a CI job that builds and tests on the latest Swift release by @dfed in #301
- Update CI actions to latest majors and add Dependabot by @dfed in #302
- Bump SwiftFormat to 0.64.0 by @dfed in #304
- Document where generated code lives per integration by @dfed in #306
- Report aliased @received type mismatches at the user's declaration by @dfed in #305
- Process instantiables in a deterministic order by @dfed in #307
- Cut macOS CI time without changing what is verified by @dfed in #308
- Skip build and test jobs when a change touches only Markdown by @dfed in #309
- Build the non-macOS destinations with shipped runtimes and cache the CodeQL Swift build by @dfed in #312
- Make Codecov wait for both platform uploads before reporting by @dfed in #318
- Tuist example: bump SafeDI to 2.0.0 by @dfed in #317
- Gate the Codecov project status per platform flag instead of globally by @dfed in #323
- Emit validation errors as compiler-format diagnostics and add a fix hint by @dfed in #315
- Offer a fix-it for near-match @received properties by @dfed in #319
- Offer fix-its for a retyped @received and for onlyIfAvailable by @dfed in #324
- Run the CodeQL Swift build natively instead of under Rosetta by @dfed in #325
- Scan only the active #if clauses and assert them at compile time by @dfed in #316
- Restore generate's speed on large multi-module builds by @dfed in #326
- Keep the opt-mode Bazel assertion working with a warm cache by @dfed in #327
- Remove the no-op branch-replacement steps from CI by @dfed in #328
- Stop re-walking received dependencies during cycle validation by @dfed in #329
- Emit if-let builder calls in generated mock bodies by @dfed in #330
- Remove #SafeDIConfiguration(activeCompilationConditions:) by @dfed in #335
- Resolve #SafeDIConfiguration before walking files its mock condition affects by @dfed in #339
- Point install instructions at 2.1.0 by @dfed in #332
- Read every override before the outermost @sendable mock builder by @dfed in #338
- Prove the examples build against SafeDI 2.1.0-beta-3 by @dfed in #331
- Require customMockName when a mockOnly mock pairs with a generated mock() by @dfed in #341
- Report NDEBUG from safedi_compile in the opt compilation mode by @dfed in #333
- Accept SafeDITool manifests that omit activeCompilationConditions by @dfed in #334
- Stop asserting #if blocks around initializers SafeDI could never select by @dfed in #336
- Regenerate when only the compilation conditions change by @dfed in #342
- Size the received-chain tests so their time limit is only a hang guard by @dfed in #343
- Offer removal from additionalMocksToGenerate for types from other modules by @dfed in #344
- Prove the examples build against SafeDI 2.1.0-rc-1 by @dfed in #345
- Walk every file in the first pass and re-walk only when the mock condition changes by @dfed in #346
- Read SafeDIConfiguration.swift before scanning the rest of the module by @dfed in #347
- Prove the examples build against SafeDI 2.1.0-rc-2 by @dfed in #348
- Read compilation conditions from the build settings in Xcode and Swift Build builds by @dfed in #350
- Document SwiftPM's plugin input limit for very large packages by @dfed in #352
- Declare a SafeDI module's two Bazel targets from one macro by @dfed in #349
- Pass nil to onlyIfAvailable receivers whose dependency can only be built after them by @dfed in #351
- Promote a required dependency that is also aliased only if available by @dfed in #354
- Prove the examples build against SafeDI 2.1.0-rc-3 by @dfed in #358
- Update SwiftFormat to 0.64.2 by @dfed in #356
- Pass the build-setting arguments to every SafeDITool by @dfed in #361
- Stop resolving paths that are already resolved by @dfed in #357
- Check scan-cache freshness with stat instead of Foundation resource values by @dfed in #359
- Name an aliased dependency's declaration without the compiler's #fileID warning by @dfed in #362
- Prove the examples build against SafeDI 2.1.0-rc-4 by @dfed in #365
- Name an aliased declaration in a comment when the project root is reported through a symbolic link by @dfed in #374
- Generate a file for every root and mock in any #if clause by @dfed in #377
- Import what generated mocks need when the build does not report DEBUG by @dfed in #368
Full Changelog: 2.0.0...2.1.0