Skip to main content

tsz_checker/error_reporter/
operator_errors.rs

1//! Binary operator error reporting (TS2362, TS2363, TS2365, TS2469).
2
3use crate::diagnostics::{Diagnostic, diagnostic_codes, diagnostic_messages, format_message};
4use crate::state::CheckerState;
5use tsz_parser::parser::NodeIndex;
6use tsz_solver::TypeId;
7
8impl<'a> CheckerState<'a> {
9    /// Report TS2506: Circular class inheritance (class C extends C).
10    pub(crate) fn error_circular_class_inheritance(
11        &mut self,
12        extends_expr_idx: NodeIndex,
13        class_idx: NodeIndex,
14    ) {
15        // Get the class name for the error message
16        let class_name = if let Some(class_node) = self.ctx.arena.get(class_idx)
17            && let Some(class) = self.ctx.arena.get_class(class_node)
18            && class.name.is_some()
19            && let Some(name_node) = self.ctx.arena.get(class.name)
20        {
21            self.ctx
22                .arena
23                .get_identifier(name_node)
24                .map(|id| id.escaped_text.clone())
25        } else {
26            None
27        };
28
29        let name = class_name.unwrap_or_else(|| String::from("<class>"));
30
31        let Some(loc) = self.get_source_location(extends_expr_idx) else {
32            return;
33        };
34
35        let message = format_message(
36            diagnostic_messages::IS_REFERENCED_DIRECTLY_OR_INDIRECTLY_IN_ITS_OWN_BASE_EXPRESSION,
37            &[&name],
38        );
39
40        self.ctx.error(
41            loc.start,
42            loc.length(),
43            message,
44            diagnostic_codes::IS_REFERENCED_DIRECTLY_OR_INDIRECTLY_IN_ITS_OWN_BASE_EXPRESSION,
45        );
46    }
47
48    /// Report TS2507: "Type 'X' is not a constructor function type"
49    /// This is for extends clauses where the base type isn't a constructor.
50    pub fn error_not_a_constructor_at(&mut self, type_id: TypeId, idx: NodeIndex) {
51        // Suppress error if type is ERROR/ANY/UNKNOWN - prevents cascading errors
52        if type_id == TypeId::ERROR || type_id == TypeId::ANY || type_id == TypeId::UNKNOWN {
53            return;
54        }
55
56        let Some(loc) = self.get_source_location(idx) else {
57            return;
58        };
59
60        let mut formatter = self.ctx.create_type_formatter();
61        let type_str = formatter.format(type_id);
62
63        let message =
64            diagnostic_messages::TYPE_IS_NOT_A_CONSTRUCTOR_FUNCTION_TYPE.replace("{0}", &type_str);
65
66        self.ctx.diagnostics.push(Diagnostic::error(
67            self.ctx.file_name.clone(),
68            loc.start,
69            loc.length(),
70            message,
71            diagnostic_codes::TYPE_IS_NOT_A_CONSTRUCTOR_FUNCTION_TYPE,
72        ));
73    }
74
75    /// Report TS2351: "This expression is not constructable. Type 'X' has no construct signatures."
76    /// This is for `new` expressions where the expression type has no construct signatures.
77    pub fn error_not_constructable_at(&mut self, type_id: TypeId, idx: NodeIndex) {
78        if type_id == TypeId::ERROR || type_id == TypeId::ANY || type_id == TypeId::UNKNOWN {
79            return;
80        }
81
82        let Some(loc) = self.get_source_location(idx) else {
83            return;
84        };
85
86        let mut formatter = self.ctx.create_type_formatter();
87        let type_str = formatter.format(type_id);
88
89        let message =
90            diagnostic_messages::THIS_EXPRESSION_IS_NOT_CONSTRUCTABLE.replace("{0}", &type_str);
91
92        self.ctx.diagnostics.push(Diagnostic::error(
93            self.ctx.file_name.clone(),
94            loc.start,
95            loc.length(),
96            message,
97            diagnostic_codes::THIS_EXPRESSION_IS_NOT_CONSTRUCTABLE,
98        ));
99    }
100
101    // =========================================================================
102    // Binary Operator Errors
103    // =========================================================================
104
105    /// Emits TS18050 for null/undefined operands in binary operations.
106    pub(crate) fn check_and_emit_nullish_binary_operands(
107        &mut self,
108        left_idx: NodeIndex,
109        right_idx: NodeIndex,
110        left_type: TypeId,
111        right_type: TypeId,
112        op: &str,
113    ) -> bool {
114        if left_type == TypeId::ERROR
115            || right_type == TypeId::ERROR
116            || left_type == TypeId::UNKNOWN
117            || right_type == TypeId::UNKNOWN
118        {
119            return false;
120        }
121
122        let left_is_nullish = left_type == TypeId::NULL || left_type == TypeId::UNDEFINED;
123        let right_is_nullish = right_type == TypeId::NULL || right_type == TypeId::UNDEFINED;
124        let mut emitted_nullish_error = false;
125
126        let should_emit_nullish_error = self.ctx.compiler_options.strict_null_checks
127            && matches!(
128                op,
129                "+" | "-"
130                    | "*"
131                    | "/"
132                    | "%"
133                    | "**"
134                    | "&"
135                    | "|"
136                    | "^"
137                    | "<<"
138                    | ">>"
139                    | ">>>"
140                    | "<"
141                    | ">"
142                    | "<="
143                    | ">="
144            );
145
146        if left_is_nullish && should_emit_nullish_error {
147            let value_name = if left_type == TypeId::NULL {
148                "null"
149            } else {
150                "undefined"
151            };
152            if let Some(loc) = self.get_source_location(left_idx) {
153                let message = format_message(
154                    diagnostic_messages::THE_VALUE_CANNOT_BE_USED_HERE,
155                    &[value_name],
156                );
157                self.ctx.diagnostics.push(Diagnostic::error(
158                    self.ctx.file_name.clone(),
159                    loc.start,
160                    loc.length(),
161                    message,
162                    diagnostic_codes::THE_VALUE_CANNOT_BE_USED_HERE,
163                ));
164                emitted_nullish_error = true;
165            }
166        }
167
168        if right_is_nullish && should_emit_nullish_error {
169            let value_name = if right_type == TypeId::NULL {
170                "null"
171            } else {
172                "undefined"
173            };
174            if let Some(loc) = self.get_source_location(right_idx) {
175                let message = format_message(
176                    diagnostic_messages::THE_VALUE_CANNOT_BE_USED_HERE,
177                    &[value_name],
178                );
179                self.ctx.diagnostics.push(Diagnostic::error(
180                    self.ctx.file_name.clone(),
181                    loc.start,
182                    loc.length(),
183                    message,
184                    diagnostic_codes::THE_VALUE_CANNOT_BE_USED_HERE,
185                ));
186                emitted_nullish_error = true;
187            }
188        }
189
190        emitted_nullish_error
191    }
192
193    /// Emit errors for binary operator type mismatches.
194    /// TS2362 for left-hand side, TS2363 for right-hand side, or TS2365 for general operator errors.
195    pub(crate) fn emit_binary_operator_error(
196        &mut self,
197        node_idx: NodeIndex,
198        left_idx: NodeIndex,
199        right_idx: NodeIndex,
200        left_type: TypeId,
201        right_type: TypeId,
202        op: &str,
203        emitted_nullish_error: bool,
204    ) {
205        // Suppress cascade errors from unresolved types
206        if left_type == TypeId::ERROR
207            || right_type == TypeId::ERROR
208            || left_type == TypeId::UNKNOWN
209            || right_type == TypeId::UNKNOWN
210        {
211            return;
212        }
213
214        // Track nullish operands for proper error reporting
215        let left_is_nullish = left_type == TypeId::NULL || left_type == TypeId::UNDEFINED;
216        let right_is_nullish = right_type == TypeId::NULL || right_type == TypeId::UNDEFINED;
217
218        // TS18050 is emitted for null/undefined operands in arithmetic, bitwise, AND `+` operations.
219        // tsc emits TS18050 per-operand for each null/undefined value, not TS2365 for the expression.
220        // Relational operators (<, >, <=, >=) also emit TS18050, but only for literal null/undefined.
221        // For now, we only handle arithmetic/bitwise/+ since our evaluator doesn't distinguish
222        // literal values from variables typed as null/undefined.
223        // TS18050 only applies under strictNullChecks — without it, null/undefined are in
224        // every type's domain and should not trigger this error.
225        let should_emit_nullish_error = self.ctx.compiler_options.strict_null_checks
226            && matches!(
227                op,
228                "+" | "-"
229                    | "*"
230                    | "/"
231                    | "%"
232                    | "**"
233                    | "&"
234                    | "|"
235                    | "^"
236                    | "<<"
237                    | ">>"
238                    | ">>>"
239                    | "<"
240                    | ">"
241                    | "<="
242                    | ">="
243            );
244
245        use tsz_solver::BinaryOpEvaluator;
246
247        let evaluator = BinaryOpEvaluator::new(self.ctx.types);
248
249        // TS2469: Check if either operand is a symbol type
250        // TS2469 is emitted when an operator cannot be applied to type 'symbol'
251        // We check both operands and emit TS2469 for the symbol operand(s)
252        let left_is_symbol = evaluator.is_symbol_like(left_type);
253        let right_is_symbol = evaluator.is_symbol_like(right_type);
254
255        if left_is_symbol || right_is_symbol {
256            // Format type strings first to avoid holding formatter across mutable borrows
257            let left_type_str =
258                left_is_symbol.then(|| self.ctx.create_type_formatter().format(left_type));
259            let right_type_str =
260                right_is_symbol.then(|| self.ctx.create_type_formatter().format(right_type));
261
262            // Emit TS2469 for symbol operands
263            if let (Some(loc), Some(type_str)) =
264                (self.get_source_location(left_idx), left_type_str.as_deref())
265            {
266                let message = format_message(
267                    diagnostic_messages::OPERATOR_CANNOT_BE_APPLIED_TO_TYPE,
268                    &[op, type_str],
269                );
270                self.ctx.diagnostics.push(Diagnostic::error(
271                    self.ctx.file_name.clone(),
272                    loc.start,
273                    loc.length(),
274                    message,
275                    diagnostic_codes::OPERATOR_CANNOT_BE_APPLIED_TO_TYPE,
276                ));
277            }
278
279            if let (Some(loc), Some(type_str)) = (
280                self.get_source_location(right_idx),
281                right_type_str.as_deref(),
282            ) {
283                let message = format_message(
284                    diagnostic_messages::OPERATOR_CANNOT_BE_APPLIED_TO_TYPE,
285                    &[op, type_str],
286                );
287                self.ctx.diagnostics.push(Diagnostic::error(
288                    self.ctx.file_name.clone(),
289                    loc.start,
290                    loc.length(),
291                    message,
292                    diagnostic_codes::OPERATOR_CANNOT_BE_APPLIED_TO_TYPE,
293                ));
294            }
295
296            // If both are symbols, we're done (no need for TS2365)
297            if left_is_symbol && right_is_symbol {
298                return;
299            }
300
301            // If only one is symbol, continue to check the other operand
302            // (but we've already emitted TS2469 for the symbol)
303        }
304
305        let mut formatter = self.ctx.create_type_formatter();
306        let left_str = formatter.format(left_type);
307        let right_str = formatter.format(right_type);
308
309        // Check if this is an arithmetic or bitwise operator
310        // These operators require integer operands and emit TS2362/TS2363
311        // Note: + is handled separately - it can be string concatenation or arithmetic
312        let is_arithmetic = matches!(op, "-" | "*" | "/" | "%" | "**");
313        let is_bitwise = matches!(op, "&" | "|" | "^" | "<<" | ">>" | ">>>");
314        let requires_numeric_operands = is_arithmetic || is_bitwise;
315
316        // TS2447: For &, |, ^ with both boolean operands, emit special error
317        // This must be checked before TS2362/TS2363 because boolean is not a valid arithmetic operand
318        if is_bitwise {
319            let left_is_boolean = evaluator.is_boolean_like(left_type);
320            let right_is_boolean = evaluator.is_boolean_like(right_type);
321            let is_boolean_bitwise =
322                matches!(op, "&" | "|" | "^") && left_is_boolean && right_is_boolean;
323
324            if is_boolean_bitwise {
325                let suggestion = if op == "&" {
326                    "&&"
327                } else if op == "|" {
328                    "||"
329                } else {
330                    "!=="
331                };
332                if let Some(loc) = self.get_source_location(node_idx) {
333                    let message = format!(
334                        "The '{op}' operator is not allowed for boolean types. Consider using '{suggestion}' instead."
335                    );
336                    self.ctx.diagnostics.push(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::THE_OPERATOR_IS_NOT_ALLOWED_FOR_BOOLEAN_TYPES_CONSIDER_USING_INSTEAD));
337                }
338                return;
339            }
340        }
341
342        // Evaluate types to resolve unevaluated conditional/mapped types before checking.
343        // e.g., DeepPartial<number> | number → number
344        let eval_left = self.evaluate_type_for_binary_ops(left_type);
345        let eval_right = self.evaluate_type_for_binary_ops(right_type);
346
347        // Check if operands have valid arithmetic types using BinaryOpEvaluator
348        // This properly handles number, bigint, any, and enum types (unions of number literals)
349        // Note: evaluator was already created above for symbol checking
350        // Skip arithmetic checks for symbol operands (we already emitted TS2469)
351        // When strictNullChecks is off, null/undefined are implicitly assignable to
352        // number, so they should not trigger arithmetic errors.
353        let snc_off = !self.ctx.compiler_options.strict_null_checks;
354        let left_is_valid_arithmetic = !left_is_symbol
355            && (evaluator.is_arithmetic_operand(eval_left)
356                || (snc_off && (eval_left == TypeId::NULL || eval_left == TypeId::UNDEFINED)));
357        let right_is_valid_arithmetic = !right_is_symbol
358            && (evaluator.is_arithmetic_operand(eval_right)
359                || (snc_off && (eval_right == TypeId::NULL || eval_right == TypeId::UNDEFINED)));
360
361        // For + operator, TSC emits TS2365 ("Operator '+' cannot be applied to types"),
362        // never TS2362/TS2363. But if null/undefined operands already got TS18050,
363        // don't also emit TS2365 - tsc only emits the per-operand TS18050 errors.
364        if op == "+" {
365            if !emitted_nullish_error && let Some(loc) = self.get_source_location(node_idx) {
366                let message = format!(
367                    "Operator '{op}' cannot be applied to types '{left_str}' and '{right_str}'."
368                );
369                self.ctx.diagnostics.push(Diagnostic::error(
370                    self.ctx.file_name.clone(),
371                    loc.start,
372                    loc.length(),
373                    message,
374                    diagnostic_codes::OPERATOR_CANNOT_BE_APPLIED_TO_TYPES_AND,
375                ));
376            }
377            return;
378        }
379
380        if requires_numeric_operands {
381            // For arithmetic and bitwise operators, emit specific left/right errors (TS2362, TS2363)
382            // Skip operands that already got TS18050 (null/undefined with strictNullChecks)
383            // tsc suppresses TS2362/TS2363 when TS18050 was already emitted for the operand.
384            let mut emitted_specific_error = emitted_nullish_error;
385            if !(left_is_valid_arithmetic || left_is_nullish && should_emit_nullish_error)
386                && let Some(loc) = self.get_source_location(left_idx)
387            {
388                let message = "The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.".to_string();
389                self.ctx.diagnostics.push(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::THE_LEFT_HAND_SIDE_OF_AN_ARITHMETIC_OPERATION_MUST_BE_OF_TYPE_ANY_NUMBER_BIGINT));
390                emitted_specific_error = true;
391            }
392            if !(right_is_valid_arithmetic || right_is_nullish && should_emit_nullish_error)
393                && let Some(loc) = self.get_source_location(right_idx)
394            {
395                let message = "The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.".to_string();
396                self.ctx.diagnostics.push(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::THE_RIGHT_HAND_SIDE_OF_AN_ARITHMETIC_OPERATION_MUST_BE_OF_TYPE_ANY_NUMBER_BIGINT));
397                emitted_specific_error = true;
398            }
399            // If both operands are valid arithmetic types but the operation still failed
400            // (e.g., mixing number and bigint), emit TS2365
401            if !emitted_specific_error && let Some(loc) = self.get_source_location(node_idx) {
402                let message = format!(
403                    "Operator '{op}' cannot be applied to types '{left_str}' and '{right_str}'."
404                );
405                self.ctx.diagnostics.push(Diagnostic::error(
406                    self.ctx.file_name.clone(),
407                    loc.start,
408                    loc.length(),
409                    message,
410                    diagnostic_codes::OPERATOR_CANNOT_BE_APPLIED_TO_TYPES_AND,
411                ));
412            }
413            return;
414        }
415
416        // Handle relational operators: <, >, <=, >=
417        // These require both operands to be comparable. When types have no relationship,
418        // emit TS2365: "Operator '<' cannot be applied to types 'X' and 'Y'."
419        let is_relational = matches!(op, "<" | ">" | "<=" | ">=");
420        if is_relational
421            && !emitted_nullish_error
422            && let Some(loc) = self.get_source_location(node_idx)
423        {
424            let message = format!(
425                "Operator '{op}' cannot be applied to types '{left_str}' and '{right_str}'."
426            );
427            self.ctx.diagnostics.push(Diagnostic::error(
428                self.ctx.file_name.clone(),
429                loc.start,
430                loc.length(),
431                message,
432                diagnostic_codes::OPERATOR_CANNOT_BE_APPLIED_TO_TYPES_AND,
433            ));
434        }
435    }
436}