Skip to main content

tsz_checker/classes/
constructor_checker.rs

1//! Constructor type checking (accessibility, signatures, instantiation, mixins).
2//! - Instance type extraction from constructors
3//! - Abstract constructor assignability
4//!
5//! This module extends `CheckerState` with utilities for constructor-related
6//! type checking operations.
7
8use crate::query_boundaries::constructor_checker::{
9    AbstractConstructorAnchor, ConstructorAccessKind, ConstructorReturnMergeKind, InstanceTypeKind,
10    classify_for_constructor_access, classify_for_constructor_return_merge,
11    classify_for_instance_type, construct_signatures_for_type, has_construct_signatures,
12    resolve_abstract_constructor_anchor,
13};
14use crate::state::{CheckerState, MAX_TREE_WALK_ITERATIONS, MemberAccessLevel};
15use rustc_hash::FxHashSet;
16use tsz_binder::{SymbolId, symbol_flags};
17use tsz_common::interner::Atom;
18use tsz_parser::parser::NodeIndex;
19use tsz_scanner::SyntaxKind;
20use tsz_solver::TypeId;
21
22// =============================================================================
23// Constructor Type Checking Utilities
24// =============================================================================
25
26impl<'a> CheckerState<'a> {
27    // =========================================================================
28    // Constructor Accessibility
29    // =========================================================================
30
31    /// Check if a type is an abstract constructor type.
32    ///
33    /// Abstract constructors cannot be instantiated directly with `new`.
34    pub fn is_abstract_ctor(&self, type_id: TypeId) -> bool {
35        self.ctx.abstract_constructor_types.contains(&type_id)
36    }
37
38    /// Check if a type is a private constructor.
39    ///
40    /// Private constructors can only be called from within the class.
41    pub fn is_private_ctor(&self, type_id: TypeId) -> bool {
42        self.ctx.private_constructor_types.contains(&type_id)
43    }
44
45    /// Check if a type is a protected constructor.
46    ///
47    /// Protected constructors can be called from the class and its subclasses.
48    pub fn is_protected_ctor(&self, type_id: TypeId) -> bool {
49        self.ctx.protected_constructor_types.contains(&type_id)
50    }
51
52    /// Check if a type is a public constructor.
53    ///
54    /// Public constructors have no access restrictions.
55    pub fn is_public_ctor(&self, type_id: TypeId) -> bool {
56        !self.is_private_ctor(type_id) && !self.is_protected_ctor(type_id)
57    }
58
59    // =========================================================================
60    // Constructor Signature Utilities
61    // =========================================================================
62
63    /// Check if a type has any construct signature.
64    ///
65    /// Construct signatures allow a type to be called with `new`.
66    pub fn has_construct_sig(&self, type_id: TypeId) -> bool {
67        has_construct_signatures(self.ctx.types, type_id)
68    }
69
70    /// Get the number of construct signatures for a type.
71    ///
72    /// Multiple construct signatures indicate constructor overloading.
73    pub fn construct_signature_count(&self, type_id: TypeId) -> usize {
74        construct_signatures_for_type(self.ctx.types, type_id).map_or(0, |sigs| sigs.len())
75    }
76
77    // =========================================================================
78    // Constructor Instantiation
79    // =========================================================================
80
81    /// Check if a constructor can be instantiated.
82    ///
83    /// Returns false for abstract constructors which cannot be instantiated.
84    pub fn can_instantiate(&self, constructor_type: TypeId) -> bool {
85        !self.is_abstract_ctor(constructor_type)
86    }
87
88    /// Check if `new` can be applied to a type.
89    ///
90    /// This is a convenience check combining constructor type detection
91    /// with abstract constructor checking.
92    pub fn can_use_new(&self, type_id: TypeId) -> bool {
93        self.has_construct_sig(type_id) && self.can_instantiate(type_id)
94    }
95
96    /// Check if a type is a class constructor (typeof Class).
97    ///
98    /// Returns true for Callable types with only construct signatures (no call signatures).
99    /// This is used to detect when a class constructor is being called without `new`.
100    pub fn is_class_constructor_type(&self, type_id: TypeId) -> bool {
101        // A class constructor is a Callable with construct signatures but no call signatures
102        self.has_construct_sig(type_id)
103            && !crate::query_boundaries::class_type::has_call_signatures(self.ctx.types, type_id)
104    }
105
106    /// Check if two constructor types have compatible accessibility.
107    ///
108    /// Returns true if source can be assigned to target based on their constructor accessibility.
109    /// - Public constructors are compatible with everything
110    /// - Private constructors are only compatible with the same private constructor
111    /// - Protected constructors are compatible with protected or public targets
112    pub fn ctor_access_compatible(&self, source: TypeId, target: TypeId) -> bool {
113        // Public constructors are compatible with everything
114        if !self.is_private_ctor(source) && !self.is_protected_ctor(source) {
115            return true;
116        }
117
118        // Private constructors are only compatible with the same private constructor
119        if self.is_private_ctor(source) {
120            if self.is_private_ctor(target) {
121                source == target
122            } else {
123                false
124            }
125        } else {
126            // Protected constructors are compatible with protected or public targets
127            !self.is_private_ctor(target)
128        }
129    }
130
131    /// Check if a type should be treated as a constructor in `new` expressions.
132    ///
133    /// This determines if a type can be used with the `new` operator.
134    pub fn is_newable(&self, type_id: TypeId) -> bool {
135        self.has_construct_sig(type_id)
136    }
137
138    // =========================================================================
139    // Mixin Call Return Type Refinement
140    // =========================================================================
141
142    /// Refine the return type of a mixin call by merging base constructor properties.
143    ///
144    /// When a mixin function returns a class that extends a base parameter,
145    /// this function merges the base type's instance type and static properties
146    /// into the return type.
147    pub(crate) fn refine_mixin_call_return_type(
148        &mut self,
149        callee_idx: NodeIndex,
150        arg_types: &[TypeId],
151        return_type: TypeId,
152    ) -> TypeId {
153        if return_type == TypeId::ANY || return_type == TypeId::ERROR {
154            return return_type;
155        }
156
157        let Some(func_decl_idx) = self.function_decl_from_callee(callee_idx) else {
158            return return_type;
159        };
160        let Some(func_node) = self.ctx.arena.get(func_decl_idx) else {
161            return return_type;
162        };
163        let Some(func) = self.ctx.arena.get_function(func_node) else {
164            return return_type;
165        };
166        let Some(class_expr_idx) = self.returned_class_expression(func.body) else {
167            return return_type;
168        };
169        let Some(base_param_index) = self.mixin_base_param_index(class_expr_idx, func) else {
170            return return_type;
171        };
172        let Some(&base_arg_type) = arg_types.get(base_param_index) else {
173            return return_type;
174        };
175        if matches!(base_arg_type, TypeId::ANY | TypeId::ERROR) {
176            return return_type;
177        }
178
179        let factory = self.ctx.types.factory();
180        let mut refined_return = factory.intersection(vec![return_type, base_arg_type]);
181
182        if let Some(base_instance_type) = self.instance_type_from_constructor_type(base_arg_type) {
183            refined_return = self
184                .merge_base_instance_into_constructor_return(refined_return, base_instance_type);
185        }
186
187        let base_props = self.static_properties_from_type(base_arg_type);
188        if !base_props.is_empty() {
189            refined_return = self.merge_base_constructor_properties_into_constructor_return(
190                refined_return,
191                &base_props,
192            );
193        }
194
195        refined_return
196    }
197
198    fn mixin_base_param_index(
199        &self,
200        class_expr_idx: NodeIndex,
201        func: &tsz_parser::parser::node::FunctionData,
202    ) -> Option<usize> {
203        let class_data = self.ctx.arena.get_class_at(class_expr_idx)?;
204        let heritage_clauses = class_data.heritage_clauses.as_ref()?;
205
206        let mut base_name = None;
207        for &clause_idx in &heritage_clauses.nodes {
208            let heritage = self.ctx.arena.get_heritage_clause_at(clause_idx)?;
209            if heritage.token != SyntaxKind::ExtendsKeyword as u16 {
210                continue;
211            }
212            let &type_idx = heritage.types.nodes.first()?;
213            let type_node = self.ctx.arena.get(type_idx)?;
214            let expr_idx =
215                if let Some(expr_type_args) = self.ctx.arena.get_expr_type_args(type_node) {
216                    expr_type_args.expression
217                } else {
218                    type_idx
219                };
220            let expr_node = self.ctx.arena.get(expr_idx)?;
221            if expr_node.kind != SyntaxKind::Identifier as u16 {
222                return None;
223            }
224            let ident = self.ctx.arena.get_identifier(expr_node)?;
225            base_name = Some(ident.escaped_text.clone());
226            break;
227        }
228
229        let base_name = base_name?;
230        let mut arg_index = 0usize;
231        for &param_idx in &func.parameters.nodes {
232            let param = self.ctx.arena.get_parameter_at(param_idx)?;
233            let ident = self.ctx.arena.get_identifier_at(param.name)?;
234            if ident.escaped_text == "this" {
235                continue;
236            }
237            if ident.escaped_text == base_name {
238                return Some(arg_index);
239            }
240            arg_index += 1;
241        }
242
243        None
244    }
245
246    // =========================================================================
247    // Instance Type Extraction
248    // =========================================================================
249
250    pub(crate) fn instance_type_from_constructor_type(
251        &mut self,
252        ctor_type: TypeId,
253    ) -> Option<TypeId> {
254        let mut visited = FxHashSet::default();
255        self.instance_type_from_constructor_type_inner(ctor_type, &mut visited)
256    }
257
258    fn instance_type_from_constructor_type_inner(
259        &mut self,
260        ctor_type: TypeId,
261        visited: &mut FxHashSet<TypeId>,
262    ) -> Option<TypeId> {
263        if ctor_type == TypeId::NULL {
264            return Some(TypeId::NULL);
265        }
266        if ctor_type == TypeId::ERROR {
267            return None;
268        }
269        if ctor_type == TypeId::ANY {
270            return Some(TypeId::ANY);
271        }
272
273        let mut current = ctor_type;
274        let mut iterations = 0;
275        loop {
276            iterations += 1;
277            if iterations > MAX_TREE_WALK_ITERATIONS {
278                return None;
279            }
280            if !visited.insert(current) {
281                return None;
282            }
283            current = self.evaluate_application_type(current);
284            // Resolve Lazy types so the classifier can see construct signatures.
285            let resolved = self.resolve_lazy_type(current);
286            if resolved != current {
287                current = resolved;
288            }
289            match classify_for_instance_type(self.ctx.types, current) {
290                InstanceTypeKind::Callable(shape_id) => {
291                    let instance_type = tsz_solver::type_queries::get_construct_return_type_union(
292                        self.ctx.types,
293                        shape_id,
294                    )?;
295                    return Some(self.resolve_type_for_property_access(instance_type));
296                }
297                InstanceTypeKind::Function(shape_id) => {
298                    let shape = self.ctx.types.function_shape(shape_id);
299                    if !shape.is_constructor {
300                        return None;
301                    }
302                    return Some(self.resolve_type_for_property_access(shape.return_type));
303                }
304                InstanceTypeKind::Intersection(members) => {
305                    let instance_types: Vec<TypeId> = members
306                        .into_iter()
307                        .filter_map(|m| self.instance_type_from_constructor_type_inner(m, visited))
308                        .collect();
309                    if instance_types.is_empty() {
310                        return None;
311                    }
312                    let instance_type =
313                        tsz_solver::utils::intersection_or_single(self.ctx.types, instance_types);
314                    return Some(self.resolve_type_for_property_access(instance_type));
315                }
316                InstanceTypeKind::Union(members) => {
317                    let instance_types: Vec<TypeId> = members
318                        .into_iter()
319                        .filter_map(|m| self.instance_type_from_constructor_type_inner(m, visited))
320                        .collect();
321                    if instance_types.is_empty() {
322                        return None;
323                    }
324                    let instance_type =
325                        tsz_solver::utils::union_or_single(self.ctx.types, instance_types);
326                    return Some(self.resolve_type_for_property_access(instance_type));
327                }
328                InstanceTypeKind::Readonly(inner) => {
329                    return self.instance_type_from_constructor_type_inner(inner, visited);
330                }
331                InstanceTypeKind::TypeParameter { constraint } => {
332                    let constraint = constraint?;
333                    current = constraint;
334                }
335                InstanceTypeKind::SymbolRef(sym_ref) => {
336                    // Symbol reference (class name or typeof expression)
337                    // Resolve to the class instance type
338                    use tsz_binder::SymbolId;
339                    let sym_id = SymbolId(sym_ref.0);
340                    if let Some(instance_type) = self.class_instance_type_from_symbol(sym_id) {
341                        return Some(self.resolve_type_for_property_access(instance_type));
342                    }
343                    // Not a class symbol - might be a variable holding a constructor
344                    // Try to get its type and recurse
345                    let var_type = self.get_type_of_symbol(sym_id);
346                    if var_type != TypeId::ERROR && var_type != current {
347                        current = var_type;
348                    } else {
349                        return None;
350                    }
351                }
352                InstanceTypeKind::NeedsEvaluation => {
353                    let evaluated = self.evaluate_type_with_env(current);
354                    if evaluated == current {
355                        return None;
356                    }
357                    current = evaluated;
358                }
359                InstanceTypeKind::NotConstructor => return None,
360            }
361        }
362    }
363
364    // =========================================================================
365    // Constructor Return Type Merging
366    // =========================================================================
367
368    fn merge_base_instance_into_constructor_return(
369        &mut self,
370        ctor_type: TypeId,
371        base_instance_type: TypeId,
372    ) -> TypeId {
373        // Resolve Lazy types before classification.
374        let ctor_type = {
375            let resolved = self.resolve_lazy_type(ctor_type);
376            if resolved != ctor_type {
377                resolved
378            } else {
379                ctor_type
380            }
381        };
382        match classify_for_constructor_return_merge(self.ctx.types, ctor_type) {
383            ConstructorReturnMergeKind::Callable(shape_id) => {
384                let shape = self.ctx.types.callable_shape(shape_id);
385                if shape.construct_signatures.is_empty() {
386                    return ctor_type;
387                }
388                let mut new_shape = (*shape).clone();
389                new_shape.construct_signatures = shape
390                    .construct_signatures
391                    .iter()
392                    .map(|sig| {
393                        let mut updated = sig.clone();
394                        updated.return_type = self
395                            .ctx
396                            .types
397                            .factory()
398                            .intersection(vec![updated.return_type, base_instance_type]);
399                        updated
400                    })
401                    .collect();
402                self.ctx.types.factory().callable(new_shape)
403            }
404            ConstructorReturnMergeKind::Function(shape_id) => {
405                let shape = self.ctx.types.function_shape(shape_id);
406                if !shape.is_constructor {
407                    return ctor_type;
408                }
409                let mut new_shape = (*shape).clone();
410                new_shape.return_type = self
411                    .ctx
412                    .types
413                    .factory()
414                    .intersection(vec![new_shape.return_type, base_instance_type]);
415                self.ctx.types.factory().function(new_shape)
416            }
417            ConstructorReturnMergeKind::Intersection(members) => {
418                let mut updated_members = Vec::with_capacity(members.len());
419                let mut changed = false;
420                for member in members {
421                    let updated = self
422                        .merge_base_instance_into_constructor_return(member, base_instance_type);
423                    if updated != member {
424                        changed = true;
425                    }
426                    updated_members.push(updated);
427                }
428                if changed {
429                    self.ctx.types.factory().intersection(updated_members)
430                } else {
431                    ctor_type
432                }
433            }
434            ConstructorReturnMergeKind::Other => ctor_type,
435        }
436    }
437
438    fn merge_base_constructor_properties_into_constructor_return(
439        &mut self,
440        ctor_type: TypeId,
441        base_props: &rustc_hash::FxHashMap<Atom, tsz_solver::PropertyInfo>,
442    ) -> TypeId {
443        use rustc_hash::FxHashMap;
444        if base_props.is_empty() {
445            return ctor_type;
446        }
447
448        // Resolve Lazy types before classification.
449        let ctor_type = {
450            let resolved = self.resolve_lazy_type(ctor_type);
451            if resolved != ctor_type {
452                resolved
453            } else {
454                ctor_type
455            }
456        };
457        match classify_for_constructor_return_merge(self.ctx.types, ctor_type) {
458            ConstructorReturnMergeKind::Callable(shape_id) => {
459                let shape = self.ctx.types.callable_shape(shape_id);
460                let mut prop_map: FxHashMap<Atom, tsz_solver::PropertyInfo> = shape
461                    .properties
462                    .iter()
463                    .map(|prop| (prop.name, prop.clone()))
464                    .collect();
465                for (name, prop) in base_props {
466                    prop_map.entry(*name).or_insert_with(|| prop.clone());
467                }
468                let mut new_shape = (*shape).clone();
469                new_shape.properties = prop_map.into_values().collect();
470                self.ctx.types.factory().callable(new_shape)
471            }
472            ConstructorReturnMergeKind::Intersection(members) => {
473                let mut updated_members = Vec::with_capacity(members.len());
474                let mut changed = false;
475                for member in members {
476                    let updated = self.merge_base_constructor_properties_into_constructor_return(
477                        member, base_props,
478                    );
479                    if updated != member {
480                        changed = true;
481                    }
482                    updated_members.push(updated);
483                }
484                if changed {
485                    self.ctx.types.factory().intersection(updated_members)
486                } else {
487                    ctor_type
488                }
489            }
490            ConstructorReturnMergeKind::Function(_) | ConstructorReturnMergeKind::Other => {
491                ctor_type
492            }
493        }
494    }
495
496    // =========================================================================
497    // Abstract Constructor Assignability
498    // =========================================================================
499
500    pub(crate) fn abstract_constructor_assignability_override(
501        &self,
502        source: TypeId,
503        target: TypeId,
504        _env: Option<&tsz_solver::TypeEnvironment>,
505    ) -> Option<bool> {
506        // Helper to check if a TypeId is abstract
507        // This handles both TypeQuery types (before resolution) and resolved Callable types
508        let is_abstract_type = |type_id: TypeId| -> bool {
509            // First check the cached set (handles resolved types)
510            if self.is_abstract_ctor(type_id) {
511                return true;
512            }
513
514            // Let solver unwrap application/type-query chains first.
515            match resolve_abstract_constructor_anchor(self.ctx.types, type_id) {
516                AbstractConstructorAnchor::TypeQuery(sym_ref) => {
517                    if let Some(symbol) =
518                        self.ctx.binder.get_symbol(tsz_binder::SymbolId(sym_ref.0))
519                    {
520                        symbol.flags & symbol_flags::ABSTRACT != 0
521                    } else {
522                        false
523                    }
524                }
525                AbstractConstructorAnchor::CallableType(callable_type) => {
526                    self.is_abstract_ctor(callable_type)
527                }
528                _ => false,
529            }
530        };
531
532        let source_is_abstract = is_abstract_type(source);
533        let target_is_abstract = is_abstract_type(target);
534
535        // Case 1: Source is concrete, target is abstract -> Allow (concrete can be assigned to abstract)
536        if !source_is_abstract && target_is_abstract {
537            // Let the structural subtype checker handle it
538            return None;
539        }
540
541        // Case 2: Source is abstract, target is also abstract -> Let structural check handle it
542        if source_is_abstract && target_is_abstract {
543            return None;
544        }
545
546        // Case 3: Source is abstract, target is NOT abstract -> Reject
547        if source_is_abstract && !target_is_abstract {
548            let target_is_constructor = self.has_construct_sig(target);
549            if target_is_constructor {
550                return Some(false);
551            }
552        }
553
554        None
555    }
556
557    // =========================================================================
558    // Constructor Access Level
559    // =========================================================================
560
561    fn constructor_access_level(
562        &self,
563        type_id: TypeId,
564        env: Option<&tsz_solver::TypeEnvironment>,
565        visited: &mut FxHashSet<TypeId>,
566    ) -> Option<MemberAccessLevel> {
567        if !visited.insert(type_id) {
568            return None;
569        }
570
571        if self.is_private_ctor(type_id) {
572            return Some(MemberAccessLevel::Private);
573        }
574        if self.is_protected_ctor(type_id) {
575            return Some(MemberAccessLevel::Protected);
576        }
577
578        match classify_for_constructor_access(self.ctx.types, type_id) {
579            ConstructorAccessKind::SymbolRef(symbol) => self
580                .resolve_type_env_symbol(symbol, env)
581                .and_then(|resolved| {
582                    if resolved != type_id {
583                        self.constructor_access_level(resolved, env, visited)
584                    } else {
585                        None
586                    }
587                }),
588            ConstructorAccessKind::Application(app_id) => {
589                let app = self.ctx.types.type_application(app_id);
590                if app.base != type_id {
591                    self.constructor_access_level(app.base, env, visited)
592                } else {
593                    None
594                }
595            }
596            ConstructorAccessKind::Other => None,
597        }
598    }
599
600    fn constructor_access_level_for_type(
601        &self,
602        type_id: TypeId,
603        env: Option<&tsz_solver::TypeEnvironment>,
604    ) -> Option<MemberAccessLevel> {
605        let mut visited = FxHashSet::default();
606        self.constructor_access_level(type_id, env, &mut visited)
607    }
608
609    pub(crate) fn constructor_accessibility_mismatch(
610        &self,
611        source: TypeId,
612        target: TypeId,
613        env: Option<&tsz_solver::TypeEnvironment>,
614    ) -> Option<(Option<MemberAccessLevel>, Option<MemberAccessLevel>)> {
615        let source_level = self.constructor_access_level_for_type(source, env);
616        let target_level = self.constructor_access_level_for_type(target, env);
617
618        if source_level.is_none() && target_level.is_none() {
619            return None;
620        }
621
622        let source_rank = Self::constructor_access_rank(source_level);
623        let target_rank = Self::constructor_access_rank(target_level);
624        if source_rank > target_rank {
625            return Some((source_level, target_level));
626        }
627        None
628    }
629
630    pub(crate) fn constructor_accessibility_override(
631        &self,
632        source: TypeId,
633        target: TypeId,
634        env: Option<&tsz_solver::TypeEnvironment>,
635    ) -> Option<bool> {
636        if self
637            .constructor_accessibility_mismatch(source, target, env)
638            .is_some()
639        {
640            return Some(false);
641        }
642        None
643    }
644
645    pub(crate) fn constructor_accessibility_mismatch_for_assignment(
646        &self,
647        left_idx: NodeIndex,
648        right_idx: NodeIndex,
649    ) -> Option<(Option<MemberAccessLevel>, Option<MemberAccessLevel>)> {
650        let source_sym = self.class_symbol_from_expression(right_idx)?;
651        let target_sym = self.assignment_target_class_symbol(left_idx)?;
652        let source_level = self.class_constructor_access_level(source_sym);
653        let target_level = self.class_constructor_access_level(target_sym);
654        if source_level.is_none() && target_level.is_none() {
655            return None;
656        }
657        if Self::constructor_access_rank(source_level) > Self::constructor_access_rank(target_level)
658        {
659            return Some((source_level, target_level));
660        }
661        None
662    }
663
664    pub(crate) fn constructor_accessibility_mismatch_for_var_decl(
665        &self,
666        var_decl: &tsz_parser::parser::node::VariableDeclarationData,
667    ) -> Option<(Option<MemberAccessLevel>, Option<MemberAccessLevel>)> {
668        if var_decl.initializer.is_none() {
669            return None;
670        }
671        let source_sym = self.class_symbol_from_expression(var_decl.initializer)?;
672        let target_sym = self.class_symbol_from_type_annotation(var_decl.type_annotation)?;
673        let source_level = self.class_constructor_access_level(source_sym);
674        let target_level = self.class_constructor_access_level(target_sym);
675        if source_level.is_none() && target_level.is_none() {
676            return None;
677        }
678        if Self::constructor_access_rank(source_level) > Self::constructor_access_rank(target_level)
679        {
680            return Some((source_level, target_level));
681        }
682        None
683    }
684
685    // =========================================================================
686    // Helper Methods
687    // =========================================================================
688
689    fn resolve_type_env_symbol(
690        &self,
691        symbol: tsz_solver::SymbolRef,
692        env: Option<&tsz_solver::TypeEnvironment>,
693    ) -> Option<TypeId> {
694        if let Some(env) = env {
695            return env.get(symbol);
696        }
697        let env_ref = self.ctx.type_env.borrow();
698        env_ref.get(symbol)
699    }
700
701    /// Check constructor accessibility for a `new` expression.
702    ///
703    /// Emits TS2673 for private constructors and TS2674 for protected constructors
704    /// when called from an invalid scope (outside the class or hierarchy).
705    pub(crate) fn check_constructor_accessibility_for_new(
706        &mut self,
707        new_expr_idx: tsz_parser::parser::NodeIndex,
708        constructor_type: TypeId,
709    ) {
710        // Skip check for `any` and `error` types
711        if constructor_type == TypeId::ANY || constructor_type == TypeId::ERROR {
712            return;
713        }
714
715        // Check if constructor is private or protected
716        let is_private = self.is_private_ctor(constructor_type);
717        let is_protected = self.is_protected_ctor(constructor_type);
718
719        if !is_private && !is_protected {
720            return; // Public constructor - no restrictions
721        }
722
723        // Find the class symbol being instantiated
724        let class_sym = match self.class_symbol_from_new_expr(new_expr_idx) {
725            Some(sym) => sym,
726            None => return, // Can't determine class - skip check
727        };
728
729        // Find the enclosing class by walking up the AST
730        let enclosing_class_sym = match self.find_enclosing_class_for_new(new_expr_idx) {
731            Some(sym) => sym,
732            None => {
733                // No enclosing class - this is an external instantiation
734                // Emit error based on constructor visibility
735                self.emit_constructor_access_error(new_expr_idx, class_sym, is_private);
736                return;
737            }
738        };
739
740        // Check if we're in the same class
741        if enclosing_class_sym == class_sym {
742            // Same class - always allowed (even for private constructors)
743            return;
744        }
745
746        // Check if we're in a subclass
747        let is_subclass = self
748            .ctx
749            .inheritance_graph
750            .is_derived_from(enclosing_class_sym, class_sym);
751
752        if is_private {
753            // Private constructor: only accessible within the same class
754            if enclosing_class_sym != class_sym {
755                self.emit_constructor_access_error(new_expr_idx, class_sym, true);
756            }
757        } else if is_protected {
758            // Protected constructor: accessible within the class hierarchy
759            if !is_subclass {
760                self.emit_constructor_access_error(new_expr_idx, class_sym, false);
761            }
762        }
763    }
764
765    /// Find the class symbol from a `new` expression node.
766    fn class_symbol_from_new_expr(&self, idx: tsz_parser::parser::NodeIndex) -> Option<SymbolId> {
767        use tsz_binder::symbol_flags;
768
769        let call_expr = self.ctx.arena.get_call_expr_at(idx)?;
770
771        // Get the expression being instantiated
772        let ident = self.ctx.arena.get_identifier_at(call_expr.expression)?;
773
774        // Try to find the symbol
775        let sym_id = self
776            .ctx
777            .binder
778            .get_node_symbol(call_expr.expression)
779            .or_else(|| self.ctx.binder.file_locals.get(&ident.escaped_text))
780            .or_else(|| {
781                self.ctx
782                    .binder
783                    .get_symbols()
784                    .find_by_name(&ident.escaped_text)
785            })?;
786
787        let symbol = self.ctx.binder.get_symbol(sym_id)?;
788
789        // Verify it's a class
790        (symbol.flags & symbol_flags::CLASS != 0).then_some(sym_id)
791    }
792
793    /// Find the enclosing class symbol by walking up the AST parent chain.
794    ///
795    /// This is similar to the logic in `super_checker.rs` but returns the class symbol.
796    fn find_enclosing_class_for_new(&self, idx: tsz_parser::parser::NodeIndex) -> Option<SymbolId> {
797        use tsz_parser::parser::syntax_kind_ext;
798
799        let mut current = idx;
800
801        while let Some(ext) = self.ctx.arena.get_extended(current) {
802            // Check if parent exists and get the node
803            let parent_idx = ext.parent;
804            if parent_idx.is_none() {
805                break;
806            }
807            let Some(parent_node) = self.ctx.arena.get(parent_idx) else {
808                break;
809            };
810
811            // Check for Class Declaration or Expression
812            if parent_node.kind == syntax_kind_ext::CLASS_DECLARATION
813                || parent_node.kind == syntax_kind_ext::CLASS_EXPRESSION
814            {
815                let class_data = self.ctx.arena.get_class(parent_node)?;
816
817                // PRIORITY 1: Look up symbol on the Class Name (Identifier)
818                // This is where Binder attaches symbols for named classes
819                let name_idx = class_data.name;
820                if let Some(sym_id) = self.ctx.binder.get_node_symbol(name_idx) {
821                    return Some(sym_id);
822                }
823
824                // PRIORITY 2: Look up symbol on the Class Node itself
825                // This handles:
826                // 1. Default exports: `export default class { ... }`
827                // 2. Anonymous class expressions: `const C = class { ... }` (sometimes)
828                if let Some(sym_id) = self.ctx.binder.get_node_symbol(parent_idx) {
829                    return Some(sym_id);
830                }
831
832                // If we found a class node but couldn't resolve its symbol,
833                // we can't perform accessibility checks against it.
834                return None;
835            }
836
837            current = parent_idx;
838        }
839
840        None
841    }
842
843    /// Emit the appropriate constructor accessibility error.
844    fn emit_constructor_access_error(
845        &mut self,
846        idx: tsz_parser::parser::NodeIndex,
847        class_sym: SymbolId,
848        is_private: bool,
849    ) {
850        use crate::diagnostics::diagnostic_codes;
851
852        let class_name = self.get_symbol_display_name(class_sym);
853
854        if is_private {
855            // TS2673: Constructor of class 'X' is private
856            let message = format!(
857                "Constructor of class '{class_name}' is private and only accessible within the class declaration."
858            );
859            self.error_at_node(idx, &message, diagnostic_codes::CONSTRUCTOR_OF_CLASS_IS_PRIVATE_AND_ONLY_ACCESSIBLE_WITHIN_THE_CLASS_DECLARATION);
860        } else {
861            // TS2674: Constructor of class 'X' is protected
862            let message = format!(
863                "Constructor of class '{class_name}' is protected and only accessible within the class declaration."
864            );
865            self.error_at_node(idx, &message, diagnostic_codes::CONSTRUCTOR_OF_CLASS_IS_PROTECTED_AND_ONLY_ACCESSIBLE_WITHIN_THE_CLASS_DECLARATI);
866        }
867    }
868
869    /// Get the display name of a symbol for error messages.
870    fn get_symbol_display_name(&self, sym_id: SymbolId) -> String {
871        if let Some(symbol) = self.ctx.binder.get_symbol(sym_id) {
872            symbol.escaped_name.clone()
873        } else {
874            "<unknown>".to_string()
875        }
876    }
877}