Repository navigation
Conversation
A decorator after `export default` starts a class declaration. The declaration list of the `export default` arm did not hold the `@` token, so the class was parsed as an expression. Its name was not bound in the module, a `(` on the next line became a call on the class, `abstract` was rejected, and experimentalDecorators dropped the decorator. The declaration path gave an anonymous default class the symbol of the default export as its name. Standard decorator lowering now takes that binding as a separate input and names the class "default". The binding becomes the class name only when class decorators rebind the class.
…e for default classes After `export default`, a decorator that no class follows reported `Expected "class"` and then a second `Unexpected ""` from the fallback arm. The arm now returns after the first report. `exports.replace` for `default` set the replacement and then lowered the class, which put the class back. The class arm now emits the replacement and returns, as the function arm does. `export default @dec class` kept the replacement only because it was parsed as an expression. Tests: minified and split bundles of `export default @dec class`, the `abstract = 1` and `declare = 1` tails after decorators, and the temporal dead zone cell in the bundled run.
|
Status: ready for review. How I reproduced it, on Bun 1.4.2 and on // a.ts
const dec = (v: any, c: any) => {};
export default @dec class Df { m() { return "m"; } }
console.log(typeof Df, new Df().m());
The same parse gives these results on
Test files: |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 7 remain after this review. WalkthroughThe parser now recognizes decorated default-export declarations. Class lowering passes the default-export binding to standard-decorator lowering and handles direct replacement expressions. Tests cover parsing, class names, replacements, and bundled output. ChangesDecorated default-export classes
Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to This change makes decorated default-export classes parse and lower correctly, and it adds regression coverage. No actionable merge-blocking risk was identified in the supplied context. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Beyond the inline findings, I also checked the new expect("a class statement has a name") behind get_or_insert_with in src/js_parser/lower/lower_decorators.rs: the None call site in src/js_parser/p.rs is only reached through lower_class for class statements the parser always names, since s_export_default now calls lower_standard_decorators_stmt directly, so it is not reachable from user input. The Replace arm change in src/js_parser/visit/visit_stmt.rs now mirrors the SFunction arm above it (push the export with the replacement value, then return), and the errors_before_decorators count only short-circuits the second "Unexpected" message when the nested parse already logged one.
Extended reasoning...
The change touches the Rust JS parser (parse_stmt, visit_stmt, decorator lowering), bumps the runtime transpiler cache version, and adds tests; it touches no security-sensitive surface. Inline findings were posted (Bake "use client" rebind, top-level using with non-bun targets, and the generated <file>_default name collision), so a human must still weigh them; this note only records the adjacent concerns examined and ruled out.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🔴
src/js_parser/visit/visit_stmt.rs— Bake dev-server users whose"use client"module hasexport default @ dec class Df {}with standard decorators now getReferenceErrorat module load; on main the module loaded. The class is lowered as a statement at visit_stmt.rs:840, which appends the rebindDf = _Dfafter the export (visit_stmt.rs:867); the wrap at visit_stmt.rs:874-878 then moves the class intoregisterClientReference(...), soDfis no longer declared when the rebind runs. The todo_panic at p.rs:8766 is skipped whenjsx.developmentis set, which is the dev server. Fix: whenwraps_exports()is true, lower the default class through the expression path (as main did) or emit the rebind against the wrapped value, so no assignment targets a binding the wrap removed.Why this was flagged
Trigger: a Bake app with server components and
separate_ssr_graph: false(ParseTask.rs:2585 selectsWrapExportsForClientReference) in development, where a"use client"file containsexport default @ dec class Df {}under standard decorators. On the base commit the@afterdefaultwent to parse_expr, so s_export_default took the Expr arm: the class decorators were lowered with a comma expression (lower_decorators.rs:772-777) and the wrap at visit_stmt.rs:510-512 wrapped that expression; no rebind statement existed. With this PR the Stmt arm runs: lower_standard_decorators_stmt pushesDf = _Df(lower_decorators.rs:749-752), visit_stmt.rs:867 appends it after the export, and visit_stmt.rs:874-878 then takesclass.classout of the statement and wraps it, so the printed output isexport default registerClientReference(class Df {...}, ...); Df = _Df;with noDfbinding; strict-mode ESM throws ReferenceError at load. p.rs:8763 only panics whenjsx.developmentis false; the dev server sets development.Verification:
export default @ dec class Df {}now reaches theSClassarm; visit_stmt.rs:860-868 emits theDf = _Df;suffix, then visit_stmt.rs:870-879 wraps the class inregisterClientReference(...), so the rebind is a ReferenceError under strict code. p.rs:8756-8767 only panics for non-development. On the base visit_stmt.rs:510-512 wrapped the comma expression and the module loaded. -
🔴
src/js_parser/visit/visit_stmt.rs— Users who bundle for a non-bun target (thebun builddefault) with a top-levelusingnow loseexport default @ dec class Df {}entirely; main still emitted the default export. The decorated class now takes the Stmt branch at visit_stmt.rs:778, andfinalizedrops every S::ExportDefault statement there (p.rs:10232), so importers getNo matching export in "mod.js" for import "default"orReferenceError: Df is not defined. Fix: underwill_wrap_module_in_try_catch_for_using, rewrite a default class or function statement tovar <name> = ...; export { <name> as default }as the Expr branch does at visit_stmt.rs:515, which also covers@ dec export default classandexport default function(#44720). [also at: src/js_parser/parse/parse_stmt.rs:1591 - Users who build a module with a top-levelusingandexport default @ dec class Df {}for a non-bun target lose the default export; on main it works.]Why this was flagged
Input: a module with
using resource = {...}at top level andexport default @ dec class Df {}, built withbun build --target=nodeor--target=browser(the CLI default), or run through Bun.Transpiler;lower_usingis!target.is_bun()(src/bundler/transpiler.rs:1528, src/bundler/ParseTask.rs:2549) sowill_wrap_module_in_try_catch_for_usingis true (src/js_parser/parse/parse_entry.rs:1230). On main the@afterexport defaultwent toparse_expr, sodata.valuewas an Expr and visit_stmt.rs:515 turned it intovar mod_default = ...; export { mod_default as default }, whichfinalizekeeps as an SExportClause. WithT::TAtin the declaration lookahead (src/js_parser/parse/parse_stmt.rs:1591) the value isStmtOrExpr::Stmt(SClass), handled at visit_stmt.rs:778-883, which has nousingconversion;finalizethen hitsSExportDefault(_) => continueat src/js_parser/p.rs:10232 and discards the statement, class and export together. The bundle either fails to link the default import or throwsReferenceError: Df is not definedat runtime.Verification: The
StmtData::SClassarm (visit_stmt.rs:778-884) has nousingrewrite; it lowers the class and pushes theS::ExportDefaultstmt.finalizethen unconditionally drops it:js_ast::StmtData::SExportDefault(_) => { continue; }at p.rs:10232-10234. So the class declaration and the default export are both gone, while the base branch's Expr path produced a workingvar+export {}pair for the same input.
| let name = *s_class | ||
| .class | ||
| .class_name | ||
| .expect("a class statement has a name"); | ||
| .get_or_insert_with(|| default_export.expect("a class statement has a name")); |
There was a problem hiding this comment.
🟡 (optional) Users who transpile (not bundle) export default @ dec class {} next to a local named <file>_default now get SyntaxError: Cannot declare a class twice where main loaded the module. When a class decorator needs a rebind, the anonymous class takes the default export's generated symbol as its declared name at src/js_parser/lower/lower_decorators.rs:747, and without a bundle no renamer guards that name against user bindings. Fix: rebind through a name that cannot collide (a fresh temp, or print the anonymous class as an expression assigned to a temp and export default that), covering @ dec export default class {} too, which already fails this way on main. The PR notes this under #44721 with two test.todo rows; the todo documents the cost but does not bound it.
Why this was flagged
Input: a JS or TS module test.js containing const test_default = 1; and export default @ dec class {} with standard decorators, run directly with bun test.js (no bundle). Parsing now takes the statement path (src/js_parser/parse/parse_stmt.rs:1591), and s_export_default calls lower_standard_decorators_stmt with Some(data.default_name) (src/js_parser/visit/visit_stmt.rs:840-843). Because the class has a class decorator, lowered.class_decorators is Some and get_or_insert_with at src/js_parser/lower/lower_decorators.rs:747 sets class_name to the generated <file>_default symbol; the printer then emits export default class test_default {} plus test_default = _default with no renaming pass. The engine rejects the module: SyntaxError: Cannot declare a class twice: 'test_default'. On main the same input parsed as a class expression and was lowered with temps only, so the module loaded and printed 1. The author lists this as a downside and marks it test.todo, which records the failure but leaves the user-facing load error in place.
Verification: Trigger: a module run without a bundle containing export default @ dec class {} together with a user binding whose name equals the generated default-export name. src/js_parser/lower/lower_decorators.rs:744-752 does class_name.get_or_insert_with(|| default_export.expect(..)) and emits class <name> {}. On bd599f5 the same input was parsed as a class expression, so the module loaded.
|
Updated 4:46 PM PT - Oct 7th, 2026
✅ @robobun, your commit 9981d096d72a3018ab6ec02f292ebba3931c5208 passed in 🧪 To try this PR locally: bunx bun-pr 44723That installs a local version of the PR into your bun-44723 --bun |
…rence wrap A "use client" module without a separate SSR graph exports `registerClientReference(value, ...)`. The class arm of `export default` moved the lowered class into that call. The statements that decorator lowering puts after the class assign to the class by name, so they ran with no binding: `ReferenceError: Df is not defined`. When such statements follow the class, it now stays a declaration and the export wraps its binding. The transpiler test for `export default @dec class` now compares every statement of the output.
| /// `default_export` is the binding of an `export default class` statement, | ||
| /// the one class statement that can have no name. Such a class is named | ||
| /// "default", and takes the binding as its name only when class decorators | ||
| /// have to rebind it. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // keep this function's slice contract. `s_export_default` calls | ||
| // it itself, so a class that arrives here has a name. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // "@" is in the lookahead set that rules out an expression after | ||
| // "export default": a decorator here starts a class declaration. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // ("export default interface: 0"). Decorators that no class | ||
| // follows end here too ("export default @dec abstract = 1"). | ||
| // None of these can be a default export value, so report a | ||
| // syntax error instead of building an S.ExportDefault that the | ||
| // visit and print passes don't support. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // The class is discarded. Lowering it would put it back | ||
| // in `data.value`. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // The legacy TS decorator lowering reads `class_name`, so an | ||
| // anonymous class takes the default export's symbol as its name. | ||
| // The standard lowering takes that symbol as an argument. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // Decorator lowering assigns to the class by name after it, so | ||
| // the class stays a declaration and the export wraps its binding. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Version 35: `export default @dec class` is a class declaration, and standard decorator | ||
| /// lowering names an anonymous `export default class` "default". |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| if p.options.features.server_components.wraps_exports() | ||
| && !after_class.is_empty() | ||
| { | ||
| // Decorator lowering assigns to the class by name after it, so | ||
| // the class stays a declaration and the export wraps its binding. | ||
| let name = class | ||
| .class | ||
| .class_name | ||
| .expect("decorator lowering names the class"); | ||
| stmts.push(class_stmts[class_stmt_idx]); | ||
| stmts.extend_from_slice(after_class); | ||
| p.record_usage(name.ref_); | ||
| data.value = js_ast::StmtOrExpr::Expr( | ||
| p.wrap_value_for_server_component_reference( | ||
| Expr::init_identifier(name.ref_, name.loc), | ||
| b"default", | ||
| ), | ||
| ); | ||
| stmts.push(*stmt); | ||
| restore_dead!(); | ||
| record_on_exit!(); | ||
| return Ok(()); |
There was a problem hiding this comment.
🔴 Production Bake builds of a "use client" or "use server" module with a decorated export default class now emit a bundle that throws a SyntaxError on load; on main the bundle loaded. The new branch at src/js_parser/visit/visit_stmt.rs:872-881 keeps the class as a declaration named by class_name and sets data.value to registerClientReference(<same name>), while data.default_name is that same ref. src/bundler/linker_context/convertStmtsForChunk.rs:625-641 then prints var Df = ... next to class Df {}. Fix: give the export a symbol distinct from the class binding on this path (e.g. a fresh create_default_name for data.default_name before wrapping) for named, anonymous, legacy and standard cases.
Why this was flagged
Trigger: a Bake production build, a module under "use server" (src/bundler/ParseTask.rs:2588) or "use client" without a separate SSR graph (src/bundler/ParseTask.rs:2585), containing export default @ dec class Df {} or @ dec export default class Df {}. The new branch at src/js_parser/visit/visit_stmt.rs:863-884 pushes the class statement as a standalone declaration named class_name and sets data.value to an identifier wrapped by wrap_value_for_server_component_reference. data.default_name is the same ref as the class name: for a named class parse_stmt.rs:1620-1624 takes it from the class. The production linker rewrites the export default <expr> into var <default_name> = <expr> (convertStmtsForChunk.rs:625-641), so the chunk contains class Df {} and var Df = registerClientReference(Df, "default"), and a var sharing a name with a class declaration in the same scope is an early SyntaxError when the bundle is loaded. On main the class was wrapped as a class expression and the chunk loaded. The dev server does not hit this because InternalBakeDev uses convertStmtsForChunkForDevServer.
Verification: src/js_parser/visit/visit_stmt.rs:872-880 pushes the lowered class as a standalone declaration and sets data.value to registerClientReference(Identifier(name.ref_), "default"); default_name is the class's own ref (src/js_parser/parse/parse_stmt.rs:1619-1625). src/bundler/linker_context/convertStmtsForChunk.rs:625-641 rewrites it to var Df = registerClientReference(Df, "default") in the same scope as class Df {}, an early SyntaxError.
|
Result of the work on #44721, the
Measured cost of the check, for the record (release builds, main bd599f5 against the check): +2 instructions per transpile with no exports, +38 for a named default declaration, 0 allocator calls, +3,666 B of text. |
Problem
export default @dec class Df {}leavesDfunbound:ReferenceError: Df is not defined. WithexperimentalDecoratorsthe decorator never runs. Found by audit.export defaultdeclaration list (src/js_parser/parse/parse_stmt.rs:1587) has no@, so the class goes toparse_expr. That also miscompiles--minifybundles and panics--splitting --minify:index out of bounds: the len is 0 but the index is 0.Fix
T::TAt. The class is a declaration in every pass, as in tsc, esbuild and the decorators spec (Add Class and Class Element Decorators andaccessorKeyword tc39/ecma262#2417).export default @dec class {}to<file>_default. Solower_standard_decorators_stmttakes the default export's binding and names an anonymous class"default".exports.replace.defaultand keeps a decorated class bound under the"use client"export wrap.es-decorators.test.ts(60 new tests fail on main),transpiler.test.js,decorators.test.ts,bake/dev/bundle.test.ts. Self-reviewed: 21 concerns, 19 addressed.Background
export default class {}has no binding. The parser makes a symbol for it, printed as<file>_default.Name = _Name. Only that needs the binding.es-decorators.test.ts:849), js_parser: fix TypeScript experimental decorator lowering #40830's expression conversion, a rebase of js_parser: fix TypeScript experimental decorator lowering #40830. Supersedes js_parser: name an anonymous decorated export default class "default" #38758.Downsides
using,export default @dec classfails as@dec export default classdoes on main (A lowered top-levelusingdropsexport defaultfunction and class declarations #44720).export default @dec class {}declares<file>_default. A module that uses that name fails to load (The generated<file>_defaultbinding of an anonymous default export has no collision guard without a bundle #44721).export defaultstatement, 0 allocator calls, +1,792 B of text.Notes
What changes, main bd599f5 and this PR (release builds)
export default @dec class Df {}, thennew Df()ReferenceError: Df is not definedexperimentalDecoratorsexport default @dec abstract class Df {}Expected "class" but found "abstract"(or[TypeError: Cannot call a class constructor Df without |new|new User(name),bun build --minifyTypeError: "x" is not a constructor (evaluating 'new t(t)')["x","t:users"]bun build --splitting --minifypanic: index out of bounds: the len is 0 but the index is 0@dec export default class {}.nameandcontext.nameare<file>_defaultdefaultexport default class { @dec m() {} },export default class { accessor x = 1 }.nameis<file>_defaultdefault(without a bundle)replace: { default: 42 }onexport default class Df {}export default 42;"use client"module,experimentalDecorators,@dec export default class Df {}ReferenceError: Df is not definedexport default @dec class Df {}bun checkon main already typesnew Df().m()for the first row, so the type checker and the runtime disagreed on one file. Its tolerant parse mode takes a decorator afterdefaultas a class modifier (parse_stmt.rs:1492).T::TAtnever reaches the changed list in that mode. The output ofbun checkis the same on both builds.The minify rows are fixed for this spelling only. Their cause is
default_name_for_expr(src/js_parser/p.rs). It makes the inner name of a named class or function expression the binding of the default export.export default (class X {})andexport default (function f() {})still fail both ways after this PR. #23810 is a user report of that family. It is not fixed here.The token is the one that #34245 added to the list of plain
export. Bun 1.3.13 and 1.4.2 parse the class as an expression too. No issue reports this spelling.Measurements (release builds of main bd599f5 and of this PR, llvm-nm 23,
size, qemu-x86_64 10.0.13 with-d exec)parse_statement_after_export+221 B (5 copies),s_export_default+820 B (2),lower_class-2,139 B (2),lower_standard_decorators_stmt2,483 B (2, inlined intolower_classon main),lower_class_body+0 B (2),s_class+0 B (2), text total +1,792 B (88,770,351 to 88,772,143,size bun)parse_statement_after_exportperexport default foo;: 162 -> 164. Perexport default {a:1};: 159 -> 161. Perexport default class A {}: 117 -> 121. Perexport default function(){}: 120 -> 126 (one activation each)s_class73 -> 73,lower_class45 -> 38 (js) and 113 -> 110 (ts) instructions (1,000 classes)s_export_defaultperexport default foo;: 133 -> 132. Perexport default {a:1};: 125 -> 124. Perexport default class A {}: 153 -> 157. Perexport default function(){}: 172 -> 166. The largest sum of parse and visit is +8, for the classtransformSync: 5 unaffected inputs 26, 24, 25, 28, 27 -> the same (delta 0).export default @dec class Df {}86 -> 88 (js), 89 -> 91 (ts).export default @dec class {}91 -> 93 (js), 95 -> 97 (ts). Calls of 11mi_*entry points, slope over 200 callsEXPECTED_VERSION34 -> 35. bun-v1.4.2 is at 28, so the bump adds 0 invalidations for a user who moves from a stable release, and one transpile per cached source for a canary usertest/andsrc/js(sha1 per file and decorator mode, 5 of 77,026): the 2 new fixtures anddecorator-export-default-class-fixture-anon.ts(an anonymous default class with a decorated method)experimentalDecorators: 32 of 33 cells match (main: 7). The cell left isaccessorwithexperimentalDecorators, a parse error on both. Anonymous default classes with the namedefault: 8 of 8 without a bundle (main: 1), 4 of 8 inbun buildoutput (main: 1), 4 of 8 with--minify(main: 1)export default @dec class {}: 490 -> 513 B of output,Bun.inspect[class (anonymous)]->[class mod_default].@dec export default class {}: 533 -> 513 B..nameandcontext.namearedefaultfor bothexport default @dec class X {} as any,export default @dec class X {}.name,const X = 1; export default @dec class X {}). 4 of 4 cells with a lowered top-levelusing(named and anonymous,bun build --target=nodeandBun.Transpiler). The controls@dec export default class Df {}andexport default class Df {}fail 4 of 4 on mainThe two downsides in detail
usingdropsexport defaultfunction and class declarations #44720. With a top-levelusingand a target other thanbun,finalizedrops everyexport defaultfunction and class declaration. A default import fails the build:No matching export in "mod.js" for import "default". A namespace import or a bare import builds, and the bundle throwsReferenceError: Df is not defined, because the statements after the class stay in thetryblock.export default (class Df {})is the one form that works. Threetest.todorows hold the three shapes. js_parser: keep export default function/class when lowering top-level using #38287 pinsdecorate anonymous_defaultin its testUsingExportDefaultDecoratedClass. With this PR that value isdecorate default. The PR that lands second updates that line and removes thetodo.<file>_defaultbinding of an anonymous default export has no collision guard without a bundle #44721. Without a bundle no renamer runs, and the generated<file>_defaultname is printed with no guard. On main that happens for an anonymous default function, for@dec export default class {}, and for an anonymous default class with a decorated member or an accessor. This PR removes the last two and addsexport default @dec class {}(standard and legacy). Twotest.todorows hold it.Self-review: 21 concerns raised, 19 addressed
Replacearm ofexports.replacefor a default class, one message for a decorator with no class (a count of the log before the nested parse, only when the entry token is@), two comments that did not match the code.abstract = 1anddeclare = 1tails intranspiler.test.js, the rows with one message, the temporal dead zone cell in the bundled run.usingdropsexport defaultfunction and class declarations #44720 and The generated<file>_defaultbinding of an anonymous default export has no collision guard without a bundle #44721, the landing order.usingrewrite. It is the subject of js_parser: keep export default function/class when lowering top-level using #38287, and A lowered top-levelusingdropsexport defaultfunction and class declarations #44720 tracks it.<file>_defaultin this PR. More than one lowering prints the name, so the guard belongs where the name is made (The generated<file>_defaultbinding of an anonymous default export has no collision guard without a bundle #44721)."use client"export wrap. The class arm moved the lowered class intoregisterClientReference(...)and leftDf = ...with no binding. When statements follow the class, it now stays a declaration and the export wraps its binding. The three tests useexperimentalDecorators. With standard decorators a module does not load in the dev server on main (__decoratorStart is not a function, TC39 decorators fail in the HMR runtime: missing bun:wrap helpers (source patch included) #44463).Other PRs
Bun.inspect, which prints the binding<file>_defaultfor a class-decorated anonymous default class.parse_stmt.rshunk holds the same token and a conversion of the class to an expression. Both go away on its rebase.Replacehunk for the class arm.Left as it is
bun buildnames every default class statement<file>_default, so an anonymous default class with only member decorators keeps that.namein a bundle.experimentalDecoratorsan anonymous default class keeps the binding and the name<file>_default(esbuild prints the same, tsc printsdefault_1).@x export default @y classkeepsExpected "class" but found "@".export defaultreports once for the common tails.@dec[0] class Foo {},@dec namespace N {}and@dec label: 1report a second message from the statement that follows.default, which is the spec text and tsc.Tests
es-decorators.test.ts: 484 pass, 5 todo on this PR. 424 pass and 60 fail on main. The matrix runs one module per cell as.js, as.tsand as a bundle.transpiler.test.js: 255 pass on this PR, 18 fail on main.decorators.test.ts: 26 pass on this PR. On main the file does not load (ReferenceError: DecoratedClass is not defined).bake/dev/bundle.test.ts: 26 pass on this PR, 3 fail on main.export default @dec function f() {}and the four tails with one message). They hold texts that this route could have moved.es-decorators-esbuild,decorator-metadata,bundler_decorator_metadata,esbuild/ts,bundler_minify,bundler_edgecase,ts-use-define-for-class-fields. The last one passes on the release build. On the debug build its 11 concurrent spawns pass the 5 s limit now and then on my machine.[human-review] gate passed · iteration 0 · 11 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file