Skip to content

feat(type-info): add Partial, Required, and Readonly type synthesis#9875

Merged
ematipico merged 18 commits intobiomejs:mainfrom
minseong0324:feat/partial-required-readonly
Apr 13, 2026
Merged

feat(type-info): add Partial, Required, and Readonly type synthesis#9875
ematipico merged 18 commits intobiomejs:mainfrom
minseong0324:feat/partial-required-readonly

Conversation

@minseong0324
Copy link
Copy Markdown
Contributor

I used Claude Code to assist with this implementation.

Summary

Addresses items from #9810: adds is_optional and is_readonly to TypeMember, synthesizes Partial<T>, Required<T>, and Readonly<T>.

Parses readonly modifiers from source. Strips undefined from member types for Required<T>.

Test Plan

Snapshot tests.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 9, 2026

🦋 Changeset detected

Latest commit: a8d1800

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 13 packages
Name Type
@biomejs/biome Patch
@biomejs/cli-win32-x64 Patch
@biomejs/cli-win32-arm64 Patch
@biomejs/cli-darwin-x64 Patch
@biomejs/cli-darwin-arm64 Patch
@biomejs/cli-linux-x64 Patch
@biomejs/cli-linux-arm64 Patch
@biomejs/cli-linux-x64-musl Patch
@biomejs/cli-linux-arm64-musl Patch
@biomejs/wasm-web Patch
@biomejs/wasm-bundler Patch
@biomejs/wasm-nodejs Patch
@biomejs/backend-jsonrpc Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added A-Project Area: project A-Linter Area: linter L-JavaScript Language: JavaScript and super languages A-Type-Inference Area: type inference labels Apr 9, 2026
@minseong0324 minseong0324 marked this pull request as ready for review April 9, 2026 00:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This change extends the type inference system to properly resolve Partial<T>, Required<T>, and Readonly<T> utility types whilst preserving optional, readonly, and nullable member flags. Core modifications include: introducing a NamedOptional member kind variant in the type system, updating the resolver to specialise Partial, Required, and Readonly qualifiers by applying appropriate member transformations, and implementing helper utilities for optional member detection. Test coverage is expanded across multiple lint rule specifications with new test cases exercising these utility types.

Suggested reviewers

  • ematipico
  • dyc3
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and clearly summarises the main change: adding type synthesis for three TypeScript utility types (Partial, Required, Readonly).
Description check ✅ Passed The description is well-related to the changeset, detailing the additions to TypeMember, the synthesis of the three utility types, readonly parsing, and undefined stripping for Required.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 9, 2026

Merging this PR will not alter performance

✅ 58 untouched benchmarks
⏩ 196 skipped benchmarks1


Comparing minseong0324:feat/partial-required-readonly (a8d1800) with main (9956f1d)

Open in CodSpeed

Footnotes

  1. 196 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
crates/biome_js_type_info/src/local_inference.rs (2)

2353-2363: ⚠️ Potential issue | 🟠 Major

Use named_param.is_optional instead of hardcoding false.

Constructor parameter properties lose their optionality here. The NamedFunctionParameter carries the correct optional flag, but it's discarded when synthesising the class member. This breaks type inference for utilities like Partial<T> and Required<T>.

Fix
                         members.push(Self {
                             kind: TypeMemberKind::Named(named_param.name.clone()),
                             ty: param.parameter.ty().clone(),
-                            is_optional: false,
+                            is_optional: named_param.is_optional,
                             is_readonly: false,
                         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_type_info/src/local_inference.rs` around lines 2353 - 2363,
The constructor parameter property synthesis currently hardcodes is_optional to
false inside the loop over constructor.parameters (the branch matching
FunctionParameter::Named(named_param)), which drops the optionality; change the
created TypeMember (Self { ... }) to set is_optional using the
NamedFunctionParameter flag (named_param.is_optional) instead of false so the
synthesized class member preserves the parameter's optionality when constructing
the member list in local_inference.rs.

1881-1913: ⚠️ Potential issue | 🟠 Major

Apply readonly modifier detection to JsPropertyClassMember to match TsPropertySignatureClassMember.

At line 1913, JsPropertyClassMember passes false for the readonly parameter, missing class C { readonly x: number } syntax. The same detection pattern already works at line 1974–1977 for TsPropertySignatureClassMember; reuse it here.

Suggested fix
                    let is_static = member
                        .modifiers()
                        .into_iter()
                        .any(|modifier| modifier.as_js_static_modifier().is_some());
+                    let is_readonly = member
+                        .modifiers()
+                        .into_iter()
+                        .any(|modifier| modifier.as_ts_readonly_modifier().is_some());
                    let is_optional = member
                        .property_annotation()
                        .as_ref()
                        .and_then(|annotation| annotation.as_ts_optional_property_annotation())
                        .is_some();
                    Self::from_class_member_info(
                        resolver,
                        scope_id,
                        name,
                        ty,
                        is_static,
                        is_optional,
-                        false,
+                        is_readonly,
                     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_type_info/src/local_inference.rs` around lines 1881 - 1913,
The JsPropertyClassMember branch currently passes a hardcoded false for the
readonly flag to Self::from_class_member_info; detect readonly the same way as
the TsPropertySignatureClassMember branch by examining member.modifiers() for a
TS readonly modifier (e.g. let is_readonly =
member.modifiers().into_iter().any(|m| m.as_ts_readonly_modifier().is_some());)
and pass is_readonly instead of false to from_class_member_info for
AnyJsClassMember::JsPropertyClassMember.
🧹 Nitpick comments (1)
crates/biome_js_type_info/src/globals.rs (1)

40-41: Optional tidy-up: centralise default TypeMember flags.

All these updates are correct; consider a tiny helper/builder for “plain” members so future field additions don’t require a hunt across many call sites.

Also applies to: 123-124, 130-131, 208-209, 258-259, 459-460, 489-490

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_type_info/src/globals.rs` around lines 40 - 41, Multiple
places set identical default flags for TypeMember (is_optional: false,
is_readonly: false); introduce a small helper factory to centralize those
defaults and update all call sites. Add a constructor or associated function
like TypeMember::plain(name, ty) or a helper plain_member(...) that sets
is_optional and is_readonly to false and fills the other common fields, then
replace the repeated struct literals in globals.rs (the places constructing
TypeMember instances) to call that helper (e.g., use TypeMember::plain or
plain_member for the members currently using is_optional/is_readonly flags).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_type_info/src/resolver.rs`:
- Around line 819-835: The code creates a new TypeMember but only normalises
member.ty with resolved.apply_module_id_to_reference(...), leaving member.kind
(especially TypeMemberKind::IndexSignature(...)) containing raw references that
point to the wrong module; instead clone and normalise the entire member so keys
get the correct ResolverId. Update the construction of TypeMember to start from
a fully normalised member (e.g. clone a member that has had
apply_module_id_to_reference applied to all internal references) rather than
only normalising ty, and ensure whenever you call ResolvedTypeData.as_raw_data()
you pass or apply the correct ResolverId so subsequent resolution calls don’t
panic.
- Around line 958-976: In strip_undefined_from_member, resolve each union arm
via resolver.resolve_and_get before testing for undefined instead of comparing
raw TypeReference values to GLOBAL_UNDEFINED_ID; iterate union.0, call
resolver.resolve_and_get on each variant and drop arms whose resolved raw data
is TypeData::Primitive/Undefined (or resolves to GLOBAL_UNDEFINED_ID), then if
one arm remains set member.ty to that arm's TypeReference, else if some arms
were removed register a new Union via resolver.register_and_resolve and set
member.ty to TypeReference::from(new_id) (do not rebuild from raw union.0
directly); when calling resolved.as_raw_data() ensure you use the
ResolvedTypeData tied to the correct ResolverId (use the resolved value returned
from resolver.resolve_and_get) to preserve resolver context and avoid panics,
and handle the all-undefined case by registering an appropriate empty/undefined
union result rather than constructing an empty Union raw vector.

---

Outside diff comments:
In `@crates/biome_js_type_info/src/local_inference.rs`:
- Around line 2353-2363: The constructor parameter property synthesis currently
hardcodes is_optional to false inside the loop over constructor.parameters (the
branch matching FunctionParameter::Named(named_param)), which drops the
optionality; change the created TypeMember (Self { ... }) to set is_optional
using the NamedFunctionParameter flag (named_param.is_optional) instead of false
so the synthesized class member preserves the parameter's optionality when
constructing the member list in local_inference.rs.
- Around line 1881-1913: The JsPropertyClassMember branch currently passes a
hardcoded false for the readonly flag to Self::from_class_member_info; detect
readonly the same way as the TsPropertySignatureClassMember branch by examining
member.modifiers() for a TS readonly modifier (e.g. let is_readonly =
member.modifiers().into_iter().any(|m| m.as_ts_readonly_modifier().is_some());)
and pass is_readonly instead of false to from_class_member_info for
AnyJsClassMember::JsPropertyClassMember.

---

Nitpick comments:
In `@crates/biome_js_type_info/src/globals.rs`:
- Around line 40-41: Multiple places set identical default flags for TypeMember
(is_optional: false, is_readonly: false); introduce a small helper factory to
centralize those defaults and update all call sites. Add a constructor or
associated function like TypeMember::plain(name, ty) or a helper
plain_member(...) that sets is_optional and is_readonly to false and fills the
other common fields, then replace the repeated struct literals in globals.rs
(the places constructing TypeMember instances) to call that helper (e.g., use
TypeMember::plain or plain_member for the members currently using
is_optional/is_readonly flags).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 45cc2c21-7663-4427-9798-e5cae9829109

📥 Commits

Reviewing files that changed from the base of the PR and between ccf9770 and 7292a66.

⛔ Files ignored due to path filters (6)
  • crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_export_const_type_declaration_with_namespace.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_merged_namespace_with_type.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_country_info.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (11)
  • crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts
  • crates/biome_js_type_info/src/flattening/expressions.rs
  • crates/biome_js_type_info/src/flattening/intersections.rs
  • crates/biome_js_type_info/src/globals.rs
  • crates/biome_js_type_info/src/local_inference.rs
  • crates/biome_js_type_info/src/resolver.rs
  • crates/biome_js_type_info/src/type_data.rs
  • crates/biome_js_type_info/tests/type_data.rs
  • crates/biome_js_type_info_macros/src/resolvable_derive.rs
  • crates/biome_module_graph/src/js_module_info/collector.rs

Comment thread crates/biome_js_type_info_macros/src/resolvable_derive.rs
Comment thread crates/biome_js_type_info/src/resolver.rs Outdated
Comment thread crates/biome_js_type_info/src/resolver.rs
@minseong0324 minseong0324 marked this pull request as draft April 9, 2026 01:26
@minseong0324 minseong0324 force-pushed the feat/partial-required-readonly branch from 7292a66 to 45d7874 Compare April 9, 2026 03:37
@minseong0324 minseong0324 marked this pull request as ready for review April 9, 2026 04:04
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

Same thing as the other PR:

  • tests
  • changeset

I'll check the code once it's ready

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/invalid.ts (1)

85-87: Optional follow-up: add one composed utility-type fixture.
A case like Readonly<Required<Partial<{ a: string | null }>>> with || would lock in composition behaviour and catch mapper-chaining regressions early.

As per coding guidelines, "All code changes MUST include appropriate tests: ... bug fixes require tests that reproduce and validate the fix."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/invalid.ts`
around lines 85 - 87, Add a composed utility-type fixture that uses chained
mapped/utility types and the || operator to catch mapper-chaining regressions:
create a test similar to the existing `roOpt` case but declaring a variable like
`const composed: Readonly<Required<Partial<{ a: string | null }>>>` (or a
similarly named symbol) and then use `composed.a || "default"` (store result in
a variable such as `vComposed`) so the analyzer exercises composition of
Readonly/Required/Partial with a nullable property; add this to the same specs
directory as the other fixtures so the test suite validates the composed-type
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/invalid.ts`:
- Around line 85-87: Add a composed utility-type fixture that uses chained
mapped/utility types and the || operator to catch mapper-chaining regressions:
create a test similar to the existing `roOpt` case but declaring a variable like
`const composed: Readonly<Required<Partial<{ a: string | null }>>>` (or a
similarly named symbol) and then use `composed.a || "default"` (store result in
a variable such as `vComposed`) so the analyzer exercises composition of
Readonly/Required/Partial with a nullable property; add this to the same specs
directory as the other fixtures so the test suite validates the composed-type
behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b67ac57d-0333-4f63-9317-3996adaa91a2

📥 Commits

Reviewing files that changed from the base of the PR and between d25720d and ff9e04e.

⛔ Files ignored due to path filters (8)
  • crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/checksConditionalsInvalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUnsafePlusOperands/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/noUselessTypeConversion/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/useExhaustiveSwitchCases/invalid.ts.snap is excluded by !**/*.snap and included by **
  • crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/invalid.ts.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (9)
  • .changeset/young-tips-swim.md
  • crates/biome_js_analyze/tests/specs/nursery/noFloatingPromises/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/noMisusedPromises/checksConditionalsInvalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/noUnnecessaryConditions/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/noUnsafePlusOperands/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/noUselessTypeConversion/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/useArraySortCompare/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/useExhaustiveSwitchCases/invalid.ts
  • crates/biome_js_analyze/tests/specs/nursery/useNullishCoalescing/invalid.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/young-tips-swim.md

@minseong0324 minseong0324 force-pushed the feat/partial-required-readonly branch from ff9e04e to 6e0bf5d Compare April 11, 2026 17:21
@minseong0324
Copy link
Copy Markdown
Contributor Author

minseong0324 commented Apr 11, 2026

Same thing as the other PR:

  • tests
  • changeset

I'll check the code once it's ready

@ematipico Same structure as #9874 - tests for the 8 type-aware rules in ac046f0, changeset in 6e0bf5d.

@minseong0324 minseong0324 requested a review from ematipico April 11, 2026 18:28
Copy link
Copy Markdown
Member

@ematipico ematipico left a comment

Choose a reason for hiding this comment

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

I think the entity of the change isn't there yet. We increased the memory usage. We should explore different solutions

Comment on lines +46 to +47
is_optional: false,
is_readonly: false,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be nice to add new code so that we can see if the new fields are correctly populated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in f9170e2.

@@ -59,7 +59,7 @@ fn verify_type_sizes() {

assert_eq!(
std::mem::size_of::<TypeMember>(),
40,
48,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test is here for a reason; we can't increase this type lightly. Have you considered storing the flags somewhere else?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Moved to TypeMemberKind::NamedOptional, 40 bytes restored. 09e12c8

@@ -880,3 +964,24 @@ macro_rules! derive_primitive_resolved {
}

derive_primitive_resolved!(bool, f64, u32, u64, usize);

fn strip_undefined_from_member(resolver: &mut dyn TypeResolver, member: &mut TypeMember) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

docstrings missing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in c20b779.

Comment on lines +799 to +802
} else if (qualifier.is_partial()
|| qualifier.is_required()
|| qualifier.is_readonly_type())
&& qualifier.type_parameters.len() == 1
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do they need to be handled all in the same branch? For example is_readonly isn't checked against the other two flags.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d49c39f.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
crates/biome_js_type_info/src/resolver.rs (1)

1020-1025: ⚠️ Potential issue | 🟠 Major

Union arms still compared as raw references without resolving.

The filtering logic compares **v != TypeReference::from(GLOBAL_UNDEFINED_ID) against raw references from union.0. If undefined arrives through an aliased reference path, it won't be filtered out. Additionally, the cloned references lack module ID normalisation.

Consider resolving each variant before testing for undefined, and applying module IDs when rebuilding the filtered union. Based on learnings: when iterating over union variant TypeReferences, always resolve the variant via resolver.resolve_and_get() first rather than comparing raw TypeReferences against global IDs.

Sketch of approach
let resolver_id = resolved.resolver_id();
let filtered: Vec<_> = union
    .0
    .iter()
    .filter(|v| {
        let normalised = resolver_id.apply_module_id_to_reference(v);
        resolver
            .resolve_and_get(&normalised)
            .map_or(true, |r| !matches!(r.as_raw_data(), TypeData::Undefined))
    })
    .map(|v| resolver_id.apply_module_id_to_reference(v).into_owned())
    .collect();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_type_info/src/resolver.rs` around lines 1020 - 1025, The
current union filtering compares raw TypeReference values and clones them
directly, which misses aliased/normalized undefineds and skips module ID
normalization; change the loop over union.0 to first obtain resolver_id via
resolved.resolver_id(), apply module ID normalization to each variant using
resolver_id.apply_module_id_to_reference(), then call
resolver.resolve_and_get(&normalised) and filter out any resolved variant whose
as_raw_data() is TypeData::Undefined; finally, rebuild the filtered Vec by
mapping each original variant to the normalized (owned) TypeReference so the
reconstructed union uses normalized module IDs (use resolver.resolve_and_get(),
resolver_id.apply_module_id_to_reference(), TypeData::Undefined checks, and
avoid direct comparison to GLOBAL_UNDEFINED_ID).
🧹 Nitpick comments (1)
crates/biome_module_graph/tests/spec_tests.rs (1)

2623-2646: Please also hit utility-type synthesis in this fixture.

Good baseline test, but it currently validates member parsing only. Add Partial<Config>, Required<Config>, and Readonly<Config> usages so this snapshot protects the exact synthesis path introduced by this PR.

Suggested fixture tweak
 fn test_optional_and_readonly_members() {
     let fs = MemoryFileSystem::default();
     fs.insert(
         "/src/index.ts".into(),
         r#"export interface Config {
     name: string;
     age?: number;
     readonly id: string;
     readonly label?: string;
 }
+
+export type ConfigPartial = Partial<Config>;
+export type ConfigRequired = Required<Config>;
+export type ConfigReadonly = Readonly<Config>;
 "#,
     );

As per coding guidelines, “All code changes MUST include appropriate tests … bug fixes require tests that reproduce and validate the fix.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_module_graph/tests/spec_tests.rs` around lines 2623 - 2646, The
test currently only defines interface Config but doesn't exercise utility-type
synthesis; update the fixture inserted into MemoryFileSystem in the
test_optional_and_readonly_members test so the /src/index.ts content also
references Partial<Config>, Required<Config>, and Readonly<Config> (for example
create exported type aliases like ExportedPartial = Partial<Config>,
ExportedRequired = Required<Config>, ExportedReadonly = Readonly<Config>) so
ModuleGraph::update_graph_for_js_paths and ModuleGraphSnapshot::new will
synthesize those utility types and the snapshot
("test_optional_and_readonly_members") will capture the new synthesis path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/biome_js_type_info/src/local_inference.rs`:
- Around line 2265-2267: The current member-kind encoding prioritizes is_static
over is_optional causing `static foo?: T` to be encoded as NamedStatic and lose
optional semantics; update the enum TypeMemberKind to include a
NamedStaticOptional variant (or otherwise ensure both flags are represented),
change from_class_member_info to emit NamedStaticOptional when both is_static
and is_optional are true, and then update TypeMember::is_optional(),
TypeMemberKind::is_static(), the formatter output for the new variant, and the
resolver transforms that handle Partial<T>/Required<T> so they respect the new
combined variant.

---

Duplicate comments:
In `@crates/biome_js_type_info/src/resolver.rs`:
- Around line 1020-1025: The current union filtering compares raw TypeReference
values and clones them directly, which misses aliased/normalized undefineds and
skips module ID normalization; change the loop over union.0 to first obtain
resolver_id via resolved.resolver_id(), apply module ID normalization to each
variant using resolver_id.apply_module_id_to_reference(), then call
resolver.resolve_and_get(&normalised) and filter out any resolved variant whose
as_raw_data() is TypeData::Undefined; finally, rebuild the filtered Vec by
mapping each original variant to the normalized (owned) TypeReference so the
reconstructed union uses normalized module IDs (use resolver.resolve_and_get(),
resolver_id.apply_module_id_to_reference(), TypeData::Undefined checks, and
avoid direct comparison to GLOBAL_UNDEFINED_ID).

---

Nitpick comments:
In `@crates/biome_module_graph/tests/spec_tests.rs`:
- Around line 2623-2646: The test currently only defines interface Config but
doesn't exercise utility-type synthesis; update the fixture inserted into
MemoryFileSystem in the test_optional_and_readonly_members test so the
/src/index.ts content also references Partial<Config>, Required<Config>, and
Readonly<Config> (for example create exported type aliases like ExportedPartial
= Partial<Config>, ExportedRequired = Required<Config>, ExportedReadonly =
Readonly<Config>) so ModuleGraph::update_graph_for_js_paths and
ModuleGraphSnapshot::new will synthesize those utility types and the snapshot
("test_optional_and_readonly_members") will capture the new synthesis path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b08943b4-1e53-4161-bb70-e8b123bd9ecf

📥 Commits

Reviewing files that changed from the base of the PR and between 6e0bf5d and 9e0e681.

⛔ Files ignored due to path filters (2)
  • crates/biome_module_graph/tests/snapshots/test_optional_and_readonly_members.snap is excluded by !**/*.snap and included by **
  • crates/biome_module_graph/tests/snapshots/test_resolve_recursive_looking_vfile.snap is excluded by !**/*.snap and included by **
📒 Files selected for processing (5)
  • crates/biome_js_type_info/src/format_type_info.rs
  • crates/biome_js_type_info/src/local_inference.rs
  • crates/biome_js_type_info/src/resolver.rs
  • crates/biome_js_type_info/src/type_data.rs
  • crates/biome_module_graph/tests/spec_tests.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/biome_js_type_info/src/type_data.rs

Comment on lines +2265 to 2267
} else if is_optional {
TypeMemberKind::NamedOptional(name)
} else {
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.

⚠️ Potential issue | 🟠 Major

static + optional is still lossy in member-kind encoding.

On Line 2265, is_static wins before is_optional, so static foo?: T is encoded as NamedStatic, not optional. That drops optional-kind semantics used elsewhere (e.g. is_optional() checks).

Direction for a robust fix
- pub enum TypeMemberKind {
+ pub enum TypeMemberKind {
    CallSignature,
    Constructor,
    Getter(Text),
    IndexSignature(TypeReference),
    Named(Text),
    NamedOptional(Text),
    NamedStatic(Text),
+   NamedStaticOptional(Text),
 }

Then update:

  • TypeMember::is_optional() and TypeMemberKind::is_static()
  • formatter output for the new variant
  • resolver transforms for Partial<T> / Required<T>
  • from_class_member_info to emit NamedStaticOptional when both flags are true.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/biome_js_type_info/src/local_inference.rs` around lines 2265 - 2267,
The current member-kind encoding prioritizes is_static over is_optional causing
`static foo?: T` to be encoded as NamedStatic and lose optional semantics;
update the enum TypeMemberKind to include a NamedStaticOptional variant (or
otherwise ensure both flags are represented), change from_class_member_info to
emit NamedStaticOptional when both is_static and is_optional are true, and then
update TypeMember::is_optional(), TypeMemberKind::is_static(), the formatter
output for the new variant, and the resolver transforms that handle
Partial<T>/Required<T> so they respect the new combined variant.

@minseong0324 minseong0324 force-pushed the feat/partial-required-readonly branch 7 times, most recently from 6d0ba70 to be54031 Compare April 12, 2026 23:32
@minseong0324 minseong0324 force-pushed the feat/partial-required-readonly branch from be54031 to f9170e2 Compare April 12, 2026 23:54
…tional-readonly

# Conflicts:
#	crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts
#	crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/invalid.ts.snap
#	crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts
#	crates/biome_js_analyze/tests/specs/nursery/noMisleadingReturnType/valid.ts.snap
@minseong0324
Copy link
Copy Markdown
Contributor Author

I think the entity of the change isn't there yet. We increased the memory usage. We should explore different solutions

@ematipico Dropped both bool fields, optionality is now a NamedOptional variant. 40 bytes. 09e12c8.

@minseong0324 minseong0324 requested a review from ematipico April 13, 2026 00:13
@ematipico ematipico merged commit a951586 into biomejs:main Apr 13, 2026
20 checks passed
@github-actions github-actions Bot mentioned this pull request Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Linter Area: linter A-Project Area: project A-Type-Inference Area: type inference L-JavaScript Language: JavaScript and super languages

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants