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