Skip to content

Generate one output file per root @Instantiable#202

Draft
dfed wants to merge 19 commits intodfed/major-version-bumpfrom
dfed/per-root-output
Draft

Generate one output file per root @Instantiable#202
dfed wants to merge 19 commits intodfed/major-version-bumpfrom
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 (27aaf91) to head (f6229a1).

Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##           dfed/major-version-bump     #202      +/-   ##
===========================================================
+ Coverage                    99.88%   99.91%   +0.03%     
===========================================================
  Files                           36       37       +1     
  Lines                         3452     3501      +49     
===========================================================
+ Hits                          3448     3498      +50     
+ Misses                           4        3       -1     
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.35%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

dfed and others added 18 commits March 30, 2026 21:57
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>
@dfed dfed force-pushed the dfed/per-root-output branch from 2e474ed to aeff016 Compare March 31, 2026 04:58
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>
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