Skip to content

D codemods — a distinct product on the formatter's substrate ​

Status: roadmap, nothing implemented · Date: 2026-08-25 · Scope: automated, type-aware source transformation for D · Requirement IDs: CDM*

D9 put token rewriting inside the formatter's scope, and the moment it did, a second thing became easy to conflate with the first. This page draws the line and gives the second thing its own roadmap.

The formatter respells; a codemod transforms. Both emit TextEdit[], both run on the same token spine, and a user might reasonably ask for either with the words "fix my code" — but they differ in the one property that decides how they can be shipped: whether the tool needs to know types.


Where the line is ​

Formatter rewrites (D9 tier 2)Codemods
Inputtokens, plus the offset oracletokens and resolved types (sparkles:dmd-lsp)
Guaranteethe program means the same thingthe program means something deliberately different or narrower
Verificationmechanical, per rule (decode-and-compare, syntactic equivalence)type-checked at the site; ultimately, the build
Applicationwhole file, unattended, on saveper site, reviewed, usually once
Idempotentrequiredusually, but not definitionally
Failure modewrong byteswrong program
Examplesalias X = Y;, q"…" instead of escapes, import sorting, => bodiesformat(…) → IES, deprecated-API migration, enum → static immutable, library version bumps

The test is not "how big is the change" — swapping a string literal's delimiter and swapping format for an IES literal look equally local. The test is whether a purely syntactic argument establishes safety. q"EOS…EOS" and "…" decode to identical code units, so a decoder settles it. format("%s %s", a, b) returns a string while i"$(a) $(b)" is a compile-time sequence that deliberately does not convert to one — so only the callee's signature settles it, and that needs a type. Anything in the second class belongs here, not in the formatter.

Precedent ​

The ecosystems that split these two tools cleanly are the ones whose users are happiest running either unattended:

ToolPosition
go fixseparate binary from gofmt; migrates APIs across releases
clang-tidyseparate from clang-format; type-aware checks with --fix, per-check opt-in
dotnet format analyzersanalyzers-with-fixers, distinct from whitespace formatting
ruffone binary, but format and check --fix are separate subcommands with separate rule sets
jscodeshift / ts-morphcodemods as a library; formatting deliberately delegated to prettier
IntelliJ / Roslyn code actionsper-site, reviewed, offered by the editor rather than applied in bulk

The recurring shape: codemods run through the formatter, never instead of it. A codemod edits, and the formatter then lays the result out — which means a codemod author never writes layout code, and the two tools' test suites do not overlap.


Architecture ​

sparkles:dmd-lsp   ──types, symbols, ddoc──┐
                                           ├─→  sparkles:dmd-codemod  ──TextEdit[]──→  sparkles:dmd-fmt  ──→  file
sparkles:dmd-fmt   ──spine, offsets────────┘        (rules)                              (layout)
  • CDM1 — Rules are data + a predicate + an edit producer. A rule declares what it matches (token pattern, oracle fact, or resolved type), and returns edits. No rule prints layout; the formatter runs last, unconditionally.
  • CDM2 — Every rule is opt-in and individually named, like clang-tidy checks. There is no "run all codemods" default, ever.
  • CDM3 — Application is per site with a reason string, so a CLI can print, an LSP can offer a code action, and a review can read why a site changed.
  • CDM4 — The verification story is the build. A codemod run is followed by a compile of the affected modules; a rule that cannot be validated that way (because the change is in a version-gated arm, say) reports the site as unverified rather than applying silently.
  • CDM5 — Reuse the formatter's suppression mechanism. // dfmt off ranges, verbatim regions and inactive arms are already the formatter's single suppression path (D5); codemods honour the same markers rather than inventing a second spelling.

Roadmap ​

PhaseContentsDepends on
C0The rule API, edit application and conflict resolution, the test harness (reuse the formatter's .cases shape from the testing spec)dmd-fmt M5 edits
C1Syntactic rules that the formatter declines because they are migrations, not respellings — deprecated-API renames, dfmt→dmd-fmt config migrationC0
C2Type-aware rules. First rule: format("%s %s", a, b) → i"$(a) $(b)", applied only where the callee accepts IES (P155)sparkles:dmd-lsp type oracle
C3Editor surface — LSP code actions, so a rule is offered at the site rather than run in bulkC2, dmd-lsp server
C4Project-wide runs: git-aware application, per-rule commits, dry-run reportsC1–C3

C2 is the phase that justifies the split. Until types are available, a codemod tool is just a formatter with a worse name.

What this is not ​

  • Not a linter. A rule here always has a fix; diagnostics without fixes belong in sparkles:dmd-lsp.
  • Not a refactoring engine. Renames, extract-function and signature changes are editor operations over a whole project's symbol graph; they are dmd-lsp's, and their correctness argument is different again.
  • Not on by default, and not part of dmd-fmt --check. A formatter that fails CI because a codemod is available would make the formatter unusable, which is precisely the confusion this page exists to prevent.

Traceability ​

  • Spec: the decision record (D9 fixes the formatter's half of the line) · the testing spec (the .cases harness C0 reuses)
  • Research: the decision inventory — P155 is C2's first rule, and §J's hazard notes are the seed of the rule catalogue
  • Libraries: sparkles:dmd-lsp (types), sparkles:dmd-fmt (spine, edits, layout)