tsz_solver/relations/compat_overrides.rs
1//! Nominal typing overrides for the compatibility checker.
2//!
3//! This module contains assignability override methods that implement
4//! TypeScript's nominal typing rules on top of the structural compatibility
5//! engine. These overrides handle:
6//!
7//! - **Enum nominality**: Different enums are not assignable even if structurally
8//! identical. String enums are strictly nominal; numeric enums allow number
9//! assignability.
10//! - **Private brand checking**: Classes with private/protected fields use nominal
11//! typing — the `parent_id` on each property must match exactly.
12//! - **Redeclaration compatibility**: Variable redeclarations (`var x: T; var x: U`)
13//! require nominal identity for enums and bidirectional structural subtyping for
14//! other types.
15
16use crate::relations::compat::{AssignabilityOverrideProvider, ShapeExtractor, StringLikeVisitor};
17use crate::relations::subtype::TypeResolver;
18use crate::types::{LiteralValue, TypeData, TypeId};
19use crate::visitor::TypeVisitor;
20
21use super::compat::CompatChecker;
22
23// =============================================================================
24// Assignability Override Functions
25// =============================================================================
26
27impl<'a, R: TypeResolver> CompatChecker<'a, R> {
28 /// Check if `source` is assignable to `target` using TS compatibility rules,
29 /// with checker-provided overrides for enums, abstract constructors, and accessibility.
30 ///
31 /// This is the main entry point for assignability checking when checker context is available.
32 pub fn is_assignable_with_overrides<P: AssignabilityOverrideProvider + ?Sized>(
33 &mut self,
34 source: TypeId,
35 target: TypeId,
36 overrides: &P,
37 ) -> bool {
38 // Check override provider for enum assignability
39 if let Some(result) = overrides.enum_assignability_override(source, target) {
40 return result;
41 }
42
43 // Check override provider for abstract constructor assignability
44 if let Some(result) = overrides.abstract_constructor_assignability_override(source, target)
45 {
46 return result;
47 }
48
49 // Check override provider for constructor accessibility
50 if let Some(result) = overrides.constructor_accessibility_override(source, target) {
51 return result;
52 }
53
54 // Check private brand assignability (can be done with TypeDatabase alone)
55 if let Some(result) = self.private_brand_assignability_override(source, target) {
56 return result;
57 }
58
59 // Fall through to regular assignability check
60 self.is_assignable(source, target)
61 }
62
63 /// Private brand assignability override.
64 /// If both source and target types have private brands, they must match exactly.
65 /// This implements nominal typing for classes with private fields.
66 ///
67 /// Uses recursive structure to preserve Union/Intersection semantics:
68 /// - Union (A | B): OR logic - must satisfy at least one branch
69 /// - Intersection (A & B): AND logic - must satisfy all branches
70 pub fn private_brand_assignability_override(
71 &self,
72 source: TypeId,
73 target: TypeId,
74 ) -> Option<bool> {
75 use crate::types::Visibility;
76
77 // Fast path: identical types don't need nominal brand override logic.
78 // Let the regular assignability path decide.
79 if source == target {
80 return None;
81 }
82
83 // 1. Handle Target Union (OR logic)
84 // S -> (A | B) : Valid if S -> A OR S -> B
85 if let Some(TypeData::Union(members)) = self.interner.lookup(target) {
86 let members = self.interner.type_list(members);
87 // If source matches ANY target member, it's valid
88 for &member in members.iter() {
89 match self.private_brand_assignability_override(source, member) {
90 Some(true) | None => return None, // Pass (or structural fallback)
91 Some(false) => {} // Keep checking other members
92 }
93 }
94 return Some(false); // Failed against all members
95 }
96
97 // 2. Handle Source Union (AND logic)
98 // (A | B) -> T : Valid if A -> T AND B -> T
99 if let Some(TypeData::Union(members)) = self.interner.lookup(source) {
100 let members = self.interner.type_list(members);
101 for &member in members.iter() {
102 if let Some(false) = self.private_brand_assignability_override(member, target) {
103 return Some(false); // Fail if any member fails
104 }
105 }
106 return None; // All passed or fell back
107 }
108
109 // 3. Handle Target Intersection (AND logic)
110 // S -> (A & B) : Valid if S -> A AND S -> B
111 if let Some(TypeData::Intersection(members)) = self.interner.lookup(target) {
112 let members = self.interner.type_list(members);
113 for &member in members.iter() {
114 if let Some(false) = self.private_brand_assignability_override(source, member) {
115 return Some(false); // Fail if any member fails
116 }
117 }
118 return None; // All passed or fell back
119 }
120
121 // 4. Handle Source Intersection (OR logic)
122 // (A & B) -> T : Valid if A -> T OR B -> T
123 if let Some(TypeData::Intersection(members)) = self.interner.lookup(source) {
124 let members = self.interner.type_list(members);
125 for &member in members.iter() {
126 match self.private_brand_assignability_override(member, target) {
127 Some(true) | None => return None, // Pass (or structural fallback)
128 Some(false) => {} // Keep checking other members
129 }
130 }
131 return Some(false); // Failed against all members
132 }
133
134 // 5. Handle Lazy types (recursive resolution)
135 if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(source)
136 && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
137 {
138 // Guard against non-progressing lazy resolution (e.g. DefId -> same Lazy type),
139 // which would otherwise recurse forever.
140 if resolved == source {
141 return None;
142 }
143 return self.private_brand_assignability_override(resolved, target);
144 }
145
146 if let Some(TypeData::Lazy(def_id)) = self.interner.lookup(target)
147 && let Some(resolved) = self.subtype.resolver.resolve_lazy(def_id, self.interner)
148 {
149 // Same non-progress guard for target-side lazy resolution.
150 if resolved == target {
151 return None;
152 }
153 return self.private_brand_assignability_override(source, resolved);
154 }
155
156 // 6. Base case: Extract and compare object shapes
157 let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
158
159 // Get source shape
160 let source_shape_id = extractor.extract(source)?;
161 let source_shape = self
162 .interner
163 .object_shape(crate::types::ObjectShapeId(source_shape_id));
164
165 // Get target shape
166 let mut extractor = ShapeExtractor::new(self.interner, self.subtype.resolver);
167 let target_shape_id = extractor.extract(target)?;
168 let target_shape = self
169 .interner
170 .object_shape(crate::types::ObjectShapeId(target_shape_id));
171
172 let mut has_private_brands = false;
173
174 // Check Target requirements (Nominality)
175 // If Target has a private/protected property, Source MUST match its origin exactly.
176 for target_prop in &target_shape.properties {
177 if target_prop.visibility == Visibility::Private
178 || target_prop.visibility == Visibility::Protected
179 {
180 has_private_brands = true;
181 let source_prop = source_shape
182 .properties
183 .binary_search_by_key(&target_prop.name, |p| p.name)
184 .ok()
185 .map(|idx| &source_shape.properties[idx]);
186
187 match source_prop {
188 Some(sp) => {
189 // CRITICAL: The parent_id must match exactly.
190 if sp.parent_id != target_prop.parent_id {
191 return Some(false);
192 }
193 }
194 None => {
195 return Some(false);
196 }
197 }
198 }
199 }
200
201 // Check Source restrictions (Visibility leakage)
202 // If Source has a private/protected property, it cannot be assigned to a Target
203 // that expects it to be Public.
204 for source_prop in &source_shape.properties {
205 if source_prop.visibility == Visibility::Private
206 || source_prop.visibility == Visibility::Protected
207 {
208 has_private_brands = true;
209 if let Some(target_prop) = target_shape
210 .properties
211 .binary_search_by_key(&source_prop.name, |p| p.name)
212 .ok()
213 .map(|idx| &target_shape.properties[idx])
214 && target_prop.visibility == Visibility::Public
215 {
216 return Some(false);
217 }
218 }
219 }
220
221 has_private_brands.then_some(true)
222 }
223
224 /// Enum member assignability override.
225 /// Implements nominal typing for enum members: EnumA.X is NOT assignable to `EnumB` even if values match.
226 ///
227 /// TypeScript enum rules:
228 /// 1. Different enums with different `DefIds` are NOT assignable (nominal typing)
229 /// 2. Numeric enums are bidirectionally assignable to number (Rule #7 - Open Numeric Enums)
230 /// 3. String enums are strictly nominal (string literals NOT assignable to string enums)
231 /// 4. Same enum members with different values are NOT assignable (EnumA.X != EnumA.Y)
232 /// 5. Unions containing enums: Source union assigned to target enum checks all members
233 pub fn enum_assignability_override(&self, source: TypeId, target: TypeId) -> Option<bool> {
234 use crate::type_queries;
235 use crate::visitor;
236
237 // Special case: Source union -> Target enum
238 // When assigning a union to an enum, ALL enum members in the union must match the target enum.
239 // This handles cases like: (EnumA | EnumB) assigned to EnumC
240 // And allows: (Choice.Yes | Choice.No) assigned to Choice (subset of same enum)
241 if let Some((t_def, _)) = visitor::enum_components(self.interner, target)
242 && type_queries::is_union_type(self.interner, source)
243 {
244 let union_members = type_queries::get_union_members(self.interner, source)?;
245
246 let mut all_same_enum = true;
247 let mut has_non_enum = false;
248 for &member in &union_members {
249 if let Some((member_def, _)) = visitor::enum_components(self.interner, member) {
250 // Check if this member belongs to the target enum.
251 // Members have their own DefIds (different from parent enum's DefId),
252 // so we must also check the parent relationship.
253 let member_parent = self.subtype.resolver.get_enum_parent_def_id(member_def);
254 if member_def != t_def && member_parent != Some(t_def) {
255 // Found an enum member from a different enum than target
256 return Some(false);
257 }
258 } else {
259 all_same_enum = false;
260 has_non_enum = true;
261 }
262 }
263
264 // If ALL union members are enum members from the same enum as the target,
265 // the union is a subset of the enum and therefore assignable.
266 // This handles: `type YesNo = Choice.Yes | Choice.No` assignable to `Choice`.
267 if all_same_enum && !has_non_enum && !union_members.is_empty() {
268 return Some(true);
269 }
270 // Otherwise fall through to structural check for non-enum union members.
271 }
272
273 // String enums are assignable to string (like numeric enums are to number).
274 // Fall through to structural checking for this case.
275
276 // Fast path: Check if both are enum types with same DefId but different TypeIds
277 // This handles the test case where enum members aren't in the resolver
278 if let (Some((s_def, _)), Some((t_def, _))) = (
279 visitor::enum_components(self.interner, source),
280 visitor::enum_components(self.interner, target),
281 ) && s_def == t_def
282 && source != target
283 {
284 // Same enum DefId but different TypeIds
285 // Check if both are literal enum members (not union-based enums)
286 if self.is_literal_enum_member(source) && self.is_literal_enum_member(target) {
287 // Both are enum literals with same DefId but different values
288 // Nominal rule: E.A is NOT assignable to E.B
289 return Some(false);
290 }
291 }
292
293 let source_def = self.get_enum_def_id(source);
294 let target_def = self.get_enum_def_id(target);
295
296 match (source_def, target_def) {
297 // Case 1: Both are enums (or enum members or Union-based enums)
298 // Note: Same-DefId, different-TypeId case is now handled above before get_enum_def_id
299 (Some(s_def), Some(t_def)) => {
300 if s_def == t_def {
301 // Same DefId: Same type (E.A -> E.A or E -> E)
302 return Some(true);
303 }
304
305 // Gap A: Different DefIds, but might be member -> parent relationship
306 // Check if they share a parent enum (e.g., E.A -> E)
307 let s_parent = self.subtype.resolver.get_enum_parent_def_id(s_def);
308 let t_parent = self.subtype.resolver.get_enum_parent_def_id(t_def);
309
310 match (s_parent, t_parent) {
311 (Some(sp), Some(tp)) if sp == tp => {
312 // Same parent enum
313 // If target is the Enum Type (e.g., 'E'), allow structural check
314 if self.subtype.resolver.is_enum_type(target, self.interner) {
315 return None;
316 }
317 // If target is a different specific member (e.g., 'E.B'), reject nominally
318 // E.A -> E.B should fail even if they have the same value
319 Some(false)
320 }
321 (Some(sp), None) => {
322 // Source is a member, target doesn't have a parent (target is not a member)
323 // Check if target is the parent enum type
324 if t_def == sp {
325 // Target is the parent enum of source member
326 // Allow member to parent enum assignment (E.A -> E)
327 return Some(true);
328 }
329 // Target is an enum type but not the parent
330 Some(false)
331 }
332 _ => {
333 // Different parents (or one/both are types, not members)
334 // Nominal mismatch: EnumA.X is not assignable to EnumB
335 Some(false)
336 }
337 }
338 }
339
340 // Case 2: Target is an enum, source is a primitive
341 (None, Some(t_def)) => {
342 // Check if target is a numeric enum
343 if self.subtype.resolver.is_numeric_enum(t_def) {
344 // Rule #7: Numeric enums allow number assignability
345 // BUT we need to distinguish between:
346 // - `let x: E = 1` (enum TYPE - allowed)
347 // - `let x: E.A = 1` (enum MEMBER - rejected)
348
349 // Check if source is number-like (number or number literal)
350 let is_source_number = source == TypeId::NUMBER
351 || matches!(
352 self.interner.lookup(source),
353 Some(TypeData::Literal(LiteralValue::Number(_)))
354 );
355
356 if is_source_number {
357 // If target is the full Enum Type (e.g., `let x: E = 1`), allow it.
358 if self.subtype.resolver.is_enum_type(target, self.interner) {
359 // Allow bare `number` type but not arbitrary literals
360 if source == TypeId::NUMBER {
361 return Some(true);
362 }
363 // For number literals, fall through to structural check
364 return None;
365 }
366
367 // If target is a specific member (e.g., `let x: E.A = 1`),
368 // fall through to structural check.
369 // - `1 -> E.A(0)` will fail structural check (Correct)
370 // - `0 -> E.A(0)` will pass structural check (Correct)
371 return None;
372 }
373
374 None
375 } else {
376 // String enums do NOT allow raw string assignability
377 // If source is string or string literal, reject
378 if self.is_string_like(source) {
379 return Some(false);
380 }
381 None
382 }
383 }
384
385 // Case 3: Source is an enum, target is a primitive
386 // String enums (both types and members) are assignable to string via structural checking
387 (Some(s_def), None) => {
388 // Check if source is a string enum
389 if !self.subtype.resolver.is_numeric_enum(s_def) {
390 // Source is a string enum
391 if target == TypeId::STRING {
392 // Both enum types (Union of members) and enum members (string literals)
393 // are assignable to string. Fall through to structural checking.
394 return None;
395 }
396 }
397 // Numeric enums and non-string targets: fall through to structural check
398 None
399 }
400
401 // Case 4: Neither is an enum
402 (None, None) => None,
403 }
404 }
405
406 /// Check if a type is string-like (string, string literal, or template literal).
407 /// Used to reject primitive-to-string-enum assignments.
408 fn is_string_like(&self, type_id: TypeId) -> bool {
409 if type_id == TypeId::STRING {
410 return true;
411 }
412 // Use visitor to check for string literals, template literals, etc.
413 let mut visitor = StringLikeVisitor { db: self.interner };
414 visitor.visit_type(self.interner, type_id)
415 }
416
417 /// Get the `DefId` of an enum type, handling both direct Enum members and Union-based Enums.
418 /// Check whether `type_id` is an enum whose underlying member is a string or number literal.
419 fn is_literal_enum_member(&self, type_id: TypeId) -> bool {
420 matches!(
421 self.interner.lookup(type_id),
422 Some(TypeData::Enum(_, member_type))
423 if matches!(
424 self.interner.lookup(member_type),
425 Some(TypeData::Literal(LiteralValue::Number(_) | LiteralValue::String(_)))
426 )
427 )
428 }
429
430 /// Returns `Some(def_id)` if the type is an Enum or a Union of Enum members from the same enum.
431 /// Returns None if the type is not an enum or contains mixed enums.
432 fn get_enum_def_id(&self, type_id: TypeId) -> Option<crate::def::DefId> {
433 use crate::{type_queries, visitor};
434
435 // Resolve Lazy types first (handles imported/forward-declared enums)
436 let resolved =
437 if let Some(lazy_def_id) = type_queries::get_lazy_def_id(self.interner, type_id) {
438 // Try to resolve the Lazy type
439 if let Some(resolved_type) = self
440 .subtype
441 .resolver
442 .resolve_lazy(lazy_def_id, self.interner)
443 {
444 // Guard against self-referential lazy types
445 if resolved_type == type_id {
446 return None;
447 }
448 // Recursively check the resolved type
449 return self.get_enum_def_id(resolved_type);
450 }
451 // Lazy type couldn't be resolved yet, return None
452 return None;
453 } else {
454 type_id
455 };
456
457 // 1. Check for Intrinsic Primitives first (using visitor, not TypeId constants)
458 // This filters out intrinsic types like string, number, boolean which are stored
459 // as TypeData::Enum for definition store purposes but are NOT user enums
460 if visitor::intrinsic_kind(self.interner, resolved).is_some() {
461 return None;
462 }
463
464 // 2. Check direct Enum member
465 if let Some((def_id, _inner)) = visitor::enum_components(self.interner, resolved) {
466 // Use the new is_user_enum_def method to check if this is a user-defined enum
467 // This properly filters out intrinsic types from lib.d.ts
468 if self.subtype.resolver.is_user_enum_def(def_id) {
469 return Some(def_id);
470 }
471 // Not a user-defined enum (intrinsic type or type alias)
472 return None;
473 }
474
475 // 3. Check Union of Enum members (handles Enum types represented as Unions)
476 if let Some(members) = visitor::union_list_id(self.interner, resolved) {
477 let members = self.interner.type_list(members);
478 if members.is_empty() {
479 return None;
480 }
481
482 let first_def = self.get_enum_def_id(members[0])?;
483 for &member in members.iter().skip(1) {
484 if self.get_enum_def_id(member) != Some(first_def) {
485 return None; // Mixed union or non-enum members
486 }
487 }
488 return Some(first_def);
489 }
490
491 None
492 }
493
494 /// Checks if two types are compatible for variable redeclaration (TS2403).
495 ///
496 /// This applies TypeScript's nominal identity rules for enums and
497 /// respects 'any' propagation. Used for checking if multiple variable
498 /// declarations have compatible types.
499 ///
500 /// # Examples
501 /// - `var x: number; var x: number` → true
502 /// - `var x: E.A; var x: E.A` → true
503 /// - `var x: E.A; var x: E.B` → false
504 /// - `var x: E; var x: F` → false (different enums)
505 /// - `var x: E; var x: number` → false
506 pub fn are_types_identical_for_redeclaration(&mut self, a: TypeId, b: TypeId) -> bool {
507 // 1. Fast path: physical identity
508 if a == b {
509 return true;
510 }
511
512 // 2. Error propagation — suppress cascading errors from ERROR types.
513 if a == TypeId::ERROR || b == TypeId::ERROR {
514 return true;
515 }
516
517 // For redeclaration, `any` is only identical to `any`.
518 // `a == b` already caught the `any == any` case above.
519 if a == TypeId::ANY || b == TypeId::ANY {
520 return false;
521 }
522
523 // 4. Enum Nominality Check
524 // If one is an enum and the other isn't, or they are different enums,
525 // they are not identical for redeclaration, even if structurally compatible.
526 if let Some(res) = self.enum_redeclaration_check(a, b) {
527 return res;
528 }
529
530 // 5. Normalize Application/Mapped/Lazy types before structural comparison.
531 // Required<{a?: string}> must evaluate to {a: string} before bidirectional
532 // subtype checking, just as is_assignable_impl() does via normalize_assignability_operands.
533 let (a_norm, b_norm) = self.normalize_assignability_operands(a, b);
534 tracing::trace!(
535 a = a.0,
536 b = b.0,
537 a_norm = a_norm.0,
538 b_norm = b_norm.0,
539 a_changed = a != a_norm,
540 b_changed = b != b_norm,
541 "are_types_identical_for_redeclaration: normalized"
542 );
543 let a = a_norm;
544 let b = b_norm;
545
546 // 5. Structural Identity
547 // Delegate to the Judge to check bidirectional subtyping
548 let fwd = self.subtype.is_subtype_of(a, b);
549 let bwd = self.subtype.is_subtype_of(b, a);
550 tracing::trace!(
551 a = a.0,
552 b = b.0,
553 fwd,
554 bwd,
555 "are_types_identical_for_redeclaration: result"
556 );
557 fwd && bwd
558 }
559
560 /// Check if two types involving enums are compatible for redeclaration.
561 ///
562 /// Returns Some(bool) if either type is an enum:
563 /// - Some(false) if different enums or enum vs primitive
564 /// - None if neither is an enum (delegate to structural check)
565 fn enum_redeclaration_check(&self, a: TypeId, b: TypeId) -> Option<bool> {
566 let a_def = self.get_enum_def_id(a);
567 let b_def = self.get_enum_def_id(b);
568
569 match (a_def, b_def) {
570 (Some(def_a), Some(def_b)) => {
571 // Both are enums: must be the same enum definition
572 if def_a != def_b {
573 Some(false)
574 } else {
575 // Same enum DefId: compatible for redeclaration
576 // This allows: var x: MyEnum; var x = MyEnum.Member;
577 // where MyEnum.Member (enum member) is compatible with MyEnum (enum type)
578 Some(true)
579 }
580 }
581 (Some(_), None) | (None, Some(_)) => {
582 // One is an enum, the other is a primitive (e.g., number)
583 // In TS, Enum E and 'number' are NOT identical for redeclaration
584 Some(false)
585 }
586 (None, None) => None,
587 }
588 }
589}