Skip to main content

tsz_core/
lib.rs

1use once_cell::sync::Lazy;
2use rustc_hash::FxHashMap;
3use std::sync::Mutex;
4use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
5
6type LibFileCache = FxHashMap<(String, u64), Arc<lib_loader::LibFile>>;
7
8// Global cache for parsed lib files to avoid re-parsing lib.d.ts per test
9// Key: (file_name, content_hash), Value: Arc<LibFile>
10static LIB_FILE_CACHE: Lazy<Mutex<LibFileCache>> = Lazy::new(|| Mutex::new(FxHashMap::default()));
11
12/// Simple hash function for lib file content
13fn hash_lib_content(content: &str) -> u64 {
14    use std::collections::hash_map::DefaultHasher;
15    use std::hash::{Hash, Hasher};
16    let mut hasher = DefaultHasher::new();
17    content.hash(&mut hasher);
18    hasher.finish()
19}
20
21/// Get or create a cached lib file. This avoids re-parsing lib.d.ts for every test.
22fn get_or_create_lib_file(file_name: String, source_text: String) -> Arc<lib_loader::LibFile> {
23    let content_hash = hash_lib_content(&source_text);
24    let cache_key = (file_name.clone(), content_hash);
25
26    // Try to get from cache
27    {
28        let cache = LIB_FILE_CACHE.lock().unwrap();
29        if let Some(cached) = cache.get(&cache_key) {
30            return Arc::clone(cached);
31        }
32    }
33
34    // Not in cache - parse and bind
35    let mut lib_parser = ParserState::new(file_name.clone(), source_text);
36    let source_file_idx = lib_parser.parse_source_file();
37
38    let mut lib_binder = BinderState::new();
39    lib_binder.bind_source_file(lib_parser.get_arena(), source_file_idx);
40
41    let arena = Arc::new(lib_parser.into_arena());
42    let binder = Arc::new(lib_binder);
43
44    let lib_file = Arc::new(lib_loader::LibFile::new(file_name, arena, binder));
45
46    // Store in cache
47    {
48        let mut cache = LIB_FILE_CACHE.lock().unwrap();
49        cache.insert(cache_key, Arc::clone(&lib_file));
50    }
51
52    lib_file
53}
54
55// Shared test fixtures for reduced allocation overhead
56#[cfg(test)]
57#[path = "../tests/test_fixtures.rs"]
58pub mod test_fixtures;
59
60// Re-export foundation types from tsz-common workspace crate
61pub use tsz_common::interner;
62pub use tsz_common::interner::{Atom, Interner, ShardedInterner};
63#[cfg(test)]
64#[path = "../tests/interner_tests.rs"]
65mod interner_tests;
66
67pub use tsz_common::common;
68pub use tsz_common::common::{ModuleKind, NewLineKind, ScriptTarget};
69
70pub use tsz_common::limits;
71
72// Scanner module - re-exported from tsz-scanner workspace crate
73pub use tsz_scanner as scanner;
74pub use tsz_scanner::char_codes;
75pub use tsz_scanner::scanner_impl;
76pub use tsz_scanner::scanner_impl::{ScannerState, TokenFlags};
77pub use tsz_scanner::*;
78#[cfg(test)]
79#[path = "../tests/scanner_impl_tests.rs"]
80mod scanner_impl_tests;
81#[cfg(test)]
82#[path = "../tests/scanner_tests.rs"]
83mod scanner_tests;
84
85// Parser AST types - re-exported from tsz-parser workspace crate
86pub use tsz_parser::parser;
87
88// Syntax utilities - re-exported from tsz-parser workspace crate
89pub use tsz_parser::syntax;
90
91// Parser - Cache-optimized parser using NodeArena (Phase 0.1)
92#[cfg(test)]
93#[path = "../tests/parser_state_tests.rs"]
94mod parser_state_tests;
95
96// TS1038 - declare modifier in ambient context tests
97#[cfg(test)]
98#[path = "../tests/parser_ts1038_tests.rs"]
99mod parser_ts1038_tests;
100
101// Control flow validation tests (TS1104, TS1105)
102#[cfg(test)]
103#[path = "../tests/control_flow_validation_tests.rs"]
104mod control_flow_validation_tests;
105
106// Regex flag error detection tests
107#[cfg(test)]
108#[path = "../tests/regex_flag_tests.rs"]
109mod regex_flag_tests;
110
111// Binder types and implementation - re-exported from tsz-binder workspace crate
112pub use tsz_binder as binder;
113
114// BinderState - Binder using NodeArena (Phase 0.1)
115#[cfg(test)]
116#[path = "../tests/binder_state_tests.rs"]
117mod binder_state_tests;
118
119// Module Resolution Debugging - re-exported from tsz-binder
120pub use tsz_binder::module_resolution_debug;
121
122// Lib Loader - re-exported from tsz-binder
123pub use tsz_binder::lib_loader;
124
125// Checker types and implementation (Phase 5) - re-exported from tsz-checker workspace crate
126pub use tsz_checker as checker;
127
128#[cfg(test)]
129#[path = "../tests/checker_state_tests.rs"]
130mod checker_state_tests;
131
132#[cfg(test)]
133#[path = "../tests/variable_redeclaration_tests.rs"]
134mod variable_redeclaration_tests;
135
136#[cfg(test)]
137#[path = "../tests/strict_mode_and_module_tests.rs"]
138mod strict_mode_and_module_tests;
139
140#[cfg(test)]
141#[path = "../tests/overload_compatibility_tests.rs"]
142mod overload_compatibility_tests;
143
144// Cross-file module resolution tests
145#[cfg(test)]
146#[path = "../tests/module_resolution_tests.rs"]
147mod module_resolution_tests;
148
149pub use checker::state::{CheckerState, MAX_CALL_DEPTH, MAX_INSTANTIATION_DEPTH};
150
151// Emitter - re-exported from tsz-emitter workspace crate
152pub use tsz_emitter::emitter;
153#[cfg(test)]
154#[path = "../tests/transform_api_tests.rs"]
155mod transform_api_tests;
156
157// Printer - re-exported from tsz-emitter workspace crate
158pub use tsz_emitter::output::printer;
159
160// Safe string slice utilities - re-exported from tsz-emitter workspace crate
161pub use tsz_emitter::safe_slice;
162#[cfg(test)]
163#[path = "../tests/printer_tests.rs"]
164mod printer_tests;
165
166// Span - Source location tracking (byte offsets)
167pub use tsz_common::span;
168
169// SourceFile - Owns source text and provides &str references
170pub mod source_file;
171
172// Diagnostics - Error collection, formatting, and reporting
173pub mod diagnostics;
174
175// Enums - re-exported from tsz-emitter workspace crate
176pub use tsz_emitter::enums;
177
178// Parallel processing with Rayon (Phase 0.4)
179pub mod parallel;
180
181// Comment preservation (Phase 6.3)
182pub use tsz_common::comments;
183#[cfg(test)]
184#[path = "../tests/comments_tests.rs"]
185mod comments_tests;
186
187// Source Map generation (Phase 6.2)
188pub use tsz_common::source_map;
189#[cfg(test)]
190#[path = "../tests/source_map_test_utils.rs"]
191mod source_map_test_utils;
192#[cfg(test)]
193#[path = "../tests/source_map_tests_1.rs"]
194mod source_map_tests_1;
195#[cfg(test)]
196#[path = "../tests/source_map_tests_2.rs"]
197mod source_map_tests_2;
198#[cfg(test)]
199#[path = "../tests/source_map_tests_3.rs"]
200mod source_map_tests_3;
201#[cfg(test)]
202#[path = "../tests/source_map_tests_4.rs"]
203mod source_map_tests_4;
204
205// SourceWriter - re-exported from tsz-emitter workspace crate
206pub use tsz_emitter::output::source_writer;
207#[cfg(test)]
208#[path = "../tests/source_writer_tests.rs"]
209mod source_writer_tests;
210
211// Context (EmitContext, TransformContext) - re-exported from tsz-emitter workspace crate
212pub use tsz_emitter::context;
213
214// LoweringPass - re-exported from tsz-emitter workspace crate
215pub use tsz_emitter::lowering;
216
217// Declaration file emitter - re-exported from tsz-emitter workspace crate
218pub use tsz_emitter::declaration_emitter;
219
220// JavaScript transforms - re-exported from tsz-emitter workspace crate
221pub use tsz_emitter::transforms;
222
223// Query-based Structural Solver (Phase 7.5)
224pub use tsz_solver;
225
226// LSP (Language Server Protocol) support - re-exported from tsz-lsp workspace crate
227pub use tsz_lsp as lsp;
228
229// Test Harness - Infrastructure for unit and conformance tests
230#[cfg(test)]
231#[path = "../tests/test_harness.rs"]
232mod test_harness;
233
234// Isolated Test Runner - Process-based test execution with resource limits
235#[cfg(test)]
236#[path = "../tests/isolated_test_runner.rs"]
237mod isolated_test_runner;
238
239// Compiler configuration types (shared between core and CLI)
240pub mod config;
241
242// Re-exports from tsz-cli crate (when available as a dependency)
243// CLI code has been moved to crates/tsz-cli/
244
245// Re-exports from tsz-wasm crate (when available as a dependency)
246// WASM integration code has been moved to crates/tsz-wasm/
247
248// Module Resolution Infrastructure (non-wasm targets only - requires file system access)
249#[cfg(not(target_arch = "wasm32"))]
250pub mod module_resolver;
251#[cfg(not(target_arch = "wasm32"))]
252mod module_resolver_helpers;
253#[cfg(not(target_arch = "wasm32"))]
254pub use module_resolver::{ModuleExtension, ModuleResolver, ResolutionFailure, ResolvedModule};
255
256// Import/Export Tracking
257pub mod imports;
258pub use imports::{ImportDeclaration, ImportKind, ImportTracker, ImportedBinding};
259
260pub mod exports;
261pub use exports::{ExportDeclaration, ExportKind, ExportTracker, ExportedBinding};
262
263// Module Dependency Graph (non-wasm targets only - requires module_resolver)
264#[cfg(not(target_arch = "wasm32"))]
265pub mod module_graph;
266#[cfg(not(target_arch = "wasm32"))]
267pub use module_graph::{CircularDependency, ModuleGraph, ModuleId, ModuleInfo};
268
269// =============================================================================
270// Scanner Factory Function
271// =============================================================================
272
273/// Create a new scanner for the given source text.
274/// This is the wasm-bindgen entry point for creating scanners from JavaScript.
275#[wasm_bindgen(js_name = createScanner)]
276pub fn create_scanner(text: String, skip_trivia: bool) -> ScannerState {
277    ScannerState::new(text, skip_trivia)
278}
279
280// =============================================================================
281// Parser WASM Interface (High-Performance Parser)
282// =============================================================================
283
284use crate::binder::BinderState;
285use crate::checker::context::LibContext;
286use crate::context::emit::EmitContext;
287use crate::context::transform::TransformContext;
288use crate::emitter::{Printer, PrinterOptions};
289use crate::lib_loader::LibFile;
290use crate::lowering::LoweringPass;
291use crate::lsp::diagnostics::convert_diagnostic;
292use crate::lsp::position::{LineMap, Position, Range};
293use crate::lsp::resolver::ScopeCache;
294use crate::lsp::{
295    CodeActionContext, CodeActionKind, CodeActionProvider, Completions, DocumentSymbolProvider,
296    FindReferences, GoToDefinition, HoverProvider, ImportCandidate, ImportCandidateKind,
297    RenameProvider, SemanticTokensProvider, SignatureHelpProvider,
298};
299use crate::parser::ParserState;
300use serde::Deserialize;
301use std::sync::Arc;
302use tsz_solver::TypeInterner;
303
304#[derive(Deserialize)]
305#[serde(rename_all = "camelCase")]
306struct ImportCandidateInput {
307    module_specifier: String,
308    local_name: String,
309    kind: String,
310    export_name: Option<String>,
311    #[serde(default)]
312    is_type_only: bool,
313}
314
315#[derive(Deserialize)]
316#[serde(rename_all = "camelCase")]
317struct CodeActionContextInput {
318    #[serde(default)]
319    diagnostics: Vec<tsz_lsp::diagnostics::LspDiagnostic>,
320    #[serde(default)]
321    only: Option<Vec<CodeActionKind>>,
322    #[serde(default)]
323    import_candidates: Vec<ImportCandidateInput>,
324}
325
326/// Compiler options passed from JavaScript/WASM.
327/// Maps to TypeScript compiler options.
328#[derive(Deserialize, Clone, Debug, Default)]
329#[serde(rename_all = "camelCase")]
330struct CompilerOptions {
331    /// Enable all strict type checking options.
332    #[serde(default, deserialize_with = "deserialize_bool_option")]
333    strict: Option<bool>,
334
335    /// Raise error on expressions and declarations with an implied 'any' type.
336    #[serde(default, deserialize_with = "deserialize_bool_option")]
337    no_implicit_any: Option<bool>,
338
339    /// Enable strict null checks.
340    #[serde(default, deserialize_with = "deserialize_bool_option")]
341    strict_null_checks: Option<bool>,
342
343    /// Enable strict checking of function types.
344    #[serde(default, deserialize_with = "deserialize_bool_option")]
345    strict_function_types: Option<bool>,
346
347    /// Enable strict property initialization checks in classes.
348    #[serde(default, deserialize_with = "deserialize_bool_option")]
349    strict_property_initialization: Option<bool>,
350
351    /// Report error when not all code paths in function return a value.
352    #[serde(default, deserialize_with = "deserialize_bool_option")]
353    no_implicit_returns: Option<bool>,
354
355    /// Raise error on 'this' expressions with an implied 'any' type.
356    #[serde(default, deserialize_with = "deserialize_bool_option")]
357    no_implicit_this: Option<bool>,
358
359    /// Specify ECMAScript target version (accepts string like "ES5" or numeric).
360    #[serde(default, deserialize_with = "deserialize_target_or_module")]
361    target: Option<u32>,
362
363    /// When true, do not include any library files.
364    #[serde(default, deserialize_with = "deserialize_bool_option")]
365    no_lib: Option<bool>,
366}
367
368/// Deserialize a boolean option that can be a boolean, string, or comma-separated string.
369/// TypeScript test files often have boolean options like "true, false" for different test cases.
370fn deserialize_bool_option<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
371where
372    D: serde::Deserializer<'de>,
373{
374    use serde::de::{self, Visitor};
375
376    struct BoolOptionVisitor;
377
378    impl<'de> Visitor<'de> for BoolOptionVisitor {
379        type Value = Option<bool>;
380
381        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
382            formatter.write_str("a boolean, string, or comma-separated list of booleans")
383        }
384
385        fn visit_none<E>(self) -> Result<Self::Value, E>
386        where
387            E: de::Error,
388        {
389            Ok(None)
390        }
391
392        fn visit_unit<E>(self) -> Result<Self::Value, E>
393        where
394            E: de::Error,
395        {
396            Ok(None)
397        }
398
399        fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
400        where
401            E: de::Error,
402        {
403            Ok(Some(value))
404        }
405
406        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
407        where
408            E: de::Error,
409        {
410            // Handle comma-separated values like "true, false" - take the first value
411            let first_value = value.split(',').next().unwrap_or(value).trim();
412            let result = match first_value.to_lowercase().as_str() {
413                "true" | "1" => Some(true),
414                "false" | "0" => Some(false),
415                _ => None,
416            };
417            Ok(result)
418        }
419    }
420
421    deserializer.deserialize_any(BoolOptionVisitor)
422}
423
424/// Deserialize target/module values that can be either strings or numbers.
425/// TypeScript test files often use strings like "ES5", "ES2015", "CommonJS", etc.
426fn deserialize_target_or_module<'de, D>(deserializer: D) -> Result<Option<u32>, D::Error>
427where
428    D: serde::Deserializer<'de>,
429{
430    use serde::de::{self, Visitor};
431
432    struct TargetOrModuleVisitor;
433
434    impl<'de> Visitor<'de> for TargetOrModuleVisitor {
435        type Value = Option<u32>;
436
437        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
438            formatter.write_str("a string or integer representing target/module")
439        }
440
441        fn visit_none<E>(self) -> Result<Self::Value, E>
442        where
443            E: de::Error,
444        {
445            Ok(None)
446        }
447
448        fn visit_unit<E>(self) -> Result<Self::Value, E>
449        where
450            E: de::Error,
451        {
452            Ok(None)
453        }
454
455        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
456        where
457            E: de::Error,
458        {
459            Ok(Some(value as u32))
460        }
461
462        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
463        where
464            E: de::Error,
465        {
466            Ok(Some(value as u32))
467        }
468
469        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
470        where
471            E: de::Error,
472        {
473            // Parse string values to their TypeScript enum equivalents
474            // Note: For shared values like ES2015/ES6, we use the ScriptTarget value
475            // because both target and module use the same match arm
476            let result = match value.to_uppercase().as_str() {
477                // ScriptTarget values (0-10, 99) and ModuleKind-specific values
478                // Combined arms where ScriptTarget and ModuleKind share the same numeric value
479                "ES3" | "NONE" => 0,
480                "ES5" | "COMMONJS" => 1,
481                "ES2015" | "ES6" | "AMD" => 2,
482                "ES2016" | "UMD" => 3,
483                "ES2017" | "SYSTEM" => 4,
484                "ES2018" => 5,
485                "ES2019" => 6,
486                "ES2020" => 7,
487                "ES2021" => 8,
488                "ES2022" => 9,
489                "ES2023" => 10,
490                "ESNEXT" => 99,
491                "NODE16" => 100,
492                "NODENEXT" => 199,
493                _ => return Ok(None), // Unknown value, treat as unset
494            };
495            Ok(Some(result))
496        }
497    }
498
499    deserializer.deserialize_any(TargetOrModuleVisitor)
500}
501
502impl CompilerOptions {
503    /// Resolve a boolean option with strict mode fallback.
504    /// If the specific option is set, use it; otherwise, fall back to strict mode.
505    fn resolve_bool(&self, specific: Option<bool>, strict_implies: bool) -> bool {
506        if let Some(value) = specific {
507            return value;
508        }
509        if strict_implies {
510            return self.strict.unwrap_or(false);
511        }
512        false
513    }
514
515    /// Get the effective value for noImplicitAny.
516    pub fn get_no_implicit_any(&self) -> bool {
517        self.no_implicit_any.unwrap_or(self.strict.unwrap_or(true))
518    }
519
520    /// Get the effective value for strictNullChecks.
521    pub fn get_strict_null_checks(&self) -> bool {
522        self.resolve_bool(self.strict_null_checks, true)
523    }
524
525    /// Get the effective value for strictFunctionTypes.
526    pub fn get_strict_function_types(&self) -> bool {
527        self.resolve_bool(self.strict_function_types, true)
528    }
529
530    /// Get the effective value for strictPropertyInitialization.
531    pub fn get_strict_property_initialization(&self) -> bool {
532        self.resolve_bool(self.strict_property_initialization, true)
533    }
534
535    /// Get the effective value for noImplicitReturns.
536    pub fn get_no_implicit_returns(&self) -> bool {
537        self.resolve_bool(self.no_implicit_returns, false)
538    }
539
540    /// Get the effective value for noImplicitThis.
541    pub fn get_no_implicit_this(&self) -> bool {
542        self.resolve_bool(self.no_implicit_this, true)
543    }
544
545    fn resolve_target(&self) -> crate::checker::context::ScriptTarget {
546        use crate::checker::context::ScriptTarget;
547
548        match self.target {
549            Some(0) => ScriptTarget::ES3,
550            Some(1) => ScriptTarget::ES5,
551            Some(2) => ScriptTarget::ES2015,
552            Some(3) => ScriptTarget::ES2016,
553            Some(4) => ScriptTarget::ES2017,
554            Some(5) => ScriptTarget::ES2018,
555            Some(6) => ScriptTarget::ES2019,
556            Some(7) => ScriptTarget::ES2020,
557            Some(_) => ScriptTarget::ESNext,
558            None => ScriptTarget::default(),
559        }
560    }
561
562    /// Convert to `CheckerOptions` for type checking.
563    pub fn to_checker_options(&self) -> crate::checker::context::CheckerOptions {
564        let strict = self.strict.unwrap_or(false);
565        let strict_null_checks = self.get_strict_null_checks();
566        crate::checker::context::CheckerOptions {
567            strict,
568            no_implicit_any: self.get_no_implicit_any(),
569            no_implicit_returns: self.get_no_implicit_returns(),
570            strict_null_checks,
571            strict_function_types: self.get_strict_function_types(),
572            strict_property_initialization: self.get_strict_property_initialization(),
573            no_implicit_this: self.get_no_implicit_this(),
574            use_unknown_in_catch_variables: strict_null_checks,
575            isolated_modules: false,
576            no_unchecked_indexed_access: false,
577            strict_bind_call_apply: false,
578            exact_optional_property_types: false,
579            no_lib: self.no_lib.unwrap_or(false),
580            no_types_and_symbols: false,
581            target: self.resolve_target(),
582            module: crate::common::ModuleKind::None,
583            jsx_factory: "React.createElement".to_string(),
584            jsx_fragment_factory: "React.Fragment".to_string(),
585
586            es_module_interop: false,
587            allow_synthetic_default_imports: false,
588            allow_unreachable_code: None,
589            no_property_access_from_index_signature: false,
590            sound_mode: false,
591            experimental_decorators: false,
592            no_unused_locals: false,
593            no_unused_parameters: false,
594            always_strict: strict,
595            resolve_json_module: false, // WASM API: defaults to false
596            check_js: false,            // WASM API: defaults to false
597            no_resolve: false,
598            no_unchecked_side_effect_imports: false,
599            no_implicit_override: false,
600            jsx_mode: tsz_common::checker_options::JsxMode::None,
601            module_explicitly_set: false,
602            suppress_excess_property_errors: false,
603            suppress_implicit_any_index_errors: false,
604        }
605    }
606}
607
608impl TryFrom<ImportCandidateInput> for ImportCandidate {
609    type Error = JsValue;
610
611    fn try_from(input: ImportCandidateInput) -> Result<Self, Self::Error> {
612        let local_name = input.local_name;
613        let kind = match input.kind.as_str() {
614            "named" => {
615                let export_name = input.export_name.unwrap_or_else(|| local_name.clone());
616                ImportCandidateKind::Named { export_name }
617            }
618            "default" => ImportCandidateKind::Default,
619            "namespace" => ImportCandidateKind::Namespace,
620            other => {
621                return Err(JsValue::from_str(&format!(
622                    "Unsupported import candidate kind: {other}"
623                )));
624            }
625        };
626
627        Ok(Self {
628            module_specifier: input.module_specifier,
629            local_name,
630            kind,
631            is_type_only: input.is_type_only,
632        })
633    }
634}
635
636/// Opaque wrapper for transform directives across the wasm boundary.
637#[wasm_bindgen]
638pub struct WasmTransformContext {
639    inner: TransformContext,
640    target_es5: bool,
641    module_kind: ModuleKind,
642}
643
644#[wasm_bindgen]
645impl WasmTransformContext {
646    /// Get the number of transform directives generated.
647    #[wasm_bindgen(js_name = getCount)]
648    pub fn get_count(&self) -> usize {
649        self.inner.len()
650    }
651}
652
653/// High-performance parser using Node architecture (16 bytes/node).
654/// This is the optimized path for Phase 8 test suite evaluation.
655#[wasm_bindgen]
656pub struct Parser {
657    parser: ParserState,
658    source_file_idx: Option<parser::NodeIndex>,
659    binder: Option<BinderState>,
660    /// Local type interner for single-file checking.
661    /// For multi-file compilation, use `MergedProgram.type_interner` instead.
662    type_interner: TypeInterner,
663    /// Line map for LSP position conversion (lazy initialized)
664    line_map: Option<LineMap>,
665    /// Persistent cache for type checking results across LSP queries.
666    /// Invalidated when the file changes.
667    type_cache: Option<checker::TypeCache>,
668    /// Persistent cache for scope resolution across LSP queries.
669    /// Invalidated when the file changes.
670    scope_cache: ScopeCache,
671    /// Pre-loaded lib files (parsed and bound) for global type resolution
672    lib_files: Vec<Arc<LibFile>>,
673    /// Compiler options for type checking
674    compiler_options: CompilerOptions,
675}
676
677#[wasm_bindgen]
678impl Parser {
679    /// Create a new Parser for the given source file.
680    #[wasm_bindgen(constructor)]
681    pub fn new(file_name: String, source_text: String) -> Self {
682        Self {
683            parser: ParserState::new(file_name, source_text),
684            source_file_idx: None,
685            binder: None,
686            type_interner: TypeInterner::new(),
687            line_map: None,
688            type_cache: None,
689            scope_cache: ScopeCache::default(),
690            lib_files: Vec::new(),
691            compiler_options: CompilerOptions::default(),
692        }
693    }
694
695    /// Set compiler options from JSON.
696    ///
697    /// # Arguments
698    /// * `options_json` - JSON string containing compiler options
699    ///
700    /// # Example
701    /// ```javascript
702    /// const parser = new Parser("file.ts", "const x = 1;");
703    /// parser.setCompilerOptions(JSON.stringify({
704    ///   strict: true,
705    ///   noImplicitAny: true,
706    ///   strictNullChecks: true
707    /// }));
708    /// ```
709    #[wasm_bindgen(js_name = setCompilerOptions)]
710    pub fn set_compiler_options(&mut self, options_json: &str) -> Result<(), JsValue> {
711        match serde_json::from_str::<CompilerOptions>(options_json) {
712            Ok(options) => {
713                self.compiler_options = options;
714                // Invalidate type cache when compiler options change
715                self.type_cache = None;
716                Ok(())
717            }
718            Err(e) => Err(JsValue::from_str(&format!(
719                "Failed to parse compiler options: {e}"
720            ))),
721        }
722    }
723
724    /// Add a lib file (e.g., lib.es5.d.ts) for global type resolution.
725    /// The lib file will be parsed and bound, and its global symbols will be
726    /// available during binding and type checking.
727    #[wasm_bindgen(js_name = addLibFile)]
728    pub fn add_lib_file(&mut self, file_name: String, source_text: String) {
729        let mut lib_parser = ParserState::new(file_name.clone(), source_text);
730        let source_file_idx = lib_parser.parse_source_file();
731
732        let mut lib_binder = BinderState::new();
733        lib_binder.bind_source_file(lib_parser.get_arena(), source_file_idx);
734
735        // Wrap in Arc for sharing with LibContext during type checking
736        let arena = Arc::new(lib_parser.into_arena());
737        let binder = Arc::new(lib_binder);
738
739        // Create lib_loader::LibFile
740        let lib_file = Arc::new(LibFile::new(file_name, arena, binder));
741
742        self.lib_files.push(lib_file);
743
744        // Invalidate binder since we have new global symbols
745        self.binder = None;
746        self.type_cache = None;
747    }
748
749    /// Parse the source file and return the root node index.
750    #[wasm_bindgen(js_name = parseSourceFile)]
751    pub fn parse_source_file(&mut self) -> u32 {
752        let idx = self.parser.parse_source_file();
753        self.source_file_idx = Some(idx);
754        // Invalidate derived state on re-parse
755        self.line_map = None;
756        self.binder = None;
757        self.type_cache = None; // Invalidate type cache when file changes
758        self.scope_cache.clear();
759        idx.0
760    }
761
762    /// Get the number of nodes in the AST.
763    #[allow(clippy::missing_const_for_fn)] // wasm_bindgen does not support const fn
764    #[wasm_bindgen(js_name = getNodeCount)]
765    pub fn get_node_count(&self) -> usize {
766        self.parser.get_node_count()
767    }
768
769    /// Get parse diagnostics as JSON.
770    #[wasm_bindgen(js_name = getDiagnosticsJson)]
771    pub fn get_diagnostics_json(&self) -> String {
772        let diags: Vec<_> = self
773            .parser
774            .get_diagnostics()
775            .iter()
776            .map(|d| {
777                serde_json::json!({
778                    "message": d.message,
779                    "start": d.start,
780                    "length": d.length,
781                    "code": d.code,
782                })
783            })
784            .collect();
785        serde_json::to_string(&diags).unwrap_or_else(|_| "[]".to_string())
786    }
787
788    /// Bind the source file and return symbol count.
789    #[wasm_bindgen(js_name = bindSourceFile)]
790    pub fn bind_source_file(&mut self) -> String {
791        if let Some(root_idx) = self.source_file_idx {
792            let mut binder = BinderState::new();
793            // Use bind_source_file_with_libs to merge lib symbols into the binder
794            // This properly remaps SymbolIds to avoid collisions across lib files
795            binder.bind_source_file_with_libs(self.parser.get_arena(), root_idx, &self.lib_files);
796
797            // Collect symbol names for the result
798            let symbols: FxHashMap<String, u32> = binder
799                .file_locals
800                .iter()
801                .map(|(name, id)| (name.clone(), id.0))
802                .collect();
803
804            let result = serde_json::json!({
805                "symbols": symbols,
806                "symbolCount": binder.symbols.len(),
807            });
808
809            self.binder = Some(binder);
810            self.scope_cache.clear();
811            serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
812        } else {
813            r#"{"error": "Source file not parsed"}"#.to_string()
814        }
815    }
816
817    /// Type check the source file and return diagnostics.
818    #[wasm_bindgen(js_name = checkSourceFile)]
819    pub fn check_source_file(&mut self) -> String {
820        if self.binder.is_none() {
821            // Auto-bind if not done yet
822            if self.source_file_idx.is_some() {
823                self.bind_source_file();
824            }
825        }
826
827        if let (Some(root_idx), Some(binder)) = (self.source_file_idx, &self.binder) {
828            let file_name = self.parser.get_file_name().to_string();
829
830            // Get compiler options
831            let checker_options = self.compiler_options.to_checker_options();
832            let mut checker = if let Some(cache) = self.type_cache.take() {
833                CheckerState::with_cache_and_options(
834                    self.parser.get_arena(),
835                    binder,
836                    &self.type_interner,
837                    file_name,
838                    cache,
839                    &checker_options,
840                )
841            } else {
842                CheckerState::with_options(
843                    self.parser.get_arena(),
844                    binder,
845                    &self.type_interner,
846                    file_name,
847                    &checker_options,
848                )
849            };
850
851            // Set up lib contexts for global type resolution (Object, Array, etc.)
852            if !self.lib_files.is_empty() {
853                let lib_contexts: Vec<LibContext> = self
854                    .lib_files
855                    .iter()
856                    .map(|lib| LibContext {
857                        arena: Arc::clone(&lib.arena),
858                        binder: Arc::clone(&lib.binder),
859                    })
860                    .collect();
861                checker.ctx.set_lib_contexts(lib_contexts);
862            }
863
864            // Full source file type checking - traverse all statements
865            checker.check_source_file(root_idx);
866
867            let diagnostics = checker
868                .ctx
869                .diagnostics
870                .iter()
871                .map(|d| {
872                    serde_json::json!({
873                        "message_text": d.message_text.clone(),
874                        "code": d.code,
875                        "start": d.start,
876                        "length": d.length,
877                        "category": format!("{:?}", d.category),
878                    })
879                })
880                .collect::<Vec<_>>();
881
882            self.type_cache = Some(checker.extract_cache());
883
884            let result = serde_json::json!({
885                "typeCount": self.type_interner.len(),
886                "diagnostics": diagnostics,
887            });
888
889            serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
890        } else {
891            r#"{"error": "Source file not parsed or bound"}"#.to_string()
892        }
893    }
894
895    /// Get the type of a node as a string.
896    #[wasm_bindgen(js_name = getTypeOfNode)]
897    pub fn get_type_of_node(&mut self, node_idx: u32) -> String {
898        if let (Some(_), Some(binder)) = (self.source_file_idx, &self.binder) {
899            let file_name = self.parser.get_file_name().to_string();
900
901            // Get compiler options
902            let checker_options = self.compiler_options.to_checker_options();
903            let mut checker = if let Some(cache) = self.type_cache.take() {
904                CheckerState::with_cache_and_options(
905                    self.parser.get_arena(),
906                    binder,
907                    &self.type_interner,
908                    file_name,
909                    cache,
910                    &checker_options,
911                )
912            } else {
913                CheckerState::with_options(
914                    self.parser.get_arena(),
915                    binder,
916                    &self.type_interner,
917                    file_name,
918                    &checker_options,
919                )
920            };
921
922            let type_id = checker.get_type_of_node(parser::NodeIndex(node_idx));
923            // Use format_type for human-readable output
924            let result = checker.format_type(type_id);
925            self.type_cache = Some(checker.extract_cache());
926            result
927        } else {
928            "unknown".to_string()
929        }
930    }
931
932    /// Emit the source file as JavaScript (ES5 target, auto-detect CommonJS for modules).
933    #[wasm_bindgen(js_name = emit)]
934    pub fn emit(&self) -> String {
935        if let Some(root_idx) = self.source_file_idx {
936            let options = PrinterOptions {
937                target: ScriptTarget::ES5,
938                ..Default::default()
939            };
940
941            let mut ctx = EmitContext::with_options(options);
942            ctx.auto_detect_module = true;
943
944            self.emit_with_context(root_idx, ctx)
945        } else {
946            String::new()
947        }
948    }
949
950    /// Emit the source file as JavaScript (ES6+ modern output).
951    #[wasm_bindgen(js_name = emitModern)]
952    pub fn emit_modern(&self) -> String {
953        if let Some(root_idx) = self.source_file_idx {
954            let options = PrinterOptions {
955                target: ScriptTarget::ES2015,
956                ..Default::default()
957            };
958
959            let ctx = EmitContext::with_options(options);
960
961            self.emit_with_context(root_idx, ctx)
962        } else {
963            String::new()
964        }
965    }
966
967    fn emit_with_context(&self, root_idx: parser::NodeIndex, ctx: EmitContext) -> String {
968        let transforms = LoweringPass::new(self.parser.get_arena(), &ctx).run(root_idx);
969
970        let mut printer = Printer::with_transforms_and_options(
971            self.parser.get_arena(),
972            transforms,
973            ctx.options.clone(),
974        );
975        printer.set_target_es5(ctx.target_es5);
976        printer.set_auto_detect_module(ctx.auto_detect_module);
977        printer.set_source_text(self.parser.get_source_text());
978        printer.emit(root_idx);
979        printer.get_output().to_string()
980    }
981
982    /// Generate transform directives based on compiler options.
983    #[wasm_bindgen(js_name = generateTransforms)]
984    pub fn generate_transforms(&self, target: u32, module: u32) -> WasmTransformContext {
985        let options = PrinterOptions {
986            target: match target {
987                0 => ScriptTarget::ES3,
988                1 => ScriptTarget::ES5,
989                2 => ScriptTarget::ES2015,
990                3 => ScriptTarget::ES2016,
991                4 => ScriptTarget::ES2017,
992                5 => ScriptTarget::ES2018,
993                6 => ScriptTarget::ES2019,
994                7 => ScriptTarget::ES2020,
995                8 => ScriptTarget::ES2021,
996                9 => ScriptTarget::ES2022,
997                _ => ScriptTarget::ESNext,
998            },
999            module: match module {
1000                1 => ModuleKind::CommonJS,
1001                2 => ModuleKind::AMD,
1002                3 => ModuleKind::UMD,
1003                4 => ModuleKind::System,
1004                5 => ModuleKind::ES2015,
1005                6 => ModuleKind::ES2020,
1006                7 => ModuleKind::ES2022,
1007                99 => ModuleKind::ESNext,
1008                100 => ModuleKind::Node16,
1009                199 => ModuleKind::NodeNext,
1010                _ => ModuleKind::None,
1011            },
1012            ..Default::default()
1013        };
1014
1015        let ctx = EmitContext::with_options(options);
1016        let transforms = if let Some(root_idx) = self.source_file_idx {
1017            let lowering = LoweringPass::new(self.parser.get_arena(), &ctx);
1018            lowering.run(root_idx)
1019        } else {
1020            TransformContext::new()
1021        };
1022
1023        WasmTransformContext {
1024            inner: transforms,
1025            target_es5: ctx.target_es5,
1026            module_kind: ctx.options.module,
1027        }
1028    }
1029
1030    /// Emit the source file using pre-computed transforms.
1031    #[wasm_bindgen(js_name = emitWithTransforms)]
1032    pub fn emit_with_transforms(&self, context: &WasmTransformContext) -> String {
1033        if let Some(root_idx) = self.source_file_idx {
1034            let mut printer =
1035                Printer::with_transforms(self.parser.get_arena(), context.inner.clone());
1036            printer.set_target_es5(context.target_es5);
1037            printer.set_module_kind(context.module_kind);
1038            printer.set_source_text(self.parser.get_source_text());
1039            printer.emit(root_idx);
1040            printer.get_output().to_string()
1041        } else {
1042            String::new()
1043        }
1044    }
1045
1046    /// Get the AST as JSON (for debugging).
1047    #[wasm_bindgen(js_name = getAstJson)]
1048    pub fn get_ast_json(&self) -> String {
1049        if let Some(root_idx) = self.source_file_idx {
1050            let arena = self.parser.get_arena();
1051            format!(
1052                "{{\"nodeCount\": {}, \"rootIdx\": {}}}",
1053                arena.len(),
1054                root_idx.0
1055            )
1056        } else {
1057            "{}".to_string()
1058        }
1059    }
1060
1061    /// Debug type lowering - trace what happens when lowering an interface type
1062    #[wasm_bindgen(js_name = debugTypeLowering)]
1063    pub fn debug_type_lowering(&self, interface_name: &str) -> String {
1064        use parser::syntax_kind_ext;
1065        use tsz_lowering::TypeLowering;
1066        use tsz_solver::TypeData;
1067
1068        let arena = self.parser.get_arena();
1069        let mut result = Vec::new();
1070
1071        // Find the interface declaration
1072        let mut interface_decls = Vec::new();
1073        for i in 0..arena.len() {
1074            let idx = parser::NodeIndex(i as u32);
1075            if let Some(node) = arena.get(idx)
1076                && node.kind == syntax_kind_ext::INTERFACE_DECLARATION
1077                && let Some(interface) = arena.get_interface(node)
1078                && let Some(name_node) = arena.get(interface.name)
1079                && let Some(ident) = arena.get_identifier(name_node)
1080                && ident.escaped_text == interface_name
1081            {
1082                interface_decls.push(idx);
1083            }
1084        }
1085
1086        if interface_decls.is_empty() {
1087            return format!("Interface '{interface_name}' not found");
1088        }
1089
1090        result.push(format!(
1091            "Found {} declaration(s) for '{}'",
1092            interface_decls.len(),
1093            interface_name
1094        ));
1095
1096        // Lower the interface
1097        let lowering = TypeLowering::new(arena, &self.type_interner);
1098        let type_id = lowering.lower_interface_declarations(&interface_decls);
1099
1100        result.push(format!("Lowered type ID: {type_id:?}"));
1101
1102        // Inspect the result
1103        if let Some(key) = self.type_interner.lookup(type_id) {
1104            result.push(format!("Type key: {key:?}"));
1105            if let TypeData::Object(shape_id) = key {
1106                let shape = self.type_interner.object_shape(shape_id);
1107                result.push(format!(
1108                    "Object shape properties: {}",
1109                    shape.properties.len()
1110                ));
1111                for prop in &shape.properties {
1112                    let name = self.type_interner.resolve_atom(prop.name);
1113                    result.push(format!(
1114                        "  Property '{}': type_id={:?}, optional={}",
1115                        name, prop.type_id, prop.optional
1116                    ));
1117                    // Try to show what the type_id resolves to
1118                    if let Some(prop_key) = self.type_interner.lookup(prop.type_id) {
1119                        result.push(format!("    -> {prop_key:?}"));
1120                    }
1121                }
1122            }
1123        }
1124
1125        result.join("\n")
1126    }
1127
1128    /// Debug interface parsing - dump interface members for diagnostics
1129    #[wasm_bindgen(js_name = debugInterfaceMembers)]
1130    pub fn debug_interface_members(&self, interface_name: &str) -> String {
1131        use parser::syntax_kind_ext;
1132
1133        let arena = self.parser.get_arena();
1134        let mut result = Vec::new();
1135
1136        for i in 0..arena.len() {
1137            let idx = parser::NodeIndex(i as u32);
1138            if let Some(node) = arena.get(idx)
1139                && node.kind == syntax_kind_ext::INTERFACE_DECLARATION
1140                && let Some(interface) = arena.get_interface(node)
1141                && let Some(name_node) = arena.get(interface.name)
1142                && let Some(ident) = arena.get_identifier(name_node)
1143                && ident.escaped_text == interface_name
1144            {
1145                result.push(format!("Interface '{interface_name}' found at node {i}"));
1146                result.push(format!("  members list: {:?}", interface.members.nodes));
1147
1148                for (mi, &member_idx) in interface.members.nodes.iter().enumerate() {
1149                    if let Some(member_node) = arena.get(member_idx) {
1150                        result.push(format!(
1151                            "  Member {} (idx {}): kind={}",
1152                            mi, member_idx.0, member_node.kind
1153                        ));
1154                        result.push(format!("    data_index: {}", member_node.data_index));
1155                        if let Some(sig) = arena.get_signature(member_node) {
1156                            result.push(format!("    name_idx: {:?}", sig.name));
1157                            result.push(format!(
1158                                "    type_annotation_idx: {:?}",
1159                                sig.type_annotation
1160                            ));
1161
1162                            // Get name text
1163                            if let Some(name_n) = arena.get(sig.name) {
1164                                if let Some(name_id) = arena.get_identifier(name_n) {
1165                                    result
1166                                        .push(format!("    name_text: '{}'", name_id.escaped_text));
1167                                } else {
1168                                    result.push(format!("    name_node kind: {}", name_n.kind));
1169                                }
1170                            }
1171
1172                            // Get type annotation text
1173                            if let Some(type_n) = arena.get(sig.type_annotation) {
1174                                if let Some(type_id) = arena.get_identifier(type_n) {
1175                                    result
1176                                        .push(format!("    type_text: '{}'", type_id.escaped_text));
1177                                } else {
1178                                    result.push(format!("    type_node kind: {}", type_n.kind));
1179                                }
1180                            }
1181                        }
1182                    }
1183                }
1184            }
1185        }
1186
1187        if result.is_empty() {
1188            format!("Interface '{interface_name}' not found")
1189        } else {
1190            result.join("\n")
1191        }
1192    }
1193
1194    /// Debug namespace scoping - dump scope info for all scopes
1195    #[wasm_bindgen(js_name = debugScopes)]
1196    pub fn debug_scopes(&self) -> String {
1197        let Some(binder) = &self.binder else {
1198            return "Binder not initialized. Call parseSourceFile and bindSourceFile first."
1199                .to_string();
1200        };
1201
1202        let mut result = Vec::new();
1203        result.push(format!(
1204            "=== Persistent Scopes ({}) ===",
1205            binder.scopes.len()
1206        ));
1207
1208        for (i, scope) in binder.scopes.iter().enumerate() {
1209            result.push(format!(
1210                "\nScope {} (parent: {:?}, kind: {:?}):",
1211                i, scope.parent, scope.kind
1212            ));
1213            result.push(format!("  table entries: {}", scope.table.len()));
1214            for (name, sym_id) in scope.table.iter() {
1215                if let Some(sym) = binder.symbols.get(*sym_id) {
1216                    result.push(format!(
1217                        "    '{}' -> SymbolId({}) [flags: 0x{:x}]",
1218                        name, sym_id.0, sym.flags
1219                    ));
1220                } else {
1221                    result.push(format!(
1222                        "    '{}' -> SymbolId({}) [MISSING SYMBOL]",
1223                        name, sym_id.0
1224                    ));
1225                }
1226            }
1227        }
1228
1229        result.push(format!(
1230            "\n=== Node -> Scope Mappings ({}) ===",
1231            binder.node_scope_ids.len()
1232        ));
1233        for (&node_idx, &scope_id) in &binder.node_scope_ids {
1234            result.push(format!(
1235                "  NodeIndex({}) -> ScopeId({})",
1236                node_idx, scope_id.0
1237            ));
1238        }
1239
1240        result.push(format!(
1241            "\n=== File Locals ({}) ===",
1242            binder.file_locals.len()
1243        ));
1244        for (name, sym_id) in binder.file_locals.iter() {
1245            result.push(format!("  '{}' -> SymbolId({})", name, sym_id.0));
1246        }
1247
1248        result.join("\n")
1249    }
1250
1251    /// Trace the parent chain for a node at a given position
1252    #[wasm_bindgen(js_name = traceParentChain)]
1253    pub fn trace_parent_chain(&self, pos: u32) -> String {
1254        const IDENTIFIER_KIND: u16 = 80; // SyntaxKind::Identifier
1255        let arena = self.parser.get_arena();
1256        let binder = match &self.binder {
1257            Some(b) => b,
1258            None => return "Binder not initialized".to_string(),
1259        };
1260
1261        let mut result = Vec::new();
1262        result.push(format!("=== Tracing parent chain for position {pos} ==="));
1263
1264        // Find node at position
1265        let mut target_node = None;
1266        for i in 0..arena.len() {
1267            let idx = parser::NodeIndex(i as u32);
1268            if let Some(node) = arena.get(idx)
1269                && node.pos <= pos
1270                && pos < node.end
1271                && node.kind == IDENTIFIER_KIND
1272            {
1273                target_node = Some(idx);
1274                // Don't break - prefer smaller range
1275            }
1276        }
1277
1278        let start_idx = match target_node {
1279            Some(idx) => idx,
1280            None => return format!("No identifier node found at position {pos}"),
1281        };
1282
1283        result.push(format!("Starting node: {start_idx:?}"));
1284
1285        let mut current = start_idx;
1286        let mut depth = 0;
1287        while !current.is_none() && depth < 20 {
1288            if let Some(node) = arena.get(current) {
1289                let kind_name = format!("kind={}", node.kind);
1290                let scope_info = if let Some(&scope_id) = binder.node_scope_ids.get(&current.0) {
1291                    format!(" -> ScopeId({})", scope_id.0)
1292                } else {
1293                    String::new()
1294                };
1295                result.push(format!(
1296                    "  [{}] NodeIndex({}) {} [pos:{}-{}]{}",
1297                    depth, current.0, kind_name, node.pos, node.end, scope_info
1298                ));
1299            }
1300
1301            if let Some(ext) = arena.get_extended(current) {
1302                if ext.parent.is_none() {
1303                    result.push(format!("  [{}] Parent is NodeIndex::NONE", depth + 1));
1304                    break;
1305                }
1306                current = ext.parent;
1307            } else {
1308                result.push(format!(
1309                    "  [{}] No extended info for NodeIndex({})",
1310                    depth + 1,
1311                    current.0
1312                ));
1313                break;
1314            }
1315            depth += 1;
1316        }
1317
1318        result.join("\n")
1319    }
1320
1321    /// Dump variable declaration info for debugging
1322    #[wasm_bindgen(js_name = dumpVarDecl)]
1323    pub fn dump_var_decl(&self, var_decl_idx: u32) -> String {
1324        let arena = self.parser.get_arena();
1325        let idx = parser::NodeIndex(var_decl_idx);
1326
1327        let Some(node) = arena.get(idx) else {
1328            return format!("NodeIndex({var_decl_idx}) not found");
1329        };
1330
1331        let Some(var_decl) = arena.get_variable_declaration(node) else {
1332            return format!(
1333                "NodeIndex({}) is not a VARIABLE_DECLARATION (kind={})",
1334                var_decl_idx, node.kind
1335            );
1336        };
1337
1338        format!(
1339            "VariableDeclaration({}):\n  name: NodeIndex({})\n  type_annotation: NodeIndex({}) (is_none={})\n  initializer: NodeIndex({})",
1340            var_decl_idx,
1341            var_decl.name.0,
1342            var_decl.type_annotation.0,
1343            var_decl.type_annotation.is_none(),
1344            var_decl.initializer.0
1345        )
1346    }
1347
1348    /// Dump all nodes for debugging
1349    #[wasm_bindgen(js_name = dumpAllNodes)]
1350    pub fn dump_all_nodes(&self, start: u32, count: u32) -> String {
1351        let arena = self.parser.get_arena();
1352        let mut result = Vec::new();
1353
1354        for i in start..(start + count).min(arena.len() as u32) {
1355            let idx = parser::NodeIndex(i);
1356            if let Some(node) = arena.get(idx) {
1357                let parent_str = if let Some(ext) = arena.get_extended(idx) {
1358                    if ext.parent.is_none() {
1359                        "parent:NONE".to_string()
1360                    } else {
1361                        format!("parent:{}", ext.parent.0)
1362                    }
1363                } else {
1364                    "no-ext".to_string()
1365                };
1366                // Add identifier text if available
1367                let extra = if let Some(ident) = arena.get_identifier(node) {
1368                    format!(" \"{}\"", ident.escaped_text)
1369                } else {
1370                    String::new()
1371                };
1372                result.push(format!(
1373                    "  NodeIndex({}) kind={} [pos:{}-{}] {}{}",
1374                    i, node.kind, node.pos, node.end, parent_str, extra
1375                ));
1376            }
1377        }
1378
1379        result.join("\n")
1380    }
1381
1382    // =========================================================================
1383    // LSP Feature Methods
1384    // =========================================================================
1385
1386    /// Ensure internal `LineMap` is built.
1387    fn ensure_line_map(&mut self) {
1388        if self.line_map.is_none() {
1389            self.line_map = Some(LineMap::build(self.parser.get_source_text()));
1390        }
1391    }
1392
1393    /// Ensure source file is parsed and bound.
1394    fn ensure_bound(&mut self) -> Result<(), JsValue> {
1395        if self.source_file_idx.is_none() {
1396            return Err(JsValue::from_str("Source file not parsed"));
1397        }
1398        if self.binder.is_none() {
1399            self.bind_source_file();
1400        }
1401        Ok(())
1402    }
1403
1404    /// Go to Definition: Returns array of Location objects.
1405    #[wasm_bindgen(js_name = getDefinitionAtPosition)]
1406    pub fn get_definition_at_position(
1407        &mut self,
1408        line: u32,
1409        character: u32,
1410    ) -> Result<JsValue, JsValue> {
1411        self.ensure_bound()?;
1412        self.ensure_line_map();
1413
1414        let root = self
1415            .source_file_idx
1416            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1417        let binder = self
1418            .binder
1419            .as_ref()
1420            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1421        let line_map = self
1422            .line_map
1423            .as_ref()
1424            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1425        let file_name = self.parser.get_file_name().to_string();
1426        let source_text = self.parser.get_source_text();
1427
1428        let provider = GoToDefinition::new(
1429            self.parser.get_arena(),
1430            binder,
1431            line_map,
1432            file_name,
1433            source_text,
1434        );
1435        let pos = Position::new(line, character);
1436
1437        let result =
1438            provider.get_definition_with_scope_cache(root, pos, &mut self.scope_cache, None);
1439        Ok(serde_wasm_bindgen::to_value(&result)?)
1440    }
1441
1442    /// Find References: Returns array of Location objects.
1443    #[wasm_bindgen(js_name = getReferencesAtPosition)]
1444    pub fn get_references_at_position(
1445        &mut self,
1446        line: u32,
1447        character: u32,
1448    ) -> Result<JsValue, JsValue> {
1449        self.ensure_bound()?;
1450        self.ensure_line_map();
1451
1452        let root = self
1453            .source_file_idx
1454            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1455        let binder = self
1456            .binder
1457            .as_ref()
1458            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1459        let line_map = self
1460            .line_map
1461            .as_ref()
1462            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1463        let file_name = self.parser.get_file_name().to_string();
1464        let source_text = self.parser.get_source_text();
1465
1466        let provider = FindReferences::new(
1467            self.parser.get_arena(),
1468            binder,
1469            line_map,
1470            file_name,
1471            source_text,
1472        );
1473        let pos = Position::new(line, character);
1474
1475        let result =
1476            provider.find_references_with_scope_cache(root, pos, &mut self.scope_cache, None);
1477        Ok(serde_wasm_bindgen::to_value(&result)?)
1478    }
1479
1480    /// Completions: Returns array of `CompletionItem` objects.
1481    #[wasm_bindgen(js_name = getCompletionsAtPosition)]
1482    pub fn get_completions_at_position(
1483        &mut self,
1484        line: u32,
1485        character: u32,
1486    ) -> Result<JsValue, JsValue> {
1487        self.ensure_bound()?;
1488        self.ensure_line_map();
1489
1490        let root = self
1491            .source_file_idx
1492            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1493        let binder = self
1494            .binder
1495            .as_ref()
1496            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1497        let line_map = self
1498            .line_map
1499            .as_ref()
1500            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1501        let source_text = self.parser.get_source_text();
1502        let file_name = self.parser.get_file_name().to_string();
1503
1504        let provider = Completions::new_with_types(
1505            self.parser.get_arena(),
1506            binder,
1507            line_map,
1508            &self.type_interner,
1509            source_text,
1510            file_name,
1511        );
1512        let pos = Position::new(line, character);
1513
1514        let result = provider.get_completions_with_caches(
1515            root,
1516            pos,
1517            &mut self.type_cache,
1518            &mut self.scope_cache,
1519            None,
1520        );
1521        Ok(serde_wasm_bindgen::to_value(&result)?)
1522    }
1523
1524    /// Hover: Returns `HoverInfo` object.
1525    #[wasm_bindgen(js_name = getHoverAtPosition)]
1526    pub fn get_hover_at_position(&mut self, line: u32, character: u32) -> Result<JsValue, JsValue> {
1527        self.ensure_bound()?;
1528        self.ensure_line_map();
1529
1530        let root = self
1531            .source_file_idx
1532            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1533        let binder = self
1534            .binder
1535            .as_ref()
1536            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1537        let line_map = self
1538            .line_map
1539            .as_ref()
1540            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1541        let source_text = self.parser.get_source_text();
1542        let file_name = self.parser.get_file_name().to_string();
1543
1544        let provider = HoverProvider::new(
1545            self.parser.get_arena(),
1546            binder,
1547            line_map,
1548            &self.type_interner,
1549            source_text,
1550            file_name,
1551        );
1552        let pos = Position::new(line, character);
1553
1554        let result = provider.get_hover_with_scope_cache(
1555            root,
1556            pos,
1557            &mut self.type_cache,
1558            &mut self.scope_cache,
1559            None,
1560        );
1561        Ok(serde_wasm_bindgen::to_value(&result)?)
1562    }
1563
1564    /// Signature Help: Returns `SignatureHelp` object.
1565    #[wasm_bindgen(js_name = getSignatureHelpAtPosition)]
1566    pub fn get_signature_help_at_position(
1567        &mut self,
1568        line: u32,
1569        character: u32,
1570    ) -> Result<JsValue, JsValue> {
1571        self.ensure_bound()?;
1572        self.ensure_line_map();
1573
1574        let root = self
1575            .source_file_idx
1576            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1577        let binder = self
1578            .binder
1579            .as_ref()
1580            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1581        let line_map = self
1582            .line_map
1583            .as_ref()
1584            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1585        let source_text = self.parser.get_source_text();
1586        let file_name = self.parser.get_file_name().to_string();
1587
1588        let provider = SignatureHelpProvider::new(
1589            self.parser.get_arena(),
1590            binder,
1591            line_map,
1592            &self.type_interner,
1593            source_text,
1594            file_name,
1595        );
1596        let pos = Position::new(line, character);
1597
1598        let result = provider.get_signature_help_with_scope_cache(
1599            root,
1600            pos,
1601            &mut self.type_cache,
1602            &mut self.scope_cache,
1603            None,
1604        );
1605        Ok(serde_wasm_bindgen::to_value(&result)?)
1606    }
1607
1608    /// Document Symbols: Returns array of `DocumentSymbol` objects.
1609    #[wasm_bindgen(js_name = getDocumentSymbols)]
1610    pub fn get_document_symbols(&mut self) -> Result<JsValue, JsValue> {
1611        self.ensure_bound()?;
1612        self.ensure_line_map();
1613
1614        let root = self
1615            .source_file_idx
1616            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1617        let line_map = self
1618            .line_map
1619            .as_ref()
1620            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1621        let source_text = self.parser.get_source_text();
1622
1623        let provider = DocumentSymbolProvider::new(self.parser.get_arena(), line_map, source_text);
1624
1625        let result = provider.get_document_symbols(root);
1626        Ok(serde_wasm_bindgen::to_value(&result)?)
1627    }
1628
1629    /// Semantic Tokens: Returns flat array of u32 (delta encoded).
1630    #[wasm_bindgen(js_name = getSemanticTokens)]
1631    pub fn get_semantic_tokens(&mut self) -> Result<Vec<u32>, JsValue> {
1632        self.ensure_bound()?;
1633        self.ensure_line_map();
1634
1635        let root = self
1636            .source_file_idx
1637            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1638        let binder = self
1639            .binder
1640            .as_ref()
1641            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1642        let line_map = self
1643            .line_map
1644            .as_ref()
1645            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1646        let source_text = self.parser.get_source_text();
1647
1648        let mut provider =
1649            SemanticTokensProvider::new(self.parser.get_arena(), binder, line_map, source_text);
1650
1651        Ok(provider.get_semantic_tokens(root))
1652    }
1653
1654    /// Rename - Prepare: Check if rename is valid at position.
1655    #[wasm_bindgen(js_name = prepareRename)]
1656    pub fn prepare_rename(&mut self, line: u32, character: u32) -> Result<JsValue, JsValue> {
1657        self.ensure_bound()?;
1658        self.ensure_line_map();
1659
1660        let binder = self
1661            .binder
1662            .as_ref()
1663            .ok_or_else(|| JsValue::from_str("Internal error: binder not available"))?;
1664        let line_map = self
1665            .line_map
1666            .as_ref()
1667            .ok_or_else(|| JsValue::from_str("Internal error: line map not available"))?;
1668        let file_name = self.parser.get_file_name().to_string();
1669        let source_text = self.parser.get_source_text();
1670
1671        let provider = RenameProvider::new(
1672            self.parser.get_arena(),
1673            binder,
1674            line_map,
1675            file_name,
1676            source_text,
1677        );
1678        let pos = Position::new(line, character);
1679
1680        let result = provider.prepare_rename(pos);
1681        Ok(serde_wasm_bindgen::to_value(&result)?)
1682    }
1683
1684    /// Rename - Edits: Get workspace edits for rename.
1685    #[wasm_bindgen(js_name = getRenameEdits)]
1686    pub fn get_rename_edits(
1687        &mut self,
1688        line: u32,
1689        character: u32,
1690        new_name: String,
1691    ) -> Result<JsValue, JsValue> {
1692        self.ensure_bound()?;
1693        self.ensure_line_map();
1694
1695        let root = self
1696            .source_file_idx
1697            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1698        let binder = self
1699            .binder
1700            .as_ref()
1701            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1702        let line_map = self
1703            .line_map
1704            .as_ref()
1705            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1706        let file_name = self.parser.get_file_name().to_string();
1707        let source_text = self.parser.get_source_text();
1708
1709        let provider = RenameProvider::new(
1710            self.parser.get_arena(),
1711            binder,
1712            line_map,
1713            file_name,
1714            source_text,
1715        );
1716        let pos = Position::new(line, character);
1717
1718        match provider.provide_rename_edits_with_scope_cache(
1719            root,
1720            pos,
1721            new_name,
1722            &mut self.scope_cache,
1723            None,
1724        ) {
1725            Ok(edit) => Ok(serde_wasm_bindgen::to_value(&edit)?),
1726            Err(e) => Err(JsValue::from_str(&e)),
1727        }
1728    }
1729
1730    /// Code Actions: Get code actions for a range.
1731    #[wasm_bindgen(js_name = getCodeActions)]
1732    pub fn get_code_actions(
1733        &mut self,
1734        start_line: u32,
1735        start_char: u32,
1736        end_line: u32,
1737        end_char: u32,
1738    ) -> Result<JsValue, JsValue> {
1739        self.ensure_bound()?;
1740        self.ensure_line_map();
1741
1742        let root = self
1743            .source_file_idx
1744            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1745        let binder = self
1746            .binder
1747            .as_ref()
1748            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1749        let line_map = self
1750            .line_map
1751            .as_ref()
1752            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1753        let file_name = self.parser.get_file_name().to_string();
1754        let source_text = self.parser.get_source_text();
1755
1756        let provider = CodeActionProvider::new(
1757            self.parser.get_arena(),
1758            binder,
1759            line_map,
1760            file_name,
1761            source_text,
1762        );
1763
1764        let range = Range::new(
1765            Position::new(start_line, start_char),
1766            Position::new(end_line, end_char),
1767        );
1768
1769        let context = CodeActionContext {
1770            diagnostics: Vec::new(),
1771            only: None,
1772            import_candidates: Vec::new(),
1773        };
1774
1775        let result = provider.provide_code_actions(root, range, context);
1776        Ok(serde_wasm_bindgen::to_value(&result)?)
1777    }
1778
1779    /// Code Actions: Get code actions for a range with diagnostics context.
1780    #[wasm_bindgen(js_name = getCodeActionsWithContext)]
1781    pub fn get_code_actions_with_context(
1782        &mut self,
1783        start_line: u32,
1784        start_char: u32,
1785        end_line: u32,
1786        end_char: u32,
1787        context: JsValue,
1788    ) -> Result<JsValue, JsValue> {
1789        self.ensure_bound()?;
1790        self.ensure_line_map();
1791
1792        let context = if context.is_null() || context.is_undefined() {
1793            CodeActionContext {
1794                diagnostics: Vec::new(),
1795                only: None,
1796                import_candidates: Vec::new(),
1797            }
1798        } else {
1799            let context_input: CodeActionContextInput = serde_wasm_bindgen::from_value(context)?;
1800            let import_candidates = context_input
1801                .import_candidates
1802                .into_iter()
1803                .map(ImportCandidate::try_from)
1804                .collect::<Result<Vec<_>, _>>()?;
1805            CodeActionContext {
1806                diagnostics: context_input.diagnostics,
1807                only: context_input.only,
1808                import_candidates,
1809            }
1810        };
1811
1812        let root = self
1813            .source_file_idx
1814            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1815        let binder = self
1816            .binder
1817            .as_ref()
1818            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1819        let line_map = self
1820            .line_map
1821            .as_ref()
1822            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1823        let file_name = self.parser.get_file_name().to_string();
1824        let source_text = self.parser.get_source_text();
1825
1826        let provider = CodeActionProvider::new(
1827            self.parser.get_arena(),
1828            binder,
1829            line_map,
1830            file_name,
1831            source_text,
1832        );
1833
1834        let range = Range::new(
1835            Position::new(start_line, start_char),
1836            Position::new(end_line, end_char),
1837        );
1838
1839        let result = provider.provide_code_actions(root, range, context);
1840        Ok(serde_wasm_bindgen::to_value(&result)?)
1841    }
1842
1843    /// Diagnostics: Get checker diagnostics in LSP format.
1844    #[wasm_bindgen(js_name = getLspDiagnostics)]
1845    pub fn get_lsp_diagnostics(&mut self) -> Result<JsValue, JsValue> {
1846        self.ensure_bound()?;
1847        self.ensure_line_map();
1848
1849        let root = self
1850            .source_file_idx
1851            .ok_or_else(|| JsValue::from_str("Source file not available"))?;
1852        let binder = self
1853            .binder
1854            .as_ref()
1855            .ok_or_else(|| JsValue::from_str("Binder not available"))?;
1856        let line_map = self
1857            .line_map
1858            .as_ref()
1859            .ok_or_else(|| JsValue::from_str("Line map not available"))?;
1860        let file_name = self.parser.get_file_name().to_string();
1861        let source_text = self.parser.get_source_text();
1862
1863        // Get compiler options
1864        let checker_options = self.compiler_options.to_checker_options();
1865
1866        let mut checker = if let Some(cache) = self.type_cache.take() {
1867            CheckerState::with_cache_and_options(
1868                self.parser.get_arena(),
1869                binder,
1870                &self.type_interner,
1871                file_name,
1872                cache,
1873                &checker_options,
1874            )
1875        } else {
1876            CheckerState::with_options(
1877                self.parser.get_arena(),
1878                binder,
1879                &self.type_interner,
1880                file_name,
1881                &checker_options,
1882            )
1883        };
1884
1885        checker.check_source_file(root);
1886
1887        let lsp_diagnostics: Vec<_> = checker
1888            .ctx
1889            .diagnostics
1890            .iter()
1891            .map(|diag| convert_diagnostic(diag, line_map, source_text))
1892            .collect();
1893
1894        self.type_cache = Some(checker.extract_cache());
1895
1896        Ok(serde_wasm_bindgen::to_value(&lsp_diagnostics)?)
1897    }
1898}
1899
1900/// Create a new Parser for the given source text.
1901/// This is the recommended parser for production use.
1902#[wasm_bindgen(js_name = createParser)]
1903pub fn create_parser(file_name: String, source_text: String) -> Parser {
1904    Parser::new(file_name, source_text)
1905}
1906
1907// =============================================================================
1908// WasmProgram - Multi-file TypeScript Program Support
1909// =============================================================================
1910
1911use crate::parallel::{
1912    BindResult, MergedProgram, check_files_parallel, merge_bind_results, parse_and_bind_parallel,
1913};
1914
1915/// Result of checking a single file in a multi-file program
1916#[derive(serde::Serialize)]
1917#[serde(rename_all = "camelCase")]
1918struct FileCheckResultJson {
1919    file_name: String,
1920    parse_diagnostics: Vec<ParseDiagnosticJson>,
1921    check_diagnostics: Vec<CheckDiagnosticJson>,
1922}
1923
1924#[derive(serde::Serialize)]
1925#[serde(rename_all = "camelCase")]
1926struct ParseDiagnosticJson {
1927    message: String,
1928    start: u32,
1929    length: u32,
1930    code: u32,
1931}
1932
1933#[derive(serde::Serialize)]
1934#[serde(rename_all = "camelCase")]
1935struct CheckDiagnosticJson {
1936    message_text: String,
1937    code: u32,
1938    start: u32,
1939    length: u32,
1940    category: String,
1941}
1942
1943/// Multi-file TypeScript program for cross-file type checking.
1944///
1945/// This struct provides an API for compiling multiple TypeScript files together,
1946/// enabling proper module resolution and cross-file type checking.
1947///
1948/// # Example (JavaScript)
1949/// ```javascript
1950/// const program = new WasmProgram();
1951/// program.addFile("a.ts", "export const x = 1;");
1952/// program.addFile("b.ts", "import { x } from './a'; const y = x + 1;");
1953/// const result = program.checkAll();
1954/// console.log(result);
1955/// ```
1956#[wasm_bindgen]
1957pub struct WasmProgram {
1958    /// Accumulated files before compilation
1959    files: Vec<(String, String)>,
1960    /// Merged program state after compilation (lazy)
1961    merged: Option<MergedProgram>,
1962    /// Bind results (kept for diagnostics access)
1963    bind_results: Option<Vec<BindResult>>,
1964    /// Lib files (lib.d.ts, lib.dom.d.ts, etc.) for global symbol resolution
1965    lib_files: Vec<(String, String)>,
1966    /// Compiler options for type checking
1967    compiler_options: CompilerOptions,
1968}
1969
1970impl Default for WasmProgram {
1971    fn default() -> Self {
1972        Self::new()
1973    }
1974}
1975
1976#[wasm_bindgen]
1977impl WasmProgram {
1978    /// Create a new empty program.
1979    #[wasm_bindgen(constructor)]
1980    pub fn new() -> Self {
1981        Self {
1982            files: Vec::new(),
1983            lib_files: Vec::new(),
1984            merged: None,
1985            bind_results: None,
1986            compiler_options: CompilerOptions::default(),
1987        }
1988    }
1989
1990    /// Add a file to the program.
1991    ///
1992    /// Files are accumulated and compiled together when `checkAll` is called.
1993    /// The `file_name` should be a relative path like "src/a.ts".
1994    ///
1995    /// For TypeScript library files (lib.d.ts, lib.dom.d.ts, etc.), use `addLibFile` instead.
1996    #[wasm_bindgen(js_name = addFile)]
1997    pub fn add_file(&mut self, file_name: String, source_text: String) {
1998        // Invalidate any previous compilation
1999        self.merged = None;
2000        self.bind_results = None;
2001
2002        // Skip package.json files - they're used for module resolution but not parsed
2003        if file_name.ends_with("package.json") {
2004            return;
2005        }
2006
2007        self.files.push((file_name, source_text));
2008    }
2009
2010    /// Add a TypeScript library file (lib.d.ts, lib.dom.d.ts, etc.) to the program.
2011    ///
2012    /// Lib files are used for global symbol resolution and are merged into
2013    /// the symbol table before user files are processed.
2014    ///
2015    /// Use this method explicitly instead of relying on automatic file name detection.
2016    /// This makes the API behavior predictable and explicit.
2017    ///
2018    /// # Example (JavaScript)
2019    /// ```javascript
2020    /// const program = new WasmProgram();
2021    /// program.addLibFile("lib.d.ts", libContent);
2022    /// program.addFile("src/a.ts", userCode);
2023    /// ```
2024    #[wasm_bindgen(js_name = addLibFile)]
2025    pub fn add_lib_file(&mut self, file_name: String, source_text: String) {
2026        // Invalidate any previous compilation
2027        self.merged = None;
2028        self.bind_results = None;
2029
2030        self.lib_files.push((file_name, source_text));
2031    }
2032
2033    /// Set compiler options from JSON.
2034    ///
2035    /// # Arguments
2036    /// * `options_json` - JSON string containing compiler options
2037    #[wasm_bindgen(js_name = setCompilerOptions)]
2038    pub fn set_compiler_options(&mut self, options_json: &str) -> Result<(), JsValue> {
2039        match serde_json::from_str::<CompilerOptions>(options_json) {
2040            Ok(options) => {
2041                self.compiler_options = options;
2042                // Invalidate any previous compilation since options affect typing
2043                self.merged = None;
2044                self.bind_results = None;
2045                Ok(())
2046            }
2047            Err(e) => Err(JsValue::from_str(&format!(
2048                "Failed to parse compiler options: {e}"
2049            ))),
2050        }
2051    }
2052
2053    /// Get the number of files in the program.
2054    #[allow(clippy::missing_const_for_fn)] // wasm_bindgen does not support const fn
2055    #[wasm_bindgen(js_name = getFileCount)]
2056    pub fn get_file_count(&self) -> usize {
2057        self.files.len()
2058    }
2059
2060    /// Clear all files and reset the program state.
2061    #[wasm_bindgen]
2062    pub fn clear(&mut self) {
2063        self.files.clear();
2064        self.lib_files.clear();
2065        self.merged = None;
2066        self.bind_results = None;
2067    }
2068
2069    /// Compile all files and return diagnostics as JSON.
2070    ///
2071    /// This performs:
2072    /// 1. Load lib files for global symbol resolution
2073    /// 2. Parallel parsing of all files
2074    /// 3. Parallel binding of all files with lib symbols merged
2075    /// 4. Symbol merging (sequential)
2076    /// 5. Parallel type checking
2077    ///
2078    /// Returns a JSON object with diagnostics per file.
2079    #[wasm_bindgen(js_name = checkAll)]
2080    pub fn check_all(&mut self) -> String {
2081        if self.files.is_empty() && self.lib_files.is_empty() {
2082            return r#"{"files":[],"stats":{"totalFiles":0,"totalDiagnostics":0}}"#.to_string();
2083        }
2084
2085        // Load lib files for binding
2086        // Use cache to avoid re-parsing lib.d.ts for every test
2087        let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
2088            .lib_files
2089            .iter()
2090            .map(|(file_name, source_text)| {
2091                get_or_create_lib_file(file_name.clone(), source_text.clone())
2092            })
2093            .collect();
2094
2095        // Parse and bind all files in parallel with lib symbols
2096        let bind_results = if !lib_file_objects.is_empty() {
2097            // Use lib-aware binding
2098            use crate::parallel;
2099            parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
2100        } else {
2101            // No lib files - use regular binding
2102            parse_and_bind_parallel(self.files.clone())
2103        };
2104
2105        // Collect parse diagnostics before merging
2106        let parse_diags: Vec<Vec<_>> = bind_results
2107            .iter()
2108            .map(|r| r.parse_diagnostics.clone())
2109            .collect();
2110        let file_names: Vec<String> = bind_results.iter().map(|r| r.file_name.clone()).collect();
2111
2112        // Merge bind results into unified program
2113        let merged = merge_bind_results(bind_results);
2114
2115        // Type check all files in parallel
2116        let checker_options = self.compiler_options.to_checker_options();
2117        let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
2118
2119        // Build JSON result
2120        let mut file_results: Vec<FileCheckResultJson> = Vec::new();
2121        let mut total_diagnostics = 0;
2122
2123        for (i, file_name) in file_names.iter().enumerate() {
2124            let parse_diagnostics: Vec<ParseDiagnosticJson> = parse_diags[i]
2125                .iter()
2126                .map(|d| ParseDiagnosticJson {
2127                    message: d.message.clone(),
2128                    start: d.start,
2129                    length: d.length,
2130                    code: d.code,
2131                })
2132                .collect();
2133
2134            // Find check diagnostics for this file
2135            let check_diagnostics: Vec<CheckDiagnosticJson> = check_result
2136                .file_results
2137                .iter()
2138                .find(|r| &r.file_name == file_name)
2139                .map(|r| {
2140                    r.diagnostics
2141                        .iter()
2142                        .map(|d| CheckDiagnosticJson {
2143                            message_text: d.message_text.clone(),
2144                            code: d.code,
2145                            start: d.start,
2146                            length: d.length,
2147                            category: format!("{:?}", d.category),
2148                        })
2149                        .collect()
2150                })
2151                .unwrap_or_default();
2152
2153            total_diagnostics += parse_diagnostics.len() + check_diagnostics.len();
2154
2155            file_results.push(FileCheckResultJson {
2156                file_name: file_name.clone(),
2157                parse_diagnostics,
2158                check_diagnostics,
2159            });
2160        }
2161
2162        // Store merged program for potential future queries
2163        self.merged = Some(merged);
2164
2165        let result = serde_json::json!({
2166            "files": file_results,
2167            "stats": {
2168                "totalFiles": file_names.len(),
2169                "totalDiagnostics": total_diagnostics,
2170            }
2171        });
2172
2173        serde_json::to_string(&result).unwrap_or_else(|_| "{}".to_string())
2174    }
2175
2176    /// Get diagnostic codes for all files (for conformance testing).
2177    ///
2178    /// Returns a JSON object mapping file names to arrays of error codes.
2179    #[wasm_bindgen(js_name = getDiagnosticCodes)]
2180    pub fn get_diagnostic_codes(&mut self) -> String {
2181        if self.files.is_empty() && self.lib_files.is_empty() {
2182            return "{}".to_string();
2183        }
2184
2185        // Load lib files for binding (enables global symbol resolution: console, Array, etc.)
2186        // Use cache to avoid re-parsing lib.d.ts for every test
2187        let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
2188            .lib_files
2189            .iter()
2190            .map(|(file_name, source_text)| {
2191                get_or_create_lib_file(file_name.clone(), source_text.clone())
2192            })
2193            .collect();
2194
2195        // Parse and bind all files in parallel with lib symbols
2196        let bind_results = if !lib_file_objects.is_empty() {
2197            use crate::parallel;
2198            parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
2199        } else {
2200            parse_and_bind_parallel(self.files.clone())
2201        };
2202
2203        // Collect parse diagnostic codes
2204        let mut file_codes: FxHashMap<String, Vec<u32>> = FxHashMap::default();
2205        for result in &bind_results {
2206            let codes: Vec<u32> = result.parse_diagnostics.iter().map(|d| d.code).collect();
2207            file_codes.insert(result.file_name.clone(), codes);
2208        }
2209
2210        // Merge and check
2211        let merged = merge_bind_results(bind_results);
2212        let checker_options = self.compiler_options.to_checker_options();
2213        let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
2214
2215        // Add check diagnostic codes
2216        for file_result in &check_result.file_results {
2217            let entry = file_codes.entry(file_result.file_name.clone()).or_default();
2218            for diag in &file_result.diagnostics {
2219                entry.push(diag.code);
2220            }
2221        }
2222
2223        // Store merged program
2224        self.merged = Some(merged);
2225
2226        serde_json::to_string(&file_codes).unwrap_or_else(|_| "{}".to_string())
2227    }
2228
2229    /// Get all diagnostic codes as a flat array (for simple conformance comparison).
2230    ///
2231    /// This combines all parse and check diagnostics from all files into a single
2232    /// array of error codes, which can be compared against tsc output.
2233    #[wasm_bindgen(js_name = getAllDiagnosticCodes)]
2234    pub fn get_all_diagnostic_codes(&mut self) -> Vec<u32> {
2235        if self.files.is_empty() && self.lib_files.is_empty() {
2236            return Vec::new();
2237        }
2238
2239        // Load lib files for binding (enables global symbol resolution: console, Array, etc.)
2240        // Use cache to avoid re-parsing lib.d.ts for every test
2241        let lib_file_objects: Vec<Arc<lib_loader::LibFile>> = self
2242            .lib_files
2243            .iter()
2244            .map(|(file_name, source_text)| {
2245                get_or_create_lib_file(file_name.clone(), source_text.clone())
2246            })
2247            .collect();
2248
2249        // Parse and bind all files in parallel with lib symbols
2250        let bind_results = if !lib_file_objects.is_empty() {
2251            use crate::parallel;
2252            parallel::parse_and_bind_parallel_with_libs(self.files.clone(), &lib_file_objects)
2253        } else {
2254            parse_and_bind_parallel(self.files.clone())
2255        };
2256
2257        // Collect all parse diagnostic codes
2258        let mut all_codes: Vec<u32> = Vec::new();
2259        for result in &bind_results {
2260            for diag in &result.parse_diagnostics {
2261                all_codes.push(diag.code);
2262            }
2263        }
2264
2265        // Merge and check
2266        let merged = merge_bind_results(bind_results);
2267        let checker_options = self.compiler_options.to_checker_options();
2268        let check_result = check_files_parallel(&merged, &checker_options, &lib_file_objects);
2269
2270        // Add all check diagnostic codes
2271        for file_result in &check_result.file_results {
2272            for diag in &file_result.diagnostics {
2273                all_codes.push(diag.code);
2274            }
2275        }
2276
2277        // Store merged program
2278        self.merged = Some(merged);
2279
2280        all_codes
2281    }
2282}
2283
2284/// Create a new multi-file program.
2285#[wasm_bindgen(js_name = createProgram)]
2286pub fn create_program() -> WasmProgram {
2287    WasmProgram::new()
2288}
2289
2290// =============================================================================
2291// Comparison enum - matches TypeScript's Comparison const enum
2292// =============================================================================
2293
2294/// Comparison result for ordering operations.
2295/// Matches TypeScript's `Comparison` const enum in src/compiler/corePublic.ts
2296#[wasm_bindgen]
2297#[repr(i32)]
2298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2299pub enum Comparison {
2300    LessThan = -1,
2301    EqualTo = 0,
2302    GreaterThan = 1,
2303}
2304
2305// =============================================================================
2306// String Comparison Utilities (Phase 1.1)
2307// =============================================================================
2308
2309/// Compare two strings using a case-sensitive ordinal comparison.
2310///
2311/// Ordinal comparisons are based on the difference between the unicode code points
2312/// of both strings. Characters with multiple unicode representations are considered
2313/// unequal. Ordinal comparisons provide predictable ordering, but place "a" after "B".
2314#[wasm_bindgen(js_name = compareStringsCaseSensitive)]
2315pub fn compare_strings_case_sensitive(a: Option<String>, b: Option<String>) -> Comparison {
2316    match (a, b) {
2317        (None, None) => Comparison::EqualTo,
2318        (None, Some(_)) => Comparison::LessThan,
2319        (Some(_), None) => Comparison::GreaterThan,
2320        (Some(a), Some(b)) => match a.cmp(&b) {
2321            std::cmp::Ordering::Equal => Comparison::EqualTo,
2322            std::cmp::Ordering::Less => Comparison::LessThan,
2323            std::cmp::Ordering::Greater => Comparison::GreaterThan,
2324        },
2325    }
2326}
2327
2328/// Compare two strings using a case-insensitive ordinal comparison.
2329///
2330/// Case-insensitive comparisons compare both strings one code-point at a time using
2331/// the integer value of each code-point after applying `to_uppercase` to each string.
2332/// We always map both strings to their upper-case form as some unicode characters do
2333/// not properly round-trip to lowercase (such as `ẞ` German sharp capital s).
2334#[wasm_bindgen(js_name = compareStringsCaseInsensitive)]
2335pub fn compare_strings_case_insensitive(a: Option<String>, b: Option<String>) -> Comparison {
2336    match (a, b) {
2337        (None, None) => Comparison::EqualTo,
2338        (None, Some(_)) => Comparison::LessThan,
2339        (Some(_), None) => Comparison::GreaterThan,
2340        (Some(a), Some(b)) => {
2341            if a == b {
2342                return Comparison::EqualTo;
2343            }
2344            // Use iterator-based comparison to avoid allocating new strings
2345            compare_strings_case_insensitive_iter(&a, &b)
2346        }
2347    }
2348}
2349
2350/// Iterator-based case-insensitive comparison (no allocation).
2351/// Maps characters to uppercase on-the-fly without creating new strings.
2352#[inline]
2353fn compare_strings_case_insensitive_iter(a: &str, b: &str) -> Comparison {
2354    use std::cmp::Ordering;
2355
2356    let mut a_chars = a.chars().flat_map(char::to_uppercase);
2357    let mut b_chars = b.chars().flat_map(char::to_uppercase);
2358
2359    loop {
2360        match (a_chars.next(), b_chars.next()) {
2361            (None, None) => return Comparison::EqualTo,
2362            (None, Some(_)) => return Comparison::LessThan,
2363            (Some(_), None) => return Comparison::GreaterThan,
2364            (Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) {
2365                Ordering::Less => return Comparison::LessThan,
2366                Ordering::Greater => return Comparison::GreaterThan,
2367                Ordering::Equal => continue,
2368            },
2369        }
2370    }
2371}
2372
2373/// Compare two strings using a case-insensitive ordinal comparison (eslint-compatible).
2374///
2375/// This uses `to_lowercase` instead of `to_uppercase` to match eslint's `sort-imports`
2376/// rule behavior. The difference affects the relative order of letters and ASCII
2377/// characters 91-96, of which `_` is a valid identifier character.
2378#[wasm_bindgen(js_name = compareStringsCaseInsensitiveEslintCompatible)]
2379pub fn compare_strings_case_insensitive_eslint_compatible(
2380    a: Option<String>,
2381    b: Option<String>,
2382) -> Comparison {
2383    match (a, b) {
2384        (None, None) => Comparison::EqualTo,
2385        (None, Some(_)) => Comparison::LessThan,
2386        (Some(_), None) => Comparison::GreaterThan,
2387        (Some(a), Some(b)) => {
2388            if a == b {
2389                return Comparison::EqualTo;
2390            }
2391            // Use iterator-based comparison to avoid allocating new strings
2392            compare_strings_case_insensitive_lower_iter(&a, &b)
2393        }
2394    }
2395}
2396
2397/// Iterator-based case-insensitive comparison using lowercase (no allocation).
2398/// Used for eslint compatibility.
2399#[inline]
2400fn compare_strings_case_insensitive_lower_iter(a: &str, b: &str) -> Comparison {
2401    use std::cmp::Ordering;
2402
2403    let mut a_chars = a.chars().flat_map(char::to_lowercase);
2404    let mut b_chars = b.chars().flat_map(char::to_lowercase);
2405
2406    loop {
2407        match (a_chars.next(), b_chars.next()) {
2408            (None, None) => return Comparison::EqualTo,
2409            (None, Some(_)) => return Comparison::LessThan,
2410            (Some(_), None) => return Comparison::GreaterThan,
2411            (Some(a_char), Some(b_char)) => match a_char.cmp(&b_char) {
2412                Ordering::Less => return Comparison::LessThan,
2413                Ordering::Greater => return Comparison::GreaterThan,
2414                Ordering::Equal => continue,
2415            },
2416        }
2417    }
2418}
2419
2420/// Check if two strings are equal (case-sensitive).
2421#[wasm_bindgen(js_name = equateStringsCaseSensitive)]
2422pub fn equate_strings_case_sensitive(a: &str, b: &str) -> bool {
2423    a == b
2424}
2425
2426/// Check if two strings are equal (case-insensitive).
2427/// Uses iterator-based comparison to avoid allocating new strings.
2428#[wasm_bindgen(js_name = equateStringsCaseInsensitive)]
2429pub fn equate_strings_case_insensitive(a: &str, b: &str) -> bool {
2430    if a.len() != b.len() {
2431        // Quick length check - but note that uppercase/lowercase might change length
2432        // for some unicode characters, so we need the full comparison
2433    }
2434    a.chars()
2435        .flat_map(char::to_uppercase)
2436        .eq(b.chars().flat_map(char::to_uppercase))
2437}
2438
2439// =============================================================================
2440// Path Utilities (Phase 1.2)
2441// =============================================================================
2442
2443/// Directory separator used internally (forward slash).
2444pub const DIRECTORY_SEPARATOR: char = '/';
2445
2446/// Alternative directory separator (backslash, used on Windows).
2447pub const ALT_DIRECTORY_SEPARATOR: char = '\\';
2448
2449/// Determines whether a charCode corresponds to `/` or `\`.
2450#[allow(clippy::missing_const_for_fn)] // wasm_bindgen does not support const fn
2451#[wasm_bindgen(js_name = isAnyDirectorySeparator)]
2452pub fn is_any_directory_separator(char_code: u32) -> bool {
2453    char_code == DIRECTORY_SEPARATOR as u32 || char_code == ALT_DIRECTORY_SEPARATOR as u32
2454}
2455
2456/// Normalize path separators, converting `\` into `/`.
2457#[wasm_bindgen(js_name = normalizeSlashes)]
2458pub fn normalize_slashes(path: &str) -> String {
2459    if path.contains('\\') {
2460        path.replace('\\', "/")
2461    } else {
2462        path.to_string()
2463    }
2464}
2465
2466/// Determines whether a path has a trailing separator (`/` or `\\`).
2467#[wasm_bindgen(js_name = hasTrailingDirectorySeparator)]
2468pub fn has_trailing_directory_separator(path: &str) -> bool {
2469    let last_char = match path.chars().last() {
2470        Some(c) => c,
2471        None => return false,
2472    };
2473    last_char == DIRECTORY_SEPARATOR || last_char == ALT_DIRECTORY_SEPARATOR
2474}
2475
2476/// Determines whether a path starts with a relative path component (i.e. `.` or `..`).
2477#[wasm_bindgen(js_name = pathIsRelative)]
2478pub fn path_is_relative(path: &str) -> bool {
2479    // Matches /^\.\.?(?:$|[\\/])/
2480    if path.starts_with("./") || path.starts_with(".\\") || path == "." {
2481        return true;
2482    }
2483    if path.starts_with("../") || path.starts_with("..\\") || path == ".." {
2484        return true;
2485    }
2486    false
2487}
2488
2489/// Removes a trailing directory separator from a path, if it has one.
2490/// Uses char-based operations for UTF-8 safety.
2491#[wasm_bindgen(js_name = removeTrailingDirectorySeparator)]
2492pub fn remove_trailing_directory_separator(path: &str) -> String {
2493    if !has_trailing_directory_separator(path) || path.len() <= 1 {
2494        return path.to_string();
2495    }
2496    // Use strip_suffix for UTF-8 safe character removal
2497    path.strip_suffix(DIRECTORY_SEPARATOR)
2498        .or_else(|| path.strip_suffix(ALT_DIRECTORY_SEPARATOR))
2499        .unwrap_or(path)
2500        .to_string()
2501}
2502
2503/// Ensures a path has a trailing directory separator.
2504#[wasm_bindgen(js_name = ensureTrailingDirectorySeparator)]
2505pub fn ensure_trailing_directory_separator(path: &str) -> String {
2506    if has_trailing_directory_separator(path) {
2507        path.to_string()
2508    } else {
2509        format!("{path}/")
2510    }
2511}
2512
2513/// Determines whether a path has an extension.
2514#[wasm_bindgen(js_name = hasExtension)]
2515pub fn has_extension(file_name: &str) -> bool {
2516    get_base_file_name(file_name).contains('.')
2517}
2518
2519/// Returns the path except for its containing directory name (basename).
2520/// Uses char-based operations for UTF-8 safety.
2521#[wasm_bindgen(js_name = getBaseFileName)]
2522pub fn get_base_file_name(path: &str) -> String {
2523    let path = normalize_slashes(path);
2524    // Remove trailing separator using UTF-8 safe operations
2525    let path = if has_trailing_directory_separator(&path) && path.len() > 1 {
2526        path.strip_suffix(DIRECTORY_SEPARATOR)
2527            .or_else(|| path.strip_suffix(ALT_DIRECTORY_SEPARATOR))
2528            .unwrap_or(&path)
2529    } else {
2530        &path
2531    };
2532    // Find last separator - safe because '/' is ASCII and rfind returns valid char boundary
2533    match path.rfind('/') {
2534        Some(idx) => path[idx + 1..].to_string(),
2535        None => path.to_string(),
2536    }
2537}
2538
2539/// Check if path ends with a specific extension.
2540#[wasm_bindgen(js_name = fileExtensionIs)]
2541pub fn file_extension_is(path: &str, extension: &str) -> bool {
2542    path.len() > extension.len() && path.ends_with(extension)
2543}
2544
2545/// Convert file name to lowercase for case-insensitive file systems.
2546///
2547/// This function handles special Unicode characters that need to remain
2548/// case-sensitive for proper cross-platform file name handling:
2549/// - \u{0130} (İ - Latin capital I with dot above)
2550/// - \u{0131} (ı - Latin small letter dotless i)
2551/// - \u{00DF} (ß - Latin small letter sharp s)
2552///
2553/// These characters are excluded from lowercase conversion to maintain
2554/// compatibility with case-insensitive file systems that have special
2555/// handling for these characters (notably Turkish locale on Windows).
2556///
2557/// Matches TypeScript's `toFileNameLowerCase` in src/compiler/core.ts
2558#[wasm_bindgen(js_name = toFileNameLowerCase)]
2559pub fn to_file_name_lower_case(x: &str) -> String {
2560    // First, check if we need to do any work (optimization - avoid allocation)
2561    // The "safe" set of characters that don't need lowercasing:
2562    // - \u{0130} (İ), \u{0131} (ı), \u{00DF} (ß) - special Turkish chars
2563    // - a-z (lowercase ASCII letters)
2564    // - 0-9 (digits)
2565    // - \ / : - _ . (path separators and common filename chars)
2566    // - space
2567
2568    let needs_conversion = x.chars().any(|c| {
2569        !matches!(c,
2570            '\u{0130}' | '\u{0131}' | '\u{00DF}' |  // Special Unicode chars
2571            'a'..='z' | '0'..='9' |  // ASCII lowercase and digits
2572            '\\' | '/' | ':' | '-' | '_' | '.' | ' '  // Path chars and space
2573        )
2574    });
2575
2576    if !needs_conversion {
2577        return x.to_string();
2578    }
2579
2580    // Convert to lowercase, preserving the special characters
2581    x.to_lowercase()
2582}
2583
2584// =============================================================================
2585// Character Classification (Phase 1.3 - Scanner Prep)
2586// =============================================================================
2587
2588use crate::char_codes::CharacterCodes;
2589
2590/// Check if character is a line break (LF, CR, LS, PS).
2591#[allow(clippy::missing_const_for_fn)] // wasm_bindgen does not support const fn
2592#[wasm_bindgen(js_name = isLineBreak)]
2593pub fn is_line_break(ch: u32) -> bool {
2594    ch == CharacterCodes::LINE_FEED
2595        || ch == CharacterCodes::CARRIAGE_RETURN
2596        || ch == CharacterCodes::LINE_SEPARATOR
2597        || ch == CharacterCodes::PARAGRAPH_SEPARATOR
2598}
2599
2600/// Check if character is a single-line whitespace (not including line breaks).
2601#[wasm_bindgen(js_name = isWhiteSpaceSingleLine)]
2602pub fn is_white_space_single_line(ch: u32) -> bool {
2603    ch == CharacterCodes::SPACE
2604        || ch == CharacterCodes::TAB
2605        || ch == CharacterCodes::VERTICAL_TAB
2606        || ch == CharacterCodes::FORM_FEED
2607        || ch == CharacterCodes::NON_BREAKING_SPACE
2608        || ch == CharacterCodes::NEXT_LINE
2609        || ch == CharacterCodes::OGHAM
2610        || (CharacterCodes::EN_QUAD..=CharacterCodes::ZERO_WIDTH_SPACE).contains(&ch)
2611        || ch == CharacterCodes::NARROW_NO_BREAK_SPACE
2612        || ch == CharacterCodes::MATHEMATICAL_SPACE
2613        || ch == CharacterCodes::IDEOGRAPHIC_SPACE
2614        || ch == CharacterCodes::BYTE_ORDER_MARK
2615}
2616
2617/// Check if character is any whitespace (including line breaks).
2618#[wasm_bindgen(js_name = isWhiteSpaceLike)]
2619pub fn is_white_space_like(ch: u32) -> bool {
2620    is_white_space_single_line(ch) || is_line_break(ch)
2621}
2622
2623/// Check if character is a decimal digit (0-9).
2624#[wasm_bindgen(js_name = isDigit)]
2625pub fn is_digit(ch: u32) -> bool {
2626    (CharacterCodes::_0..=CharacterCodes::_9).contains(&ch)
2627}
2628
2629/// Check if character is an octal digit (0-7).
2630#[wasm_bindgen(js_name = isOctalDigit)]
2631pub fn is_octal_digit(ch: u32) -> bool {
2632    (CharacterCodes::_0..=CharacterCodes::_7).contains(&ch)
2633}
2634
2635/// Check if character is a hexadecimal digit (0-9, A-F, a-f).
2636#[wasm_bindgen(js_name = isHexDigit)]
2637pub fn is_hex_digit(ch: u32) -> bool {
2638    is_digit(ch)
2639        || (CharacterCodes::UPPER_A..=CharacterCodes::UPPER_F).contains(&ch)
2640        || (CharacterCodes::LOWER_A..=CharacterCodes::LOWER_F).contains(&ch)
2641}
2642
2643/// Check if character is an ASCII letter (A-Z, a-z).
2644#[wasm_bindgen(js_name = isASCIILetter)]
2645pub fn is_ascii_letter(ch: u32) -> bool {
2646    (CharacterCodes::UPPER_A..=CharacterCodes::UPPER_Z).contains(&ch)
2647        || (CharacterCodes::LOWER_A..=CharacterCodes::LOWER_Z).contains(&ch)
2648}
2649
2650/// Check if character is a word character (A-Z, a-z, 0-9, _).
2651#[wasm_bindgen(js_name = isWordCharacter)]
2652pub fn is_word_character(ch: u32) -> bool {
2653    is_ascii_letter(ch) || is_digit(ch) || ch == CharacterCodes::UNDERSCORE
2654}
2655
2656// =============================================================================
2657// Unit Tests
2658// =============================================================================
2659
2660// ASI Conformance tests for verifying TS1005/TS1109 patterns
2661#[cfg(test)]
2662#[path = "../tests/asi_conformance_tests.rs"]
2663mod asi_conformance_tests;
2664
2665#[cfg(test)]
2666#[path = "../tests/debug_asi.rs"]
2667mod debug_asi;
2668
2669// P1 Error Recovery tests for synchronization point improvements
2670#[cfg(test)]
2671#[path = "../tests/p1_error_recovery_tests.rs"]
2672mod p1_error_recovery_tests;
2673
2674// Tests moved to checker crate: strict_null_manual, generic_inference_manual,
2675// enum_nominality_tests, private_brands
2676
2677// Constructor accessibility tests
2678#[cfg(test)]
2679#[path = "../crates/tsz-checker/tests/constructor_accessibility.rs"]
2680mod constructor_accessibility;
2681
2682// Void return exception tests
2683#[cfg(test)]
2684#[path = "../crates/tsz-checker/tests/void_return_exception.rs"]
2685mod void_return_exception;
2686
2687// Any-propagation tests
2688#[cfg(test)]
2689#[path = "../crates/tsz-checker/tests/any_propagation.rs"]
2690mod any_propagation;
2691
2692// Tests that depend on test_fixtures (require root crate context)
2693#[cfg(test)]
2694#[path = "../crates/tsz-checker/tests/any_propagation_tests.rs"]
2695mod any_propagation_tests;
2696#[cfg(test)]
2697#[path = "../crates/tsz-checker/tests/const_assertion_tests.rs"]
2698mod const_assertion_tests;
2699#[cfg(test)]
2700#[path = "../crates/tsz-checker/tests/contextual_typing_tests.rs"]
2701mod contextual_typing_tests;
2702#[cfg(test)]
2703#[path = "../crates/tsz-checker/tests/freshness_stripping_tests.rs"]
2704mod freshness_stripping_tests;
2705#[cfg(test)]
2706#[path = "../crates/tsz-checker/tests/function_bivariance.rs"]
2707mod function_bivariance;
2708#[cfg(test)]
2709#[path = "../crates/tsz-checker/tests/global_type_tests.rs"]
2710mod global_type_tests;
2711#[cfg(test)]
2712#[path = "../crates/tsz-checker/tests/symbol_resolution_tests.rs"]
2713mod symbol_resolution_tests;
2714#[cfg(test)]
2715#[path = "../crates/tsz-checker/tests/ts2304_tests.rs"]
2716mod ts2304_tests;
2717#[cfg(test)]
2718#[path = "../crates/tsz-checker/tests/ts2305_tests.rs"]
2719mod ts2305_tests;
2720#[cfg(test)]
2721#[path = "../crates/tsz-checker/tests/ts2306_tests.rs"]
2722mod ts2306_tests;
2723#[cfg(test)]
2724#[path = "../crates/tsz-checker/tests/widening_integration_tests.rs"]
2725mod widening_integration_tests;