Skip to main content

tsz_checker/error_reporter/
name_resolution.rs

1//! Name resolution error reporting (TS2304, TS2552, TS2583, TS2584).
2
3use crate::diagnostics::{
4    Diagnostic, DiagnosticCategory, diagnostic_codes, diagnostic_messages, format_message,
5};
6use crate::state::CheckerState;
7use tsz_parser::parser::NodeIndex;
8
9impl<'a> CheckerState<'a> {
10    // =========================================================================
11    // Name Resolution Errors
12    // =========================================================================
13
14    fn unresolved_name_matches_enclosing_param(&self, name: &str, idx: NodeIndex) -> bool {
15        use tsz_parser::parser::syntax_kind_ext;
16
17        let mut current = idx;
18        let mut guard = 0;
19        while current.is_some() {
20            guard += 1;
21            if guard > 256 {
22                break;
23            }
24
25            let Some(ext) = self.ctx.arena.get_extended(current) else {
26                break;
27            };
28            if ext.parent.is_none() {
29                break;
30            }
31            let parent = ext.parent;
32            let Some(parent_node) = self.ctx.arena.get(parent) else {
33                break;
34            };
35
36            let matches_param = match parent_node.kind {
37                k if k == syntax_kind_ext::FUNCTION_DECLARATION
38                    || k == syntax_kind_ext::FUNCTION_EXPRESSION
39                    || k == syntax_kind_ext::ARROW_FUNCTION =>
40                {
41                    self.ctx
42                        .arena
43                        .get_function(parent_node)
44                        .is_some_and(|func| {
45                            func.parameters.nodes.iter().any(|&param_idx| {
46                                let Some(param_node) = self.ctx.arena.get(param_idx) else {
47                                    return false;
48                                };
49                                let Some(param) = self.ctx.arena.get_parameter(param_node) else {
50                                    return false;
51                                };
52                                let Some(name_node) = self.ctx.arena.get(param.name) else {
53                                    return false;
54                                };
55                                self.ctx
56                                    .arena
57                                    .get_identifier(name_node)
58                                    .is_some_and(|id| id.escaped_text == name)
59                            })
60                        })
61                }
62                k if k == syntax_kind_ext::METHOD_DECLARATION => self
63                    .ctx
64                    .arena
65                    .get_method_decl(parent_node)
66                    .is_some_and(|method| {
67                        method.parameters.nodes.iter().any(|&param_idx| {
68                            let Some(param_node) = self.ctx.arena.get(param_idx) else {
69                                return false;
70                            };
71                            let Some(param) = self.ctx.arena.get_parameter(param_node) else {
72                                return false;
73                            };
74                            let Some(name_node) = self.ctx.arena.get(param.name) else {
75                                return false;
76                            };
77                            self.ctx
78                                .arena
79                                .get_identifier(name_node)
80                                .is_some_and(|id| id.escaped_text == name)
81                        })
82                    }),
83                k if k == syntax_kind_ext::CONSTRUCTOR => self
84                    .ctx
85                    .arena
86                    .get_constructor(parent_node)
87                    .is_some_and(|ctor| {
88                        ctor.parameters.nodes.iter().any(|&param_idx| {
89                            let Some(param_node) = self.ctx.arena.get(param_idx) else {
90                                return false;
91                            };
92                            let Some(param) = self.ctx.arena.get_parameter(param_node) else {
93                                return false;
94                            };
95                            let Some(name_node) = self.ctx.arena.get(param.name) else {
96                                return false;
97                            };
98                            self.ctx
99                                .arena
100                                .get_identifier(name_node)
101                                .is_some_and(|id| id.escaped_text == name)
102                        })
103                    }),
104                _ => false,
105            };
106
107            if matches_param {
108                return true;
109            }
110            current = parent;
111        }
112
113        false
114    }
115
116    /// Check if a node is inside a `"use strict"` block by walking up the AST
117    /// to find a source file or function body with a "use strict" directive prologue.
118    fn is_in_use_strict_block(&self, idx: NodeIndex) -> bool {
119        use tsz_parser::parser::syntax_kind_ext;
120
121        let mut current = idx;
122        let mut guard = 0;
123        while current.is_some() {
124            guard += 1;
125            if guard > 256 {
126                break;
127            }
128            let Some(node) = self.ctx.arena.get(current) else {
129                break;
130            };
131            // Check source file level "use strict"
132            if node.kind == syntax_kind_ext::SOURCE_FILE {
133                if let Some(sf) = self.ctx.arena.get_source_file(node) {
134                    for &stmt_idx in &sf.statements.nodes {
135                        let Some(stmt) = self.ctx.arena.get(stmt_idx) else {
136                            continue;
137                        };
138                        if stmt.kind != syntax_kind_ext::EXPRESSION_STATEMENT {
139                            break;
140                        }
141                        if let Some(expr_stmt) = self.ctx.arena.get_expression_statement(stmt)
142                            && let Some(expr_node) = self.ctx.arena.get(expr_stmt.expression)
143                            && expr_node.kind == tsz_scanner::SyntaxKind::StringLiteral as u16
144                            && let Some(lit) = self.ctx.arena.get_literal(expr_node)
145                            && lit.text == "use strict"
146                        {
147                            return true;
148                        }
149                    }
150                }
151                return false;
152            }
153            let Some(ext) = self.ctx.arena.get_extended(current) else {
154                break;
155            };
156            if ext.parent.is_none() {
157                break;
158            }
159            current = ext.parent;
160        }
161        false
162    }
163
164    /// Check if a node is in a type-annotation context (type reference, implements, extends, etc.).
165    /// Used to determine which symbol meaning to use for spelling suggestions.
166    fn is_in_type_context(&self, idx: NodeIndex) -> bool {
167        use tsz_parser::parser::syntax_kind_ext;
168
169        // Walk up the AST to find if we're inside a type annotation
170        let mut current = idx;
171        let mut guard = 0;
172        while current.is_some() {
173            guard += 1;
174            if guard > 64 {
175                break;
176            }
177            if let Some(node) = self.ctx.arena.get(current) {
178                match node.kind {
179                    syntax_kind_ext::TYPE_REFERENCE
180                    | syntax_kind_ext::HERITAGE_CLAUSE
181                    | syntax_kind_ext::TYPE_ALIAS_DECLARATION
182                    | syntax_kind_ext::INTERFACE_DECLARATION
183                    | syntax_kind_ext::TYPE_PARAMETER
184                    | syntax_kind_ext::MAPPED_TYPE
185                    | syntax_kind_ext::CONDITIONAL_TYPE
186                    | syntax_kind_ext::INDEXED_ACCESS_TYPE
187                    | syntax_kind_ext::UNION_TYPE
188                    | syntax_kind_ext::INTERSECTION_TYPE
189                    | syntax_kind_ext::ARRAY_TYPE
190                    | syntax_kind_ext::TUPLE_TYPE
191                    | syntax_kind_ext::TYPE_LITERAL
192                    | syntax_kind_ext::FUNCTION_TYPE
193                    | syntax_kind_ext::CONSTRUCTOR_TYPE
194                    | syntax_kind_ext::PARENTHESIZED_TYPE
195                    | syntax_kind_ext::TYPE_OPERATOR
196                    | syntax_kind_ext::TYPE_QUERY
197                    | syntax_kind_ext::INFER_TYPE => return true,
198                    // Stop at expression/statement boundaries
199                    syntax_kind_ext::FUNCTION_DECLARATION
200                    | syntax_kind_ext::FUNCTION_EXPRESSION
201                    | syntax_kind_ext::ARROW_FUNCTION
202                    | syntax_kind_ext::CLASS_DECLARATION
203                    | syntax_kind_ext::CLASS_EXPRESSION
204                    | syntax_kind_ext::VARIABLE_STATEMENT
205                    | syntax_kind_ext::EXPRESSION_STATEMENT
206                    | syntax_kind_ext::BLOCK
207                    | syntax_kind_ext::SOURCE_FILE => return false,
208                    _ => {}
209                }
210            }
211            let Some(ext) = self.ctx.arena.get_extended(current) else {
212                break;
213            };
214            if ext.parent.is_none() {
215                break;
216            }
217            current = ext.parent;
218        }
219        false
220    }
221
222    /// Report a cannot find name error using solver diagnostics with source tracking.
223    /// Enhanced to provide suggestions for similar names, import suggestions, and
224    /// library change suggestions for ES2015+ types.
225    pub fn error_cannot_find_name_at(&mut self, name: &str, idx: NodeIndex) {
226        use tsz_binder::lib_loader;
227        use tsz_parser::parser::syntax_kind_ext;
228
229        // TS1212/TS1213/TS1214: Emit strict-mode reserved word diagnostic
230        // before any TS2304 suppression logic. This fires independently of TS2304.
231        if crate::state_checking::is_strict_mode_reserved_name(name) {
232            // Detect class context by walking up the AST (enclosing_class may not
233            // be set during type resolution or other non-statement-walk phases).
234            let in_class = {
235                let mut cur = idx;
236                let mut found = false;
237                let mut g = 0;
238                while cur.is_some() {
239                    g += 1;
240                    if g > 256 {
241                        break;
242                    }
243                    if let Some(n) = self.ctx.arena.get(cur) {
244                        if n.kind == syntax_kind_ext::CLASS_DECLARATION
245                            || n.kind == syntax_kind_ext::CLASS_EXPRESSION
246                        {
247                            found = true;
248                            break;
249                        }
250                        if n.kind == syntax_kind_ext::SOURCE_FILE {
251                            break;
252                        }
253                    }
254                    let Some(ext) = self.ctx.arena.get_extended(cur) else {
255                        break;
256                    };
257                    if ext.parent.is_none() {
258                        break;
259                    }
260                    cur = ext.parent;
261                }
262                found
263            };
264
265            let is_strict = self.ctx.compiler_options.always_strict
266                || self.ctx.compiler_options.strict
267                || self.ctx.binder.is_external_module()
268                || in_class
269                || self.is_in_use_strict_block(idx);
270
271            if is_strict {
272                use crate::diagnostics::{diagnostic_codes, diagnostic_messages, format_message};
273                if in_class {
274                    let message = format_message(
275                        diagnostic_messages::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_CLASS_DEFINITIONS_ARE_AUTO,
276                        &[name],
277                    );
278                    self.error_at_node(
279                        idx,
280                        &message,
281                        diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_CLASS_DEFINITIONS_ARE_AUTO,
282                    );
283                } else if self.ctx.binder.is_external_module() {
284                    let message = format_message(
285                        diagnostic_messages::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_MODULES_ARE_AUTOMATICALLY,
286                        &[name],
287                    );
288                    self.error_at_node(
289                        idx,
290                        &message,
291                        diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE_MODULES_ARE_AUTOMATICALLY,
292                    );
293                } else {
294                    let message = format_message(
295                        diagnostic_messages::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE,
296                        &[name],
297                    );
298                    self.error_at_node(
299                        idx,
300                        &message,
301                        diagnostic_codes::IDENTIFIER_EXPECTED_IS_A_RESERVED_WORD_IN_STRICT_MODE,
302                    );
303                }
304            }
305        }
306
307        // Keep TS2304 for ambiguous generic assertions such as `<<T>(x: T) => T>f`.
308        // These nodes can carry parse-error flags, but TypeScript still reports
309        // unresolved `T` alongside TS1005/TS1109.
310        let force_emit_for_ambiguous_generic = self
311            .ctx
312            .arena
313            .get(idx)
314            .and_then(|node| {
315                let source = self.ctx.arena.source_files.first()?.text.as_ref();
316                let pos = node.pos as usize;
317                if pos < 2 {
318                    return Some(false);
319                }
320                let bytes = source.as_bytes();
321                Some(
322                    bytes.get(pos.saturating_sub(2)) == Some(&b'<')
323                        && bytes.get(pos.saturating_sub(1)) == Some(&b'<'),
324                )
325            })
326            .unwrap_or(false);
327
328        // NOTE: `symbol` is intentionally excluded — tsc never emits TS2693 for
329        // lowercase `symbol`. Instead it emits TS2552 "Cannot find name 'symbol'.
330        // Did you mean 'Symbol'?" via the normal spelling-suggestion path.
331        let is_primitive_type_keyword = matches!(
332            name,
333            "number"
334                | "string"
335                | "boolean"
336                | "void"
337                | "undefined"
338                | "null"
339                | "any"
340                | "unknown"
341                | "never"
342                | "object"
343                | "bigint"
344        );
345        let is_import_equals_module_specifier = self
346            .ctx
347            .arena
348            .get_extended(idx)
349            .and_then(|ext| self.ctx.arena.get(ext.parent))
350            .is_some_and(|parent_node| {
351                if parent_node.kind
352                    != tsz_parser::parser::syntax_kind_ext::IMPORT_EQUALS_DECLARATION
353                {
354                    return false;
355                }
356                self.ctx
357                    .arena
358                    .get_import_decl(parent_node)
359                    .is_some_and(|imp| imp.module_specifier == idx)
360            });
361
362        if is_primitive_type_keyword && !is_import_equals_module_specifier {
363            self.error_type_only_value_at(name, idx);
364            return;
365        }
366
367        if !force_emit_for_ambiguous_generic
368            && self.unresolved_name_matches_enclosing_param(name, idx)
369        {
370            return;
371        }
372
373        // In `import x = <expr>` module reference position, unresolved names should
374        // report namespace/module diagnostics (TS2503/TS2307), not TS2304.
375        let mut cur = idx;
376        while let Some(ext) = self.ctx.arena.get_extended(cur) {
377            let parent = ext.parent;
378            if parent.is_none() {
379                break;
380            }
381            if let Some(parent_node) = self.ctx.arena.get(parent)
382                && parent_node.kind == syntax_kind_ext::IMPORT_EQUALS_DECLARATION
383            {
384                return;
385            }
386            cur = parent;
387        }
388
389        // Skip TS2304 for identifiers that are clearly not valid names.
390        // These are likely parse errors (e.g., ",", ";", "(", or empty names) that were
391        // added to the AST for error recovery. The parse error should have
392        // already been emitted (e.g., TS1003 "Identifier expected").
393        if name.is_empty() {
394            return;
395        }
396        let is_obviously_invalid = name.len() == 1
397            && matches!(
398                name.chars().next(),
399                Some(
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        if is_obviously_invalid {
425            return;
426        }
427
428        // Detect computed property name context: class/object vs enum.
429        // tsc emits TS2304 for computed property expressions in class/object-literal
430        // contexts, but NOT in enum contexts (only TS1164 is emitted for enum computed names).
431        let computed_ctx = self.ctx.arena.get_extended(idx).and_then(|ext| {
432            let parent = self.ctx.arena.get(ext.parent)?;
433            if parent.kind != syntax_kind_ext::COMPUTED_PROPERTY_NAME {
434                return None;
435            }
436            let gp_ext = self.ctx.arena.get_extended(ext.parent)?;
437            let gp = self.ctx.arena.get(gp_ext.parent)?;
438            if gp.kind == syntax_kind_ext::ENUM_MEMBER {
439                Some(false) // enum context: suppress TS2304/TS2552
440            } else {
441                Some(true) // class/object context: allow TS2304
442            }
443        });
444        // Suppress TS2304/TS2552 for identifiers inside enum computed property names.
445        // tsc only emits TS1164 for these and doesn't resolve the expressions.
446        if computed_ctx == Some(false) {
447            return;
448        }
449        let is_in_computed_property = computed_ctx == Some(true);
450        // When there are parse errors, modifier keywords appearing as identifiers
451        // are parser-recovery artifacts. Suppress TS2304 for these to avoid cascades.
452        // Exception: computed property name expressions like `[public]` — tsc emits
453        // TS2304 for these even in parse-error contexts (but not in enums).
454        if self.has_parse_errors()
455            && !is_in_computed_property
456            && matches!(
457                name,
458                "static"
459                    | "public"
460                    | "private"
461                    | "protected"
462                    | "readonly"
463                    | "abstract"
464                    | "declare"
465                    | "override"
466                    | "accessor"
467            )
468        {
469            return;
470        }
471
472        // In parse-error files, identifiers inside class member bodies are often
473        // secondary errors from a primary parse error (e.g. `yield foo` in a non-generator
474        // constructor — TSC emits TS1163 but not TS2304 for `foo`).
475        // Suppress TS2304 when there is a parse error at or just before the identifier
476        // (within ~10 chars) AND the identifier is in a class member body.
477        // This targets cases like `yield foo` where the error (on `yield`) immediately
478        // precedes the identifier (foo). It does NOT suppress cases like `if (a` where
479        // the parse error (missing `)`) comes AFTER the identifier `a`.
480        if self.has_syntax_parse_errors()
481            && !is_in_computed_property
482            && !force_emit_for_ambiguous_generic
483            && !self.ctx.syntax_parse_error_positions.is_empty()
484            && let Some(node) = self.ctx.arena.get(idx)
485        {
486            let ident_pos = node.pos;
487            let has_nearby_preceding_error =
488                self.ctx
489                    .syntax_parse_error_positions
490                    .iter()
491                    .any(|&err_pos| {
492                        // Error must be BEFORE the identifier (not after) and within 10 chars
493                        err_pos <= ident_pos && (ident_pos - err_pos) <= 10
494                    });
495            if has_nearby_preceding_error {
496                let mut current = idx;
497                let mut guard = 0;
498                let mut in_class = false;
499                let mut in_class_member_body = false;
500                while current.is_some() {
501                    guard += 1;
502                    if guard > 256 {
503                        break;
504                    }
505                    let Some(inner_node) = self.ctx.arena.get(current) else {
506                        break;
507                    };
508                    if inner_node.kind == syntax_kind_ext::CLASS_DECLARATION
509                        || inner_node.kind == syntax_kind_ext::CLASS_EXPRESSION
510                    {
511                        in_class = true;
512                    }
513                    if inner_node.kind == syntax_kind_ext::CONSTRUCTOR
514                        || inner_node.kind == syntax_kind_ext::METHOD_DECLARATION
515                        || inner_node.kind == syntax_kind_ext::GET_ACCESSOR
516                        || inner_node.kind == syntax_kind_ext::SET_ACCESSOR
517                    {
518                        in_class_member_body = true;
519                    }
520                    let Some(ext) = self.ctx.arena.get_extended(current) else {
521                        break;
522                    };
523                    if ext.parent.is_none() {
524                        break;
525                    }
526                    current = ext.parent;
527                }
528                if in_class && in_class_member_body {
529                    return;
530                }
531            }
532        }
533
534        // tsc propagates `ThisNodeHasError` / `ThisNodeOrAnySubNodesHasError`
535        // flags through the AST when parse errors occur. The checker skips
536        // semantic resolution for error-flagged nodes, effectively suppressing
537        // TS2304 for identifiers in or near error-recovery AST regions.
538        //
539        // Since our parser uses a compact u16 flags field that cannot store
540        // bit-18 error flags, we approximate tsc's behavior with a file-level
541        // check: when the file has "real" syntax errors (TS1005/TS1109/TS1127
542        // etc. that indicate actual parse failure), suppress TS2304 broadly.
543        //
544        // Grammar-only errors (TS1100, TS1173, TS1212) should NOT suppress
545        // TS2304 — tsc still emits TS2304 in those files.
546        //
547        // Exception: computed property name expressions — tsc always emits
548        // TS2304 for these even in files with parse errors.
549        if self.ctx.has_real_syntax_errors
550            && !is_in_computed_property
551            && !force_emit_for_ambiguous_generic
552        {
553            return;
554        }
555
556        // In files with real syntax errors, unresolved names inside `typeof` type queries
557        // are often cascades from malformed declaration syntax; TypeScript commonly keeps
558        // the primary parse diagnostic only for these.
559        // Only suppress when a parse error falls directly within the identifier's span.
560        // The wider `node_has_nearby_parse_error` margin (8 bytes) would incorrectly
561        // suppress TS2304 for `A` in `typeof A.` where the error is the missing
562        // identifier after the dot, not A itself.
563        if self.has_syntax_parse_errors() && self.node_span_contains_parse_error(idx) {
564            let mut current = idx;
565            let mut guard = 0;
566            while current.is_some() {
567                guard += 1;
568                if guard > 256 {
569                    break;
570                }
571                if let Some(node) = self.ctx.arena.get(current)
572                    && node.kind == syntax_kind_ext::TYPE_QUERY
573                {
574                    return;
575                }
576                let Some(ext) = self.ctx.arena.get_extended(current) else {
577                    break;
578                };
579                if ext.parent.is_none() {
580                    break;
581                }
582                current = ext.parent;
583            }
584        }
585
586        if let Some(original_name) =
587            self.unresolved_unused_renaming_property_in_type_query(name, idx)
588        {
589            let message = format!(
590                "'{name}' is an unused renaming of '{original_name}'. Did you intend to use it as a type annotation?"
591            );
592            self.error_at_node(
593                idx,
594                &message,
595                diagnostic_codes::IS_AN_UNUSED_RENAMING_OF_DID_YOU_INTEND_TO_USE_IT_AS_A_TYPE_ANNOTATION,
596            );
597            return;
598        }
599
600        // Check if this is an ES2015+ type that requires a specific lib
601        // If so, emit TS2583 with a suggestion to change the lib
602        if lib_loader::is_es2015_plus_type(name) {
603            self.error_cannot_find_name_change_lib(name, idx);
604            return;
605        }
606
607        // Check if this is a known DOM/ScriptHost global that requires the 'dom' lib
608        // If so, emit TS2584 with a suggestion to include 'dom'
609        if super::is_known_dom_global(name) {
610            self.error_cannot_find_name_change_target_lib(name, idx);
611            return;
612        }
613
614        // Check if this is a known Node.js global → TS2591
615        if super::is_known_node_global(name) {
616            self.error_cannot_find_name_install_node_types(name, idx);
617            return;
618        }
619
620        // Check if this is a known test runner global → TS2582
621        if super::is_known_test_runner_global(name) {
622            self.error_cannot_find_name_install_test_types(name, idx);
623            return;
624        }
625
626        // Keep TS2304 for accessibility modifier keywords recovered as identifiers.
627        // tsc does not emit TS2552 suggestions (e.g. "private" -> "print") in these cases.
628        let is_accessibility_modifier_name = matches!(name, "public" | "private" | "protected");
629        let mut is_in_spread_element = false;
630        let mut current = idx;
631        let mut guard = 0;
632        while current.is_some() {
633            guard += 1;
634            if guard > 256 {
635                break;
636            }
637            let Some(node) = self.ctx.arena.get(current) else {
638                break;
639            };
640            if node.kind == syntax_kind_ext::SPREAD_ELEMENT {
641                is_in_spread_element = true;
642                break;
643            }
644            let Some(ext) = self.ctx.arena.get_extended(current) else {
645                break;
646            };
647            if ext.parent.is_none() {
648                break;
649            }
650            current = ext.parent;
651        }
652        // Keep TS2304 (no TS2552 suggestion) for `arguments` lookups.
653        // TypeScript does not offer spelling suggestions for unresolved `arguments`.
654        let is_arguments_name = name == "arguments";
655        let suppress_spelling_suggestion =
656            is_accessibility_modifier_name || is_in_spread_element || is_arguments_name;
657
658        // Determine spelling suggestion meaning based on context.
659        // In type positions (type annotations, implements clauses, type references),
660        // only suggest TYPE-meaning symbols. In value positions, suggest VALUE symbols.
661        // This matches tsc's getSpellingSuggestionForName behavior.
662        let suggestion_meaning = if self.is_in_type_context(idx) {
663            tsz_binder::symbol_flags::TYPE
664        } else {
665            tsz_binder::symbol_flags::VALUE
666        };
667
668        let reached_max_suggestions = self.ctx.spelling_suggestions_emitted >= 10;
669        self.ctx.spelling_suggestions_emitted += 1;
670
671        // Try to find similar identifiers in scope for better error messages
672        if !suppress_spelling_suggestion
673            && !reached_max_suggestions
674            && let Some(suggestions) = self.find_similar_identifiers(name, idx, suggestion_meaning)
675            && !suggestions.is_empty()
676        {
677            // Use the first suggestion for "Did you mean?" error
678            self.error_cannot_find_name_with_suggestions(name, &suggestions, idx);
679            return;
680        }
681
682        // Fall back to standard error without suggestions
683        if let Some(loc) = self.get_source_location(idx) {
684            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
685                self.ctx.types,
686                &self.ctx.binder.symbols,
687                self.ctx.file_name.as_str(),
688            )
689            .with_def_store(&self.ctx.definition_store);
690            let diag = builder.cannot_find_name(name, loc.start, loc.length());
691            self.ctx
692                .push_diagnostic(diag.to_checker_diagnostic(&self.ctx.file_name));
693        }
694    }
695
696    /// Report error 2318/2583: Cannot find global type 'X'.
697    /// - TS2318: Cannot find global type (for @noLib tests)
698    /// - TS2583: Cannot find name - suggests changing target library (for ES2015+ types)
699    pub fn error_cannot_find_global_type(&mut self, name: &str, idx: NodeIndex) {
700        use tsz_binder::lib_loader;
701
702        // Check if this is an ES2015+ type that would require a specific lib
703        let is_es2015_type = lib_loader::is_es2015_plus_type(name);
704
705        if let Some(loc) = self.get_source_location(idx) {
706            let (code, message) = if is_es2015_type {
707                (
708                    lib_loader::MISSING_ES2015_LIB_SUPPORT,
709                    format!(
710                        "Cannot find name '{name}'. Do you need to change your target library? Try changing the 'lib' compiler option to es2015 or later."
711                    ),
712                )
713            } else {
714                (
715                    lib_loader::CANNOT_FIND_GLOBAL_TYPE,
716                    format!("Cannot find global type '{name}'."),
717                )
718            };
719
720            self.ctx.push_diagnostic(Diagnostic::error(
721                self.ctx.file_name.clone(),
722                loc.start,
723                loc.length(),
724                message,
725                code,
726            ));
727        }
728    }
729
730    /// Report TS2583: Cannot find name 'X' - suggest changing target library.
731    ///
732    /// This error is emitted when an ES2015+ global (Promise, Map, Set, Symbol, etc.)
733    /// is used as a value but is not available in the current lib configuration.
734    /// It provides a helpful suggestion to change the lib compiler option.
735    pub fn error_cannot_find_name_change_lib(&mut self, name: &str, idx: NodeIndex) {
736        if let Some(loc) = self.get_source_location(idx) {
737            let message = format_message(
738                diagnostic_messages::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB,
739                &[name],
740            );
741            self.ctx.push_diagnostic(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB));
742        }
743    }
744
745    /// Report TS2584: Cannot find name 'X' - suggest including 'dom' lib.
746    ///
747    /// This error is emitted when a known DOM/ScriptHost global (console, window,
748    /// document, `HTMLElement`, etc.) is used but the 'dom' lib is not included.
749    pub fn error_cannot_find_name_change_target_lib(&mut self, name: &str, idx: NodeIndex) {
750        if let Some(loc) = self.get_source_location(idx) {
751            let message = format_message(
752                diagnostic_messages::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB_2,
753                &[name],
754            );
755            self.ctx.push_diagnostic(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::CANNOT_FIND_NAME_DO_YOU_NEED_TO_CHANGE_YOUR_TARGET_LIBRARY_TRY_CHANGING_THE_LIB_2));
756        }
757    }
758
759    /// Report TS2580: Cannot find name 'X' - suggest installing @types/node.
760    /// tsc uses TS2580 (without tsconfig suggestion) when the tsconfig has no `types` field,
761    /// and TS2591 (with tsconfig suggestion) when a `types` field is configured.
762    /// Most conformance tests have no `types` field, so TS2580 matches tsc behavior.
763    pub fn error_cannot_find_name_install_node_types(&mut self, name: &str, idx: NodeIndex) {
764        if let Some(loc) = self.get_source_location(idx) {
765            let message = format_message(
766                diagnostic_messages::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_NODE_TRY_NPM_I_SAVE,
767                &[name],
768            );
769            self.ctx.push_diagnostic(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_NODE_TRY_NPM_I_SAVE));
770        }
771    }
772
773    /// Report TS2582: Cannot find name 'X' - suggest installing test runner types.
774    pub fn error_cannot_find_name_install_test_types(&mut self, name: &str, idx: NodeIndex) {
775        if let Some(loc) = self.get_source_location(idx) {
776            let message = format_message(
777                diagnostic_messages::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_A_TEST_RUNNER_TRY_N,
778                &[name],
779            );
780            self.ctx.push_diagnostic(Diagnostic::error(self.ctx.file_name.clone(), loc.start, loc.length(), message, diagnostic_codes::CANNOT_FIND_NAME_DO_YOU_NEED_TO_INSTALL_TYPE_DEFINITIONS_FOR_A_TEST_RUNNER_TRY_N));
781        }
782    }
783
784    /// Report error 2304/2552: Cannot find name 'X' with suggestions.
785    /// Provides a list of similar names that might be what the user intended.
786    pub fn error_cannot_find_name_with_suggestions(
787        &mut self,
788        name: &str,
789        suggestions: &[String],
790        idx: NodeIndex,
791    ) {
792        // Skip TS2304 for identifiers that are clearly not valid names.
793        // These are likely parse errors that were added to the AST for error recovery.
794        let is_obviously_invalid = name.len() == 1
795            && matches!(
796                name.chars().next(),
797                Some(
798                    ',' | ';'
799                        | ':'
800                        | '('
801                        | ')'
802                        | '['
803                        | ']'
804                        | '{'
805                        | '}'
806                        | '+'
807                        | '-'
808                        | '*'
809                        | '/'
810                        | '%'
811                        | '&'
812                        | '|'
813                        | '^'
814                        | '!'
815                        | '~'
816                        | '<'
817                        | '>'
818                        | '='
819                        | '.'
820                )
821            );
822        if is_obviously_invalid {
823            return;
824        }
825
826        if let Some(loc) = self.get_source_location(idx) {
827            // Format the suggestions list
828            let suggestions_text = if suggestions.len() == 1 {
829                format!("'{}'", suggestions[0])
830            } else {
831                let formatted: Vec<String> = suggestions.iter().map(|s| format!("'{s}")).collect();
832                formatted.join(", ")
833            };
834
835            let message = if suggestions.len() == 1 {
836                format!("Cannot find name '{name}'. Did you mean {suggestions_text}?")
837            } else {
838                format!("Cannot find name '{name}'. Did you mean one of: {suggestions_text}?")
839            };
840
841            self.ctx.push_diagnostic(Diagnostic {
842                code: if suggestions.len() == 1 {
843                    diagnostic_codes::CANNOT_FIND_NAME_DID_YOU_MEAN
844                } else {
845                    diagnostic_codes::CANNOT_FIND_NAME
846                },
847                category: DiagnosticCategory::Error,
848                message_text: message,
849                file: self.ctx.file_name.clone(),
850                start: loc.start,
851                length: loc.length(),
852                related_information: Vec::new(),
853            });
854        }
855    }
856
857    /// Report error 2552: Cannot find name 'X'. Did you mean 'Y'?
858    pub fn error_cannot_find_name_did_you_mean_at(
859        &mut self,
860        name: &str,
861        suggestion: &str,
862        idx: NodeIndex,
863    ) {
864        if let Some(loc) = self.get_source_location(idx) {
865            let message = format!("Cannot find name '{name}'. Did you mean '{suggestion}'?");
866            self.ctx.push_diagnostic(Diagnostic::error(
867                self.ctx.file_name.clone(),
868                loc.start,
869                loc.length(),
870                message,
871                diagnostic_codes::CANNOT_FIND_NAME_DID_YOU_MEAN,
872            ));
873        }
874    }
875
876    /// Report error 2662: Cannot find name 'X'. Did you mean the static member 'C.X'?
877    pub fn error_cannot_find_name_static_member_at(
878        &mut self,
879        name: &str,
880        class_name: &str,
881        idx: NodeIndex,
882    ) {
883        if let Some(loc) = self.get_source_location(idx) {
884            let message = format!(
885                "Cannot find name '{name}'. Did you mean the static member '{class_name}.{name}'?"
886            );
887            self.ctx.push_diagnostic(Diagnostic::error(
888                self.ctx.file_name.clone(),
889                loc.start,
890                loc.length(),
891                message,
892                diagnostic_codes::CANNOT_FIND_NAME_DID_YOU_MEAN_THE_STATIC_MEMBER,
893            ));
894        }
895    }
896}