tsz-checker 0.1.9

TypeScript type checker for the tsz compiler
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
//! Overload compatibility, signature utilities, and implicit-any return checks.
//!
//! Extracted from `ambient_signature_checks.rs` to keep files focused and under the
//! 2000 LOC limit. Contains:
//! - `lower_type_with_bindings` — type lowering with type parameter bindings
//! - `maybe_report_implicit_any_return` — TS7010/TS7011 implicit-any return diagnostics
//! - `check_overload_compatibility` — TS2394 overload-implementation compatibility
//! - `check_modifier_combinations` — modifier conflict checks (e.g., abstract + private)

use crate::query_boundaries::assignability::{
    get_function_return_type, replace_function_return_type, rewrite_function_error_slots_to_any,
};
use crate::state::CheckerState;
use tsz_parser::parser::NodeIndex;
use tsz_parser::parser::syntax_kind_ext;
use tsz_solver::TypeId;

impl<'a> CheckerState<'a> {
    /// Lower a type node with type parameter bindings.
    ///
    /// This is used to substitute type parameters with concrete types
    /// when extracting type arguments from generic Promise types.
    /// Made pub(crate) so it can be called from `promise_checker.rs`.
    pub(crate) fn lower_type_with_bindings(
        &self,
        type_node: NodeIndex,
        bindings: Vec<(tsz_common::interner::Atom, TypeId)>,
    ) -> TypeId {
        use tsz_lowering::TypeLowering;

        let type_resolver = |node_idx: NodeIndex| self.resolve_type_symbol_for_lowering(node_idx);
        let value_resolver = |node_idx: NodeIndex| self.resolve_value_symbol_for_lowering(node_idx);
        let lowering = TypeLowering::with_resolvers(
            self.ctx.arena,
            self.ctx.types,
            &type_resolver,
            &value_resolver,
        )
        .with_type_param_bindings(bindings);
        lowering.lower_type(type_node)
    }

    // Note: type_contains_any, implicit_any_return_display, should_report_implicit_any_return are in type_checking.rs

    pub(crate) fn maybe_report_implicit_any_return(
        &mut self,
        name: Option<String>,
        name_node: Option<NodeIndex>,
        return_type: TypeId,
        has_type_annotation: bool,
        has_contextual_return: bool,
        fallback_node: NodeIndex,
    ) {
        use crate::diagnostics::diagnostic_codes;

        if !self.ctx.no_implicit_any() || has_type_annotation || has_contextual_return {
            return;
        }

        // In checkJs mode, be conservative and skip implicit-any return diagnostics in JS files.
        if self.is_js_file() {
            return;
        }
        // TypeScript does not report TS7010/TS7011 when all value-return paths use
        // an explicit `as any`/`<any>` assertion.
        if let Some(node) = self.ctx.arena.get(fallback_node) {
            let body = if let Some(func) = self.ctx.arena.get_function(node) {
                Some(func.body)
            } else if let Some(method) = self.ctx.arena.get_method_decl(node) {
                Some(method.body)
            } else {
                self.ctx
                    .arena
                    .get_accessor(node)
                    .map(|accessor| accessor.body)
            };
            if let Some(body_idx) = body
                && body_idx.is_some()
            {
                if self.has_only_explicit_any_assertion_returns(body_idx) {
                    return;
                }
                // When the function has a body, the return type was inferred from it.
                // An inferred `any` (e.g., `return x` where `x: any`) is a valid inference
                // result, not "implicit any". TSC only emits TS7010 for bodyless
                // declarations (interfaces, abstract methods) where `any` is the default.
                if return_type == TypeId::ANY {
                    return;
                }
            }
        }
        if !self.should_report_implicit_any_return(return_type) {
            return;
        }

        // tsc suppresses the function-expression TS7011 in common cases where the
        // same closure already has implicit-any parameter errors (TS7006/TS7019).
        // Avoid double-reporting for unnamed function expressions/arrow functions.
        if name.is_none() && self.has_untyped_value_parameters(fallback_node) {
            return;
        }

        let return_text = self.implicit_any_return_display(return_type);
        if let Some(name) = name {
            self.error_at_node_msg(
                name_node.unwrap_or(fallback_node),
                diagnostic_codes::WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN_TYPE,
                &[&name, &return_text],
            );
        } else {
            self.error_at_node_msg(
                fallback_node,
                diagnostic_codes::FUNCTION_EXPRESSION_WHICH_LACKS_RETURN_TYPE_ANNOTATION_IMPLICITLY_HAS_AN_RETURN,
                &[&return_text],
            );
        }
    }

    pub(crate) fn has_untyped_value_parameters(&self, node_idx: NodeIndex) -> bool {
        let Some(node) = self.ctx.arena.get(node_idx) else {
            return false;
        };

        let has_untyped = |param_idx: NodeIndex| {
            let Some(param_node) = self.ctx.arena.get(param_idx) else {
                return false;
            };
            let Some(param) = self.ctx.arena.get_parameter(param_node) else {
                return false;
            };
            if param.type_annotation.is_some() {
                return false;
            }
            let name = self.parameter_name_for_error(param.name);
            if name.is_empty() {
                return true;
            }
            name != "this"
        };

        if let Some(func) = self.ctx.arena.get_function(node) {
            return func.parameters.nodes.iter().copied().any(has_untyped);
        }
        if let Some(method) = self.ctx.arena.get_method_decl(node) {
            return method.parameters.nodes.iter().copied().any(has_untyped);
        }
        if let Some(sig) = self.ctx.arena.get_signature(node)
            && let Some(params) = sig.parameters.as_ref()
        {
            return params.nodes.iter().copied().any(has_untyped);
        }

        false
    }

    /// Check overload compatibility: implementation must be assignable to all overload signatures.
    ///
    /// Reports TS2394 when an implementation signature is not compatible with its overload signatures.
    /// This check ensures that the implementation can handle all valid calls that match the overloads.
    ///
    /// Per TypeScript's variance rules:
    /// - Implementation parameters must be supertypes of overload parameters (contravariant)
    /// - Implementation return type must be subtype of overload return type (covariant)
    /// - Effectively: Implementation <: Overload (implementation is assignable to overload)
    ///
    /// This handles:
    /// - Function declarations
    /// - Method declarations (class methods)
    /// - Constructor declarations
    pub(crate) fn check_overload_compatibility(&mut self, impl_node_idx: NodeIndex) {
        use crate::diagnostics::{diagnostic_codes, diagnostic_messages};

        // 1. Get the implementation's symbol
        let Some(impl_sym_id) = self.ctx.binder.get_node_symbol(impl_node_idx) else {
            return;
        };

        let Some(symbol) = self.ctx.binder.get_symbol(impl_sym_id) else {
            return;
        };

        // Fast path: if there are no overload declarations for this symbol,
        // skip expensive signature lowering/compatibility setup entirely.
        let has_overload_decl = symbol.declarations.iter().copied().any(|decl_idx| {
            if decl_idx == impl_node_idx {
                return false;
            }

            let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
                return false;
            };

            match decl_node.kind {
                k if k == syntax_kind_ext::FUNCTION_DECLARATION => self
                    .ctx
                    .arena
                    .get_function(decl_node)
                    .is_some_and(|f| f.body.is_none()),
                k if k == syntax_kind_ext::METHOD_DECLARATION => self
                    .ctx
                    .arena
                    .get_method_decl(decl_node)
                    .is_some_and(|m| m.body.is_none()),
                k if k == syntax_kind_ext::CONSTRUCTOR => self
                    .ctx
                    .arena
                    .get_constructor(decl_node)
                    .is_some_and(|c| c.body.is_none()),
                _ => false,
            }
        });
        if !has_overload_decl {
            return;
        }

        // 2. Create TypeLowering instance for manual signature lowering
        // This unblocks overload validation for methods/constructors where get_type_of_node returns ERROR
        let type_resolver = |node_idx: NodeIndex| -> Option<u32> {
            self.ctx.binder.get_node_symbol(node_idx).map(|id| id.0)
        };
        let value_resolver = |node_idx: NodeIndex| -> Option<u32> {
            self.ctx.binder.get_node_symbol(node_idx).map(|id| id.0)
        };
        let lowering = tsz_lowering::TypeLowering::with_resolvers(
            self.ctx.arena,
            self.ctx.types,
            &type_resolver,
            &value_resolver,
        );

        // 3. Get the implementation's type using manual lowering
        // When the implementation has no return type annotation, lower_return_type returns ERROR.
        // Use ANY as the return type override to avoid false TS2394 errors, since `any` is
        // assignable to any return type (matching TypeScript's behavior for untyped implementations).
        let impl_return_override = self.get_impl_return_type_override(impl_node_idx);
        let mut impl_type =
            lowering.lower_signature_from_declaration(impl_node_idx, impl_return_override);
        if impl_type == tsz_solver::TypeId::ERROR {
            // Fall back to get_type_of_node for cases where manual lowering fails
            impl_type = self.get_type_of_node(impl_node_idx);
            if impl_type == tsz_solver::TypeId::ERROR {
                return;
            }
        }

        // Fix up ERROR parameter types in the implementation signature.
        // When implementation params lack type annotations, lowering produces ERROR.
        // Replace with ANY since TypeScript treats untyped impl params as `any`.
        impl_type = self.fix_error_params_in_function(impl_type);

        // 4. Check each overload declaration
        for &decl_idx in &symbol.declarations {
            // Skip the implementation itself
            if decl_idx == impl_node_idx {
                continue;
            }

            let Some(decl_node) = self.ctx.arena.get(decl_idx) else {
                continue;
            };

            // 5. Check if this declaration is an overload (has no body)
            // We must handle Functions, Methods, and Constructors
            let is_overload = match decl_node.kind {
                k if k == syntax_kind_ext::FUNCTION_DECLARATION => self
                    .ctx
                    .arena
                    .get_function(decl_node)
                    .is_some_and(|f| f.body.is_none()),
                k if k == syntax_kind_ext::METHOD_DECLARATION => self
                    .ctx
                    .arena
                    .get_method_decl(decl_node)
                    .is_some_and(|m| m.body.is_none()),
                k if k == syntax_kind_ext::CONSTRUCTOR => self
                    .ctx
                    .arena
                    .get_constructor(decl_node)
                    .is_some_and(|c| c.body.is_none()),
                _ => false, // Not a callable declaration we care about
            };

            if !is_overload {
                continue;
            }

            // 6. Get the overload's type using manual lowering
            // For overloads without return type annotations, use VOID (matching tsc behavior).
            let overload_return_override = self.get_overload_return_type_override(decl_idx);
            let mut overload_type =
                lowering.lower_signature_from_declaration(decl_idx, overload_return_override);
            if overload_type == tsz_solver::TypeId::ERROR {
                // Fall back to get_type_of_node for cases where manual lowering fails
                overload_type = self.get_type_of_node(decl_idx);
                if overload_type == tsz_solver::TypeId::ERROR {
                    continue;
                }
            }
            // Fix ERROR param types in overload (untyped params → any)
            overload_type = self.fix_error_params_in_function(overload_type);

            // 7. Check compatibility using tsc's bidirectional return type rule:
            // First check if return types are compatible in EITHER direction,
            // then check parameter-only assignability (ignoring return types).
            // This matches tsc's isImplementationCompatibleWithOverload.
            if !self.is_implementation_compatible_with_overload(impl_type, overload_type) {
                // TSC anchors the error at the function/method name, not the whole declaration.
                let error_node = self.get_declaration_name_node(decl_idx).unwrap_or(decl_idx);
                self.error_at_node(
                    error_node,
                    diagnostic_messages::THIS_OVERLOAD_SIGNATURE_IS_NOT_COMPATIBLE_WITH_ITS_IMPLEMENTATION_SIGNATURE,
                    diagnostic_codes::THIS_OVERLOAD_SIGNATURE_IS_NOT_COMPATIBLE_WITH_ITS_IMPLEMENTATION_SIGNATURE,
                );
            }
        }
    }

    /// Returns `Some(TypeId::ANY)` if the implementation node has no explicit return type annotation.
    /// Replace ERROR parameter types with ANY in a function type.
    /// Used for overload compatibility: untyped implementation params are treated as `any`.
    pub(crate) fn fix_error_params_in_function(
        &mut self,
        type_id: tsz_solver::TypeId,
    ) -> tsz_solver::TypeId {
        rewrite_function_error_slots_to_any(self.ctx.types, type_id)
    }

    /// This is used for overload compatibility checking: when the implementation omits a return type,
    /// the lowering would produce ERROR, but TypeScript treats it as `any` for compatibility purposes.
    pub(crate) fn get_impl_return_type_override(
        &self,
        node_idx: NodeIndex,
    ) -> Option<tsz_solver::TypeId> {
        let node = self.ctx.arena.get(node_idx)?;
        let has_annotation = match node.kind {
            k if k == syntax_kind_ext::FUNCTION_DECLARATION => self
                .ctx
                .arena
                .get_function(node)
                .is_some_and(|f| f.type_annotation.is_some()),
            k if k == syntax_kind_ext::METHOD_DECLARATION => self
                .ctx
                .arena
                .get_method_decl(node)
                .is_some_and(|m| m.type_annotation.is_some()),
            k if k == syntax_kind_ext::CONSTRUCTOR => {
                // Constructors never have return type annotations
                return None;
            }
            _ => return None,
        };
        if has_annotation {
            None
        } else {
            Some(tsz_solver::TypeId::ANY)
        }
    }

    /// Returns `Some(TypeId::VOID)` if an overload node has no explicit return type annotation.
    /// Overloads without return type annotations default to void (matching tsc behavior).
    pub(crate) fn get_overload_return_type_override(
        &self,
        node_idx: NodeIndex,
    ) -> Option<tsz_solver::TypeId> {
        let node = self.ctx.arena.get(node_idx)?;
        let has_annotation = match node.kind {
            k if k == syntax_kind_ext::FUNCTION_DECLARATION => self
                .ctx
                .arena
                .get_function(node)
                .is_some_and(|f| f.type_annotation.is_some()),
            k if k == syntax_kind_ext::METHOD_DECLARATION => self
                .ctx
                .arena
                .get_method_decl(node)
                .is_some_and(|m| m.type_annotation.is_some()),
            k if k == syntax_kind_ext::CONSTRUCTOR => {
                return None;
            }
            _ => return None,
        };
        if has_annotation {
            None
        } else {
            Some(tsz_solver::TypeId::VOID)
        }
    }

    /// Check overload compatibility using tsc's bidirectional return type rule.
    /// Matches tsc's `isImplementationCompatibleWithOverload`:
    /// 1. Check if return types are compatible in EITHER direction (or target is void)
    /// 2. If so, check parameter-only assignability (with return types ignored)
    ///
    /// Uses bivariant assignability because tsc uses non-strict function types
    /// for overload compatibility (implementation params can be wider or narrower).
    pub(crate) fn is_implementation_compatible_with_overload(
        &mut self,
        impl_type: tsz_solver::TypeId,
        overload_type: tsz_solver::TypeId,
    ) -> bool {
        // Get return types of both signatures
        let impl_return = get_function_return_type(self.ctx.types, impl_type);
        let overload_return = get_function_return_type(self.ctx.types, overload_type);

        match (impl_return, overload_return) {
            (Some(impl_ret), Some(overload_ret)) => {
                // Bidirectional return type check: either direction must be assignable,
                // or the overload returns void
                let return_compatible = overload_ret == tsz_solver::TypeId::VOID
                    || self.is_assignable_to_bivariant(overload_ret, impl_ret)
                    || self.is_assignable_to_bivariant(impl_ret, overload_ret);

                if !return_compatible {
                    return false;
                }

                // Now check parameter-only compatibility by creating versions
                // with ANY return types. Use bivariant check to match tsc's
                // non-strict function types for overload compatibility.
                let impl_with_any_ret =
                    self.replace_return_type(impl_type, tsz_solver::TypeId::ANY);
                let overload_with_any_ret =
                    self.replace_return_type(overload_type, tsz_solver::TypeId::ANY);
                self.is_assignable_to_bivariant(impl_with_any_ret, overload_with_any_ret)
            }
            _ => {
                // If we can't get return types, fall back to bivariant assignability
                self.is_assignable_to_bivariant(impl_type, overload_type)
            }
        }
    }

    /// Replace the return type of a function type with the given type.
    /// Returns the original type unchanged if it's not a Function.
    pub(crate) fn replace_return_type(
        &mut self,
        type_id: tsz_solver::TypeId,
        new_return: tsz_solver::TypeId,
    ) -> tsz_solver::TypeId {
        replace_function_return_type(self.ctx.types, type_id, new_return)
    }

    pub(crate) fn check_modifier_combinations(
        &mut self,
        modifiers: &Option<tsz_parser::parser::NodeList>,
    ) {
        let Some(mods) = modifiers else {
            return;
        };

        let mut abstract_node = None;
        let mut conflicting_nodes = Vec::new();

        for &m_idx in &mods.nodes {
            if let Some(m_node) = self.ctx.arena.get(m_idx) {
                let kind = m_node.kind;
                use tsz_scanner::SyntaxKind;
                if kind == SyntaxKind::AbstractKeyword as u16 {
                    abstract_node = Some(m_idx);
                } else if kind == SyntaxKind::PrivateKeyword as u16 {
                    conflicting_nodes.push((m_idx, "private"));
                } else if kind == SyntaxKind::StaticKeyword as u16 {
                    conflicting_nodes.push((m_idx, "static"));
                } else if kind == SyntaxKind::AsyncKeyword as u16 {
                    conflicting_nodes.push((m_idx, "async"));
                }
            }
        }

        if let Some(abs_node) = abstract_node {
            use crate::diagnostics::{diagnostic_codes, diagnostic_messages, format_message};
            for (conflict_idx, name) in conflicting_nodes {
                let message = format_message(
                    diagnostic_messages::MODIFIER_CANNOT_BE_USED_WITH_MODIFIER,
                    &[name, "abstract"],
                );

                // Point to whichever modifier comes second
                let (abs_start, _) = self.get_node_span(abs_node).unwrap_or((0, 0));
                let (con_start, _) = self.get_node_span(conflict_idx).unwrap_or((0, 0));

                let error_node = if con_start > abs_start {
                    conflict_idx
                } else {
                    abs_node
                };

                self.error_at_node(
                    error_node,
                    &message,
                    diagnostic_codes::MODIFIER_CANNOT_BE_USED_WITH_MODIFIER,
                );
            }
        }
    }
}