Skip to main content

tsz_checker/error_reporter/
properties.rs

1//! Property-related error reporting (TS2339, TS2741, TS2540, TS7053).
2
3use crate::diagnostics::{Diagnostic, diagnostic_codes, diagnostic_messages, format_message};
4use crate::state::CheckerState;
5use tsz_parser::parser::NodeIndex;
6use tsz_scanner::SyntaxKind;
7use tsz_solver::TypeId;
8
9impl<'a> CheckerState<'a> {
10    // =========================================================================
11    // Property Errors
12    // =========================================================================
13
14    /// Report a property missing error using solver diagnostics with source tracking.
15    pub fn error_property_missing_at(
16        &mut self,
17        prop_name: &str,
18        source: TypeId,
19        target: TypeId,
20        idx: NodeIndex,
21    ) {
22        // Suppress cascade errors from unresolved types
23        if source == TypeId::ERROR
24            || target == TypeId::ERROR
25            || source == TypeId::ANY
26            || target == TypeId::ANY
27            || source == TypeId::UNKNOWN
28            || target == TypeId::UNKNOWN
29        {
30            return;
31        }
32
33        if let Some(loc) = self.get_source_location(idx) {
34            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
35                self.ctx.types,
36                &self.ctx.binder.symbols,
37                self.ctx.file_name.as_str(),
38            )
39            .with_def_store(&self.ctx.definition_store);
40            let diag = builder.property_missing(prop_name, source, target, loc.start, loc.length());
41            self.ctx
42                .diagnostics
43                .push(diag.to_checker_diagnostic(&self.ctx.file_name));
44        }
45    }
46
47    /// Report a property not exist error using solver diagnostics with source tracking.
48    /// If a similar property name is found on the type, emits TS2551 ("Did you mean?")
49    /// instead of TS2339.
50    pub fn error_property_not_exist_at(
51        &mut self,
52        prop_name: &str,
53        type_id: TypeId,
54        idx: NodeIndex,
55    ) {
56        use tsz_solver::type_queries;
57
58        // Suppress error if type is ERROR/ANY or an Error type wrapper
59        // This prevents cascading errors when accessing properties on error types
60        // NOTE: We do NOT suppress for UNKNOWN - accessing properties on unknown should error (TS2339)
61        if type_id == TypeId::ERROR
62            || type_id == TypeId::ANY
63            || type_queries::is_error_type(self.ctx.types, type_id)
64        {
65            return;
66        }
67
68        // Suppress cascaded TS2339 from failed generic inference when the receiver
69        // remains a union that still contains unresolved type parameters.
70        // This keeps follow-on property errors from obscuring the primary root cause
71        // (typically assignability/inference diagnostics).
72        if type_queries::is_union_type(self.ctx.types, type_id)
73            && type_queries::contains_type_parameters_db(self.ctx.types, type_id)
74        {
75            return;
76        }
77
78        if let Some(loc) = self.get_source_location(idx) {
79            let suppress_did_you_mean =
80                self.has_syntax_parse_errors() || self.class_extends_any_base(type_id);
81
82            // On files with syntax parse errors, TypeScript generally avoids TS2551
83            // suggestion diagnostics and sticks with TS2339 to reduce cascades.
84            let suggestion = if suppress_did_you_mean {
85                None
86            } else {
87                self.find_similar_property(prop_name, type_id)
88            };
89
90            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
91                self.ctx.types,
92                &self.ctx.binder.symbols,
93                self.ctx.file_name.as_str(),
94            )
95            .with_def_store(&self.ctx.definition_store);
96
97            let diag = if let Some(ref suggestion) = suggestion {
98                builder.property_not_exist_did_you_mean(
99                    prop_name,
100                    type_id,
101                    suggestion,
102                    loc.start,
103                    loc.length(),
104                )
105            } else {
106                builder.property_not_exist(prop_name, type_id, loc.start, loc.length())
107            };
108            // Use push_diagnostic for deduplication
109            self.ctx
110                .push_diagnostic(diag.to_checker_diagnostic(&self.ctx.file_name));
111        }
112    }
113
114    /// Report an excess property error using solver diagnostics with source tracking.
115    pub fn error_excess_property_at(&mut self, prop_name: &str, target: TypeId, idx: NodeIndex) {
116        // Honor removed-but-still-effective suppressExcessPropertyErrors flag
117        if self.ctx.compiler_options.suppress_excess_property_errors {
118            return;
119        }
120        // Suppress cascade errors from unresolved types
121        if target == TypeId::ERROR || target == TypeId::ANY || target == TypeId::UNKNOWN {
122            return;
123        }
124
125        if let Some(loc) = self.get_source_location(idx) {
126            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
127                self.ctx.types,
128                &self.ctx.binder.symbols,
129                self.ctx.file_name.as_str(),
130            )
131            .with_def_store(&self.ctx.definition_store);
132            let diag = builder.excess_property(prop_name, target, loc.start, loc.length());
133            // Use push_diagnostic for deduplication
134            self.ctx
135                .push_diagnostic(diag.to_checker_diagnostic(&self.ctx.file_name));
136        }
137    }
138
139    /// Report a "Cannot assign to readonly property" error using solver diagnostics with source tracking.
140    pub fn error_readonly_property_at(&mut self, prop_name: &str, idx: NodeIndex) {
141        if let Some(loc) = self.get_source_location(idx) {
142            let mut builder = tsz_solver::SpannedDiagnosticBuilder::with_symbols(
143                self.ctx.types,
144                &self.ctx.binder.symbols,
145                self.ctx.file_name.as_str(),
146            )
147            .with_def_store(&self.ctx.definition_store);
148            let diag = builder.readonly_property(prop_name, loc.start, loc.length());
149            self.ctx
150                .diagnostics
151                .push(diag.to_checker_diagnostic(&self.ctx.file_name));
152        }
153    }
154
155    /// Report TS2542: Index signature in type '{0}' only permits reading.
156    pub fn error_readonly_index_signature_at(
157        &mut self,
158        object_type: tsz_solver::TypeId,
159        idx: NodeIndex,
160    ) {
161        if let Some(loc) = self.get_source_location(idx) {
162            let type_name = self.format_type(object_type);
163            let message = format_message(
164                diagnostic_messages::INDEX_SIGNATURE_IN_TYPE_ONLY_PERMITS_READING,
165                &[&type_name],
166            );
167            let diag = Diagnostic::error(
168                self.ctx.file_name.clone(),
169                loc.start,
170                loc.length(),
171                message,
172                diagnostic_codes::INDEX_SIGNATURE_IN_TYPE_ONLY_PERMITS_READING,
173            );
174            self.ctx.diagnostics.push(diag);
175        }
176    }
177
178    /// Report TS2803: Cannot assign to private method. Private methods are not writable.
179    pub fn error_private_method_not_writable(&mut self, prop_name: &str, idx: NodeIndex) {
180        if let Some(loc) = self.get_source_location(idx) {
181            let message = format_message(
182                diagnostic_messages::CANNOT_ASSIGN_TO_PRIVATE_METHOD_PRIVATE_METHODS_ARE_NOT_WRITABLE,
183                &[prop_name],
184            );
185            let diag = Diagnostic::error(
186                self.ctx.file_name.clone(),
187                loc.start,
188                loc.length(),
189                message,
190                diagnostic_codes::CANNOT_ASSIGN_TO_PRIVATE_METHOD_PRIVATE_METHODS_ARE_NOT_WRITABLE,
191            );
192            self.ctx.diagnostics.push(diag);
193        }
194    }
195
196    /// Report no index signature error.
197    pub(crate) fn error_no_index_signature_at(
198        &mut self,
199        index_type: TypeId,
200        object_type: TypeId,
201        idx: NodeIndex,
202    ) {
203        // Honor removed-but-still-effective suppressImplicitAnyIndexErrors flag
204        if self.ctx.compiler_options.suppress_implicit_any_index_errors {
205            return;
206        }
207        // TS7053 is a noImplicitAny error - suppress without it
208        if !self.ctx.no_implicit_any() {
209            return;
210        }
211        // Suppress when types are unresolved
212        if index_type == TypeId::ANY || index_type == TypeId::ERROR || index_type == TypeId::UNKNOWN
213        {
214            return;
215        }
216        if object_type == TypeId::ANY
217            || object_type == TypeId::ERROR
218            || object_type == TypeId::UNKNOWN
219        {
220            return;
221        }
222        if self.is_element_access_on_this_or_super_with_any_base(idx) {
223            return;
224        }
225
226        if let Some(atom) =
227            tsz_solver::type_queries::get_string_literal_value(self.ctx.types, index_type)
228        {
229            let prop_name = self.ctx.types.resolve_atom_ref(atom);
230            let prop_name_str: &str = &prop_name;
231            let suppress_did_you_mean =
232                self.has_syntax_parse_errors() || self.class_extends_any_base(object_type);
233
234            let suggestion = if suppress_did_you_mean {
235                None
236            } else {
237                self.find_similar_property(prop_name_str, object_type)
238            };
239
240            if suggestion.is_some() {
241                // If there's a suggestion, TypeScript emits TS2551 instead of TS7053
242                self.error_property_not_exist_at(prop_name_str, object_type, idx);
243                return;
244            }
245        }
246
247        let mut formatter = self.ctx.create_type_formatter();
248        let index_str = formatter.format(index_type);
249        let object_str = formatter.format(object_type);
250        let message = format!(
251            "Element implicitly has an 'any' type because expression of type '{index_str}' can't be used to index type '{object_str}'."
252        );
253
254        self.error_at_node(idx, &message, diagnostic_codes::ELEMENT_IMPLICITLY_HAS_AN_ANY_TYPE_BECAUSE_EXPRESSION_OF_TYPE_CANT_BE_USED_TO_IN);
255    }
256
257    /// TypeScript suppresses TS7053 for `this[...]`/`super[...]` when the class extends an `any` base.
258    fn is_element_access_on_this_or_super_with_any_base(&mut self, idx: NodeIndex) -> bool {
259        use tsz_parser::parser::syntax_kind_ext;
260
261        let Some(ext) = self.ctx.arena.get_extended(idx) else {
262            return false;
263        };
264        let Some(parent) = self.ctx.arena.get(ext.parent) else {
265            return false;
266        };
267        if parent.kind != syntax_kind_ext::ELEMENT_ACCESS_EXPRESSION {
268            return false;
269        }
270        let Some(access) = self.ctx.arena.get_access_expr(parent) else {
271            return false;
272        };
273        if access.name_or_argument != idx {
274            return false;
275        }
276        let Some(expr_node) = self.ctx.arena.get(access.expression) else {
277            return false;
278        };
279        let is_this_or_super = expr_node.kind == SyntaxKind::SuperKeyword as u16
280            || expr_node.kind == SyntaxKind::ThisKeyword as u16;
281        if !is_this_or_super {
282            return false;
283        }
284
285        let Some(class_info) = self.ctx.enclosing_class.clone() else {
286            return false;
287        };
288        let Some(class_decl) = self.ctx.arena.get_class_at(class_info.class_idx) else {
289            return false;
290        };
291        let Some(heritage_clauses) = &class_decl.heritage_clauses else {
292            return false;
293        };
294
295        for &clause_idx in &heritage_clauses.nodes {
296            let Some(clause) = self.ctx.arena.get_heritage_clause_at(clause_idx) else {
297                continue;
298            };
299            if clause.token != SyntaxKind::ExtendsKeyword as u16 {
300                continue;
301            }
302            let Some(&type_idx) = clause.types.nodes.first() else {
303                continue;
304            };
305            let expr_idx =
306                if let Some(expr_type_args) = self.ctx.arena.get_expr_type_args_at(type_idx) {
307                    expr_type_args.expression
308                } else {
309                    type_idx
310                };
311            if self.get_type_of_node(expr_idx) == TypeId::ANY {
312                return true;
313            }
314        }
315
316        false
317    }
318}