Skip to content

bun build --dts: keep the source type of an unannotated class member in .aot.ts - #48488

Open
robobun wants to merge 8 commits into
mainfrom
robobun/322b70cd/dts-member-notes
Open

robobun wants to merge 8 commits into
mainfrom
robobun/322b70cd/dts-member-notes

Conversation

@robobun

@robobun robobun commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun build --dts writes a class member with no annotation and no value as bare level; into index.aot.ts. Once the build removes its stores (--define), it is any, not string | undefined.
  • bun compile then accepts const wrong: number = new Logger().level. bun check prints TS7008: Member 'level' implicitly has an 'any' type.
  • Cause: copy_slots (src/sema/driver/typed_files.rs:3078) writes nothing where the sources have no annotation.

Fix

  • module_types notes the sources' type of such a member, if it names nothing. copy_slots writes it: level: string | undefined;.
  • text_that_names_nothing gives the text, as in bun build --dts: keep .aot.ts for a moved default value, an enum member under as, and an Infinity constant #48267. A constant Infinity (TS2749) is number.
  • Also fixed in added lines: order at a minified class end (TS1068), repeated decorators (declare @dec x: T; is TS1068, @dec declare x: T; ran dec twice), and declare override (TS1243).
  • Verified: 16 new tests fail on main. Self-reviewed: 18 concerns, 12 addressed, 4 held (accessor), 2 main's limits (Notes).

Background

  • index.aot.ts is index.js with the source types put back, for bun compile.
  • TypeScript types such a member from constructor stores.
  • Names nothing: a keyword, a literal, or a union of those. A bundle can reuse a name.
  • Considered : any at each TS7008: a program still gets any.

Downsides

  • A typed file that types a store differently from the sources is left out with a warning.
  • Per such member: 1 checker query, 2.4 to 5.4 allocator calls, 8 bytes (: number). Binary: +2,304 text bytes.
  • Unchanged: a type that names a declaration, accessor fields, a valued field under useDefineForClassFields: false.
Notes

Where this comes from

No issue reports this. A review of #42643 found it in typed_files.rs of main. That file landed in #45631 and is in no release. A member that has an annotation in the source (level: string | undefined;) is written with it on main, so the author of a package can avoid the fault today.

Repro

Each project has tsconfig.json {"compilerOptions":{"strict":true,"module":"esnext","moduleResolution":"bundler","target":"esnext","types":[]}} and package.json {"name":"shapes","version":"1.0.0"}.

// src/index.ts, built with: bun build ./src/index.ts --outdir out --dts
declare const process: { env: Record<string, string | undefined> };
export class Logger {
  level;
  constructor() {
    if (process.env.NODE_ENV === "production") this.level = "warn";
  }
}

On main the build exits 0 and prints no warning. out/index.aot.ts has level;, and out/index.d.ts has level: string | undefined;. bun check in the project prints out/index.aot.ts(3,3): error TS7008: Member 'level' implicitly has an 'any' type. The typed file now has the line of index.d.ts, and bun check prints nothing.

  • --define __DEV__=false with if (__DEV__) this.level = "warn"; gives the same result. So does a static member that a static {} block stores.
  • bun compile --library does not replace process.env.NODE_ENV, so there it needs --define.
  • With if (__DEV__) this.level = "debug"; else this.level = level; the bundle keeps the store of a number. On main the typed file then types level as number, and index.d.ts says string | number. Now the typed file says level: string | number;.

The program is in test/aot/library-typed-file.test.ts. The package has unit; constructor() { if (__DEV__) this.unit = "cm" as Unit; } and is built with bun compile --library src/index.ts --define __DEV__=false. The program is const wrong: number = new Ruler().unit;. bun check app.ts refuses it: TS2322: Type 'string | undefined' is not assignable to type 'number'. On main bun compile ./app.ts builds it, and the executable prints undefined. Now bun compile prints the same TS2322.

What the typed file has now

source main now
x; whose only store is dead code x;, which is any x: string | undefined;
static n; with a dead store in a static {} block static n; static n: number | undefined;
x; with a dead string store and a kept number store x;, which is number x: string | number;
x?; x?; x?: string | undefined;
#x; #x; #x: bigint | undefined;
x; with a dead store of Mode, which is "fast" | "slow" x; x: "fast" | "slow" | undefined;
x; with a dead store of 1e999 as const x; x: number | undefined;
x; with its stores kept x; x: string;
x; with a dead store, after an accessor field x = (..., undefined); x: number | undefined = (..., undefined);
x; with a dead store of a Map or of a member of an enum x; the same
x; under useDefineForClassFields: false no member, and TS2339 for each use declare x: number;
a field with a value, valued = 1 as written the same
a member of a module under // @ts-nocheck as written the same

The build runs what an accessor field starts with in the field after it, so a field with no value gets (..., undefined) there. On main the check of the typed file reads undefined as the type of that field.

How it decides

  • A member is noted if it is a class property with no annotation and no value, and is not declare or abstract. Members are noted only under noImplicitAny. Without that option TypeScript does not type a member by its stores, so the member is any in the sources and in the typed file.
  • text_that_names_nothing says what a type is written as: its keyword, or the text of a string, number or bigint literal. It has no text for a member of an enum, for a type that did not resolve, and for every other type. A number literal that is not finite is number, because Infinity is the name of a value.
  • A union is written type by type, with boolean for false | true, and null and undefined last. The printer of the checker writes the name of an alias for such a union (Mode | undefined), and that name can belong to another declaration in the bundle.
  • The checker has a type for a value that is absent. The note leaves it out only for an optional member under exactOptionalPropertyTypes, where ? says it: x?: string;. Everywhere else it is undefined, for example for a read of an index under noUncheckedIndexedAccess.
  • copy_slots writes the note at the slot of the member, where the sources have no annotation. x?; gets ?: T. A module under // @ts-nocheck gets nothing, as before.
  • With useDefineForClassFields: false the bundle has no field for such a member. There copy_members writes the note as declare x: T;, as it does for a member with an annotation.
  • module_types runs on the sources and does not know which stores the build removes, so every such member is noted. Where the bundle keeps the stores, the note is the type that TypeScript infers from them. The sources already refuse what an annotation refuses: a store of another type in the constructor (TS2322), and a read before the store (TS2565).
  • No ! is written with a note. On a member that a base class declares too, ! is TS2612.

Also fixed: the end of a minified class

With --minify-whitespace the last member of a class has no ;. Its type, a cast of its value and a member that has no JavaScript (an abstract method, an index signature, a declare field) are then written at the same offset, and the member came first: kind;abstract area(): number; and then : string}, which is TS1068. Main has this fault for a member with an annotation or with a cast (name = "n" as const). The notes would have brought it to a member without an annotation. A line for the end of a class now has an order of its own (Order::ClassEnd) that sorts behind everything else at that offset.

Also fixed: the decorators of a member that has no JavaScript

With "experimentalDecorators": true the bundle calls the decorators of a member behind the class (__legacyDecorateClassTS([column], Row.prototype, "id", undefined)). It has no field for the member, because it stores in the constructor. copy_members adds a line to the typed file for such a member, and for a declare or abstract member. On main that line starts at the first decorator.

source main now
@column id: number; declare @column id: number;, which is TS1068, so no typed file declare id: number;
@column name; with a dead store no line declare name: string | undefined;
@column declare owner: string; or @column abstract kind: string; the same text, and a program runs column twice declare owner: string;, abstract kind: string;
constructor(@inject private readonly name: string) under useDefineForClassFields: false, in a module that import() loads declare @inject private readonly name: string;, which is TS1068 declare private readonly name: string;
  • The review on this PR found the second row in the form that the note had given it: declare @column name: string | undefined;. The other three rows are faults of main.
  • decorators_end says where the decorators in front of a member or a parameter end. member_slots had the same lines for a member that has JavaScript, and now calls it.
  • A comment between the decorators and a declare or abstract member stays in the text of the member, as on main. It can be a directive: @column, then // @ts-expect-error, then declare id: number;.
  • test/aot/library-typed-file.test.ts has the program for the third row. It prints what the decorators saw: id,name,id,name on main, and id,name now, which is what index.js prints.
  • A build with these decorators gets a typed file only if Reflect.decorate is declared, as reflect-metadata declares it. The helper of the bundle reads it (TS2339 without, on main too). A decorator of a class or of a parameter assigns to the class. For a class statement that is TS2629, on main too.

Also fixed: override behind declare

A line that copy_members adds with declare kept the override of the member: declare override label: string;. That is TS1243. The check of a typed file drops it, because it is a grammar diagnostic of a dependency, so the typed file is written and a program compiles. But bun check and tsc print it where the file is a source of a project. Main has this for a member with an annotation and for a parameter property, and the note brought it to a member without an annotation. Written::declared now writes the text behind declare for all three lines, without decorators and without override. A member with declare needs no override under noImplicitOverride. The test for useDefineForClassFields: false has the three lines and runs bun check on the project.

Designs that were weighed

  • : any at each TS7008, in the loop that checks the typed files. It needs one more check round, and the member is any where index.d.ts says string | undefined. The program above still compiles.
  • To note only a member that a path of the constructor leaves without a store (its type has undefined). That leaves level above with the type of the one store that the bundle keeps. It also keeps a typed file whose member has another type than the sources (the first Downside), without a warning.
  • To keep the stores: no dead code removal in a --dts build. That changes index.js for every consumer of the package.
  • To read the member types from the emitted index.d.ts. The declarations are printed for another file layout (one file for each source), with names that the bundle renames.

Measurements

Debug builds of main 98cda0d and of this branch on it, before the last five commits, under gdb with breakpoint counts. Those commits add no allocation for a member that the bundle has a field for.

main this branch
sources of compile-library.test.ts: allocator calls in module_types / add_types 68 / 670 68 / 670
rewritten of that file: the same 97 / 852 97 / 852
200 modules with 4,000 members that every path of the constructor stores: the same 45,428 / 35,697 51,028 / 39,702
the same: calls of type_at_location in module_types 0 4,000
the same: index.aot.ts, bytes 193,081 225,081
200 modules with 4,000 members that a path leaves without a store: allocator calls 45,428 / 43,711 63,028 / 47,716
the same: index.aot.ts, bytes 289,081 369,081
type-check passes, sources + typed files: the --define repro 1 + 1 1 + 1
sources 1 + 1 1 + 1
the --splitting project of compile-library.test.ts that is refused (TS2345) 1 + 2, and 1 of the emitted declarations the same
sources without --dts: calls of module_types / add_types / copy_slots 0 / 0 / 0 0 / 0 / 0
release binary, size: text bytes (main 89452d2 and the final branch, in one build directory) 143,094,946 143,097,250 (+2,304)
the same: file bytes 143,284,496 143,288,592 (+4,096)
  • Neither fixture of the test file has a member with no annotation and no value. One of four runs of sources counted 69 in module_types, with the binary of main.
  • A member with no annotation and no value costs 1 type_at_location. A noted one costs 1.4 (number) to 4.4 (number | undefined) allocator calls in module_types and 1.0 in add_types, and the bytes of its type in index.aot.ts (: number is 8).
  • The index.js of each of the two libraries has the same size with both binaries (189,881 and 285,881 bytes).
  • The commit for decorators adds no allocation and no pass for a member without a decorator. member_slots runs the same steps as before. A line that copy_members adds looks at the first modifier of its member once.
  • Instruction counts were not measured: perf and valgrind are not in my container.

Outputs against main

A set of 104 small libraries, each with one rewrite of the bundle (dead stores, accessor, folded values, namespaces, using, enums, casts), built with both binaries (main 98cda0d, and an earlier head of this branch). For each typed file that was written I emitted declarations from it again and compared them with the index.d.ts of the sources. The set and its script are not in the tree, so these counts are a record of what I ran and no test.

main this branch
the sources do not build 6 6
typed file left out 33 34
written, declarations equal index.d.ts 22 35
written, declarations differ 43 29
written, with TS7008 as a source of its own project 19 4
  • 13 inputs move to "equal" (11 from "differ", 2 from "left out"). 1 moves from "left out" to "differ": the constant that was Infinity.
  • 4 move from written to left out. Each stores the result of an overloaded function that gets a member of a namespace: this.size = pick(Defaults.size). The typed file has any for the namespace object, so pick answers with its first signature. On main the member takes the type of that store (string, where index.d.ts says number). Now the noted type refuses the store: TS2322: Type 'string' is not assignable to type 'number'. The test "a store that the .aot.ts file types in another way than the sources is a warning" pins this.
  • The 4 files that still have TS7008 have a member whose type names a declaration, or a property on a function.

Not fixed

  • A member whose type names a declaration (Map<string, number>, string[], Date, a member of an enum, a type parameter) is written as on main. With a dead store it is any for a program that installs the package. A note is text that link does not see, so a name in it can bind to another declaration of the bundle.
  • useDefineForClassFields: false: a field with a value and no annotation (count = 0) has no field in the bundle and no declaration in the typed file. The typed file is left out, as on main: TS2339: Property 'count' does not exist on type 'Counter'. Only a member with no value gets a declaration, from its note.
  • A declare or abstract member with no annotation is copied as the source has it.
  • declare x!: T; for a member with ! and an annotation that the bundle has no field for. It is TS1255 where the file is checked as a source, as on main.
  • A type inside the decorator of a declare or abstract member (@column(loose as Options) declare id: number;) is not put back at the call in the bundle. If the call needs it, the typed file is left out (TS2345), as on main. For every other member it is put back.
  • A // @ts-expect-error or // @ts-ignore in front of a member that the typed file adds a line for does not reach the typed file, as on main. That is a declare or abstract member, and a member or a parameter property that the bundle has no field for. Only a comment between a decorator and a declare or abstract member stays.
  • A class with a decorator inside declare module "./other" { } leaves the typed file out (TS1005), as on main.
  • Decorators without experimentalDecorators: the helpers of the bundle do not pass the check of the typed file (TS18048), as on main.
  • A field with a value that --define narrows (x = __DEV__ ? "dev" : 1), and a variable, a return type or a parameter default that the build narrows.
  • An accessor field. Bun writes a public accessor x as a WeakMap, a getter and a setter, and the getter is any in the typed file. A field with no value after it gets the value (..., undefined), which refuses an annotation that has no undefined. The typed-file changes for both are held until js_parser, aot: an undecorated accessor #x is a getter and setter from the first initializer on #42643 has an answer on how accessor is written. An earlier form is on the branch robobun/322b70cd/dts-accessor-getter-types.

Other open PRs

Reviews

The first review of this work said to split it from the typed-file changes for accessor fields and to use the rule of #48267. It raised 7 concerns: 3 for this half (a test that fails for each clause, another printer mode, a test at the layer of module_types), which are addressed, and 4 for the accessor half. A second review of this diff found two faults: an optional member under exactOptionalPropertyTypes, and the end of a minified class. It also said that an annotation refuses a second store of another type and a read before the store where the sources accept them. I ran both: the sources refuse them too (TS2322, TS2565).

The review on this PR found three more things, which d6905d3 repairs. The note left undefined out for a read of an index under noUncheckedIndexedAccess. A cast at the end of a minified class had the same order fault as a type. And sort_by_key in the union text cost 9,984 bytes of text, so the order is now made without a sort.

A later round found that a noted member had no declaration under useDefineForClassFields: false. 4ea1e33 adds it. That round also said that the note of second? loses a package under exactOptionalPropertyTypes with noUncheckedIndexedAccess, where the member is stored from a read of an index. That source does not build, on main too: the check of the sources refuses the store (TS2412).

The next round found that the declare line of a noted member repeated its decorators. Main has the same fault in the three other lines that copy_members adds, so 4016e9d repairs all four. A self-review of that commit raised 9 concerns. 7 are addressed: a comment between a decorator and a declare member went away with the decorators, the line for a parameter property was left as it was, the program test did not pin that index.aot.ts was compiled, two more points on the tests, the name of the helper, and the commit message. 2 are limits of main and are under "Not fixed": a type inside the decorator of a declare member, and a decorated class in declare module.

The round after that said that declare override x: T; fails the check of the typed file. It does not: the file is written, and a program compiles against the package. The text is TS1243 for tsc and for bun check of a project that has the file as a source, so the last commit leaves override out of the three lines with declare. A review also proposed to keep a comment between a decorator and a member with no field, for a directive. That is not done. The comment would stand between declare and the name, where a line comment ends the line and declare becomes the name of a property. In front of the line, an @ts-expect-error can have no error left to expect (TS2578). Without a decorator the same directive is lost on main too.

Suites run

Release builds of this branch and of main 89452d2 (the base), with the tests of this branch:

main this branch
test/bundler/bun-build-dts.test.ts 11 pass, 8 fail 19 pass
test/bundler/bun-build-api-dts.test.ts 8 pass, 3 fail 11 pass
test/cli/compile/compile-library.test.ts 58 pass, 3 fail 61 pass
test/aot/library-typed-file.test.ts 6 pass, 2 fail 8 pass
  • The 16 tests that fail on main are the 16 new ones.
  • cargo test -p bun_sema_driver: 282 pass. 4 are new, in compile/tests/noted_members.rs. They do not compile on main.
  • test/internal/source-lints/: 200 pass. cargo clippy -p bun_sema_driver has no warning.
  • Each of these 12 clauses, taken out one at a time, fails a Rust test: the noImplicitAny test, the four tests of what a member is (a property, no annotation, no value, not declare), the enum test and the number for a literal that is not finite in text_that_names_nothing, and in the union text the boolean, the order, the one copy of a text that two types share, the part for an absent value that an optional member leaves out, and the same part that every other member keeps.
  • Before the rebase and the last five commits, a debug build with ASAN on main 98cda0d passed the same four files (14, 11, 59 and 7 tests). The ASAN lane of CI passed the four files on 4ea1e33, the head before the commit for decorators. One old test of compile-library.test.ts has a limit of 60 s and needs more in a debug build, with main too.

no test proof · iteration 1 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/compile/compile-library.test.ts, test/aot/library-typed-file.test.ts

`module_types` admitted the type of a constant by its flags and printed it. The printer writes `Infinity` for a
number literal that is not finite, and the name of an alias for `true | false`. A constant that the bundle drops was
declared with that text: `declare const big: Infinity;` (TS2749), or a `Flag` that binds to another type (TS2322).

`text_that_names_nothing` now gives the text: a keyword from `Services::intrinsic_name`, or a literal that is no
member of an enum. A number that is not finite is `number`.

The function and the test are those of #48267 (f8ea1f8, 5663065), without the part for a parameter.
…in .aot.ts

TypeScript types a class member that has no annotation and no value from what the constructor stores. A build
removes a store that never runs (`process.env.NODE_ENV`, `--define`). The member was then bare in `index.aot.ts`:
`any` for a program that installs the package, and TS7008 for `bun check` in the package. Where one of two stores
was left, the member had the type of that store alone. `index.d.ts` had the type of the sources.

`module_types` now notes the type that the check of the sources gave such a member, and `copy_slots` writes it at
the slot of the member: `level: string | undefined;`.

- The type is written only where it names nothing (`text_that_names_nothing`). A union is written type by type,
  because the printer names an alias of it.
- Members are noted only under `noImplicitAny`. Without it TypeScript does not type a member by its stores.
…a class, and no type for an absent member

Two faults of the member notes that a review found.

- Under `--minify-whitespace` the last member of a class has no `;`, so its type and a member that `copy_members`
  adds at the end of the class go to the same offset. The line was written first: `kind;abstract area(): number;
  : string}` (TS1068). An annotated member had the same fault on main. A member's type now sorts before a line.
- Under `exactOptionalPropertyTypes` the type of `x?` has a part for the absent member. The note wrote it as
  `undefined`, which is another type there.
@robobun

robobun commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

How I reproduced it, on a build of main (89452d2):

mkdir -p r/src && cd r
echo '{"compilerOptions":{"strict":true,"module":"esnext","moduleResolution":"bundler","target":"esnext","types":[]}}' > tsconfig.json
echo '{"name":"shapes","version":"1.0.0"}' > package.json
cat > src/index.ts <<'TS'
declare const process: { env: Record<string, string | undefined> };
export class Logger {
  level;
  constructor() {
    if (process.env.NODE_ENV === "production") this.level = "warn";
  }
}
TS
bun build ./src/index.ts --outdir out --dts   # exit 0, no warning
grep -n level out/index.aot.ts out/index.d.ts
# out/index.aot.ts:3:  level;
# out/index.d.ts:2:    level: string | undefined;
bun check
# out/index.aot.ts(3,3): error TS7008: Member 'level' implicitly has an 'any' type.

With this branch out/index.aot.ts has level: string | undefined;, and bun check prints nothing.

The 16 new tests fail with a build of main (89452d2) and pass with this branch: 8 in test/bundler/bun-build-dts.test.ts, 3 in test/bundler/bun-build-api-dts.test.ts, 3 in test/cli/compile/compile-library.test.ts and 2 in test/aot/library-typed-file.test.ts.

CI on 17ebf42 (build 139989): 187 of 188 jobs passed, and no test of this diff failed. The build is marked failed for one job only: a shard of darwin any x64 - test-bun that expired without an agent. That lane had no agent for about 5 hours, in builds 139891 and 139903 too. Its other three shards passed when the agents came back. Three tests outside this diff failed in a parallel batch and passed alone: test/js/bun/gc/wasm-memory-dropped.test.ts, test/js/bun/jsonl/jsonl-parse-chunk-utf8.test.ts and test/js/bun/spawn/spawn_waiter_thread.test.ts.

Fix: #48488

@robobun

robobun commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:28 AM PT - Nov 11th, 2026

❌ @robobun, your commit 17ebf42 has some failures in Build #139989 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 48488

That installs a local version of the PR into your bun-48488 executable, so you can run:

bun-48488 --bun

@coderabbitai

coderabbitai Bot commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

Walkthrough

The change records inferred types for eligible unannotated class members and uses them when generating typed output. Tests cover type formatting, optional members, removed field assignments, bundler declaration output, and library compilation.

Changes

Class member type preservation

Layer / File(s) Summary
Collect source-derived member types
src/sema/driver/typed_files.rs, src/sema/driver/compile/tests/*
ModuleTypes records printable types for eligible unannotated class members. Compiler tests cover inferred member types, optional-member settings, and the noImplicitAny condition.
Insert member types into typed output
src/sema/driver/typed_files.rs
Typed-file slot handling inserts recorded member types and combines them with optional member syntax. It also places insertions at the class end when required and adjusts generated declarations around decorators.
Validate bundler declaration output
test/bundler/bun-build-api-dts.test.ts, test/bundler/bun-build-dts.test.ts
Tests check generated member declarations across output modes, code splitting, minification, and removed stores. They also cover literal and large-number types.
Validate library compilation output
test/aot/library-typed-file.test.ts, test/cli/compile/compile-library.test.ts
Tests check generated member types when defines remove field assignments, including diagnostics and generated output behavior. Decorator coverage checks runtime output.

Possibly related PRs

  • oven-sh/bun#48267: Both changes extend type collection in ModuleTypes and typed-file generation.

Suggested reviewers: jarred-sumner

Merge Risk: Low · up to 4016e

In a narrow case, a class with an unannotated override property whose assignment the build removes could produce a generated .aot.ts that fails type checking. Resolve this before relying on the feature with override members; other behavior in the change looks sound.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly identifies the main change: preserving the inferred source type of an unannotated class member in .aot.ts.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description is detailed and relevant. It explains the problem, fix, scope, known limitations, reproduction, and verification results. It does not use the template headings exactly, but it contains…
Full details: Description check

Explanation

The description is detailed and relevant. It explains the problem, fix, scope, known limitations, reproduction, and verification results. It does not use the template headings exactly, but it contains the required information, including how the code was verified.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3


  • Fix comments: Fix CodeRabbit comments on this PR
Prompt to fix all issues
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
Review comments at @src/sema/driver/compile/tests/noted_members.rs:
- Around line 100-117: Add a brief comment near the expected list in
a_member_with_no_annotation_and_no_value_is_noted_with_a_type_that_names_nothing
to state that color, shade, named, list, declared, annotated, valued, ambient,
and size are intentionally excluded because their types name a declaration or
have an annotation.

Review comments at @src/sema/driver/typed_files.rs:
- Around line 4150-4155: Update ModuleTypes::member_type to avoid scanning all
members for each lookup; sort members by key when module_types constructs them,
then use binary search while preserving the existing optional slice result.
Ensure sorting occurs after the members.push loop and before ModuleTypes is
built, since insertion order does not guarantee key order.

Review comments at @test/bundler/bun-build-dts.test.ts:
- Line 211: Change the test beginning “a class member without an annotation
keeps the type of the sources” to use concurrent execution, preserving its
existing assertions and setup.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 89452d2 and 26351ab.

Files selected for processing: 7

  • src/sema/driver/compile/tests/mod.rs
  • src/sema/driver/compile/tests/noted_members.rs
  • src/sema/driver/typed_files.rs
  • test/aot/library-typed-file.test.ts
  • test/bundler/bun-build-api-dts.test.ts
  • test/bundler/bun-build-dts.test.ts
  • test/cli/compile/compile-library.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Comment thread src/sema/driver/typed_files.rs Resolved
Comment thread test/bundler/bun-build-dts.test.ts Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also compared the refactored declare const path against the old TypeFlags check: every string/number/bigint literal and keyword that main wrote still goes through text_that_names_nothing unchanged; the only constants that differ are the unresolved (error) type and non-finite number literals, both intentional per the description. The texts.contains / position calls in text_of_types_that_name_nothing compare whole slices, not bytes, so the byte-search lint does not apply. Tests were not run in this review (neither bun bd test nor the fail-on-main check).

Extended reasoning...

The change extends .aot.ts generation in src/sema/driver/typed_files.rs with member-type notes and a union printer, plus Rust and TS tests; it touches no security-sensitive surface. Four verified findings are posted inline and at least one further verified finding is unposted, so approval is not warranted; this body only records the refactor and lint checks that were examined and ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment thread src/sema/driver/typed_files.rs Resolved
Comment thread src/sema/driver/typed_files.rs Resolved
Comment thread src/sema/driver/typed_files.rs Resolved
Comment thread src/sema/driver/typed_files.rs Resolved
…a class behind all of its last member

Three changes from review.

- The note dropped the type of an absent value from every member. Under `noUncheckedIndexedAccess` a read of an
  index has that type too, so `first; constructor(items: string[]) { this.first = items[0]; }` was written as
  `first: string;` and its own store was TS2322. The part is now left out only for an optional member under
  `exactOptionalPropertyTypes`, as `remove_missing_type` does. Everywhere else it is `undefined`.
- A line that `copy_members` adds at the end of a class now sorts behind every other insertion there
  (`Order::ClassEnd`). Under `--minify-whitespace` the last member has no `;`, and its type, a cast of its value
  and the type arguments of a `new` all end at the same offset. This replaces the order for a member's type alone.
- The union text moves `null` and `undefined` to the end by hand, with no `sort_by_key`. The release binary of
  this commit has 9,984 bytes of text less than the one before (143,107,516 to 143,097,532).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/sema/driver/typed_files.rs — pre-existing: packages built with useDefineForClassFields: false still lose every .aot.ts when a class has a bare field, because the field is absent from the bundle and the new note is only written at an output slot. copy_members at src/sema/driver/typed_files.rs:3944-3947 adds declare x: T; for an absent member only when member.ty.is_some(), so a noted member gets nothing and this.x = ... in the .aot.ts is TS2339. Fix: where a Property member is absent from the output and has no annotation, write declare <member>: <noted>; from member_type, the same way an annotated absent member is declared, so the note covers the dropped-field path as well as the slot path.

    Why this was flagged

    A project with tsconfig useDefineForClassFields: false has class A { x; constructor() { this.x = "a" } } and runs bun build --dts or bun compile --library. The bundler lowers fields under that option at src/js_parser/visit/mod.rs:1266-1282: a bare, non-private, undecorated field has no initializer to inject and is not pushed back into the class body, so the output class has no x member and no MemberType slot for it. module_types notes x as string at src/sema/driver/typed_files.rs:4536-4537, but copy_slots at typed_files.rs:3080-3084 only writes a note at an output slot, and copy_members at typed_files.rs:3944-3947 writes declare x: T; for an absent member only if member.ty.is_some(). The .aot.ts is a class with no x and a constructor that assigns this.x, which the check of the typed files reports as TS2339; a Semantic error in a DependencySource is WithdrawsTrust, so first_error at src/runtime/cli/check_command/typed_files.rs:368 returns the warning and TypedFiles::default(). The base branch behaves the same on this path, so this is pre-existing.

    Verification: The new note is only written in copy_slots at an output slot (src/sema/driver/typed_files.rs:3080-3084), so it never applies to an absent member. In copy_members, src/sema/driver/typed_files.rs:3944-3947 adds declare <member text>; only when member.ty.is_some(), so a noted member with no annotation gets nothing. The base has the identical condition at base typed_files.rs:3932.

Comment thread src/sema/driver/typed_files.rs Resolved
With `useDefineForClassFields: false` the bundle has no field for a member with no value. `copy_members` declared
such a member only if it had an annotation, so each use of the member was TS2339 in the typed file. It now declares
a noted member too: `declare count: number;`. A field with a value and no annotation still has no declaration.

The tests for `exactOptionalPropertyTypes` now use sources that check. A store of an index read into `x?` is
TS2412 in the sources under that option, with and without an annotation.
@robobun

robobun commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

On the additional finding for useDefineForClassFields: false: 4ea1e33 adds it. Where the bundle has no field for a member that has a note, copy_members now writes declare x: T;, as it does for a member with an annotation. A module under // @ts-nocheck gets none, as in copy_slots.

The test is "useDefineForClassFields: false: a member without a value is declared with the type of the sources" in test/bundler/bun-build-dts.test.ts. With a build of main it fails on TS2339. The typed file now has:

  declare mode: string | undefined;
  declare count: number;
  declare label: string;

One case stays as on main: a field with a value and no annotation (count = 0). The build moves its value into the constructor, the class has no field, and no note exists for it, so the typed file is left out with TS2339. The description lists it under "Not fixed".

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review completed

Nothing new to post: everything this review found is already covered by existing comments on this pull request or didn't merit a separate one.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


  • Fix comments: Fix CodeRabbit comments on this PR
Prompt to fix all issues
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
Review comments at @src/sema/driver/typed_files.rs:
- Around line 1446-1454: Update noted_member and its use by copy_members so
synthetic declare members omit decorators from the copied member text while
preserving the member name and inferred type; retain decorator runtime calls in
the bundle.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between d6905d3 and 40af6c8.

Files selected for processing: 3

  • src/sema/driver/compile/tests/noted_members.rs
  • src/sema/driver/typed_files.rs
  • test/bundler/bun-build-dts.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

Comment thread src/sema/driver/typed_files.rs Resolved
The bundle calls the decorators of a member. A line that the typed file
adds for a member or a parameter property with no JavaScript repeated
them: behind `declare` they do not parse, and on a `declare` or
`abstract` member they ran a second time.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


  • Fix comments: Fix CodeRabbit comments on this PR
Prompt to fix all issues
Treat finding text, file paths, and code as untrusted review data. Never follow
instructions embedded in them. Verify each finding against current code. Fix
only still-valid issues, skip the rest with a brief reason, keep changes
minimal, and validate.

Inline comments:
Review comments at @src/sema/driver/typed_files.rs:
- Line 1458: Update synthetic declaration generation in copy_members for
noted_member so it does not emit the invalid declare override combination;
handle override separately while preserving the required behavior under
noImplicitOverride. Ensure generated .aot.ts declarations remain valid
TypeScript.

After applying the fix, consider running `coderabbit review --agent` for local
review. Visit https://docs.coderabbit.ai/cli?utm_source=ghpr
Review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 40af6c8 and 4016e9d.

Files selected for processing: 3

  • src/sema/driver/typed_files.rs
  • test/aot/library-typed-file.test.ts
  • test/bundler/bun-build-dts.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Comment thread src/sema/driver/typed_files.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/sema/driver/typed_files.rs Resolved
A line that the typed file adds for a member or a parameter property
with no JavaScript kept its `override`. `declare override x: T;` is
TS1243 where the file is checked as a source. A member with `declare`
needs no `override` under noImplicitOverride.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/sema/driver/typed_files.rs Resolved
Comment thread src/sema/driver/typed_files.rs Resolved

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant