Skip to content

Generate one output file per root @Instantiable#202

Merged
dfed merged 19 commits intomainfrom
dfed/per-root-output
Apr 3, 2026
Merged

Generate one output file per root @Instantiable#202
dfed merged 19 commits intomainfrom
dfed/per-root-output

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Mar 30, 2026

Summary

  • Replace --dependency-tree-output (single file) with --swift-manifest (JSON manifest mapping input files to output files)
  • One output file per @Instantiable(isRoot: true) root instead of a monolithic SafeDI.swift
  • Introduce SafeDIToolManifest Codable type as the explicit contract between plugin/build systems and the tool
  • Manifest uses an ordered array of InputOutputMap structs (not a dictionary) for concrete ordering and extensibility
  • Plugin scans source files for roots, writes a manifest with relative input paths (for remote cache compatibility), and declares per-root output files
  • Tool validates the manifest against parsed roots and writes to specified output paths
  • Add sourceFilePath to Instantiable for tracking which file each root came from
  • File header is written once per output file, even when multiple roots share a source file
  • Document manifest format in Manual.md and migration steps in README

Motivation

  1. Incremental compilation — when one root's dependency tree changes, only that root's generated file needs recompilation
  2. Futureproofing — the manifest structure scales to additional output kinds (e.g. mock generation) without growing the CLI argument list

Test plan

  • All 369 tests pass locally (swift test)
  • Code coverage does not decrease (98.99% total)
  • Lint passes (./lint.sh)
  • Package integration builds locally
  • Project integration builds locally
  • CI passes

🤖 Generated with Claude Code

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 30, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.91%. Comparing base (1a9d04b) to head (6fe9436).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #202   +/-   ##
=======================================
  Coverage   99.91%   99.91%           
=======================================
  Files          36       37    +1     
  Lines        3455     3501   +46     
=======================================
+ Hits         3452     3498   +46     
  Misses          3        3           
Files with missing lines Coverage Δ
...afeDICore/Generators/DependencyTreeGenerator.swift 100.00% <100.00%> (ø)
Sources/SafeDICore/Models/InstantiableStruct.swift 100.00% <ø> (ø)
Sources/SafeDICore/Models/SafeDIToolManifest.swift 100.00% <100.00%> (ø)
Sources/SafeDITool/SafeDITool.swift 99.65% <100.00%> (+0.04%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dfed dfed force-pushed the dfed/per-root-output branch from 2e474ed to aeff016 Compare March 31, 2026 04:58
@dfed dfed marked this pull request as ready for review April 2, 2026 22:39
@dfed dfed self-assigned this Apr 2, 2026
Base automatically changed from dfed/major-version-bump to main April 3, 2026 06:57
dfed and others added 19 commits April 2, 2026 23:58
Replace --dependency-tree-output (single file) with --swift-output-directory
(directory). The build plugin uses regex to detect root types in source files
and declares one output file per root ({TypeName}+SafeDI.swift). The tool
writes per-root files with the same naming convention.

This improves incremental compilation: when one root's dependency tree changes,
only that root's generated file needs recompilation. Targets with no roots
produce no output files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Introduce SafeDIToolManifest as the explicit contract between the plugin
and SafeDITool. The manifest maps input file paths to output file paths,
replacing the implicit naming convention where both sides independently
computed filenames.

The plugin now writes a JSON manifest and passes --swift-manifest to
the tool. The tool validates the manifest against its parsed roots and
writes to the specified output paths.

This design scales to future output kinds (e.g. mock generation) without
growing the CLI argument list, and uses relative paths for compatibility
with remote build caches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add tests for both ManifestError cases:
- Manifest lists a file that doesn't contain a root
- Root exists in a file not listed in the manifest

Also fix the empty-root test to expect a comment-only output file
(matching the new behavior where manifest mode always writes output).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The regex was matching @INSTANTIABLE(isRoot: true) inside doc comment
backtick-quoted code spans. Add a negative lookbehind for backtick to
avoid matching inside markdown code references.

Also rephrase SafeDIToolManifest doc comment to avoid containing the
literal pattern that triggers false matches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Swift's Regex does not support lookbehind assertions, causing the
previous regex to silently fail (try? returned nil). Instead, check
that each match's line prefix doesn't contain '//' or '`' to filter
out matches inside comments and doc comment code spans.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test unexpected nodes with a root declared (covers errorContent write
  in manifest mode)
- Test multiple roots in the same file (covers code combining path)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use Optional.map instead of guard-let-else-return-nil so the nil path
is implicit in the optional chaining rather than an explicit branch
that coverage tooling flags.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sourceFilePath is needed in .safedi cross-module files so the root
module's tool invocation can match dependent module roots against
the manifest. Remove unnecessary custom CodingKeys, ==, and hash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compute input file paths relative to the package/project root instead
of using absolute paths. The tool's cwd is the package root (verified
for both SPM and Xcode), so relative paths resolve correctly.

Output paths remain absolute since they reference the build system's
plugin work directory which is outside the project tree.

This enables consistent cache keys across machines for build systems
like Bazel and Buck that use remote caches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Provides concrete ordering and extensibility. Each entry is a struct
with inputFilePath and outputFilePath, with doc comments explaining
path semantics.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No longer needed outside the module after removing the type-name-based
output filename computation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GeneratedRoot.code now contains only the extension code, without the
file header. The tool prepends the header once per output file when
combining extensions. Previously each root's code included the header,
causing duplication when multiple roots mapped to the same output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
No longer needed outside the module — fileHeader wraps it.

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

- Test that running the tool twice with identical inputs does not
  rewrite the output file (verified via modification timestamp)
- Test that --swift-manifest and --dot-file-output work together

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Disambiguate output filenames when multiple root files share the same
base name (e.g. ModuleA/Root.swift and ModuleB/Root.swift now produce
ModuleA_Root+SafeDI.swift and ModuleB_Root+SafeDI.swift).

Sort extensions before joining when multiple roots share a source file,
ensuring deterministic output regardless of task-group completion order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Xcode plugin variants (#if canImport(XcodeProjectPlugin)) were
not updated when outputFileName was replaced with outputFileNames.
Linux CI doesn't compile these blocks, so this went undetected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dfed dfed force-pushed the dfed/per-root-output branch from f6229a1 to 6fe9436 Compare April 3, 2026 07:00
@dfed dfed merged commit e801810 into main Apr 3, 2026
19 checks passed
@dfed dfed deleted the dfed/per-root-output branch April 3, 2026 07:12
dfed added a commit that referenced this pull request Apr 3, 2026
## Summary
- add a lightweight `SafeDIRootScanner` executable that performs lexical
root discovery without SwiftSyntax
- make both build plugins invoke the scanner to produce
`SafeDIManifest.json` plus the explicit per-root output file list before
returning `buildCommand`
- keep `SafeDITool`'s manifest contract unchanged while removing
regex-based root discovery and manifest writing from the plugin layer
- make duplicate root file basenames deterministic and collision-safe in
the scanner output naming logic
- align `SafeDIToolTests` with the real plugin path by generating
manifests through the scanner while preserving the existing input Swift
-> expected output style
- add scanner-focused tests for comment/string/raw-string skipping,
deterministic manifest serialization, and duplicate-basename outputs
- add tool regressions for same-file multi-root exact output and
duplicate-basename manifest outputs

## Why
`buildCommand` is still required here for incremental performance and
compatibility with build systems like Buck and Bazel, which means the
plugin has to know its explicit output files up front. `#202` introduced
the manifest contract and per-root outputs, but its plugin-side root
discovery remained regex-driven.

This PR keeps the fast explicit-output path while tightening the weakest
part of the stack:
- the plugin now orchestrates instead of guessing
- the scanner owns cheap lexical discovery and manifest generation
- `SafeDITool` remains the semantic source of truth for validation and
code generation

## Validation
- [x] `swift test`
- [x] `./lint.sh`

## Stack
- base: `#202`
- this PR stays draft until the full stack is ready

---------

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