Skip to main content

tsz_checker/checkers/
enum_checker.rs

1//! Enum type checking (detection, member types, assignability, const enums).
2//!
3//! This module extends `CheckerState` with utilities for enum-related
4//! type checking operations.
5
6use crate::state::CheckerState;
7use tsz_binder::symbol_flags;
8use tsz_solver::TypeId;
9
10// =============================================================================
11// Enum Type Checking Utilities
12// =============================================================================
13
14impl<'a> CheckerState<'a> {
15    // =========================================================================
16    // Enum Type Detection
17    // =========================================================================
18
19    /// Check if a type is an enum type.
20    ///
21    /// Returns true if the type represents a TypeScript enum.
22    pub fn is_enum_type(&self, type_id: TypeId) -> bool {
23        // Use resolve_type_to_symbol_id which handles both Lazy and Enum variants
24        if let Some(sym_id) = self.ctx.resolve_type_to_symbol_id(type_id)
25            && let Some(symbol) = self.ctx.binder.get_symbol(sym_id)
26        {
27            return (symbol.flags & symbol_flags::ENUM) != 0;
28        }
29        false
30    }
31
32    /// Check if a type is a const enum type.
33    ///
34    /// Const enums are fully inlined and cannot be accessed at runtime.
35    pub fn is_const_enum_type(&self, type_id: TypeId) -> bool {
36        // Use resolve_type_to_symbol_id which handles both Lazy and Enum variants
37        if let Some(sym_id) = self.ctx.resolve_type_to_symbol_id(type_id)
38            && let Some(symbol) = self.ctx.binder.get_symbol(sym_id)
39        {
40            return (symbol.flags & symbol_flags::ENUM) != 0
41                && (symbol.flags & symbol_flags::CONST_ENUM) != 0;
42        }
43        false
44    }
45
46    /// Check if a type is a regular (non-const) enum type.
47    pub fn is_regular_enum_type(&self, type_id: TypeId) -> bool {
48        self.is_enum_type(type_id) && !self.is_const_enum_type(type_id)
49    }
50
51    // =========================================================================
52    // Enum Member Type Utilities
53    // =========================================================================
54
55    /// Check if a type could be an enum member type.
56    ///
57    /// Enum members can be:
58    /// - String literals (for string enums)
59    /// - Numeric literals (for numeric enums)
60    /// - Computed values (for heterogeneous enums)
61    pub fn is_enum_member_type(&self, type_id: TypeId) -> bool {
62        use tsz_solver::type_queries::LiteralTypeKind;
63
64        // Check for string or number literals
65        match tsz_solver::type_queries::classify_literal_type(self.ctx.types, type_id) {
66            LiteralTypeKind::String(_) | LiteralTypeKind::Number(_) => return true,
67            _ => {}
68        }
69
70        // Check for union type
71        if tsz_solver::type_queries::is_union_type(self.ctx.types, type_id) {
72            return true;
73        }
74
75        // Check for primitive types
76        type_id == TypeId::STRING || type_id == TypeId::NUMBER
77    }
78
79    /// Get the base member type for an enum (string or number).
80    ///
81    /// Returns:
82    /// - STRING for string enums
83    /// - NUMBER for numeric enums
84    /// - UNION for heterogeneous enums
85    /// - UNKNOWN if the enum kind cannot be determined
86    pub fn get_enum_member_base_type(&self, type_id: TypeId) -> TypeId {
87        // If this is already a primitive type, return it
88        if type_id == TypeId::STRING || type_id == TypeId::NUMBER {
89            return type_id;
90        }
91
92        // Check if it's a union - indicates heterogeneous enum
93        if tsz_solver::type_queries::is_union_type(self.ctx.types, type_id) {
94            return type_id; // Return the union as-is
95        }
96
97        // Check for enum types (both Lazy and Enum variants)
98        if self.ctx.resolve_type_to_symbol_id(type_id).is_some() {
99            // For enum types, we need to check the member types
100            // Default to STRING for string enums, NUMBER for numeric enums
101            return TypeId::UNKNOWN;
102        }
103
104        TypeId::UNKNOWN
105    }
106
107    // =========================================================================
108    // Enum Assignability
109    // =========================================================================
110
111    /// Check if enum member types are compatible.
112    ///
113    /// TypeScript allows enum members to be compared if they are from
114    /// compatible enum types (string enum members are string-compatible,
115    /// number enum members are number-compatible).
116    pub fn enum_members_compatible(&self, enum_type1: TypeId, enum_type2: TypeId) -> bool {
117        // If both are the same enum type, they're compatible
118        if enum_type1 == enum_type2 {
119            return true;
120        }
121
122        // Check if both are string enums or both are number enums
123        let base1 = self.get_enum_member_base_type(enum_type1);
124        let base2 = self.get_enum_member_base_type(enum_type2);
125
126        (base1 == TypeId::STRING && base2 == TypeId::STRING)
127            || (base1 == TypeId::NUMBER && base2 == TypeId::NUMBER)
128    }
129
130    /// Check if an enum type is assignable to another type.
131    ///
132    /// Enums are assignable to:
133    /// - Their exact enum type
134    /// - The primitive type (string/number) for literal enum members
135    pub fn is_enum_assignable_to(&mut self, enum_type: TypeId, target_type: TypeId) -> bool {
136        // Exact type match
137        if enum_type == target_type {
138            return true;
139        }
140
141        // Check if target is the base primitive type
142        let base_type = self.get_enum_member_base_type(enum_type);
143        if base_type == target_type {
144            return true;
145        }
146
147        // Use subtype checking for more complex cases
148        self.is_assignable_to(enum_type, target_type)
149    }
150    // =========================================================================
151    // Enum Expression Utilities
152    // =========================================================================
153
154    /// Check if an enum access is allowed (not a const enum).
155    ///
156    /// Const enums cannot be accessed as values at runtime - they are fully inlined.
157    /// This check prevents runtime errors from accessing const enum members.
158    pub fn is_enum_access_allowed(&self, enum_type: TypeId) -> bool {
159        !self.is_const_enum_type(enum_type)
160    }
161
162    /// Get the type of an enum member access.
163    ///
164    /// For enum members, this returns the literal type of the member
165    /// (e.g., "Red" for `Color.Red` in a string enum).
166    pub fn get_enum_member_access_type(&self, enum_type: TypeId) -> TypeId {
167        self.get_enum_member_base_type(enum_type)
168    }
169
170    // =========================================================================
171    // Boxed Primitive Type Detection
172    // =========================================================================
173
174    /// Check if a type is a boxed primitive type (Number, String, Boolean, `BigInt`, Symbol).
175    ///
176    /// TypeScript has two representations for primitives:
177    /// - `number`, `string`, `boolean` - primitive types (valid for arithmetic)
178    /// - `Number`, `String`, `Boolean` - interface wrapper types from lib.d.ts (NOT valid for arithmetic)
179    ///
180    /// This method detects the boxed interface types to emit proper TS2362/TS2363/TS2365 errors.
181    pub fn is_boxed_primitive_type(&self, type_id: TypeId) -> bool {
182        // Get the symbol associated with this type
183        let sym_id = match self.ctx.resolve_type_to_symbol_id(type_id) {
184            Some(sym_id) => sym_id,
185            None => return false,
186        };
187
188        // Get the symbol
189        let symbol = match self.ctx.binder.get_symbol(sym_id) {
190            Some(symbol) => symbol,
191            None => return false,
192        };
193
194        // Check if it's an interface (not a class or type alias)
195        if (symbol.flags & symbol_flags::INTERFACE) == 0 {
196            return false;
197        }
198
199        // Get the symbol name
200        let name = &symbol.escaped_name;
201
202        // Check if it's one of the known boxed primitive types
203        matches!(
204            name.as_str(),
205            "Number" | "String" | "Boolean" | "BigInt" | "Symbol"
206        )
207    }
208}