Skip to content

2.1.0

Choose a tag to compare

@dfed dfed released this 26 Oct 19:06
· 307 commits to main since this release

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 build on 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 your mockConditionalCompilation) keep working in every integration, including an import they need under its own #if DEBUG.
  • A root or a generateMock: true type 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 @Received label or type, a missing onlyIfAvailable: true, a stale entry in additionalMocksToGenerate.
  • A type mismatch on a renamed dependency (@Received(fulfilledByDependencyNamed:ofType:)) is reported at your @Received declaration 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_library macro declares a module's two targets (SafeDI generation and the Swift library) in one call.
  • safedi_compile reports NDEBUG in opt builds, so SafeDI's view of DEBUG matches the compiler's.

Smaller things

  • Put your #SafeDIConfiguration in a file named SafeDIConfiguration.swift and 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.
  • onlyIfAvailable dependencies are ordered correctly among siblings: a receiver whose dependency can only be built after it gets nil instead 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 @Sendable builder compile in Swift 6.
  • Generated output and .safedi ordering 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:

  • generate is about 30% faster.
  • scan takes the same time, even though it now evaluates #if blocks.
  • 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 let builder calls, which cut compiler time for mock code by about a quarter on a synthetic module.
  • The scan cache's freshness check uses stat and 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 build on the Swift Build backend: SafeDI reads the build settings. Nothing to do.
  • swift build on SwiftPM's native backend (the default through Swift 6.2, the toolchain in Xcode 26): SwiftPM does not report its implicit DEBUG to plugins. Add .define("DEBUG", .when(configuration: .debug)) to the target's swiftSettings, 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 use safedi_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 DEBUG needs the swiftSettings define 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