Skip to main content

tsz_cli/
driver.rs

1use anyhow::{Result, bail};
2use std::collections::VecDeque;
3
4use rustc_hash::{FxHashMap, FxHashSet};
5use std::hash::{Hash, Hasher};
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::time::Instant;
9
10use crate::args::{CliArgs, ModuleDetection};
11use crate::config::{
12    ResolvedCompilerOptions, TsConfig, checker_target_from_emitter, load_tsconfig,
13    load_tsconfig_with_diagnostics, resolve_compiler_options, resolve_default_lib_files,
14    resolve_lib_files,
15};
16use tsz::binder::BinderOptions;
17use tsz::binder::BinderState;
18use tsz::binder::{SymbolId, SymbolTable, symbol_flags};
19use tsz::checker::TypeCache;
20use tsz::checker::context::LibContext;
21use tsz::checker::diagnostics::{
22    Diagnostic, DiagnosticCategory, DiagnosticRelatedInformation, diagnostic_codes,
23    diagnostic_messages, format_message,
24};
25use tsz::checker::state::CheckerState;
26use tsz::lib_loader::LibFile;
27use tsz::module_resolver::ModuleResolver;
28use tsz::span::Span;
29use tsz_binder::state::BinderStateScopeInputs;
30use tsz_common::common::ModuleKind;
31// Re-export functions that other modules (e.g. watch) access via `driver::`.
32use crate::driver_emit::{EmitOutputsContext, emit_outputs, normalize_type_roots, write_outputs};
33pub(crate) use crate::driver_emit::{normalize_base_url, normalize_output_dir, normalize_root_dir};
34use crate::driver_resolution::{
35    ModuleResolutionCache, canonicalize_or_owned, collect_export_binding_nodes,
36    collect_import_bindings, collect_module_specifiers, collect_module_specifiers_from_text,
37    collect_star_export_specifiers, collect_type_packages_from_root, default_type_roots, env_flag,
38    resolve_module_specifier, resolve_type_package_entry, resolve_type_package_from_roots,
39};
40use crate::fs::{FileDiscoveryOptions, discover_ts_files, is_js_file};
41use crate::incremental::{BuildInfo, default_build_info_path};
42use rustc_hash::FxHasher;
43#[cfg(test)]
44use std::cell::RefCell;
45use tsz::parallel::{self, BindResult, BoundFile, MergedProgram};
46use tsz::parser::NodeIndex;
47use tsz::parser::ParseDiagnostic;
48use tsz::parser::node::{NodeAccess, NodeArena};
49use tsz::parser::syntax_kind_ext;
50use tsz::scanner::SyntaxKind;
51use tsz_solver::{QueryCache, TypeFormatter, TypeId};
52
53/// Reason why a file was included in compilation
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub enum FileInclusionReason {
56    /// File specified as a root file (CLI argument or files list)
57    RootFile,
58    /// File matched by include pattern in tsconfig
59    IncludePattern(String),
60    /// File imported from another file
61    ImportedFrom(PathBuf),
62    /// File is a lib file (e.g., lib.es2020.d.ts)
63    LibFile,
64    /// Type reference from another file
65    TypeReference(PathBuf),
66    /// Referenced in a /// <reference> directive
67    TripleSlashReference(PathBuf),
68}
69
70impl std::fmt::Display for FileInclusionReason {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        match self {
73            Self::RootFile => write!(f, "Root file specified"),
74            Self::IncludePattern(pattern) => {
75                write!(f, "Matched by include pattern '{pattern}'")
76            }
77            Self::ImportedFrom(path) => {
78                write!(f, "Imported from '{}'", path.display())
79            }
80            Self::LibFile => write!(f, "Library file"),
81            Self::TypeReference(path) => {
82                write!(f, "Type reference from '{}'", path.display())
83            }
84            Self::TripleSlashReference(path) => {
85                write!(f, "Referenced from '{}'", path.display())
86            }
87        }
88    }
89}
90
91/// Information about an included file
92#[derive(Debug, Clone)]
93pub struct FileInfo {
94    /// Path to the file
95    pub path: PathBuf,
96    /// Why this file was included
97    pub reasons: Vec<FileInclusionReason>,
98}
99
100#[derive(Debug, Clone)]
101pub struct CompilationResult {
102    pub diagnostics: Vec<Diagnostic>,
103    pub emitted_files: Vec<PathBuf>,
104    pub files_read: Vec<PathBuf>,
105    /// Files with their inclusion reasons (for --explainFiles)
106    pub file_infos: Vec<FileInfo>,
107}
108
109const TYPES_VERSIONS_COMPILER_VERSION_ENV_KEY: &str = "TSZ_TYPES_VERSIONS_COMPILER_VERSION";
110
111#[cfg(test)]
112thread_local! {
113    static TEST_TYPES_VERSIONS_COMPILER_VERSION_OVERRIDE: RefCell<Option<Option<String>>> =
114        const { RefCell::new(None) };
115}
116
117#[cfg(test)]
118struct TestTypesVersionsEnvGuard {
119    previous: Option<Option<String>>,
120}
121
122#[cfg(test)]
123impl Drop for TestTypesVersionsEnvGuard {
124    fn drop(&mut self) {
125        TEST_TYPES_VERSIONS_COMPILER_VERSION_OVERRIDE.with(|slot| {
126            let mut slot = slot.borrow_mut();
127            *slot = self.previous.clone();
128        });
129    }
130}
131
132#[cfg(test)]
133pub(crate) fn with_types_versions_env<T>(value: Option<&str>, f: impl FnOnce() -> T) -> T {
134    let value = value.map(str::to_string);
135    let previous = TEST_TYPES_VERSIONS_COMPILER_VERSION_OVERRIDE.with(|slot| {
136        let mut slot = slot.borrow_mut();
137        let previous = slot.clone();
138        *slot = Some(value);
139        previous
140    });
141    let _guard = TestTypesVersionsEnvGuard { previous };
142    f()
143}
144
145#[cfg(test)]
146fn test_types_versions_compiler_version_override() -> Option<Option<String>> {
147    TEST_TYPES_VERSIONS_COMPILER_VERSION_OVERRIDE.with(|slot| slot.borrow().clone())
148}
149
150fn types_versions_compiler_version_env() -> Option<String> {
151    #[cfg(test)]
152    if let Some(override_value) = test_types_versions_compiler_version_override() {
153        return override_value;
154    }
155    std::env::var(TYPES_VERSIONS_COMPILER_VERSION_ENV_KEY).ok()
156}
157
158#[derive(Default)]
159pub(crate) struct CompilationCache {
160    type_caches: FxHashMap<PathBuf, TypeCache>,
161    bind_cache: FxHashMap<PathBuf, BindCacheEntry>,
162    dependencies: FxHashMap<PathBuf, FxHashSet<PathBuf>>,
163    reverse_dependencies: FxHashMap<PathBuf, FxHashSet<PathBuf>>,
164    diagnostics: FxHashMap<PathBuf, Vec<Diagnostic>>,
165    export_hashes: FxHashMap<PathBuf, u64>,
166    import_symbol_ids: FxHashMap<PathBuf, FxHashMap<PathBuf, Vec<SymbolId>>>,
167    star_export_dependencies: FxHashMap<PathBuf, FxHashSet<PathBuf>>,
168}
169
170struct BindCacheEntry {
171    hash: u64,
172    bind_result: BindResult,
173}
174
175impl CompilationCache {
176    #[cfg(test)]
177    pub(crate) fn len(&self) -> usize {
178        self.type_caches.len()
179    }
180
181    #[cfg(test)]
182    pub(crate) fn bind_len(&self) -> usize {
183        self.bind_cache.len()
184    }
185
186    #[cfg(test)]
187    pub(crate) fn diagnostics_len(&self) -> usize {
188        self.diagnostics.len()
189    }
190
191    #[cfg(test)]
192    pub(crate) fn symbol_cache_len(&self, path: &Path) -> Option<usize> {
193        self.type_caches
194            .get(path)
195            .map(|cache| cache.symbol_types.len())
196    }
197
198    #[cfg(test)]
199    pub(crate) fn node_cache_len(&self, path: &Path) -> Option<usize> {
200        self.type_caches
201            .get(path)
202            .map(|cache| cache.node_types.len())
203    }
204
205    #[cfg(test)]
206    pub(crate) fn invalidate_paths_with_dependents<I>(&mut self, paths: I)
207    where
208        I: IntoIterator<Item = PathBuf>,
209    {
210        let changed: FxHashSet<PathBuf> = paths.into_iter().collect();
211        let affected = self.collect_dependents(changed.iter().cloned());
212        for path in affected {
213            self.type_caches.remove(&path);
214            self.bind_cache.remove(&path);
215            self.diagnostics.remove(&path);
216            self.export_hashes.remove(&path);
217            self.import_symbol_ids.remove(&path);
218            self.star_export_dependencies.remove(&path);
219        }
220    }
221
222    pub(crate) fn invalidate_paths_with_dependents_symbols<I>(&mut self, paths: I)
223    where
224        I: IntoIterator<Item = PathBuf>,
225    {
226        let changed: FxHashSet<PathBuf> = paths.into_iter().collect();
227        let affected = self.collect_dependents(changed.iter().cloned());
228        for path in affected {
229            if changed.contains(&path) {
230                self.type_caches.remove(&path);
231                self.bind_cache.remove(&path);
232                self.diagnostics.remove(&path);
233                self.export_hashes.remove(&path);
234                self.import_symbol_ids.remove(&path);
235                self.star_export_dependencies.remove(&path);
236                continue;
237            }
238
239            self.diagnostics.remove(&path);
240            self.export_hashes.remove(&path);
241
242            let mut roots = Vec::new();
243            if let Some(dep_map) = self.import_symbol_ids.get(&path) {
244                for changed_path in &changed {
245                    if let Some(symbols) = dep_map.get(changed_path) {
246                        roots.extend(symbols.iter().copied());
247                    }
248                }
249            }
250
251            if roots.is_empty() {
252                let has_star_export =
253                    self.star_export_dependencies
254                        .get(&path)
255                        .is_some_and(|deps| {
256                            changed
257                                .iter()
258                                .any(|changed_path| deps.contains(changed_path))
259                        });
260                if has_star_export {
261                    if let Some(cache) = self.type_caches.get_mut(&path) {
262                        cache.node_types.clear();
263                    }
264                } else {
265                    self.type_caches.remove(&path);
266                }
267                continue;
268            }
269
270            if let Some(cache) = self.type_caches.get_mut(&path) {
271                cache.invalidate_symbols(&roots);
272            }
273        }
274    }
275
276    pub(crate) fn invalidate_paths<I>(&mut self, paths: I)
277    where
278        I: IntoIterator<Item = PathBuf>,
279    {
280        for path in paths {
281            self.type_caches.remove(&path);
282            self.bind_cache.remove(&path);
283            self.diagnostics.remove(&path);
284            self.export_hashes.remove(&path);
285            self.import_symbol_ids.remove(&path);
286            self.star_export_dependencies.remove(&path);
287        }
288    }
289
290    pub(crate) fn clear(&mut self) {
291        self.type_caches.clear();
292        self.bind_cache.clear();
293        self.dependencies.clear();
294        self.reverse_dependencies.clear();
295        self.diagnostics.clear();
296        self.export_hashes.clear();
297        self.import_symbol_ids.clear();
298        self.star_export_dependencies.clear();
299    }
300
301    pub(crate) fn update_dependencies(
302        &mut self,
303        dependencies: FxHashMap<PathBuf, FxHashSet<PathBuf>>,
304    ) {
305        let mut reverse = FxHashMap::default();
306        for (source, deps) in &dependencies {
307            for dep in deps {
308                reverse
309                    .entry(dep.clone())
310                    .or_insert_with(FxHashSet::default)
311                    .insert(source.clone());
312            }
313        }
314        self.dependencies = dependencies;
315        self.reverse_dependencies = reverse;
316    }
317
318    fn collect_dependents<I>(&self, paths: I) -> FxHashSet<PathBuf>
319    where
320        I: IntoIterator<Item = PathBuf>,
321    {
322        let mut pending = VecDeque::new();
323        let mut affected = FxHashSet::default();
324
325        for path in paths {
326            if affected.insert(path.clone()) {
327                pending.push_back(path);
328            }
329        }
330
331        while let Some(path) = pending.pop_front() {
332            let Some(dependents) = self.reverse_dependencies.get(&path) else {
333                continue;
334            };
335            for dependent in dependents {
336                if affected.insert(dependent.clone()) {
337                    pending.push_back(dependent.clone());
338                }
339            }
340        }
341
342        affected
343    }
344}
345
346/// Convert `CompilationCache` to `BuildInfo` for persistence
347fn compilation_cache_to_build_info(
348    cache: &CompilationCache,
349    root_files: &[PathBuf],
350    base_dir: &Path,
351    options: &ResolvedCompilerOptions,
352) -> BuildInfo {
353    use crate::incremental::{
354        BuildInfoOptions, CachedDiagnostic, CachedRelatedInformation, EmitSignature,
355        FileInfo as IncrementalFileInfo,
356    };
357    use std::collections::BTreeMap;
358
359    let mut file_infos = BTreeMap::new();
360    let mut dependencies = BTreeMap::new();
361    let mut emit_signatures = BTreeMap::new();
362
363    // Convert each file's cache entry to BuildInfo format
364    for (path, hash) in &cache.export_hashes {
365        let relative_path: String = path
366            .strip_prefix(base_dir)
367            .unwrap_or(path)
368            .to_string_lossy()
369            .replace('\\', "/");
370
371        // Create file info with version (hash) and signature
372        let version = format!("{hash:016x}");
373        let signature = Some(format!("{hash:016x}"));
374        file_infos.insert(
375            relative_path.clone(),
376            IncrementalFileInfo {
377                version,
378                signature,
379                affected_files_pending_emit: false,
380                implied_format: None,
381            },
382        );
383
384        // Convert dependencies
385        if let Some(deps) = cache.dependencies.get(path) {
386            let dep_strs: Vec<String> = deps
387                .iter()
388                .map(|d| {
389                    d.strip_prefix(base_dir)
390                        .unwrap_or(d)
391                        .to_string_lossy()
392                        .replace('\\', "/")
393                })
394                .collect();
395            dependencies.insert(relative_path.clone(), dep_strs);
396        }
397
398        // Add emit signature (empty for now, populated during emit)
399        emit_signatures.insert(
400            relative_path,
401            EmitSignature {
402                js: None,
403                dts: None,
404                map: None,
405            },
406        );
407    }
408
409    // Convert diagnostics to cached format
410    let mut semantic_diagnostics_per_file = BTreeMap::new();
411    for (path, diagnostics) in &cache.diagnostics {
412        let relative_path: String = path
413            .strip_prefix(base_dir)
414            .unwrap_or(path)
415            .to_string_lossy()
416            .replace('\\', "/");
417
418        let cached_diagnostics: Vec<CachedDiagnostic> = diagnostics
419            .iter()
420            .map(|d| {
421                let file_path = Path::new(&d.file);
422                CachedDiagnostic {
423                    file: file_path
424                        .strip_prefix(base_dir)
425                        .unwrap_or(file_path)
426                        .to_string_lossy()
427                        .replace('\\', "/"),
428                    start: d.start,
429                    length: d.length,
430                    message_text: d.message_text.clone(),
431                    category: d.category as u8,
432                    code: d.code,
433                    related_information: d
434                        .related_information
435                        .iter()
436                        .map(|r| {
437                            let rel_file_path = Path::new(&r.file);
438                            CachedRelatedInformation {
439                                file: rel_file_path
440                                    .strip_prefix(base_dir)
441                                    .unwrap_or(rel_file_path)
442                                    .to_string_lossy()
443                                    .replace('\\', "/"),
444                                start: r.start,
445                                length: r.length,
446                                message_text: r.message_text.clone(),
447                                category: r.category as u8,
448                                code: r.code,
449                            }
450                        })
451                        .collect(),
452                }
453            })
454            .collect();
455
456        if !cached_diagnostics.is_empty() {
457            semantic_diagnostics_per_file.insert(relative_path, cached_diagnostics);
458        }
459    }
460
461    // Convert root files to relative paths
462    let root_files_str: Vec<String> = root_files
463        .iter()
464        .map(|p| {
465            p.strip_prefix(base_dir)
466                .unwrap_or(p)
467                .to_string_lossy()
468                .replace('\\', "/")
469        })
470        .collect();
471
472    // Build compiler options
473    let build_options = BuildInfoOptions {
474        target: Some(format!("{:?}", options.checker.target)),
475        module: Some(format!("{:?}", options.printer.module)),
476        declaration: Some(options.emit_declarations),
477        strict: Some(options.checker.strict),
478    };
479
480    BuildInfo {
481        version: crate::incremental::BUILD_INFO_VERSION.to_string(),
482        compiler_version: env!("CARGO_PKG_VERSION").to_string(),
483        root_files: root_files_str,
484        file_infos,
485        dependencies,
486        semantic_diagnostics_per_file,
487        emit_signatures,
488        latest_changed_dts_file: None, // TODO: Track most recently changed .d.ts file
489        options: build_options,
490        build_time: std::time::SystemTime::now()
491            .duration_since(std::time::UNIX_EPOCH)
492            .map(|d| d.as_secs())
493            .unwrap_or(0),
494    }
495}
496
497/// Load `BuildInfo` and create an initial `CompilationCache` from it
498fn build_info_to_compilation_cache(build_info: &BuildInfo, base_dir: &Path) -> CompilationCache {
499    let mut cache = CompilationCache::default();
500
501    // Convert string paths back to PathBuf and populate export_hashes
502    for (path_str, file_info) in &build_info.file_infos {
503        let full_path = base_dir.join(path_str);
504
505        // Parse version hash back to u64
506        if let Ok(hash) = u64::from_str_radix(&file_info.version, 16) {
507            cache.export_hashes.insert(full_path.clone(), hash);
508        }
509
510        // Convert dependencies
511        if let Some(deps) = build_info.get_dependencies(path_str) {
512            let mut dep_paths = FxHashSet::default();
513            for dep in deps {
514                let dep_path = base_dir.join(dep);
515                cache
516                    .reverse_dependencies
517                    .entry(dep_path.clone())
518                    .or_default()
519                    .insert(full_path.clone());
520                dep_paths.insert(dep_path);
521            }
522            cache.dependencies.insert(full_path, dep_paths);
523        }
524    }
525
526    // Load diagnostics from BuildInfo
527    for (path_str, cached_diagnostics) in &build_info.semantic_diagnostics_per_file {
528        let full_path = base_dir.join(path_str);
529
530        let diagnostics: Vec<Diagnostic> = cached_diagnostics
531            .iter()
532            .map(|cd| Diagnostic {
533                file: full_path.to_string_lossy().into_owned(),
534                start: cd.start,
535                length: cd.length,
536                message_text: cd.message_text.clone(),
537                category: match cd.category {
538                    0 => DiagnosticCategory::Warning,
539                    1 => DiagnosticCategory::Error,
540                    2 => DiagnosticCategory::Suggestion,
541                    _ => DiagnosticCategory::Message,
542                },
543                code: cd.code,
544                related_information: cd
545                    .related_information
546                    .iter()
547                    .map(|r| DiagnosticRelatedInformation {
548                        file: base_dir.join(&r.file).to_string_lossy().into_owned(),
549                        start: r.start,
550                        length: r.length,
551                        message_text: r.message_text.clone(),
552                        category: match r.category {
553                            0 => DiagnosticCategory::Warning,
554                            1 => DiagnosticCategory::Error,
555                            2 => DiagnosticCategory::Suggestion,
556                            _ => DiagnosticCategory::Message,
557                        },
558                        code: r.code,
559                    })
560                    .collect(),
561            })
562            .collect();
563
564        if !diagnostics.is_empty() {
565            cache.diagnostics.insert(full_path, diagnostics);
566        }
567    }
568
569    cache
570}
571
572/// Get the .tsbuildinfo file path based on compiler options
573fn get_build_info_path(
574    tsconfig_path: Option<&Path>,
575    options: &ResolvedCompilerOptions,
576    base_dir: &Path,
577) -> Option<PathBuf> {
578    if !options.incremental && options.ts_build_info_file.is_none() {
579        return None;
580    }
581
582    if let Some(ref explicit_path) = options.ts_build_info_file {
583        return Some(base_dir.join(explicit_path));
584    }
585
586    // Use tsconfig path to determine default buildinfo location
587    let config_path = tsconfig_path?;
588    let out_dir = options.out_dir.as_ref().map(|od| base_dir.join(od));
589    Some(default_build_info_path(config_path, out_dir.as_deref()))
590}
591
592pub fn compile(args: &CliArgs, cwd: &Path) -> Result<CompilationResult> {
593    compile_inner(args, cwd, None, None, None, None)
594}
595
596/// Compile a specific project by config path (used for --build mode with project references)
597pub fn compile_project(
598    args: &CliArgs,
599    cwd: &Path,
600    config_path: &Path,
601) -> Result<CompilationResult> {
602    compile_inner(args, cwd, None, None, None, Some(config_path))
603}
604
605pub(crate) fn compile_with_cache(
606    args: &CliArgs,
607    cwd: &Path,
608    cache: &mut CompilationCache,
609) -> Result<CompilationResult> {
610    compile_inner(args, cwd, Some(cache), None, None, None)
611}
612
613pub(crate) fn compile_with_cache_and_changes(
614    args: &CliArgs,
615    cwd: &Path,
616    cache: &mut CompilationCache,
617    changed_paths: &[PathBuf],
618) -> Result<CompilationResult> {
619    let canonical_paths: Vec<PathBuf> = changed_paths
620        .iter()
621        .map(|path| canonicalize_or_owned(path))
622        .collect();
623    let mut old_hashes = FxHashMap::default();
624    for path in &canonical_paths {
625        if let Some(&hash) = cache.export_hashes.get(path) {
626            old_hashes.insert(path.clone(), hash);
627        }
628    }
629
630    cache.invalidate_paths(canonical_paths.iter().cloned());
631    let result = compile_inner(args, cwd, Some(cache), Some(&canonical_paths), None, None)?;
632
633    let exports_changed = canonical_paths
634        .iter()
635        .any(|path| old_hashes.get(path).copied() != cache.export_hashes.get(path).copied());
636    if !exports_changed {
637        return Ok(result);
638    }
639
640    // If --assumeChangesOnlyAffectDirectDependencies is set, only recompile direct dependents
641    let dependents = if args.assume_changes_only_affect_direct_dependencies {
642        // Only get direct dependents (one level deep)
643        let mut direct_dependents = FxHashSet::default();
644        for path in &canonical_paths {
645            if let Some(deps) = cache.reverse_dependencies.get(path) {
646                direct_dependents.extend(deps.iter().cloned());
647            }
648        }
649        direct_dependents
650    } else {
651        // Get all transitive dependents (default behavior)
652        cache.collect_dependents(canonical_paths.iter().cloned())
653    };
654
655    cache.invalidate_paths_with_dependents_symbols(canonical_paths);
656    compile_inner(
657        args,
658        cwd,
659        Some(cache),
660        Some(changed_paths),
661        Some(&dependents),
662        None,
663    )
664}
665
666fn compile_inner(
667    args: &CliArgs,
668    cwd: &Path,
669    mut cache: Option<&mut CompilationCache>,
670    changed_paths: Option<&[PathBuf]>,
671    forced_dirty_paths: Option<&FxHashSet<PathBuf>>,
672    explicit_config_path: Option<&Path>,
673) -> Result<CompilationResult> {
674    let _compile_span = tracing::info_span!("compile", cwd = %cwd.display()).entered();
675    let perf_enabled = std::env::var_os("TSZ_PERF").is_some();
676    let compile_start = Instant::now();
677
678    let perf_log_phase = |phase: &'static str, start: Instant| {
679        if perf_enabled {
680            tracing::info!(
681                target: "wasm::perf",
682                phase,
683                ms = start.elapsed().as_secs_f64() * 1000.0
684            );
685        }
686    };
687
688    let cwd = canonicalize_or_owned(cwd);
689    let tsconfig_path = if let Some(path) = explicit_config_path {
690        Some(path.to_path_buf())
691    } else {
692        match resolve_tsconfig_path(&cwd, args.project.as_deref()) {
693            Ok(path) => path,
694            Err(err) => {
695                return Ok(config_error_result(
696                    None,
697                    err.to_string(),
698                    diagnostic_codes::CANNOT_FIND_A_TSCONFIG_JSON_FILE_AT_THE_SPECIFIED_DIRECTORY,
699                ));
700            }
701        }
702    };
703    let loaded = load_config_with_diagnostics(tsconfig_path.as_deref())?;
704    let config = loaded.config;
705    let config_diagnostics = loaded.diagnostics;
706
707    // TS5103 (invalid ignoreDeprecations value) is fatal in tsc: it stops compilation
708    // and reports only the config error. Match this behavior to avoid extra diagnostics.
709    if config_diagnostics
710        .iter()
711        .any(|d| d.code == diagnostic_codes::INVALID_VALUE_FOR_IGNOREDEPRECATIONS)
712    {
713        return Ok(CompilationResult {
714            diagnostics: config_diagnostics,
715            emitted_files: Vec::new(),
716            files_read: Vec::new(),
717            file_infos: Vec::new(),
718        });
719    }
720
721    let mut resolved = resolve_compiler_options(
722        config
723            .as_ref()
724            .and_then(|cfg| cfg.compiler_options.as_ref()),
725    )?;
726    apply_cli_overrides(&mut resolved, args)?;
727
728    // Wire removed-but-honored suppress flags from config
729    if loaded.suppress_excess_property_errors {
730        resolved.checker.suppress_excess_property_errors = true;
731    }
732    if loaded.suppress_implicit_any_index_errors {
733        resolved.checker.suppress_implicit_any_index_errors = true;
734    }
735    if config.is_none()
736        && args.module.is_none()
737        && matches!(resolved.printer.module, ModuleKind::None)
738    {
739        // When no tsconfig is present, align with tsc's computed module default:
740        // ES2015+ targets -> ES2015 modules, older targets -> CommonJS.
741        let default_module = if resolved.printer.target.supports_es2015() {
742            ModuleKind::ES2015
743        } else {
744            ModuleKind::CommonJS
745        };
746        resolved.printer.module = default_module;
747        resolved.checker.module = default_module;
748    }
749
750    if let Some(diag) = check_module_resolution_compatibility(&resolved, tsconfig_path.as_deref()) {
751        return Ok(CompilationResult {
752            diagnostics: vec![diag],
753            emitted_files: Vec::new(),
754            files_read: Vec::new(),
755            file_infos: Vec::new(),
756        });
757    }
758
759    let base_dir = config_base_dir(&cwd, tsconfig_path.as_deref());
760    let base_dir = canonicalize_or_owned(&base_dir);
761    let root_dir = normalize_root_dir(&base_dir, resolved.root_dir.clone());
762    let out_dir = normalize_output_dir(&base_dir, resolved.out_dir.clone());
763    let declaration_dir = normalize_output_dir(&base_dir, resolved.declaration_dir.clone());
764    let base_url = normalize_base_url(&base_dir, resolved.base_url.clone());
765    resolved.base_url = base_url;
766    resolved.type_roots = normalize_type_roots(&base_dir, resolved.type_roots.clone());
767
768    let discovery = build_discovery_options(
769        args,
770        &base_dir,
771        tsconfig_path.as_deref(),
772        config.as_ref(),
773        out_dir.as_deref(),
774        &resolved,
775    )?;
776    let mut file_paths = discover_ts_files(&discovery)?;
777
778    // Track if we should save BuildInfo after successful compilation
779    let mut should_save_build_info = false;
780
781    // Local cache for BuildInfo-loaded compilation state
782    // Only create when loading from BuildInfo (not when a cache is provided)
783    let mut local_cache: Option<CompilationCache> = None;
784
785    // Load BuildInfo if incremental compilation is enabled and no cache was provided
786    if cache.is_none() && (resolved.incremental || resolved.ts_build_info_file.is_some()) {
787        let tsconfig_path_ref = tsconfig_path.as_deref();
788        if let Some(build_info_path) = get_build_info_path(tsconfig_path_ref, &resolved, &base_dir)
789        {
790            if build_info_path.exists() {
791                match BuildInfo::load(&build_info_path) {
792                    Ok(Some(build_info)) => {
793                        // Create a local cache from BuildInfo
794                        local_cache = Some(build_info_to_compilation_cache(&build_info, &base_dir));
795                        tracing::info!("Loaded BuildInfo from: {}", build_info_path.display());
796                    }
797                    Ok(None) => {
798                        tracing::info!(
799                            "BuildInfo at {} is outdated or incompatible, starting fresh",
800                            build_info_path.display()
801                        );
802                    }
803                    Err(e) => {
804                        tracing::warn!(
805                            "Failed to load BuildInfo from {}: {}, starting fresh",
806                            build_info_path.display(),
807                            e
808                        );
809                    }
810                }
811            } else {
812                // BuildInfo doesn't exist yet, create empty local cache for new compilation
813                local_cache = Some(CompilationCache::default());
814            }
815            should_save_build_info = true;
816        }
817    }
818
819    // Determine which cache to use: local cache from BuildInfo, or provided cache, or none
820    // When cache is None, we can use local_cache; otherwise we use the provided cache
821    if file_paths.is_empty() {
822        // Emit TS18003: No inputs were found in config file.
823        // Match tsc: use the resolved config path shown to the compiler.
824        let config_name = tsconfig_path
825            .as_ref()
826            .map(|path| path.to_string_lossy().to_string())
827            .unwrap_or_else(|| "tsconfig.json".to_string());
828        let include_str = discovery
829            .include
830            .as_ref()
831            .filter(|v| !v.is_empty())
832            .map(|v| {
833                v.iter()
834                    .map(|s| format!("\"{s}\""))
835                    .collect::<Vec<_>>()
836                    .join(",")
837            })
838            .unwrap_or_default();
839        let exclude_str = discovery
840            .exclude
841            .as_ref()
842            .filter(|v| !v.is_empty())
843            .map(|v| {
844                v.iter()
845                    .map(|s| format!("\"{s}\""))
846                    .collect::<Vec<_>>()
847                    .join(",")
848            })
849            .unwrap_or_default();
850        let message = format!(
851            "No inputs were found in config file '{config_name}'. Specified 'include' paths were '[{include_str}]' and 'exclude' paths were '[{exclude_str}]'."
852        );
853        return Ok(CompilationResult {
854            // tsc emits TS18003 without file position (file="", pos=0).
855            diagnostics: vec![Diagnostic::error(String::new(), 0, 0, message, 18003)],
856            emitted_files: Vec::new(),
857            files_read: Vec::new(),
858            file_infos: Vec::new(),
859        });
860    }
861
862    let (type_files, unresolved_types) = collect_type_root_files(&base_dir, &resolved);
863
864    // Add type definition files (e.g., @types packages) to the source file list.
865    // Note: lib.d.ts files are NOT added here - they are loaded separately via
866    // lib preloading + checker lib contexts. This prevents them from
867    // being type-checked as regular source files (which would emit spurious errors).
868    if !type_files.is_empty() {
869        let mut merged = std::collections::BTreeSet::new();
870        merged.extend(file_paths);
871        merged.extend(type_files);
872        file_paths = merged.into_iter().collect();
873    }
874
875    let changed_set = changed_paths.map(|paths| {
876        paths
877            .iter()
878            .map(|path| canonicalize_or_owned(path))
879            .collect::<FxHashSet<_>>()
880    });
881
882    // Create a unified effective cache reference that works for both cases
883    // This follows Gemini's recommended pattern to handle the two cache sources
884    let local_cache_ref = local_cache.as_mut();
885    let mut effective_cache = local_cache_ref.or(cache.as_deref_mut());
886
887    let read_sources_start = Instant::now();
888    let SourceReadResult {
889        sources: all_sources,
890        dependencies,
891        type_reference_errors,
892    } = {
893        read_source_files(
894            &file_paths,
895            &base_dir,
896            &resolved,
897            effective_cache.as_deref(),
898            changed_set.as_ref(),
899        )?
900    };
901    perf_log_phase("read_sources", read_sources_start);
902
903    // Update dependencies in the cache
904    if let Some(ref mut c) = effective_cache {
905        c.update_dependencies(dependencies);
906    }
907
908    // Separate binary files from regular sources - binary files get TS1490
909    let mut type_file_diagnostics: Vec<Diagnostic> = Vec::new();
910    for (path, type_name) in type_reference_errors {
911        let file_name = path.to_string_lossy().into_owned();
912        type_file_diagnostics.push(Diagnostic::error(
913            file_name,
914            0,
915            0,
916            format!("Cannot find type definition file for '{type_name}'."),
917            diagnostic_codes::CANNOT_FIND_TYPE_DEFINITION_FILE_FOR,
918        ));
919    }
920    // Emit TS2688 for unresolved entries in tsconfig `types` array
921    for type_name in &unresolved_types {
922        type_file_diagnostics.push(Diagnostic::error(
923            String::new(),
924            0,
925            0,
926            format!("Cannot find type definition file for '{type_name}'."),
927            diagnostic_codes::CANNOT_FIND_TYPE_DEFINITION_FILE_FOR,
928        ));
929    }
930
931    let mut binary_file_diagnostics: Vec<Diagnostic> = Vec::new();
932    let mut binary_file_names: FxHashSet<String> = FxHashSet::default();
933    let mut sources: Vec<SourceEntry> = Vec::with_capacity(all_sources.len());
934    for source in all_sources {
935        if source.is_binary {
936            // Emit TS1490 "File appears to be binary." for binary files.
937            // Track the file name so we can suppress parser diagnostics
938            // (e.g. TS1127 "Invalid character") that cascade from parsing
939            // UTF-16/corrupted content as UTF-8.
940            let file_name = source.path.to_string_lossy().into_owned();
941            binary_file_names.insert(file_name.clone());
942            binary_file_diagnostics.push(Diagnostic::error(
943                file_name,
944                0,
945                0,
946                "File appears to be binary.".to_string(),
947                diagnostic_codes::FILE_APPEARS_TO_BE_BINARY,
948            ));
949        }
950        sources.push(source);
951    }
952
953    // Collect all files that were read (including dependencies) before sources is moved
954    let mut files_read: Vec<PathBuf> = sources.iter().map(|s| s.path.clone()).collect();
955    files_read.sort();
956
957    // Build file info with inclusion reasons
958    let file_infos = build_file_infos(&sources, &file_paths, args, config.as_ref(), &base_dir);
959
960    let disable_default_libs = resolved.lib_is_default && sources_have_no_default_lib(&sources);
961    // `@noTypesAndSymbols` in source comments is a conformance-harness directive.
962    // It should not change CLI semantic compilation behavior (tsc ignores it when
963    // compiling files directly), so keep detection for harness plumbing only.
964    let _no_types_and_symbols =
965        resolved.checker.no_types_and_symbols || sources_have_no_types_and_symbols(&sources);
966    resolved.checker.no_types_and_symbols = _no_types_and_symbols;
967    let lib_paths: Vec<PathBuf> =
968        if (resolved.checker.no_lib && resolved.lib_is_default) || disable_default_libs {
969            Vec::new()
970        } else {
971            resolved.lib_files.clone()
972        };
973    let lib_path_refs: Vec<&Path> = lib_paths.iter().map(PathBuf::as_path).collect();
974    // Load and bind each lib exactly once, then reuse for:
975    // 1) user-file binding (global symbol availability during bind)
976    // 2) checker lib contexts (global symbol/type resolution)
977    let load_libs_start = Instant::now();
978    let lib_files: Vec<Arc<LibFile>> = parallel::load_lib_files_for_binding_strict(&lib_path_refs)?;
979    perf_log_phase("load_libs", load_libs_start);
980
981    let build_program_start = Instant::now();
982    let (program, dirty_paths) = if let Some(ref mut c) = effective_cache {
983        let result = build_program_with_cache(sources, c, &lib_files);
984        (result.program, Some(result.dirty_paths))
985    } else {
986        let compile_inputs: Vec<(String, String)> = sources
987            .into_iter()
988            .map(|source| {
989                let text = source.text.unwrap_or_else(|| {
990                    // If source text is missing during compilation, use empty string
991                    // This allows compilation to continue with a diagnostic error later
992                    String::new()
993                });
994                (source.path.to_string_lossy().into_owned(), text)
995            })
996            .collect();
997        let bind_results = parallel::parse_and_bind_parallel_with_libs(compile_inputs, &lib_files);
998        (parallel::merge_bind_results(bind_results), None)
999    };
1000    perf_log_phase("build_program", build_program_start);
1001
1002    // Update import symbol IDs if we have a cache
1003    if let Some(ref mut c) = effective_cache {
1004        update_import_symbol_ids(&program, &resolved, &base_dir, c);
1005    }
1006
1007    // Load lib files only when type checking is needed (lazy loading for faster startup)
1008    let build_lib_contexts_start = Instant::now();
1009    let lib_contexts = if resolved.no_check {
1010        Vec::new() // Skip lib loading when --noCheck is set
1011    } else {
1012        load_lib_files_for_contexts(&lib_files)
1013    };
1014    perf_log_phase("build_lib_contexts", build_lib_contexts_start);
1015
1016    let collect_diagnostics_start = Instant::now();
1017    let mut diagnostics: Vec<Diagnostic> = collect_diagnostics(
1018        &program,
1019        &resolved,
1020        &base_dir,
1021        effective_cache,
1022        &lib_contexts,
1023    );
1024    perf_log_phase("collect_diagnostics", collect_diagnostics_start);
1025
1026    // Get reference to type caches for declaration emit
1027    // Create a longer-lived empty FxHashMap for the fallback case
1028    let empty_type_caches = FxHashMap::default();
1029    let type_caches_ref: &FxHashMap<_, _> = local_cache
1030        .as_ref()
1031        .map(|c| &c.type_caches)
1032        .or_else(|| cache.as_ref().map(|c| &c.type_caches))
1033        .unwrap_or(&empty_type_caches);
1034    // For binary files, suppress all diagnostics except TS1490.
1035    // Parsing UTF-16/corrupted content as UTF-8 produces cascading
1036    // TS1127 "Invalid character" false positives; TSC only emits TS1490.
1037    if !binary_file_names.is_empty() {
1038        diagnostics.retain(|d| !binary_file_names.contains(&d.file));
1039    }
1040    diagnostics.extend(config_diagnostics);
1041    diagnostics.extend(binary_file_diagnostics);
1042    diagnostics.extend(type_file_diagnostics);
1043    diagnostics.sort_by(|left, right| {
1044        left.file
1045            .cmp(&right.file)
1046            .then(left.start.cmp(&right.start))
1047            .then(left.code.cmp(&right.code))
1048    });
1049
1050    let has_error = diagnostics
1051        .iter()
1052        .any(|diag| diag.category == DiagnosticCategory::Error);
1053    let should_emit = !(resolved.no_emit || (resolved.no_emit_on_error && has_error));
1054
1055    let mut dirty_paths = dirty_paths;
1056    if let Some(forced) = forced_dirty_paths {
1057        match &mut dirty_paths {
1058            Some(existing) => {
1059                existing.extend(forced.iter().cloned());
1060            }
1061            None => {
1062                dirty_paths = Some(forced.clone());
1063            }
1064        }
1065    }
1066
1067    let emit_outputs_start = Instant::now();
1068    let emitted_files = if !should_emit {
1069        Vec::new()
1070    } else {
1071        let outputs = emit_outputs(EmitOutputsContext {
1072            program: &program,
1073            options: &resolved,
1074            base_dir: &base_dir,
1075            root_dir: root_dir.as_deref(),
1076            out_dir: out_dir.as_deref(),
1077            declaration_dir: declaration_dir.as_deref(),
1078            dirty_paths: dirty_paths.as_ref(),
1079            type_caches: type_caches_ref,
1080        })?;
1081        write_outputs(&outputs)?
1082    };
1083    perf_log_phase("emit_outputs", emit_outputs_start);
1084
1085    // Find the most recent .d.ts file for BuildInfo tracking
1086    let latest_changed_dts_file = if !emitted_files.is_empty() {
1087        find_latest_dts_file(&emitted_files, &base_dir)
1088    } else {
1089        None
1090    };
1091
1092    // Save BuildInfo if incremental compilation is enabled
1093    if should_save_build_info && !has_error {
1094        let tsconfig_path_ref = tsconfig_path.as_deref();
1095        if let Some(build_info_path) = get_build_info_path(tsconfig_path_ref, &resolved, &base_dir)
1096        {
1097            // Build BuildInfo from the cache (which has been updated by collect_diagnostics)
1098            // If local_cache exists (from BuildInfo), use it; otherwise create minimal info
1099            let mut build_info = if let Some(ref lc) = local_cache {
1100                compilation_cache_to_build_info(lc, &file_paths, &base_dir, &resolved)
1101            } else {
1102                // No cache available - create minimal BuildInfo with just file info
1103                BuildInfo {
1104                    version: crate::incremental::BUILD_INFO_VERSION.to_string(),
1105                    compiler_version: env!("CARGO_PKG_VERSION").to_string(),
1106                    root_files: file_paths
1107                        .iter()
1108                        .map(|p| {
1109                            p.strip_prefix(&base_dir)
1110                                .unwrap_or(p)
1111                                .to_string_lossy()
1112                                .replace('\\', "/")
1113                        })
1114                        .collect(),
1115                    ..Default::default()
1116                }
1117            };
1118
1119            // Set the most recent .d.ts file for cross-project invalidation
1120            build_info.latest_changed_dts_file = latest_changed_dts_file;
1121
1122            if let Err(e) = build_info.save(&build_info_path) {
1123                tracing::warn!(
1124                    "Failed to save BuildInfo to {}: {}",
1125                    build_info_path.display(),
1126                    e
1127                );
1128            } else {
1129                tracing::info!("Saved BuildInfo to: {}", build_info_path.display());
1130            }
1131        }
1132    }
1133
1134    if perf_enabled {
1135        tracing::info!(
1136            target: "wasm::perf",
1137            phase = "compile_total",
1138            ms = compile_start.elapsed().as_secs_f64() * 1000.0,
1139            files = file_paths.len(),
1140            libs = lib_files.len(),
1141            diagnostics = diagnostics.len(),
1142            emitted = emitted_files.len(),
1143            no_check = resolved.no_check
1144        );
1145    }
1146
1147    Ok(CompilationResult {
1148        diagnostics,
1149        emitted_files,
1150        files_read,
1151        file_infos,
1152    })
1153}
1154
1155fn config_error_result(file_path: Option<&Path>, message: String, code: u32) -> CompilationResult {
1156    let file = file_path
1157        .map(|p| p.display().to_string())
1158        .unwrap_or_default();
1159    CompilationResult {
1160        diagnostics: vec![Diagnostic::error(file, 0, 0, message, code)],
1161        emitted_files: Vec::new(),
1162        files_read: Vec::new(),
1163        file_infos: Vec::new(),
1164    }
1165}
1166
1167fn check_module_resolution_compatibility(
1168    resolved: &ResolvedCompilerOptions,
1169    tsconfig_path: Option<&Path>,
1170) -> Option<Diagnostic> {
1171    use tsz::config::ModuleResolutionKind;
1172    use tsz_common::common::ModuleKind;
1173
1174    let module_resolution = resolved.module_resolution?;
1175    let required = match module_resolution {
1176        ModuleResolutionKind::Node16 => ModuleKind::Node16,
1177        ModuleResolutionKind::NodeNext => ModuleKind::NodeNext,
1178        _ => return None,
1179    };
1180
1181    if resolved.printer.module == required {
1182        return None;
1183    }
1184
1185    let required_str = match required {
1186        ModuleKind::NodeNext => "NodeNext",
1187        _ => "Node16",
1188    };
1189    let resolution_str = match module_resolution {
1190        ModuleResolutionKind::NodeNext => "NodeNext",
1191        _ => "Node16",
1192    };
1193
1194    let message = format_message(
1195        diagnostic_messages::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
1196        &[required_str, resolution_str],
1197    );
1198    let file = tsconfig_path
1199        .map(|p| p.display().to_string())
1200        .unwrap_or_default();
1201    Some(Diagnostic::error(
1202        file,
1203        0,
1204        0,
1205        message,
1206        diagnostic_codes::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
1207    ))
1208}
1209
1210/// Build file info with inclusion reasons
1211fn build_file_infos(
1212    sources: &[SourceEntry],
1213    root_file_paths: &[PathBuf],
1214    args: &CliArgs,
1215    config: Option<&crate::config::TsConfig>,
1216    _base_dir: &Path,
1217) -> Vec<FileInfo> {
1218    let root_set: FxHashSet<_> = root_file_paths.iter().collect();
1219    let cli_files: FxHashSet<_> = args.files.iter().collect();
1220
1221    // Get include patterns if available
1222    let include_patterns = config
1223        .and_then(|c| c.include.as_ref())
1224        .map_or_else(|| "**/*".to_string(), |patterns| patterns.join(", "));
1225
1226    sources
1227        .iter()
1228        .map(|source| {
1229            let mut reasons = Vec::new();
1230
1231            // Check if it's a CLI-specified file
1232            if cli_files.iter().any(|f| source.path.ends_with(f)) {
1233                reasons.push(FileInclusionReason::RootFile);
1234            }
1235            // Check if it's a lib file (based on filename pattern)
1236            else if is_lib_file(&source.path) {
1237                reasons.push(FileInclusionReason::LibFile);
1238            }
1239            // Check if it's a root file from discovery
1240            else if root_set.contains(&source.path) {
1241                reasons.push(FileInclusionReason::IncludePattern(
1242                    include_patterns.clone(),
1243                ));
1244            }
1245            // Otherwise it was likely imported (we don't track precise imports yet)
1246            else {
1247                reasons.push(FileInclusionReason::ImportedFrom(PathBuf::from("<import>")));
1248            }
1249
1250            FileInfo {
1251                path: source.path.clone(),
1252                reasons,
1253            }
1254        })
1255        .collect()
1256}
1257
1258/// Check if a file is a TypeScript library file
1259fn is_lib_file(path: &Path) -> bool {
1260    let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
1261
1262    file_name.starts_with("lib.") && file_name.ends_with(".d.ts")
1263}
1264
1265struct SourceMeta {
1266    path: PathBuf,
1267    file_name: String,
1268    hash: u64,
1269    cached_ok: bool,
1270}
1271
1272struct BuildProgramResult {
1273    program: MergedProgram,
1274    dirty_paths: FxHashSet<PathBuf>,
1275}
1276
1277fn build_program_with_cache(
1278    sources: Vec<SourceEntry>,
1279    cache: &mut CompilationCache,
1280    lib_files: &[Arc<LibFile>],
1281) -> BuildProgramResult {
1282    let mut meta = Vec::with_capacity(sources.len());
1283    let mut to_parse = Vec::new();
1284    let mut dirty_paths = FxHashSet::default();
1285
1286    for source in sources {
1287        let file_name = source.path.to_string_lossy().into_owned();
1288        let (hash, cached_ok) = match source.text {
1289            Some(text) => {
1290                let hash = hash_text(&text);
1291                let cached_ok = cache
1292                    .bind_cache
1293                    .get(&source.path)
1294                    .is_some_and(|entry| entry.hash == hash);
1295                if !cached_ok {
1296                    dirty_paths.insert(source.path.clone());
1297                    to_parse.push((file_name.clone(), text));
1298                }
1299                (hash, cached_ok)
1300            }
1301            None => {
1302                // Missing source text without cached result - treat as error
1303                // Return default hash and mark as dirty to force re-parsing
1304                // This avoids crashing when cache is incomplete
1305                (0, false)
1306            }
1307        };
1308
1309        meta.push(SourceMeta {
1310            path: source.path,
1311            file_name,
1312            hash,
1313            cached_ok,
1314        });
1315    }
1316
1317    let parsed_results = if to_parse.is_empty() {
1318        Vec::new()
1319    } else {
1320        // Use parse_and_bind_parallel_with_libs to load prebound lib symbols
1321        // This ensures global symbols like console, Array, Promise are available
1322        // during binding, which prevents "Any poisoning" where unresolved symbols
1323        // default to Any type instead of emitting TS2304 errors.
1324        parallel::parse_and_bind_parallel_with_libs(to_parse, lib_files)
1325    };
1326
1327    let mut parsed_map: FxHashMap<String, BindResult> = parsed_results
1328        .into_iter()
1329        .map(|result| (result.file_name.clone(), result))
1330        .collect();
1331
1332    for entry in &meta {
1333        if entry.cached_ok {
1334            continue;
1335        }
1336
1337        let result = match parsed_map.remove(&entry.file_name) {
1338            Some(r) => r,
1339            None => {
1340                // Missing parse result - this shouldn't happen in normal operation
1341                // Create a fallback empty result to allow compilation to continue
1342                // The error will be reported through diagnostics
1343                BindResult {
1344                    file_name: entry.file_name.clone(),
1345                    source_file: NodeIndex::NONE, // Invalid node index
1346                    arena: std::sync::Arc::new(NodeArena::new()),
1347                    symbols: Default::default(),
1348                    file_locals: Default::default(),
1349                    declared_modules: Default::default(),
1350                    module_exports: Default::default(),
1351                    node_symbols: Default::default(),
1352                    symbol_arenas: Default::default(),
1353                    declaration_arenas: Default::default(),
1354                    scopes: Vec::new(),
1355                    node_scope_ids: Default::default(),
1356                    parse_diagnostics: Vec::new(),
1357                    shorthand_ambient_modules: Default::default(),
1358                    global_augmentations: Default::default(),
1359                    module_augmentations: Default::default(),
1360                    reexports: Default::default(),
1361                    wildcard_reexports: Default::default(),
1362                    lib_binders: Vec::new(),
1363                    lib_symbol_ids: Default::default(),
1364                    lib_symbol_reverse_remap: Default::default(),
1365                    flow_nodes: Default::default(),
1366                    node_flow: Default::default(),
1367                    switch_clause_to_switch: Default::default(),
1368                    is_external_module: false, // Default to false for missing files
1369                    expando_properties: Default::default(),
1370                }
1371            }
1372        };
1373        cache.bind_cache.insert(
1374            entry.path.clone(),
1375            BindCacheEntry {
1376                hash: entry.hash,
1377                bind_result: result,
1378            },
1379        );
1380    }
1381
1382    let mut current_paths: FxHashSet<PathBuf> =
1383        FxHashSet::with_capacity_and_hasher(meta.len(), Default::default());
1384    for entry in &meta {
1385        current_paths.insert(entry.path.clone());
1386    }
1387    cache
1388        .bind_cache
1389        .retain(|path, _| current_paths.contains(path));
1390
1391    let mut ordered = Vec::with_capacity(meta.len());
1392    for entry in &meta {
1393        let Some(cached) = cache.bind_cache.get(&entry.path) else {
1394            continue;
1395        };
1396        ordered.push(&cached.bind_result);
1397    }
1398
1399    BuildProgramResult {
1400        program: parallel::merge_bind_results_ref(&ordered),
1401        dirty_paths,
1402    }
1403}
1404
1405fn update_import_symbol_ids(
1406    program: &MergedProgram,
1407    options: &ResolvedCompilerOptions,
1408    base_dir: &Path,
1409    cache: &mut CompilationCache,
1410) {
1411    let mut resolution_cache = ModuleResolutionCache::default();
1412    let mut import_symbol_ids: FxHashMap<PathBuf, FxHashMap<PathBuf, Vec<SymbolId>>> =
1413        FxHashMap::default();
1414    let mut star_export_dependencies: FxHashMap<PathBuf, FxHashSet<PathBuf>> = FxHashMap::default();
1415
1416    // Build set of known file paths for module resolution
1417    let known_files: FxHashSet<PathBuf> = program
1418        .files
1419        .iter()
1420        .map(|f| PathBuf::from(&f.file_name))
1421        .collect();
1422
1423    for (file_idx, file) in program.files.iter().enumerate() {
1424        let file_path = PathBuf::from(&file.file_name);
1425        let mut by_dep: FxHashMap<PathBuf, Vec<SymbolId>> = FxHashMap::default();
1426        let mut star_exports: FxHashSet<PathBuf> = FxHashSet::default();
1427        for (specifier, local_names) in collect_import_bindings(&file.arena, file.source_file) {
1428            let resolved = resolve_module_specifier(
1429                Path::new(&file.file_name),
1430                &specifier,
1431                options,
1432                base_dir,
1433                &mut resolution_cache,
1434                &known_files,
1435            );
1436            let Some(resolved) = resolved else {
1437                continue;
1438            };
1439            let canonical = canonicalize_or_owned(&resolved);
1440            let entry = by_dep.entry(canonical).or_default();
1441            if let Some(file_locals) = program.file_locals.get(file_idx) {
1442                for name in local_names {
1443                    if let Some(sym_id) = file_locals.get(&name) {
1444                        entry.push(sym_id);
1445                    }
1446                }
1447            }
1448        }
1449        for (specifier, binding_nodes) in
1450            collect_export_binding_nodes(&file.arena, file.source_file)
1451        {
1452            let resolved = resolve_module_specifier(
1453                Path::new(&file.file_name),
1454                &specifier,
1455                options,
1456                base_dir,
1457                &mut resolution_cache,
1458                &known_files,
1459            );
1460            let Some(resolved) = resolved else {
1461                continue;
1462            };
1463            let canonical = canonicalize_or_owned(&resolved);
1464            let entry = by_dep.entry(canonical).or_default();
1465            for node_idx in binding_nodes {
1466                if let Some(sym_id) = file.node_symbols.get(&node_idx.0).copied() {
1467                    entry.push(sym_id);
1468                }
1469            }
1470        }
1471        for specifier in collect_star_export_specifiers(&file.arena, file.source_file) {
1472            let resolved = resolve_module_specifier(
1473                Path::new(&file.file_name),
1474                &specifier,
1475                options,
1476                base_dir,
1477                &mut resolution_cache,
1478                &known_files,
1479            );
1480            let Some(resolved) = resolved else {
1481                continue;
1482            };
1483            let canonical = canonicalize_or_owned(&resolved);
1484            star_exports.insert(canonical);
1485        }
1486        for symbols in by_dep.values_mut() {
1487            symbols.sort_by_key(|sym| sym.0);
1488            symbols.dedup();
1489        }
1490        if !star_exports.is_empty() {
1491            star_export_dependencies.insert(file_path.clone(), star_exports);
1492        }
1493        import_symbol_ids.insert(file_path, by_dep);
1494    }
1495
1496    cache.import_symbol_ids = import_symbol_ids;
1497    cache.star_export_dependencies = star_export_dependencies;
1498}
1499
1500fn hash_text(text: &str) -> u64 {
1501    let mut hasher = FxHasher::default();
1502    text.hash(&mut hasher);
1503    hasher.finish()
1504}
1505
1506#[path = "driver_sources.rs"]
1507mod driver_sources;
1508#[cfg(test)]
1509pub(crate) use driver_sources::has_no_types_and_symbols_directive;
1510pub use driver_sources::{FileReadResult, read_source_file};
1511use driver_sources::{
1512    SourceEntry, SourceReadResult, build_discovery_options, collect_type_root_files,
1513    read_source_files, sources_have_no_default_lib, sources_have_no_types_and_symbols,
1514};
1515pub(crate) use driver_sources::{
1516    config_base_dir, load_config, load_config_with_diagnostics, resolve_tsconfig_path,
1517};
1518
1519#[path = "driver_check.rs"]
1520mod driver_check;
1521use driver_check::{collect_diagnostics, load_lib_files_for_contexts};
1522
1523pub fn apply_cli_overrides(options: &mut ResolvedCompilerOptions, args: &CliArgs) -> Result<()> {
1524    if let Some(target) = args.target {
1525        options.printer.target = target.to_script_target();
1526        options.checker.target = checker_target_from_emitter(options.printer.target);
1527    }
1528    if let Some(module) = args.module {
1529        options.printer.module = module.to_module_kind();
1530        options.checker.module = module.to_module_kind();
1531        options.checker.module_explicitly_set = true;
1532    }
1533    if let Some(module_resolution) = args.module_resolution {
1534        options.module_resolution = Some(module_resolution.to_module_resolution_kind());
1535    }
1536    if let Some(resolve_package_json_exports) = args.resolve_package_json_exports {
1537        options.resolve_package_json_exports = resolve_package_json_exports;
1538    }
1539    if let Some(resolve_package_json_imports) = args.resolve_package_json_imports {
1540        options.resolve_package_json_imports = resolve_package_json_imports;
1541    }
1542    if let Some(module_suffixes) = args.module_suffixes.as_ref() {
1543        options.module_suffixes = module_suffixes.clone();
1544    }
1545    if args.resolve_json_module {
1546        options.resolve_json_module = true;
1547    }
1548    if args.allow_arbitrary_extensions {
1549        options.allow_arbitrary_extensions = true;
1550    }
1551    if args.allow_importing_ts_extensions {
1552        options.allow_importing_ts_extensions = true;
1553    }
1554    if let Some(use_define_for_class_fields) = args.use_define_for_class_fields {
1555        options.printer.use_define_for_class_fields = use_define_for_class_fields;
1556    } else {
1557        // Default: true for target >= ES2022, false otherwise (matches tsc behavior)
1558        options.printer.use_define_for_class_fields =
1559            (options.printer.target as u32) >= (tsz::emitter::ScriptTarget::ES2022 as u32);
1560    }
1561    if args.rewrite_relative_import_extensions {
1562        options.rewrite_relative_import_extensions = true;
1563    }
1564    if let Some(custom_conditions) = args.custom_conditions.as_ref() {
1565        options.custom_conditions = custom_conditions.clone();
1566    }
1567    if let Some(out_dir) = args.out_dir.as_ref() {
1568        options.out_dir = Some(out_dir.clone());
1569    }
1570    if let Some(root_dir) = args.root_dir.as_ref() {
1571        options.root_dir = Some(root_dir.clone());
1572    }
1573    if args.declaration {
1574        options.emit_declarations = true;
1575    }
1576    if args.declaration_map {
1577        options.declaration_map = true;
1578    }
1579    if args.source_map {
1580        options.source_map = true;
1581    }
1582    if let Some(out_file) = args.out_file.as_ref() {
1583        options.out_file = Some(out_file.clone());
1584    }
1585    if let Some(ts_build_info_file) = args.ts_build_info_file.as_ref() {
1586        options.ts_build_info_file = Some(ts_build_info_file.clone());
1587    }
1588    if args.incremental {
1589        options.incremental = true;
1590    }
1591    if args.import_helpers {
1592        options.import_helpers = true;
1593    }
1594    if args.strict {
1595        options.checker.strict = true;
1596        // Expand --strict to individual flags (matching TypeScript behavior)
1597        options.checker.no_implicit_any = true;
1598        options.checker.no_implicit_returns = true;
1599        options.checker.strict_null_checks = true;
1600        options.checker.strict_function_types = true;
1601        options.checker.strict_bind_call_apply = true;
1602        options.checker.strict_property_initialization = true;
1603        options.checker.no_implicit_this = true;
1604        options.checker.use_unknown_in_catch_variables = true;
1605        options.checker.always_strict = true;
1606        options.printer.always_strict = true;
1607    }
1608    // Individual strict flag overrides (must come after --strict expansion)
1609    if let Some(val) = args.strict_null_checks {
1610        options.checker.strict_null_checks = val;
1611    }
1612    if let Some(val) = args.strict_function_types {
1613        options.checker.strict_function_types = val;
1614    }
1615    if let Some(val) = args.strict_property_initialization {
1616        options.checker.strict_property_initialization = val;
1617    }
1618    if let Some(val) = args.strict_bind_call_apply {
1619        options.checker.strict_bind_call_apply = val;
1620    }
1621    if let Some(val) = args.no_implicit_this {
1622        options.checker.no_implicit_this = val;
1623    }
1624    if let Some(val) = args.no_implicit_any {
1625        options.checker.no_implicit_any = val;
1626    }
1627    if let Some(val) = args.use_unknown_in_catch_variables {
1628        options.checker.use_unknown_in_catch_variables = val;
1629    }
1630    if args.no_unchecked_indexed_access {
1631        options.checker.no_unchecked_indexed_access = true;
1632    }
1633    if args.no_implicit_returns {
1634        options.checker.no_implicit_returns = true;
1635    }
1636    if let Some(val) = args.always_strict {
1637        options.checker.always_strict = val;
1638        options.printer.always_strict = val;
1639    }
1640    if let Some(val) = args.allow_unreachable_code {
1641        options.checker.allow_unreachable_code = Some(val);
1642    }
1643    if args.sound {
1644        options.checker.sound_mode = true;
1645    }
1646    if args.experimental_decorators {
1647        options.checker.experimental_decorators = true;
1648        options.printer.legacy_decorators = true;
1649    }
1650    if args.no_unused_locals {
1651        options.checker.no_unused_locals = true;
1652    }
1653    if args.no_unused_parameters {
1654        options.checker.no_unused_parameters = true;
1655    }
1656    if args.no_implicit_override {
1657        options.checker.no_implicit_override = true;
1658    }
1659    if args.es_module_interop {
1660        options.es_module_interop = true;
1661        options.checker.es_module_interop = true;
1662        options.printer.es_module_interop = true;
1663        // esModuleInterop implies allowSyntheticDefaultImports
1664        options.allow_synthetic_default_imports = true;
1665        options.checker.allow_synthetic_default_imports = true;
1666    }
1667    if args.no_emit {
1668        options.no_emit = true;
1669    }
1670    if args.no_resolve {
1671        options.no_resolve = true;
1672        options.checker.no_resolve = true;
1673    }
1674    if args.no_check {
1675        options.no_check = true;
1676    }
1677    if args.skip_lib_check {
1678        options.skip_lib_check = true;
1679    }
1680    if args.allow_js {
1681        options.allow_js = true;
1682    }
1683    if args.check_js {
1684        options.check_js = true;
1685    }
1686    if let Some(version) = args.types_versions_compiler_version.as_ref() {
1687        options.types_versions_compiler_version = Some(version.clone());
1688    } else if let Some(version) = types_versions_compiler_version_env() {
1689        let version = version.trim();
1690        if !version.is_empty() {
1691            options.types_versions_compiler_version = Some(version.to_string());
1692        }
1693    }
1694    if let Some(lib_list) = args.lib.as_ref() {
1695        options.lib_files = resolve_lib_files(lib_list)?;
1696        options.lib_is_default = false;
1697    }
1698    if args.no_lib {
1699        options.checker.no_lib = true;
1700        options.lib_files.clear();
1701        options.lib_is_default = false;
1702    }
1703    if args.downlevel_iteration {
1704        options.printer.downlevel_iteration = true;
1705    }
1706    if args.no_emit_helpers {
1707        options.printer.no_emit_helpers = true;
1708    }
1709    if let Some(ModuleDetection::Force) = args.module_detection {
1710        options.printer.module_detection_force = true;
1711    }
1712    if args.target.is_some() && options.lib_is_default && !options.checker.no_lib {
1713        options.lib_files = resolve_default_lib_files(options.printer.target)?;
1714    }
1715
1716    // Wire removed-but-honored suppress flags from CLI
1717    if args.suppress_excess_property_errors {
1718        options.checker.suppress_excess_property_errors = true;
1719    }
1720    if args.suppress_implicit_any_index_errors {
1721        options.checker.suppress_implicit_any_index_errors = true;
1722    }
1723
1724    Ok(())
1725}
1726
1727/// Find the most recent .d.ts file from a list of emitted files
1728/// Returns the relative path (from `base_dir`) as a String, or None if no .d.ts files were found
1729fn find_latest_dts_file(emitted_files: &[PathBuf], base_dir: &Path) -> Option<String> {
1730    use std::collections::BTreeMap;
1731
1732    let mut dts_files_with_times: BTreeMap<std::time::SystemTime, PathBuf> = BTreeMap::new();
1733
1734    // Filter for .d.ts files and get their modification times
1735    for path in emitted_files {
1736        if path.extension().and_then(|s| s.to_str()) == Some("d.ts")
1737            && let Ok(metadata) = std::fs::metadata(path)
1738            && let Ok(modified) = metadata.modified()
1739        {
1740            dts_files_with_times.insert(modified, path.clone());
1741        }
1742    }
1743
1744    // Get the most recent file (highest time in BTreeMap)
1745    if let Some((_, latest_path)) = dts_files_with_times.last_key_value() {
1746        // Convert to relative path from base_dir
1747        let relative = latest_path
1748            .strip_prefix(base_dir)
1749            .unwrap_or(latest_path)
1750            .to_string_lossy()
1751            .replace('\\', "/");
1752        Some(relative)
1753    } else {
1754        None
1755    }
1756}
1757
1758#[cfg(test)]
1759#[path = "driver_tests.rs"]
1760mod tests;