-
-
Notifications
You must be signed in to change notification settings - Fork 793
fix(analyze/types): union static-member flattening #8546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🦋 Changeset detectedLatest commit: 58a37de The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
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 |
WalkthroughThis change hardens static-member flattening for union types by switching from a pre-collected variant mapping to an iterative per-variant resolution loop. It skips global-undefined variants, pushes unknown on resolution failures, aborts if a variant is a typeof-expression, and preserves unknown variants in resulting unions. Tests were added to ensure unknown variants are preserved and typeof-expression variants are not flattened. Two new public diagnostic types (JsModuleInfoDiagnostic, ModuleDiagnostic) and a test preventing types-limit explosions for a recursive parentElement pattern were also introduced. Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches✅ Passed checks (4 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
This patch tightens the behavior for TypeofExpression::StaticMember on unions: - If a union variant is unknown, propagate unknown into the result. - Resolve the member across union variants directly (no distribution into nested inferred expressions). - If a union variant is still an inferred expression type (TypeofExpression), refuse to flatten to avoid re-entering expression-based inference that can lead to runaway growth.
575d3e4 to
cdc136e
Compare
|
At a glance, this looks a lot like #8536, which is already merged. |
|
@dyc3 At a glance, yes, and it's because I started working on it yesterday and finished it up after a sleep (at which point this PR was made). But it has 3 benefits in addition to that PR: it avoids vec allocation, correctly handles unknown whereas 8536 stripped unknowns, and it fixes the recursion whereas 8536 did not. The added tests fail on 8536. |
CodSpeed Performance ReportMerging #8546 will not alter performanceComparing Summary
Footnotes
|
ematipico
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Tests were already added, so one isn't needed anymore
| #[test] | ||
| fn infer_flattened_type_of_static_member_on_union_does_not_flatten_expression_variants() { | ||
| const CODE: &str = r#"interface Node { | ||
| parentNode: Node | null; | ||
| } | ||
| node.parentNode"#; | ||
|
|
||
| let root = parse_ts(CODE); | ||
| let interface_decl = get_interface_declaration(&root); | ||
| let mut resolver = GlobalsResolver::default(); | ||
| let interface_ty = | ||
| TypeData::from_ts_interface_declaration(&mut resolver, ScopeId::GLOBAL, &interface_decl) | ||
| .expect("interface must be inferred"); | ||
| resolver.run_inference(); | ||
|
|
||
| let interface_ref = resolver.reference_to_owned_data(interface_ty); | ||
| let expression_variant = | ||
| resolver.reference_to_owned_data(TypeData::TypeofExpression(Box::new( | ||
| TypeofExpression::StaticMember(TypeofStaticMemberExpression { | ||
| object: TypeReference::unknown(), | ||
| member: Text::new_static("parentNode"), | ||
| }), | ||
| ))); | ||
| let union_ty = TypeData::union_of(&resolver, [interface_ref, expression_variant].into()); | ||
|
|
||
| let expr = get_expression(&root); | ||
| let mut resolver = HardcodedSymbolResolver::new("node", union_ty, resolver); | ||
| let expr_ty = TypeData::from_any_js_expression(&mut resolver, ScopeId::GLOBAL, &expr); | ||
| let expr_ty = expr_ty.inferred(&mut resolver); | ||
|
|
||
| assert!( | ||
| matches!(&expr_ty, TypeData::TypeofExpression(expr) if matches!(expr.as_ref(), TypeofExpression::StaticMember(_))), | ||
| "expected to keep typeof-expression unflattened, got: {expr_ty}", | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test isn't needed anymore because it was already added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I searched for it but I couldn't find it. The closest was infer_flattened_type_of_static_member_on_union but it checks Node | null instead of the recursive Node | TypeofExpr that this test checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changeset is also not needed, since it was added in the other PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, changeset-bot told me the changeset is needed for it to cause a version bump. This is a meaningful improvement, fixing the recursive behavior as shown by the added tests. Can you please explain why it's not needed because I'm getting conflicting information? This is not a noop change.
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other PR already added the changeset for the original fix. If the changeset from this PR lands, we will end up with 2 changelog entries for 1 fix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, while both PRs touch the same area of the code, they fix slightly different things. The added tests in this PR fail on the original. The original PR did not resolve issue like #8204 on my codebase. They look, at a glance, the same but they are really not. I now see I should have explained it better because I can see how similar and confusing this gets.
Most notable changes are:
- fix recursion on typeof expression (original did not)
- fix unknown handling (regression after the original)
- avoid allocating temporary vec (very minor optimization after the original)
🙂!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, nevermind then. Thanks for clarifying.
This PR was written primarily by GPT 5.2.
Summary
Unknown branch propagation
If node might be unknown, node.parentNode must include unknown.
Without this, inference can become too narrow by effectively “dropping” the unknown branch
Expression-derived union branches
If a union variant is still an inferred expression type (Biome’s internal “type-of-expression”), flattening obj.member can re-enter expression reasoning in a way that’s unstable for patterns like:
So we conservatively refuse to flatten in that case (leave it unflattened for this pass).
Probably fixes #8204.
Verified fix on https://github.com/openstreetmap-ng/openstreetmap-ng/blob/main/app/views/lib/standard-form.ts
Test Plan