tsz_solver/relations/lawyer.rs
1//! The "Lawyer" layer for TypeScript compatibility.
2//!
3//! This module implements the compatibility layer that sits between the public API
4//! and the core structural subtype checking ("Judge" layer). It applies TypeScript-
5//! specific business logic, including nuanced rules for `any` propagation.
6//!
7//! ## Judge vs. Lawyer Architecture (SOLVER.md Section 8)
8//!
9//! - **Judge (SubtypeChecker):** Implements strict, sound set theory semantics.
10//! It knows nothing about TypeScript legacy behavior.
11//! - **Lawyer (`AnyPropagationRules` + CompatChecker):** Applies TypeScript-specific
12//! rules and delegates to the Judge with appropriate configuration.
13//!
14//! ## TypeScript Quirks Handled
15//!
16//! ### A. `any` Propagation (The Black Hole)
17//! `any` violates the partial order of sets - it's both a subtype and supertype
18//! of everything. The `AnyPropagationRules` struct handles this short-circuit.
19//!
20//! ### B. Function Variance
21//! - **Strict mode (strictFunctionTypes):** Parameters are contravariant (sound)
22//! - **Legacy mode:** Parameters are bivariant (unsound but backward-compatible)
23//! - **Methods:** Always bivariant regardless of strictFunctionTypes
24//!
25//! ### C. Freshness (Excess Property Checking)
26//! Object literals are "fresh" and trigger excess property checking.
27//! Once assigned to a variable, they lose freshness and allow width subtyping.
28//! Freshness is tracked on the `TypeId` via `ObjectFlags`, with object literals
29//! interning to fresh shapes and widening removing the fresh flag. Sound Mode's
30//! binding-level tracking lives in the Checker.
31//!
32//! ### D. The Void Exception
33//! TypeScript allows `() => void` to match `() => T` for any T, because
34//! the caller promises to ignore the return value.
35//!
36//! ### E. Weak Type Detection (TS2559)
37//! Types with only optional properties require at least one common property
38//! with the source type to prevent accidental assignment mistakes.
39//!
40//! ### F. Nominality Overrides (The "Brand" Check)
41//!
42//! TypeScript is primarily structurally typed, but has specific exceptions where
43//! nominality is enforced. These are "escape hatches" from structural subtyping
44//! that prevent unsound or surprising assignments.
45//!
46//! #### F.1. Enum Nominality (TS2322)
47//! Enum members are nominally typed, not structurally.
48//!
49//! **Rule**: `EnumA.Member1` is NOT assignable to `EnumB.Member2` even if both
50//! have the same underlying value (e.g., both are `0`).
51//!
52//! **Implementation**:
53//! - Enum members are wrapped in `TypeData::Enum(def_id, literal_type)`
54//! - The `def_id` provides nominal identity (which enum)
55//! - The `literal_type` preserves the value (for assignability checks)
56//! - `enum_assignability_override` in `CompatChecker` enforces this rule
57//!
58//! **Examples**:
59//! ```typescript
60//! enum E { A = 0, B = 1 }
61//! enum F { A = 0, B = 1 }
62//!
63//! let x: E.A = E.B; // ❌ TS2322: different members
64//! let y: E.A = F.A; // ❌ TS2322: different enums
65//! let z: E.A = 0; // ✅ OK: numeric enum to number
66//! let w: number = E.A; // ✅ OK: numeric enum to number
67//! ```
68//!
69//! #### F.2. Private/Protected Brands (TS2322)
70//! Classes with private/protected members behave nominally, not structurally.
71//!
72//! **Rule**: Two classes with the same private member signature are NOT compatible
73//! unless they share the same declaration (or one extends the other).
74//!
75//! **Rationale**: Private members create a "brand" that distinguishes otherwise
76//! structurally identical types. This prevents accidentally mixing objects that
77//! happen to have the same shape but represent different concepts.
78//!
79//! **Implementation**:
80//! - `private_brand_assignability_override` in `CompatChecker`
81//! - Uses `SymbolId` comparison to verify private members originate from same declaration
82//! - Subclasses inherit the parent's private brand (are compatible)
83//! - Public members remain structural (do not create brands)
84//!
85//! **Examples**:
86//! ```typescript
87//! class A { private x: number = 1; }
88//! class B { private x: number = 1; }
89//!
90//! let a: A = new B(); // ❌ TS2322: separate private declarations
91//! let b: B = new A(); // ❌ TS2322: separate private declarations
92//!
93//! class C extends A {}
94//! let c: A = new C(); // ✅ OK: subclass inherits brand
95//! ```
96//!
97//! #### F.3. Constructor Accessibility (TS2673, TS2674)
98//! Classes with private/protected constructors cannot be instantiated from
99//! invalid scopes.
100//!
101//! **Rule**:
102//! - `private constructor()`: Only accessible within the class declaration
103//! - `protected constructor()`: Only accessible within the class or subclasses
104//! - `public constructor()` or no modifier: Accessible everywhere (default)
105//!
106//! **Implementation**:
107//! - `constructor_accessibility_override` in `CompatChecker`
108//! - Checks constructor symbol flags when assigning class type to constructable
109//! - Validates scope (inside class, subclass, or external)
110//!
111//! **Examples**:
112//! ```typescript
113//! class A { private constructor() {} }
114//! let a = new A(); // ❌ TS2673: private constructor
115//! A.staticCreate(); // ✅ OK: inside class
116//!
117//! class B { protected constructor() {} }
118//! class C extends B { constructor() { super(); } }
119//! let b = new B(); // ❌ TS2674: protected constructor
120//! let c = new C(); // ✅ OK: subclass access
121//! ```
122//!
123//! ### Why These Override The Judge
124//!
125//! The **Judge** (`SubtypeChecker`) implements sound, structural set theory semantics.
126//! It would correctly determine that `class A { private x }` and `class B { private x }`
127//! have the same shape and are structurally compatible.
128//!
129//! The **Lawyer** (`CompatChecker`) steps in and says "Wait, TypeScript says these
130//! are incompatible because of the private brand." This is TypeScript-specific
131//! legacy behavior that violates soundness principles for practical/ergonomic reasons.
132//!
133//! **Key Principle**: The Lawyer never makes types MORE compatible. It only
134//! makes them LESS compatible by adding restrictions on top of the Judge's
135//! structural analysis.
136//!
137//! The key principle is that `any` should NOT silence structural mismatches.
138//! While `any` is TypeScript's escape hatch, we still want to catch real errors
139//! even when `any` is involved.
140
141use crate::AnyPropagationMode;
142
143/// Rules for `any` propagation in type checking.
144///
145/// In TypeScript, `any` is both a top type (everything is assignable to `any`)
146/// and a bottom type (`any` is assignable to everything). This struct captures
147/// whether `any` is allowed to suppress nested structural mismatches by
148/// configuring the subtype engine's propagation mode.
149pub struct AnyPropagationRules {
150 /// Whether to allow `any` to silence structural mismatches.
151 /// When false, `any` is treated more strictly and structural errors
152 /// are still reported even when `any` is involved.
153 pub(crate) allow_any_suppression: bool,
154}
155
156impl AnyPropagationRules {
157 /// Create a new `AnyPropagationRules` with default settings.
158 ///
159 /// By default, `any` suppression is enabled for backward compatibility
160 /// with existing TypeScript behavior.
161 pub const fn new() -> Self {
162 Self {
163 allow_any_suppression: true,
164 }
165 }
166
167 /// Create strict `AnyPropagationRules` where `any` does not silence
168 /// structural mismatches.
169 ///
170 /// In strict mode, even when `any` is involved, the type checker will
171 /// perform structural checking and report mismatches.
172 pub const fn strict() -> Self {
173 Self {
174 allow_any_suppression: false,
175 }
176 }
177
178 /// Set whether `any` is allowed to suppress structural mismatches.
179 pub const fn set_allow_any_suppression(&mut self, allow: bool) {
180 self.allow_any_suppression = allow;
181 }
182
183 /// Return the propagation mode for `any` handling in the subtype engine.
184 pub const fn any_propagation_mode(&self) -> AnyPropagationMode {
185 if self.allow_any_suppression {
186 AnyPropagationMode::All
187 } else {
188 AnyPropagationMode::TopLevelOnly
189 }
190 }
191}
192
193impl Default for AnyPropagationRules {
194 fn default() -> Self {
195 Self::new()
196 }
197}
198
199#[cfg(test)]
200/// Summary of TypeScript quirks handled by the Lawyer layer.
201///
202/// Kept as a test-visible contract for parity regression coverage.
203pub struct TypeScriptQuirks;
204
205#[cfg(test)]
206impl TypeScriptQuirks {
207 /// List of TypeScript quirks handled by the Lawyer layer.
208 pub const QUIRKS: &'static [(&'static str, &'static str)] = &[
209 (
210 "any-propagation",
211 "any is both top and bottom type (assignable to/from everything)",
212 ),
213 (
214 "function-bivariance",
215 "Function parameters are bivariant in legacy mode",
216 ),
217 (
218 "method-bivariance",
219 "Methods are always bivariant regardless of strictFunctionTypes",
220 ),
221 ("void-return", "() => void accepts () => T for any T"),
222 (
223 "weak-types",
224 "Objects with only optional properties require common properties (TS2559)",
225 ),
226 (
227 "freshness",
228 "Object literals trigger excess property checking",
229 ),
230 (
231 "empty-object",
232 "{} accepts any non-nullish value including primitives",
233 ),
234 (
235 "null-undefined",
236 "null and undefined are assignable to everything without strictNullChecks",
237 ),
238 (
239 "bivariant-rest",
240 "Rest parameters of any/unknown are treated as bivariant",
241 ),
242 ];
243}
244
245#[cfg(test)]
246#[path = "../../tests/lawyer_tests.rs"]
247mod tests;