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}