Skip to content

Fix additionalDirectoriesToInclude on the Xcode/plugin path#205

Merged
dfed merged 7 commits intomainfrom
dfed--additionalDirectoriesToInclude-xcode
Apr 3, 2026
Merged

Fix additionalDirectoriesToInclude on the Xcode/plugin path#205
dfed merged 7 commits intomainfrom
dfed--additionalDirectoriesToInclude-xcode

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 3, 2026

Summary

  • The plugin pre-scan (runRootScanner) now discovers @SafeDIConfiguration's additionalDirectoriesToInclude via text-based scanning, enumerates those directories, and includes their roots in the manifest and output file list
  • Additional-directory Swift files are declared as build inputs so edits trigger rebuilds
  • Extracts RootScanner and RelativePath into a SafeDIRootScannerCore library target shared by SafeDITool, SafeDIRootScanner, and both plugins (via symlinks)
  • Manifest validation in SafeDITool is scoped to current-module roots only (not dependent-module roots)
  • Config discovery scans only the current module's own files, matching SafeDITool's configurations.first behavior

Test plan

  • New test: root in additional directory gets output file generated
  • New test: multiple roots in additional directory all get output files
  • New test: only additional directory has roots (no target roots)
  • New test: cross-boundary dependency resolution (additional root depends on target dep)
  • Config extraction tests: paths extracted, no config, empty array, commented-out config, missing property, truncated source (2 variants), unmatched bracket, malformed string literal, no string literals
  • Non-directory baseURL coverage test
  • Unmatched parenthesis coverage test
  • Existing rootNotInManifest error test preserved
  • All 395 tests pass

🤖 Generated with Claude Code

…e path

On the Xcode/plugin path, the manifest is built from target files before
SafeDITool runs, so roots discovered from additionalDirectoriesToInclude
directories were not in the manifest and tripped rootNotInManifest
validation. This change auto-generates manifest entries for unmapped roots
using RootScanner's collision-safe naming logic.

Extracts RootScanner and RelativePath into a SafeDIRootScannerCore library
target so SafeDITool can call RootScanner.scan() without duplicating code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.92%. Comparing base (3a877fd) to head (be9f743).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #205   +/-   ##
=======================================
  Coverage   99.92%   99.92%           
=======================================
  Files          40       40           
  Lines        3809     3866   +57     
=======================================
+ Hits         3806     3863   +57     
  Misses          3        3           
Files with missing lines Coverage Δ
...s/SafeDIRootScanner/SafeDIRootScannerCommand.swift 100.00% <ø> (ø)
Sources/SafeDIRootScannerCore/RelativePath.swift 100.00% <100.00%> (ø)
Sources/SafeDIRootScannerCore/RootScanner.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/SafeDITool.swift 99.65% <100.00%> (+<0.01%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dfed and others added 6 commits April 3, 2026 08:38
The previous commit auto-extended the manifest for any unmapped root,
which incorrectly applied to dependent-module roots and ordinary
manifest-drift scenarios. Now only roots whose sourceFilePath came from
additionalDirectoriesToInclude are auto-extended; other unmapped roots
still throw rootNotInManifest as before.

Restores the original error test for plain target roots missing from the
manifest.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…o current module

The plugin pre-scan (runRootScanner) now discovers @SafeDIConfiguration's
additionalDirectoriesToInclude, enumerates those directories for Swift files,
and includes them in the root scan. This ensures:
- The manifest includes all roots (target + additional directories)
- Output files are declared in the build graph
- SafeDITool is invoked even when only additional directories have roots

Also scopes manifest validation in SafeDITool to only current-module roots
(not dependent-module roots which don't belong in this target's manifest).
Removes the auto-extension safety net since the pre-scan now handles it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
runRootScanner now returns both output files and additional input files
discovered from additionalDirectoriesToInclude. Both plugin variants
(SPM and Xcode) include these in the build command's inputFiles so that
edits to additional-directory Swift files trigger rebuilds.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Moves config discovery out of runRootScanner into a new
discoverAdditionalDirectorySwiftFiles helper that plugins call with only
their own module's files. This matches SafeDITool's configurations.first
behavior and prevents dependent-module configs from leaking in.

Both plugin variants (SafeDIGenerator and SafeDIPrebuiltGenerator) now
call discoverAdditionalDirectorySwiftFiles with target-only files and
pass the results explicitly to runRootScanner. The prebuilt plugin's
Xcode path is updated to consume RootScannerResult and propagate
additionalInputFiles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Applies SwiftFormat fixes (import sorting, redundantType, redundantSelf,
indent, trailingCommas, opaqueGenericParameters).

Adds 8 new RootScanner tests covering:
- Config present without additionalDirectoriesToInclude property
- Truncated source before = and before [
- Unmatched brackets in array literal
- Malformed/partial string literals in array
- Array with no string literals
- Unmatched parenthesis in @INSTANTIABLE
- Non-directory baseURL in scan(inputFilePaths:)

The only remaining uncovered region is a dictionary subscript default
that is structurally unreachable (the key is always present).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed changed the title Auto-extend manifest for additionalDirectoriesToInclude roots Fix additionalDirectoriesToInclude on the Xcode/plugin path Apr 3, 2026
@dfed dfed marked this pull request as ready for review April 3, 2026 19:11
@dfed dfed self-assigned this Apr 3, 2026
@dfed dfed merged commit ff5a90e into main Apr 3, 2026
19 checks passed
@dfed dfed deleted the dfed--additionalDirectoriesToInclude-xcode branch April 3, 2026 19:48
dfed added a commit that referenced this pull request Apr 3, 2026
Merge additionalDirectoriesToInclude support (PR #205) with mock
generation. Resolves conflicts in RootScanner (now in
SafeDIRootScannerCore), SharedRootScanner, plugin files, and test
helper. Both feature sets coexist: additional directory scanning for
roots and mock generation scoping via targetSwiftFiles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dfed added a commit that referenced this pull request Apr 3, 2026
The merge of PR #205 broke mock module scoping. The CSV written by
the plugin includes ALL swift files (target + dependencies), but
runRootScanner was treating the entire CSV as targetSwiftFiles for
mock scanning. This caused mocks to be generated for types from
dependent modules.

Fix: add mockScopedSwiftFiles parameter so plugins pass only the
target's own files (+ additional directories) for mock scoping,
separate from the full CSV used for root scanning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
dfed added a commit that referenced this pull request Apr 4, 2026
## Summary

SafeDI now automatically generates `mock()` methods for `@Instantiable`
types, enabling lightweight test doubles with full dependency tree
support.

### Mock API

Each `@Instantiable` type gets a `public static func mock(...)` method.
Parameters use one of three styles depending on the dependency kind:

| Kind | Parameter style | Default |
|------|----------------|---------|
| Tree child with subtree | `child: Child? = nil` | Inline construction
|
| Leaf / uncovered dep | `@autoclosure @escaping () -> T = T()` |
Autoclosure |
| Default-valued init param | `@autoclosure @escaping () -> T =
originalDefault` | Original default |
| Forwarded | `name: String` (bare) | Required |
| onlyIfAvailable | `@autoclosure @escaping () -> T? = nil` | Nil |
| Closure-typed default | `@escaping T = default` | Original default (no
@autoclosure) |

### Parameter disambiguation

When multiple dependencies share a label (e.g., `service: LocalService`
and `service: ExternalService`), parameter names are disambiguated using
type-based suffixes:
- `MockParameterIdentifier` (Hashable struct with `propertyLabel` +
`sourceType`) replaces string-based keys throughout
- `TypeDescription.asIdentifier` walks the type tree to produce
identifier-safe suffixes (e.g., `service_ExternalService`)
- `TypeDescription.simplified` strips Optional/attribute/metatype
wrappers for cleaner suffixes, falling back to full suffix on collision
- Disambiguated names propagate consistently through parameter lists,
local bindings, init arguments, and nested scopes via
`MockContext.parameterLabelMap`

### Dependency resolution

- `resolvedParameters: Set<MockParameterIdentifier>` tracks which deps
are already bound — accumulated through siblings and parent-to-child
descent to prevent duplicate bindings
- `rootBindingIdentifiers` / `rootDefaultIdentifiers` control which
params get root-level `let x = x()` bindings vs scoped bindings inside
child functions
- Forwarded deps are excluded from label remapping (always use their
bare parameter name)
- `@Sendable` / `@MainActor` annotations preserved on closure-typed
parameters

### User-defined mock() methods

- When a type declares `static func mock(...)`, SafeDI skips generating
a mock for it
- Parent types call `Child.mock(...)` instead of `Child(...)`, threading
deps through the user method
- `@Instantiable` macro validates mock methods are `public`,
`static`/`class`, with parameters for all deps (with fix-its)
- Misconfigured mocks emit `/* @INSTANTIABLE type is incorrectly
configured */` comment

### Default-valued init parameters

- Non-dependency init params with defaults (e.g., `flag: Bool = false`)
bubble up to the root mock level as `@autoclosure @escaping` parameters
- Bubbling stops at
`Instantiator`/`SendableInstantiator`/`ErasedInstantiator`/`SendableErasedInstantiator`
boundaries and at types with no-arg user-defined `mock()` methods
- `@escaping` stripped from closure defaults (invalid in return
position); `@Sendable`/`@MainActor` preserved
- Root's own default-valued params bind at root scope; child defaults
bind inside scoped functions

### Cross-module support

- `@Instantiated` deps from parallel modules (type not in current
module's dependency chain) surface as required `@autoclosure @escaping`
parameters
- Cross-module deps with `.safedi` module info get optional parameters
with tree construction as default
- Uncovered transitive deps detected by `collectMockDeclarations` and
surfaced as required parameters

### Configuration

- `@SafeDIConfiguration` properties: `generateMocks: Bool` and
`mockConditionalCompilation: StaticString?`
- `@Instantiable` parameter: `mockAttributes: StaticString` for global
actor annotations (e.g., `@MainActor`)
- Per-module generation: each module generates mocks for its own types
only

### Documentation & examples

- Manual updated with mock generation section (autoclosure API,
user-defined mocks, default bubbling)
- Example projects updated: `#Preview` blocks use `.mock()`, NoteView
demonstrates default-valued parameter bubbling

### Testing

- 600+ tests total (150+ mock generation tests + existing + PR #205
additions)
- Mock tests use exact full-output `==` comparison (no `.contains`
assertions except for dynamic-path error stubs)
- Coverage includes: all Instantiator variants,
erasedToConcreteExistential, extension-based types, deep nesting,
protocol fulfillment, lazy cycles, default-valued params
(Bool/String/closures/nil/complex), user-defined mocks, disambiguation
(simplified/full suffix/3-way collision), Instantiator boundaries,
cross-module deps, promoted property ordering, sibling resolution, and
more

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant