Skip to main content

tsz_core/
config.rs

1use anyhow::{Context, Result, anyhow, bail};
2use serde::{Deserialize, Deserializer};
3use std::collections::VecDeque;
4
5use rustc_hash::{FxHashMap, FxHashSet};
6use std::env;
7use std::path::{Path, PathBuf};
8
9use crate::checker::context::ScriptTarget as CheckerScriptTarget;
10use crate::emitter::{ModuleKind, PrinterOptions, ScriptTarget};
11
12/// Custom deserializer for boolean options that accepts both bool and string values.
13/// This handles cases where tsconfig.json contains `"strict": "true"` instead of `"strict": true`.
14fn deserialize_bool_or_string<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
15where
16    D: Deserializer<'de>,
17{
18    use serde::de::Error;
19
20    // Use a helper enum to deserialize either a bool or a string
21    #[derive(Deserialize)]
22    #[serde(untagged)]
23    enum BoolOrString {
24        Bool(bool),
25        String(String),
26    }
27
28    match Option::<BoolOrString>::deserialize(deserializer)? {
29        None => Ok(None),
30        Some(BoolOrString::Bool(b)) => Ok(Some(b)),
31        Some(BoolOrString::String(s)) => {
32            // Parse common string representations of boolean values
33            let normalized = s.trim().to_lowercase();
34            match normalized.as_str() {
35                "true" | "1" | "yes" | "on" => Ok(Some(true)),
36                "false" | "0" | "no" | "off" => Ok(Some(false)),
37                _ => {
38                    // Invalid boolean string - return error with helpful message
39                    Err(Error::custom(format!(
40                        "invalid boolean value: '{s}'. Expected true, false, 'true', or 'false'",
41                    )))
42                }
43            }
44        }
45    }
46}
47
48#[derive(Debug, Clone, Deserialize, Default)]
49#[serde(rename_all = "camelCase")]
50pub struct TsConfig {
51    #[serde(default)]
52    pub extends: Option<String>,
53    #[serde(default)]
54    pub compiler_options: Option<CompilerOptions>,
55    #[serde(default)]
56    pub include: Option<Vec<String>>,
57    #[serde(default)]
58    pub exclude: Option<Vec<String>>,
59    #[serde(default)]
60    pub files: Option<Vec<String>>,
61}
62
63#[derive(Debug, Clone, Deserialize, Default)]
64#[serde(rename_all = "camelCase")]
65pub struct CompilerOptions {
66    #[serde(default)]
67    pub target: Option<String>,
68    #[serde(default)]
69    pub module: Option<String>,
70    #[serde(default)]
71    pub module_resolution: Option<String>,
72    /// Use the package.json 'exports' field when resolving package imports.
73    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
74    pub resolve_package_json_exports: Option<bool>,
75    /// Use the package.json 'imports' field when resolving imports.
76    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
77    pub resolve_package_json_imports: Option<bool>,
78    /// List of file name suffixes to search when resolving a module.
79    #[serde(default)]
80    pub module_suffixes: Option<Vec<String>>,
81    /// Enable importing .json files.
82    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
83    pub resolve_json_module: Option<bool>,
84    /// Enable importing files with any extension, provided a declaration file is present.
85    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
86    pub allow_arbitrary_extensions: Option<bool>,
87    /// Allow imports to include TypeScript file extensions.
88    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
89    pub allow_importing_ts_extensions: Option<bool>,
90    /// Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths.
91    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
92    pub rewrite_relative_import_extensions: Option<bool>,
93    #[serde(default)]
94    pub types_versions_compiler_version: Option<String>,
95    #[serde(default)]
96    pub types: Option<Vec<String>>,
97    #[serde(default)]
98    pub type_roots: Option<Vec<String>>,
99    #[serde(default)]
100    pub jsx: Option<String>,
101    #[serde(default)]
102    #[serde(rename = "jsxFactory")]
103    pub jsx_factory: Option<String>,
104    #[serde(default)]
105    #[serde(rename = "jsxFragmentFactory")]
106    pub jsx_fragment_factory: Option<String>,
107    #[serde(default)]
108    #[serde(rename = "reactNamespace")]
109    pub react_namespace: Option<String>,
110
111    #[serde(default)]
112    pub lib: Option<Vec<String>>,
113    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
114    pub no_lib: Option<bool>,
115    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
116    pub no_types_and_symbols: Option<bool>,
117    #[serde(default)]
118    pub base_url: Option<String>,
119    #[serde(default)]
120    pub paths: Option<FxHashMap<String, Vec<String>>>,
121    #[serde(default)]
122    pub root_dir: Option<String>,
123    #[serde(default)]
124    pub out_dir: Option<String>,
125    #[serde(default)]
126    pub out_file: Option<String>,
127    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
128    pub declaration: Option<bool>,
129    #[serde(default)]
130    pub declaration_dir: Option<String>,
131    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
132    pub source_map: Option<bool>,
133    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
134    pub declaration_map: Option<bool>,
135    #[serde(default)]
136    pub ts_build_info_file: Option<String>,
137    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
138    pub incremental: Option<bool>,
139    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
140    pub strict: Option<bool>,
141    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
142    pub no_emit: Option<bool>,
143    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
144    pub no_resolve: Option<bool>,
145    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
146    pub no_emit_on_error: Option<bool>,
147    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
148    pub isolated_modules: Option<bool>,
149    /// Custom conditions for package.json exports resolution
150    #[serde(default)]
151    pub custom_conditions: Option<Vec<String>>,
152    /// Emit additional JavaScript to ease support for importing CommonJS modules
153    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
154    pub es_module_interop: Option<bool>,
155    /// Allow 'import x from y' when a module doesn't have a default export
156    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
157    pub allow_synthetic_default_imports: Option<bool>,
158    /// Enable experimental support for legacy experimental decorators
159    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
160    pub experimental_decorators: Option<bool>,
161    /// Import emit helpers from tslib instead of inlining them per-file
162    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
163    pub import_helpers: Option<bool>,
164    /// Allow JavaScript files to be a part of your program
165    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
166    pub allow_js: Option<bool>,
167    /// Enable error reporting in type-checked JavaScript files
168    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
169    pub check_js: Option<bool>,
170    /// Parse in strict mode and emit "use strict" for each source file
171    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
172    pub always_strict: Option<bool>,
173    /// Use `Object.defineProperty` semantics for class fields when downleveling.
174    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
175    pub use_define_for_class_fields: Option<bool>,
176    /// Raise error on expressions and declarations with an implied 'any' type
177    #[serde(
178        default,
179        alias = "noImplicitAny",
180        deserialize_with = "deserialize_bool_or_string"
181    )]
182    pub no_implicit_any: Option<bool>,
183    /// Enable error reporting when a function doesn't explicitly return in all code paths
184    #[serde(
185        default,
186        alias = "noImplicitReturns",
187        deserialize_with = "deserialize_bool_or_string"
188    )]
189    pub no_implicit_returns: Option<bool>,
190    /// Enable strict null checks
191    #[serde(
192        default,
193        alias = "strictNullChecks",
194        deserialize_with = "deserialize_bool_or_string"
195    )]
196    pub strict_null_checks: Option<bool>,
197    /// Enable strict checking of function types
198    #[serde(
199        default,
200        alias = "strictFunctionTypes",
201        deserialize_with = "deserialize_bool_or_string"
202    )]
203    pub strict_function_types: Option<bool>,
204    /// Check for class properties that are declared but not set in the constructor
205    #[serde(
206        default,
207        alias = "strictPropertyInitialization",
208        deserialize_with = "deserialize_bool_or_string"
209    )]
210    pub strict_property_initialization: Option<bool>,
211    /// Raise error on 'this' expressions with an implied 'any' type
212    #[serde(
213        default,
214        alias = "noImplicitThis",
215        deserialize_with = "deserialize_bool_or_string"
216    )]
217    pub no_implicit_this: Option<bool>,
218    /// Default catch clause variables as 'unknown' instead of 'any'
219    #[serde(
220        default,
221        alias = "useUnknownInCatchVariables",
222        deserialize_with = "deserialize_bool_or_string"
223    )]
224    pub use_unknown_in_catch_variables: Option<bool>,
225    /// Add 'undefined' to a type when accessed using an index
226    #[serde(
227        default,
228        alias = "noUncheckedIndexedAccess",
229        deserialize_with = "deserialize_bool_or_string"
230    )]
231    pub no_unchecked_indexed_access: Option<bool>,
232    /// Check that the arguments for 'bind', 'call', and 'apply' methods match the original function
233    #[serde(
234        default,
235        alias = "strictBindCallApply",
236        deserialize_with = "deserialize_bool_or_string"
237    )]
238    pub strict_bind_call_apply: Option<bool>,
239    /// Report errors on unused local variables
240    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
241    pub no_unused_locals: Option<bool>,
242    /// Report errors on unused parameters
243    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
244    pub no_unused_parameters: Option<bool>,
245    /// Do not report errors on unreachable code
246    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
247    pub allow_unreachable_code: Option<bool>,
248    /// Check side-effect imports for module resolution errors
249    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
250    pub no_unchecked_side_effect_imports: Option<bool>,
251    /// Require 'override' modifier on members that override base class members
252    #[serde(
253        default,
254        alias = "noImplicitOverride",
255        deserialize_with = "deserialize_bool_or_string"
256    )]
257    pub no_implicit_override: Option<bool>,
258}
259
260// Re-export CheckerOptions from checker::context for unified API
261pub use crate::checker::context::CheckerOptions;
262
263#[derive(Debug, Clone, Default)]
264pub struct ResolvedCompilerOptions {
265    pub printer: PrinterOptions,
266    pub checker: CheckerOptions,
267    pub jsx: Option<JsxEmit>,
268    pub lib_files: Vec<PathBuf>,
269    pub lib_is_default: bool,
270    pub module_resolution: Option<ModuleResolutionKind>,
271    pub resolve_package_json_exports: bool,
272    pub resolve_package_json_imports: bool,
273    pub module_suffixes: Vec<String>,
274    pub resolve_json_module: bool,
275    pub allow_arbitrary_extensions: bool,
276    pub allow_importing_ts_extensions: bool,
277    pub rewrite_relative_import_extensions: bool,
278    pub types_versions_compiler_version: Option<String>,
279    pub types: Option<Vec<String>>,
280    pub type_roots: Option<Vec<PathBuf>>,
281    pub base_url: Option<PathBuf>,
282    pub paths: Option<Vec<PathMapping>>,
283    pub root_dir: Option<PathBuf>,
284    pub out_dir: Option<PathBuf>,
285    pub out_file: Option<PathBuf>,
286    pub declaration_dir: Option<PathBuf>,
287    pub emit_declarations: bool,
288    pub source_map: bool,
289    pub declaration_map: bool,
290    pub ts_build_info_file: Option<PathBuf>,
291    pub incremental: bool,
292    pub no_emit: bool,
293    pub no_emit_on_error: bool,
294    /// Skip module graph expansion from imports/references when checking.
295    pub no_resolve: bool,
296    pub import_helpers: bool,
297    /// Disable full type checking (only parse and emit errors reported).
298    pub no_check: bool,
299    /// Custom conditions for package.json exports resolution
300    pub custom_conditions: Vec<String>,
301    /// Emit additional JavaScript to ease support for importing CommonJS modules
302    pub es_module_interop: bool,
303    /// Allow 'import x from y' when a module doesn't have a default export
304    pub allow_synthetic_default_imports: bool,
305    /// Allow JavaScript files to be part of the program
306    pub allow_js: bool,
307    /// Enable error reporting in type-checked JavaScript files
308    pub check_js: bool,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum JsxEmit {
313    Preserve,
314    React,
315    ReactJsx,
316    ReactJsxDev,
317    ReactNative,
318}
319
320#[derive(Debug, Clone, Copy, PartialEq, Eq)]
321pub enum ModuleResolutionKind {
322    Classic,
323    Node,
324    Node16,
325    NodeNext,
326    Bundler,
327}
328
329#[derive(Debug, Clone)]
330pub struct PathMapping {
331    pub pattern: String,
332    pub(crate) prefix: String,
333    pub(crate) suffix: String,
334    pub targets: Vec<String>,
335}
336
337impl PathMapping {
338    pub fn match_specifier(&self, specifier: &str) -> Option<String> {
339        if !self.pattern.contains('*') {
340            return (self.pattern == specifier).then(String::new);
341        }
342
343        if !specifier.starts_with(&self.prefix) || !specifier.ends_with(&self.suffix) {
344            return None;
345        }
346
347        let start = self.prefix.len();
348        let end = specifier.len().saturating_sub(self.suffix.len());
349        if end < start {
350            return None;
351        }
352
353        Some(specifier[start..end].to_string())
354    }
355
356    pub const fn specificity(&self) -> usize {
357        self.prefix.len() + self.suffix.len()
358    }
359}
360
361impl ResolvedCompilerOptions {
362    pub const fn effective_module_resolution(&self) -> ModuleResolutionKind {
363        if let Some(resolution) = self.module_resolution {
364            return resolution;
365        }
366
367        // Match tsc's computed moduleResolution defaults exactly:
368        // None/AMD/UMD/System → Classic
369        // CommonJS → Node (node10)
370        // NodeNext → NodeNext
371        // Node16 → Node16
372        // Everything else (ES2015, ES2020, ES2022, ESNext, Preserve) → Bundler
373        match self.printer.module {
374            ModuleKind::None | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
375                ModuleResolutionKind::Classic
376            }
377            ModuleKind::CommonJS => ModuleResolutionKind::Node,
378            ModuleKind::NodeNext => ModuleResolutionKind::NodeNext,
379            ModuleKind::Node16 => ModuleResolutionKind::Node16,
380            _ => ModuleResolutionKind::Bundler,
381        }
382    }
383}
384
385pub fn resolve_compiler_options(
386    options: Option<&CompilerOptions>,
387) -> Result<ResolvedCompilerOptions> {
388    let mut resolved = ResolvedCompilerOptions::default();
389    let Some(options) = options else {
390        resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
391        resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
392        resolved.lib_is_default = true;
393        resolved.module_suffixes = vec![String::new()];
394        let default_resolution = resolved.effective_module_resolution();
395        resolved.resolve_package_json_exports = matches!(
396            default_resolution,
397            ModuleResolutionKind::Node16
398                | ModuleResolutionKind::NodeNext
399                | ModuleResolutionKind::Bundler
400        );
401        resolved.resolve_package_json_imports = resolved.resolve_package_json_exports;
402        return Ok(resolved);
403    };
404
405    if let Some(target) = options.target.as_deref() {
406        resolved.printer.target = parse_script_target(target)?;
407    }
408    resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
409
410    let module_explicitly_set = options.module.is_some();
411    if let Some(module) = options.module.as_deref() {
412        let kind = parse_module_kind(module)?;
413        resolved.printer.module = kind;
414        resolved.checker.module = kind;
415    } else {
416        // Match tsc: when --module is omitted, default depends on target.
417        // ES2015+ targets default to ES2015 modules; lower targets default to CommonJS.
418        let default_module = if resolved.printer.target.supports_es2015() {
419            ModuleKind::ES2015
420        } else {
421            ModuleKind::CommonJS
422        };
423        resolved.printer.module = default_module;
424        resolved.checker.module = default_module;
425    }
426
427    if let Some(module_resolution) = options.module_resolution.as_deref() {
428        let value = module_resolution.trim();
429        if !value.is_empty() {
430            resolved.module_resolution = Some(parse_module_resolution(value)?);
431        }
432    }
433
434    // When module is not explicitly set, infer it from moduleResolution (matches tsc behavior).
435    // tsc infers module: node16 when moduleResolution: node16, etc.
436    if !module_explicitly_set && let Some(mr) = resolved.module_resolution {
437        let inferred = match mr {
438            ModuleResolutionKind::Node16 => Some(ModuleKind::Node16),
439            ModuleResolutionKind::NodeNext => Some(ModuleKind::NodeNext),
440            _ => None,
441        };
442        if let Some(kind) = inferred {
443            resolved.printer.module = kind;
444            resolved.checker.module = kind;
445        }
446    }
447    let effective_resolution = resolved.effective_module_resolution();
448    resolved.resolve_package_json_exports = options.resolve_package_json_exports.unwrap_or({
449        matches!(
450            effective_resolution,
451            ModuleResolutionKind::Node16
452                | ModuleResolutionKind::NodeNext
453                | ModuleResolutionKind::Bundler
454        )
455    });
456    resolved.resolve_package_json_imports = options.resolve_package_json_imports.unwrap_or({
457        matches!(
458            effective_resolution,
459            ModuleResolutionKind::Node
460                | ModuleResolutionKind::Node16
461                | ModuleResolutionKind::NodeNext
462                | ModuleResolutionKind::Bundler
463        )
464    });
465    if let Some(module_suffixes) = options.module_suffixes.as_ref() {
466        resolved.module_suffixes = module_suffixes.clone();
467    } else {
468        resolved.module_suffixes = vec![String::new()];
469    }
470    if let Some(resolve_json_module) = options.resolve_json_module {
471        resolved.resolve_json_module = resolve_json_module;
472        resolved.checker.resolve_json_module = resolve_json_module;
473    }
474    if let Some(import_helpers) = options.import_helpers {
475        resolved.import_helpers = import_helpers;
476    }
477    if let Some(allow_arbitrary_extensions) = options.allow_arbitrary_extensions {
478        resolved.allow_arbitrary_extensions = allow_arbitrary_extensions;
479    }
480    if let Some(allow_importing_ts_extensions) = options.allow_importing_ts_extensions {
481        resolved.allow_importing_ts_extensions = allow_importing_ts_extensions;
482    }
483    if let Some(rewrite_relative_import_extensions) = options.rewrite_relative_import_extensions {
484        resolved.rewrite_relative_import_extensions = rewrite_relative_import_extensions;
485    }
486
487    if let Some(types_versions_compiler_version) =
488        options.types_versions_compiler_version.as_deref()
489    {
490        let value = types_versions_compiler_version.trim();
491        if !value.is_empty() {
492            resolved.types_versions_compiler_version = Some(value.to_string());
493        }
494    }
495
496    if let Some(types) = options.types.as_ref() {
497        let list: Vec<String> = types
498            .iter()
499            .filter_map(|value| {
500                let trimmed = value.trim();
501                if trimmed.is_empty() {
502                    None
503                } else {
504                    Some(trimmed.to_string())
505                }
506            })
507            .collect();
508        resolved.types = Some(list);
509    }
510
511    if let Some(type_roots) = options.type_roots.as_ref() {
512        let roots: Vec<PathBuf> = type_roots
513            .iter()
514            .filter_map(|value| {
515                let trimmed = value.trim();
516                if trimmed.is_empty() {
517                    None
518                } else {
519                    Some(PathBuf::from(trimmed))
520                }
521            })
522            .collect();
523        resolved.type_roots = Some(roots);
524    }
525
526    if let Some(factory) = options.jsx_factory.as_deref() {
527        resolved.checker.jsx_factory = factory.to_string();
528    } else if let Some(ns) = options.react_namespace.as_deref() {
529        resolved.checker.jsx_factory = format!("{ns}.createElement");
530    }
531    if let Some(frag) = options.jsx_fragment_factory.as_deref() {
532        resolved.checker.jsx_fragment_factory = frag.to_string();
533    }
534
535    if let Some(jsx) = options.jsx.as_deref() {
536        let jsx_emit = parse_jsx_emit(jsx)?;
537        resolved.jsx = Some(jsx_emit);
538        resolved.checker.jsx_mode = jsx_emit_to_mode(jsx_emit);
539    }
540
541    if let Some(no_lib) = options.no_lib {
542        resolved.checker.no_lib = no_lib;
543    }
544
545    if resolved.checker.no_lib && options.lib.is_some() {
546        return Err(anyhow::anyhow!(
547            "Option 'lib' cannot be specified with option 'noLib'."
548        ));
549    }
550
551    if let Some(no_types_and_symbols) = options.no_types_and_symbols {
552        resolved.checker.no_types_and_symbols = no_types_and_symbols;
553    }
554
555    if resolved.checker.no_lib && options.lib.is_some() {
556        bail!("Option 'lib' cannot be specified with option 'noLib'.");
557    }
558
559    if let Some(lib_list) = options.lib.as_ref() {
560        resolved.lib_files = resolve_lib_files(lib_list)?;
561        resolved.lib_is_default = false;
562    } else if !resolved.checker.no_lib && !resolved.checker.no_types_and_symbols {
563        resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
564        resolved.lib_is_default = true;
565    }
566
567    let base_url = options.base_url.as_deref().map(str::trim);
568    if let Some(base_url) = base_url
569        && !base_url.is_empty()
570    {
571        resolved.base_url = Some(PathBuf::from(base_url));
572    }
573
574    if let Some(paths) = options.paths.as_ref()
575        && !paths.is_empty()
576    {
577        resolved.paths = Some(build_path_mappings(paths));
578    }
579
580    if let Some(root_dir) = options.root_dir.as_deref()
581        && !root_dir.is_empty()
582    {
583        resolved.root_dir = Some(PathBuf::from(root_dir));
584    }
585
586    if let Some(out_dir) = options.out_dir.as_deref()
587        && !out_dir.is_empty()
588    {
589        resolved.out_dir = Some(PathBuf::from(out_dir));
590    }
591
592    if let Some(out_file) = options.out_file.as_deref()
593        && !out_file.is_empty()
594    {
595        resolved.out_file = Some(PathBuf::from(out_file));
596    }
597
598    if let Some(declaration_dir) = options.declaration_dir.as_deref()
599        && !declaration_dir.is_empty()
600    {
601        resolved.declaration_dir = Some(PathBuf::from(declaration_dir));
602    }
603
604    if let Some(declaration) = options.declaration {
605        resolved.emit_declarations = declaration;
606    }
607
608    if let Some(source_map) = options.source_map {
609        resolved.source_map = source_map;
610    }
611
612    if let Some(declaration_map) = options.declaration_map {
613        resolved.declaration_map = declaration_map;
614    }
615
616    if let Some(ts_build_info_file) = options.ts_build_info_file.as_deref()
617        && !ts_build_info_file.is_empty()
618    {
619        resolved.ts_build_info_file = Some(PathBuf::from(ts_build_info_file));
620    }
621
622    if let Some(incremental) = options.incremental {
623        resolved.incremental = incremental;
624    }
625
626    if let Some(strict) = options.strict {
627        resolved.checker.strict = strict;
628        if strict {
629            resolved.checker.no_implicit_any = true;
630            resolved.checker.strict_null_checks = true;
631            resolved.checker.strict_function_types = true;
632            resolved.checker.strict_bind_call_apply = true;
633            resolved.checker.strict_property_initialization = true;
634            resolved.checker.no_implicit_this = true;
635            resolved.checker.use_unknown_in_catch_variables = true;
636            resolved.checker.always_strict = true;
637            resolved.printer.always_strict = true;
638        } else {
639            resolved.checker.no_implicit_any = false;
640            resolved.checker.strict_null_checks = false;
641            resolved.checker.strict_function_types = false;
642            resolved.checker.strict_bind_call_apply = false;
643            resolved.checker.strict_property_initialization = false;
644            resolved.checker.no_implicit_this = false;
645            resolved.checker.use_unknown_in_catch_variables = false;
646            resolved.checker.always_strict = false;
647            resolved.printer.always_strict = false;
648        }
649    }
650
651    // TypeScript defaults to `noImplicitAny: true` when not explicitly set.
652    // `strict: false` should remain the only source that disables it by default.
653    if options.strict.is_none() && options.no_implicit_any.is_none() {
654        resolved.checker.no_implicit_any = true;
655    }
656
657    // Individual strict-family options (override strict if set explicitly)
658    if let Some(v) = options.no_implicit_any {
659        resolved.checker.no_implicit_any = v;
660    }
661    if let Some(v) = options.no_implicit_returns {
662        resolved.checker.no_implicit_returns = v;
663    }
664    if let Some(v) = options.strict_null_checks {
665        resolved.checker.strict_null_checks = v;
666    }
667    if let Some(v) = options.strict_function_types {
668        resolved.checker.strict_function_types = v;
669    }
670    if let Some(v) = options.strict_property_initialization {
671        resolved.checker.strict_property_initialization = v;
672    }
673    if let Some(v) = options.no_unchecked_indexed_access {
674        resolved.checker.no_unchecked_indexed_access = v;
675    }
676    if let Some(v) = options.no_implicit_this {
677        resolved.checker.no_implicit_this = v;
678    }
679    if let Some(v) = options.use_unknown_in_catch_variables {
680        resolved.checker.use_unknown_in_catch_variables = v;
681    }
682    if let Some(v) = options.strict_bind_call_apply {
683        resolved.checker.strict_bind_call_apply = v;
684    }
685    if let Some(v) = options.no_implicit_override {
686        resolved.checker.no_implicit_override = v;
687    }
688    if let Some(v) = options.no_unchecked_side_effect_imports {
689        resolved.checker.no_unchecked_side_effect_imports = v;
690    }
691
692    if let Some(no_emit) = options.no_emit {
693        resolved.no_emit = no_emit;
694    }
695    if let Some(no_resolve) = options.no_resolve {
696        resolved.no_resolve = no_resolve;
697        resolved.checker.no_resolve = no_resolve;
698    }
699
700    if let Some(no_emit_on_error) = options.no_emit_on_error {
701        resolved.no_emit_on_error = no_emit_on_error;
702    }
703
704    if let Some(isolated_modules) = options.isolated_modules {
705        resolved.checker.isolated_modules = isolated_modules;
706    }
707
708    if let Some(always_strict) = options.always_strict {
709        resolved.checker.always_strict = always_strict;
710        resolved.printer.always_strict = always_strict;
711    }
712
713    if let Some(use_define_for_class_fields) = options.use_define_for_class_fields {
714        resolved.printer.use_define_for_class_fields = use_define_for_class_fields;
715    }
716
717    if let Some(no_unused_locals) = options.no_unused_locals {
718        resolved.checker.no_unused_locals = no_unused_locals;
719    }
720
721    if let Some(no_unused_parameters) = options.no_unused_parameters {
722        resolved.checker.no_unused_parameters = no_unused_parameters;
723    }
724
725    if let Some(allow_unreachable_code) = options.allow_unreachable_code {
726        resolved.checker.allow_unreachable_code = Some(allow_unreachable_code);
727    }
728
729    if let Some(ref custom_conditions) = options.custom_conditions {
730        resolved.custom_conditions = custom_conditions.clone();
731    }
732
733    if let Some(es_module_interop) = options.es_module_interop {
734        resolved.es_module_interop = es_module_interop;
735        resolved.checker.es_module_interop = es_module_interop;
736        // esModuleInterop implies allowSyntheticDefaultImports
737        if es_module_interop {
738            resolved.allow_synthetic_default_imports = true;
739            resolved.checker.allow_synthetic_default_imports = true;
740        }
741    }
742
743    if let Some(allow_synthetic_default_imports) = options.allow_synthetic_default_imports {
744        resolved.allow_synthetic_default_imports = allow_synthetic_default_imports;
745        resolved.checker.allow_synthetic_default_imports = allow_synthetic_default_imports;
746    } else if !resolved.allow_synthetic_default_imports {
747        // TSC defaults allowSyntheticDefaultImports to true when:
748        // - esModuleInterop is true (already handled above)
749        // - module is "system"
750        // - moduleResolution is "bundler"
751        // Otherwise defaults to false.
752        let should_default_true = matches!(resolved.checker.module, ModuleKind::System)
753            || matches!(
754                resolved.module_resolution,
755                Some(ModuleResolutionKind::Bundler)
756            );
757        if should_default_true {
758            resolved.allow_synthetic_default_imports = true;
759            resolved.checker.allow_synthetic_default_imports = true;
760        }
761    }
762
763    if let Some(experimental_decorators) = options.experimental_decorators {
764        resolved.checker.experimental_decorators = experimental_decorators;
765        resolved.printer.legacy_decorators = experimental_decorators;
766    }
767
768    if let Some(allow_js) = options.allow_js {
769        resolved.allow_js = allow_js;
770    }
771
772    if let Some(check_js) = options.check_js {
773        resolved.check_js = check_js;
774        resolved.checker.check_js = check_js;
775    }
776
777    Ok(resolved)
778}
779
780pub fn parse_tsconfig(source: &str) -> Result<TsConfig> {
781    let stripped = strip_jsonc(source);
782    let normalized = remove_trailing_commas(&stripped);
783    let config = serde_json::from_str(&normalized).context("failed to parse tsconfig JSON")?;
784    Ok(config)
785}
786
787pub fn load_tsconfig(path: &Path) -> Result<TsConfig> {
788    let mut visited = FxHashSet::default();
789    load_tsconfig_inner(path, &mut visited)
790}
791
792fn load_tsconfig_inner(path: &Path, visited: &mut FxHashSet<PathBuf>) -> Result<TsConfig> {
793    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
794    if !visited.insert(canonical.clone()) {
795        bail!("tsconfig extends cycle detected at {}", canonical.display());
796    }
797
798    let source = std::fs::read_to_string(path)
799        .with_context(|| format!("failed to read tsconfig: {}", path.display()))?;
800    let mut config = parse_tsconfig(&source)
801        .with_context(|| format!("failed to parse tsconfig: {}", path.display()))?;
802
803    let extends = config.extends.take();
804    if let Some(extends_path) = extends {
805        let base_path = resolve_extends_path(path, &extends_path)?;
806        let base_config = load_tsconfig_inner(&base_path, visited)?;
807        config = merge_configs(base_config, config);
808    }
809
810    visited.remove(&canonical);
811    Ok(config)
812}
813
814fn resolve_extends_path(current_path: &Path, extends: &str) -> Result<PathBuf> {
815    let base_dir = current_path
816        .parent()
817        .ok_or_else(|| anyhow!("tsconfig has no parent directory"))?;
818    let mut candidate = PathBuf::from(extends);
819    if candidate.extension().is_none() {
820        candidate.set_extension("json");
821    }
822
823    if candidate.is_absolute() {
824        Ok(candidate)
825    } else {
826        Ok(base_dir.join(candidate))
827    }
828}
829
830fn merge_configs(base: TsConfig, mut child: TsConfig) -> TsConfig {
831    let merged_compiler_options = match (base.compiler_options, child.compiler_options.take()) {
832        (Some(base_opts), Some(child_opts)) => Some(merge_compiler_options(base_opts, child_opts)),
833        (Some(base_opts), None) => Some(base_opts),
834        (None, Some(child_opts)) => Some(child_opts),
835        (None, None) => None,
836    };
837
838    TsConfig {
839        extends: None,
840        compiler_options: merged_compiler_options,
841        include: child.include.or(base.include),
842        exclude: child.exclude.or(base.exclude),
843        files: child.files.or(base.files),
844    }
845}
846
847/// Merge two `CompilerOptions` structs, preferring child values over base.
848/// Every `Option` field in `CompilerOptions` uses `.or()` — child wins when present.
849macro_rules! merge_options {
850    ($child:expr, $base:expr, $Struct:ident { $($field:ident),* $(,)? }) => {
851        $Struct { $( $field: $child.$field.or($base.$field), )* }
852    };
853}
854
855fn merge_compiler_options(base: CompilerOptions, child: CompilerOptions) -> CompilerOptions {
856    merge_options!(
857        child,
858        base,
859        CompilerOptions {
860            target,
861            module,
862            module_resolution,
863            resolve_package_json_exports,
864            resolve_package_json_imports,
865            module_suffixes,
866            resolve_json_module,
867            allow_arbitrary_extensions,
868            allow_importing_ts_extensions,
869            rewrite_relative_import_extensions,
870            types_versions_compiler_version,
871            types,
872            type_roots,
873            jsx,
874            jsx_factory,
875            jsx_fragment_factory,
876            react_namespace,
877
878            lib,
879            no_lib,
880            no_types_and_symbols,
881            base_url,
882            paths,
883            root_dir,
884            out_dir,
885            out_file,
886            declaration,
887            declaration_dir,
888            source_map,
889            declaration_map,
890            ts_build_info_file,
891            incremental,
892            strict,
893            no_emit,
894            no_emit_on_error,
895            isolated_modules,
896            custom_conditions,
897            es_module_interop,
898            allow_synthetic_default_imports,
899            experimental_decorators,
900            import_helpers,
901            allow_js,
902            check_js,
903            always_strict,
904            use_define_for_class_fields,
905            no_implicit_any,
906            no_implicit_returns,
907            strict_null_checks,
908            strict_function_types,
909            strict_property_initialization,
910            no_implicit_this,
911            use_unknown_in_catch_variables,
912            strict_bind_call_apply,
913            no_unchecked_indexed_access,
914            no_unused_locals,
915            no_unused_parameters,
916            allow_unreachable_code,
917            no_resolve,
918            no_unchecked_side_effect_imports,
919            no_implicit_override,
920        }
921    )
922}
923
924fn parse_script_target(value: &str) -> Result<ScriptTarget> {
925    // Strip trailing comma — multi-target test directives like `esnext, es2022`
926    // can pass `esnext,` through the tsconfig pipeline.
927    let cleaned = value.trim_end_matches(',');
928    let normalized = normalize_option(cleaned);
929    let target = match normalized.as_str() {
930        "es3" => ScriptTarget::ES3,
931        "es5" => ScriptTarget::ES5,
932        "es6" | "es2015" => ScriptTarget::ES2015,
933        "es2016" => ScriptTarget::ES2016,
934        "es2017" => ScriptTarget::ES2017,
935        "es2018" => ScriptTarget::ES2018,
936        "es2019" => ScriptTarget::ES2019,
937        "es2020" => ScriptTarget::ES2020,
938        "es2021" => ScriptTarget::ES2021,
939        "es2022" | "es2023" | "es2024" => ScriptTarget::ES2022,
940        "esnext" => ScriptTarget::ESNext,
941        _ => bail!("unsupported compilerOptions.target '{value}'"),
942    };
943
944    Ok(target)
945}
946
947fn parse_module_kind(value: &str) -> Result<ModuleKind> {
948    let cleaned = value.split(',').next().unwrap_or(value).trim();
949    let normalized = normalize_option(cleaned);
950    let module = match normalized.as_str() {
951        "none" => ModuleKind::None,
952        "commonjs" => ModuleKind::CommonJS,
953        "amd" => ModuleKind::AMD,
954        "umd" => ModuleKind::UMD,
955        "system" => ModuleKind::System,
956        "es6" | "es2015" => ModuleKind::ES2015,
957        "es2020" => ModuleKind::ES2020,
958        "es2022" => ModuleKind::ES2022,
959        "esnext" => ModuleKind::ESNext,
960        "node16" | "node18" | "node20" => ModuleKind::Node16,
961        "nodenext" => ModuleKind::NodeNext,
962        "preserve" => ModuleKind::Preserve,
963        _ => bail!("unsupported compilerOptions.module '{value}'"),
964    };
965
966    Ok(module)
967}
968
969fn parse_module_resolution(value: &str) -> Result<ModuleResolutionKind> {
970    let cleaned = value.split(',').next().unwrap_or(value).trim();
971    let normalized = normalize_option(cleaned);
972    let resolution = match normalized.as_str() {
973        "classic" => ModuleResolutionKind::Classic,
974        "node" | "node10" => ModuleResolutionKind::Node,
975        "node16" => ModuleResolutionKind::Node16,
976        "nodenext" => ModuleResolutionKind::NodeNext,
977        "bundler" => ModuleResolutionKind::Bundler,
978        _ => bail!("unsupported compilerOptions.moduleResolution '{value}'"),
979    };
980
981    Ok(resolution)
982}
983
984fn parse_jsx_emit(value: &str) -> Result<JsxEmit> {
985    let normalized = normalize_option(value);
986    let jsx = match normalized.as_str() {
987        "preserve" => JsxEmit::Preserve,
988        "react" => JsxEmit::React,
989        "react-jsx" | "reactjsx" => JsxEmit::ReactJsx,
990        "react-jsxdev" | "reactjsxdev" => JsxEmit::ReactJsxDev,
991        "reactnative" | "react-native" => JsxEmit::ReactNative,
992        _ => bail!("unsupported compilerOptions.jsx '{value}'"),
993    };
994
995    Ok(jsx)
996}
997
998const fn jsx_emit_to_mode(emit: JsxEmit) -> tsz_common::checker_options::JsxMode {
999    use tsz_common::checker_options::JsxMode;
1000    match emit {
1001        JsxEmit::Preserve => JsxMode::Preserve,
1002        JsxEmit::React => JsxMode::React,
1003        JsxEmit::ReactJsx => JsxMode::ReactJsx,
1004        JsxEmit::ReactJsxDev => JsxMode::ReactJsxDev,
1005        JsxEmit::ReactNative => JsxMode::ReactNative,
1006    }
1007}
1008
1009fn build_path_mappings(paths: &FxHashMap<String, Vec<String>>) -> Vec<PathMapping> {
1010    let mut mappings = Vec::new();
1011    for (pattern, targets) in paths {
1012        if targets.is_empty() {
1013            continue;
1014        }
1015        let pattern = normalize_path_pattern(pattern);
1016        let targets = targets
1017            .iter()
1018            .map(|target| normalize_path_pattern(target))
1019            .collect();
1020        let (prefix, suffix) = split_path_pattern(&pattern);
1021        mappings.push(PathMapping {
1022            pattern,
1023            prefix,
1024            suffix,
1025            targets,
1026        });
1027    }
1028    mappings.sort_by(|left, right| {
1029        right
1030            .specificity()
1031            .cmp(&left.specificity())
1032            .then_with(|| right.pattern.len().cmp(&left.pattern.len()))
1033            .then_with(|| left.pattern.cmp(&right.pattern))
1034    });
1035    mappings
1036}
1037
1038fn normalize_path_pattern(value: &str) -> String {
1039    value.trim().replace('\\', "/")
1040}
1041
1042fn split_path_pattern(pattern: &str) -> (String, String) {
1043    match pattern.find('*') {
1044        Some(star_idx) => {
1045            let (prefix, rest) = pattern.split_at(star_idx);
1046            (prefix.to_string(), rest[1..].to_string())
1047        }
1048        None => (pattern.to_string(), String::new()),
1049    }
1050}
1051
1052/// Resolve lib files from names, optionally following `/// <reference lib="..." />` directives.
1053///
1054/// When `follow_references` is true, each lib file is scanned for reference directives
1055/// and those referenced libs are also loaded. When false, only the explicitly listed
1056/// libs are loaded without following their internal references.
1057///
1058/// TypeScript always follows `/// <reference lib="..." />` directives when loading libs.
1059/// For example, `lib.dom.d.ts` references `es2015` and `es2018.asynciterable`, so even
1060/// `--target es5` (which loads lib.d.ts -> dom) transitively loads ES2015 features.
1061/// Verified with `tsc 6.0.0-dev --target es5 --listFiles`.
1062pub fn resolve_lib_files_with_options(
1063    lib_list: &[String],
1064    follow_references: bool,
1065) -> Result<Vec<PathBuf>> {
1066    if lib_list.is_empty() {
1067        return Ok(Vec::new());
1068    }
1069
1070    let lib_dir = default_lib_dir()?;
1071    resolve_lib_files_from_dir_with_options(lib_list, follow_references, &lib_dir)
1072}
1073
1074pub fn resolve_lib_files_from_dir_with_options(
1075    lib_list: &[String],
1076    follow_references: bool,
1077    lib_dir: &Path,
1078) -> Result<Vec<PathBuf>> {
1079    if lib_list.is_empty() {
1080        return Ok(Vec::new());
1081    }
1082
1083    let lib_map = build_lib_map(lib_dir)?;
1084    let mut resolved = Vec::new();
1085    let mut pending: VecDeque<String> = lib_list
1086        .iter()
1087        .map(|value| normalize_lib_name(value))
1088        .collect();
1089    let mut visited = FxHashSet::default();
1090
1091    while let Some(lib_name) = pending.pop_front() {
1092        if lib_name.is_empty() || !visited.insert(lib_name.clone()) {
1093            continue;
1094        }
1095
1096        let path = match lib_map.get(&lib_name) {
1097            Some(path) => path.clone(),
1098            None => {
1099                // Handle tsc compatibility aliases:
1100                // - "lib" refers to lib.d.ts which is equivalent to es5.full.d.ts
1101                // - "es6" refers to lib.es6.d.ts which is equivalent to es2015.full.d.ts
1102                // - "es7" refers to lib.es2016.d.ts which is equivalent to es2016.d.ts
1103                let alias = match lib_name.as_str() {
1104                    "lib" => Some("es5.full"),
1105                    "es6" => Some("es2015.full"),
1106                    "es7" => Some("es2016"),
1107                    _ => None,
1108                };
1109                let Some(alias) = alias else {
1110                    return Err(anyhow!(
1111                        "unsupported compilerOptions.lib '{}' (not found in {})",
1112                        lib_name,
1113                        lib_dir.display()
1114                    ));
1115                };
1116                lib_map.get(alias).cloned().ok_or_else(|| {
1117                    anyhow!(
1118                        "unsupported compilerOptions.lib '{}' (alias '{}' not found in {})",
1119                        lib_name,
1120                        alias,
1121                        lib_dir.display()
1122                    )
1123                })?
1124            }
1125        };
1126        resolved.push(path.clone());
1127
1128        // Only follow /// <reference lib="..." /> directives if requested
1129        if follow_references {
1130            let contents = std::fs::read_to_string(&path)
1131                .with_context(|| format!("failed to read lib file {}", path.display()))?;
1132            for reference in extract_lib_references(&contents) {
1133                pending.push_back(reference);
1134            }
1135        }
1136    }
1137
1138    Ok(resolved)
1139}
1140
1141/// Resolve lib files from names, following `/// <reference lib="..." />` directives.
1142/// This is used when explicitly specifying libs via `--lib`.
1143pub fn resolve_lib_files(lib_list: &[String]) -> Result<Vec<PathBuf>> {
1144    resolve_lib_files_with_options(lib_list, true)
1145}
1146
1147pub fn resolve_lib_files_from_dir(lib_list: &[String], lib_dir: &Path) -> Result<Vec<PathBuf>> {
1148    resolve_lib_files_from_dir_with_options(lib_list, true, lib_dir)
1149}
1150
1151/// Resolve default lib files for a given target.
1152///
1153/// Matches tsc's behavior exactly:
1154/// 1. Get the root lib file for the target (e.g., "lib" for ES5, "es6" for ES2015)
1155/// 2. Follow ALL `/// <reference lib="..." />` directives recursively
1156///
1157/// This means `--target es5` loads lib.d.ts -> dom -> es2015 (transitively),
1158/// which is exactly what tsc does (verified with `tsc --target es5 --listFiles`).
1159pub fn resolve_default_lib_files(target: ScriptTarget) -> Result<Vec<PathBuf>> {
1160    let lib_dir = default_lib_dir()?;
1161    resolve_default_lib_files_from_dir(target, &lib_dir)
1162}
1163
1164pub fn resolve_default_lib_files_from_dir(
1165    target: ScriptTarget,
1166    lib_dir: &Path,
1167) -> Result<Vec<PathBuf>> {
1168    let root_lib = default_lib_name_for_target(target);
1169    resolve_lib_files_from_dir(&[root_lib.to_string()], lib_dir)
1170}
1171
1172/// Get the default lib name for a target.
1173///
1174/// This matches tsc's default behavior exactly:
1175/// - Each target loads the corresponding `.full` lib which includes:
1176///   - The ES version libs (e.g., es5, es2015.promise, etc.)
1177///   - DOM types (document, window, console, fetch, etc.)
1178///   - `ScriptHost` types
1179///
1180/// The mapping matches TypeScript's `getDefaultLibFileName()` in utilitiesPublic.ts:
1181/// - ES3/ES5 → lib.d.ts (equivalent to es5.full.d.ts in source tree)
1182/// - ES2015  → lib.es6.d.ts (equivalent to es2015.full.d.ts in source tree)
1183/// - ES2016+ → lib.es20XX.full.d.ts
1184/// - `ESNext`  → lib.esnext.full.d.ts
1185///
1186/// Note: The source tree uses `es5.full.d.ts` naming, while built TypeScript uses `lib.d.ts`.
1187/// We use the source tree naming since that's what exists in TypeScript/src/lib.
1188pub const fn default_lib_name_for_target(target: ScriptTarget) -> &'static str {
1189    match target {
1190        // ES3/ES5 -> lib.d.ts (ES5 + DOM + ScriptHost)
1191        ScriptTarget::ES3 | ScriptTarget::ES5 => "lib",
1192        // ES2015 -> lib.es6.d.ts (ES2015 + DOM + DOM.Iterable + ScriptHost)
1193        // Note: NOT "es2015.full" (doesn't exist), use "es6" per tsc convention
1194        ScriptTarget::ES2015 => "es6",
1195        // ES2016+ use .full variants (ES + DOM + ScriptHost + others)
1196        ScriptTarget::ES2016 => "es2016.full",
1197        ScriptTarget::ES2017 => "es2017.full",
1198        ScriptTarget::ES2018 => "es2018.full",
1199        ScriptTarget::ES2019 => "es2019.full",
1200        ScriptTarget::ES2020 => "es2020.full",
1201        ScriptTarget::ES2021 => "es2021.full",
1202        ScriptTarget::ES2022 => "es2022.full",
1203        ScriptTarget::ES2023
1204        | ScriptTarget::ES2024
1205        | ScriptTarget::ES2025
1206        | ScriptTarget::ESNext => "esnext.full",
1207    }
1208}
1209
1210/// Get the core lib name for a target (without DOM/ScriptHost).
1211///
1212/// This is useful for conformance testing where:
1213/// 1. Tests don't need DOM types
1214/// 2. Core libs are smaller and faster to load
1215/// 3. Tests that need DOM should specify @lib: dom explicitly
1216pub const fn core_lib_name_for_target(target: ScriptTarget) -> &'static str {
1217    match target {
1218        ScriptTarget::ES3 | ScriptTarget::ES5 => "es5",
1219        ScriptTarget::ES2015 => "es2015",
1220        ScriptTarget::ES2016 => "es2016",
1221        ScriptTarget::ES2017 => "es2017",
1222        ScriptTarget::ES2018 => "es2018",
1223        ScriptTarget::ES2019 => "es2019",
1224        ScriptTarget::ES2020 => "es2020",
1225        ScriptTarget::ES2021 => "es2021",
1226        ScriptTarget::ES2022 => "es2022",
1227        ScriptTarget::ES2023
1228        | ScriptTarget::ES2024
1229        | ScriptTarget::ES2025
1230        | ScriptTarget::ESNext => "esnext",
1231    }
1232}
1233
1234/// Get the default lib directory.
1235///
1236/// Searches in order:
1237/// 1. `TSZ_LIB_DIR` environment variable
1238/// 2. Relative to the executable
1239/// 3. Relative to current working directory
1240/// 4. TypeScript/src/lib in the source tree
1241pub fn default_lib_dir() -> Result<PathBuf> {
1242    if let Some(dir) = env::var_os("TSZ_LIB_DIR") {
1243        let dir = PathBuf::from(dir);
1244        if !dir.is_dir() {
1245            bail!(
1246                "TSZ_LIB_DIR does not point to a directory: {}",
1247                dir.display()
1248            );
1249        }
1250        return Ok(canonicalize_or_owned(&dir));
1251    }
1252
1253    if let Some(dir) = lib_dir_from_exe() {
1254        return Ok(dir);
1255    }
1256
1257    if let Some(dir) = lib_dir_from_cwd() {
1258        return Ok(dir);
1259    }
1260
1261    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
1262    if let Some(dir) = lib_dir_from_root(manifest_dir) {
1263        return Ok(dir);
1264    }
1265
1266    bail!("lib directory not found under {}", manifest_dir.display());
1267}
1268
1269fn lib_dir_from_exe() -> Option<PathBuf> {
1270    let exe = env::current_exe().ok()?;
1271    let exe_dir = exe.parent()?;
1272    let candidate = exe_dir.join("lib");
1273    if candidate.is_dir() {
1274        return Some(canonicalize_or_owned(&candidate));
1275    }
1276    lib_dir_from_root(exe_dir)
1277}
1278
1279fn lib_dir_from_cwd() -> Option<PathBuf> {
1280    let cwd = env::current_dir().ok()?;
1281    lib_dir_from_root(&cwd)
1282}
1283
1284fn lib_dir_from_root(root: &Path) -> Option<PathBuf> {
1285    let candidates = [
1286        // Built/compiled libs from tsc build output (highest priority)
1287        root.join("TypeScript").join("built").join("local"),
1288        root.join("TypeScript").join("lib"),
1289        // npm-installed TypeScript libs (self-contained, matching tsc's shipped format).
1290        // Prefer these over TypeScript/src/lib which has source-format files with
1291        // cross-module /// <reference lib> directives that pull in ES2015+ content
1292        // even for ES5 targets (e.g., dom.generated.d.ts references es2015.symbol.d.ts).
1293        root.join("node_modules").join("typescript").join("lib"),
1294        root.join("scripts")
1295            .join("emit")
1296            .join("node_modules")
1297            .join("typescript")
1298            .join("lib"),
1299        root.join("TypeScript").join("src").join("lib"),
1300        root.join("TypeScript")
1301            .join("node_modules")
1302            .join("typescript")
1303            .join("lib"),
1304        root.join("tests").join("lib"),
1305    ];
1306
1307    for candidate in candidates {
1308        if candidate.is_dir() {
1309            return Some(canonicalize_or_owned(&candidate));
1310        }
1311    }
1312
1313    None
1314}
1315
1316fn build_lib_map(lib_dir: &Path) -> Result<FxHashMap<String, PathBuf>> {
1317    let mut map = FxHashMap::default();
1318    for entry in std::fs::read_dir(lib_dir)
1319        .with_context(|| format!("failed to read lib directory {}", lib_dir.display()))?
1320    {
1321        let entry = entry?;
1322        let path = entry.path();
1323        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
1324            continue;
1325        };
1326        if !file_name.ends_with(".d.ts") {
1327            continue;
1328        }
1329
1330        let stem = file_name.trim_end_matches(".d.ts");
1331        let stem = stem.strip_suffix(".generated").unwrap_or(stem);
1332        let key = normalize_lib_name(stem);
1333        map.insert(key, canonicalize_or_owned(&path));
1334    }
1335
1336    Ok(map)
1337}
1338
1339/// Extract /// <reference lib="..." /> directives from a lib file source.
1340/// Returns a list of referenced lib names.
1341pub(crate) fn extract_lib_references(source: &str) -> Vec<String> {
1342    let mut refs = Vec::new();
1343    let mut in_block_comment = false;
1344    for line in source.lines() {
1345        let line = line.trim_start();
1346        if in_block_comment {
1347            if line.contains("*/") {
1348                in_block_comment = false;
1349            }
1350            continue;
1351        }
1352        if line.starts_with("/*") {
1353            if !line.contains("*/") {
1354                in_block_comment = true;
1355            }
1356            continue;
1357        }
1358        if line.is_empty() {
1359            continue;
1360        }
1361        if line.starts_with("///") {
1362            if let Some(value) = parse_reference_lib_value(line) {
1363                refs.push(normalize_lib_name(value));
1364            }
1365            continue;
1366        }
1367        if line.starts_with("//") {
1368            continue;
1369        }
1370        break;
1371    }
1372    refs
1373}
1374
1375fn parse_reference_lib_value(line: &str) -> Option<&str> {
1376    let mut offset = 0;
1377    let bytes = line.as_bytes();
1378    while let Some(idx) = line[offset..].find("lib=") {
1379        let start = offset + idx;
1380        if start > 0 {
1381            let prev = bytes[start - 1];
1382            if !prev.is_ascii_whitespace() && prev != b'<' {
1383                offset = start + 4;
1384                continue;
1385            }
1386        }
1387        let quote = *bytes.get(start + 4)?;
1388        if quote != b'"' && quote != b'\'' {
1389            offset = start + 4;
1390            continue;
1391        }
1392        let rest = &line[start + 5..];
1393        let end = rest.find(quote as char)?;
1394        return Some(&rest[..end]);
1395    }
1396    None
1397}
1398
1399fn normalize_lib_name(value: &str) -> String {
1400    let normalized = value.trim().to_ascii_lowercase();
1401    normalized
1402        .strip_prefix("lib.")
1403        .unwrap_or(normalized.as_str())
1404        .to_string()
1405}
1406
1407/// Convert emitter `ScriptTarget` to checker `ScriptTarget`.
1408/// The emitter has more variants (`ES2021`, `ES2022`) which map to `ESNext` in the checker.
1409pub const fn checker_target_from_emitter(target: ScriptTarget) -> CheckerScriptTarget {
1410    match target {
1411        ScriptTarget::ES3 => CheckerScriptTarget::ES3,
1412        ScriptTarget::ES5 => CheckerScriptTarget::ES5,
1413        ScriptTarget::ES2015 => CheckerScriptTarget::ES2015,
1414        ScriptTarget::ES2016 => CheckerScriptTarget::ES2016,
1415        ScriptTarget::ES2017 => CheckerScriptTarget::ES2017,
1416        ScriptTarget::ES2018 => CheckerScriptTarget::ES2018,
1417        ScriptTarget::ES2019 => CheckerScriptTarget::ES2019,
1418        ScriptTarget::ES2020 => CheckerScriptTarget::ES2020,
1419        ScriptTarget::ES2021
1420        | ScriptTarget::ES2022
1421        | ScriptTarget::ES2023
1422        | ScriptTarget::ES2024
1423        | ScriptTarget::ES2025
1424        | ScriptTarget::ESNext => CheckerScriptTarget::ESNext,
1425    }
1426}
1427
1428fn canonicalize_or_owned(path: &Path) -> PathBuf {
1429    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
1430}
1431
1432fn normalize_option(value: &str) -> String {
1433    let mut normalized = String::with_capacity(value.len());
1434    for ch in value.chars() {
1435        if ch == '-' || ch == '_' || ch.is_whitespace() {
1436            continue;
1437        }
1438        normalized.push(ch.to_ascii_lowercase());
1439    }
1440    normalized
1441}
1442
1443fn strip_jsonc(input: &str) -> String {
1444    let mut out = String::with_capacity(input.len());
1445    let mut chars = input.chars().peekable();
1446    let mut in_string = false;
1447    let mut escape = false;
1448    let mut in_line_comment = false;
1449    let mut in_block_comment = false;
1450
1451    while let Some(ch) = chars.next() {
1452        if in_line_comment {
1453            if ch == '\n' {
1454                in_line_comment = false;
1455                out.push(ch);
1456            }
1457            continue;
1458        }
1459
1460        if in_block_comment {
1461            if ch == '*' {
1462                if let Some('/') = chars.peek().copied() {
1463                    chars.next();
1464                    in_block_comment = false;
1465                }
1466            } else if ch == '\n' {
1467                out.push(ch);
1468            }
1469            continue;
1470        }
1471
1472        if in_string {
1473            out.push(ch);
1474            if escape {
1475                escape = false;
1476            } else if ch == '\\' {
1477                escape = true;
1478            } else if ch == '"' {
1479                in_string = false;
1480            }
1481            continue;
1482        }
1483
1484        if ch == '"' {
1485            in_string = true;
1486            out.push(ch);
1487            continue;
1488        }
1489
1490        if ch == '/'
1491            && let Some(&next) = chars.peek()
1492        {
1493            if next == '/' {
1494                chars.next();
1495                in_line_comment = true;
1496                continue;
1497            }
1498            if next == '*' {
1499                chars.next();
1500                in_block_comment = true;
1501                continue;
1502            }
1503        }
1504
1505        out.push(ch);
1506    }
1507
1508    out
1509}
1510
1511fn remove_trailing_commas(input: &str) -> String {
1512    let mut out = String::with_capacity(input.len());
1513    let mut chars = input.chars().peekable();
1514    let mut in_string = false;
1515    let mut escape = false;
1516
1517    while let Some(ch) = chars.next() {
1518        if in_string {
1519            out.push(ch);
1520            if escape {
1521                escape = false;
1522            } else if ch == '\\' {
1523                escape = true;
1524            } else if ch == '"' {
1525                in_string = false;
1526            }
1527            continue;
1528        }
1529
1530        if ch == '"' {
1531            in_string = true;
1532            out.push(ch);
1533            continue;
1534        }
1535
1536        if ch == ',' {
1537            let mut lookahead = chars.clone();
1538            while let Some(next) = lookahead.peek().copied() {
1539                if next.is_whitespace() {
1540                    lookahead.next();
1541                    continue;
1542                }
1543                if next == '}' || next == ']' {
1544                    break;
1545                }
1546                break;
1547            }
1548
1549            if let Some(next) = lookahead.peek().copied()
1550                && (next == '}' || next == ']')
1551            {
1552                continue;
1553            }
1554        }
1555
1556        out.push(ch);
1557    }
1558
1559    out
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564    use super::*;
1565
1566    #[test]
1567    fn test_parse_boolean_true() {
1568        let json = r#"{"strict": true}"#;
1569        let opts: CompilerOptions = serde_json::from_str(json).unwrap();
1570        assert_eq!(opts.strict, Some(true));
1571    }
1572
1573    #[test]
1574    fn test_parse_string_true() {
1575        let json = r#"{"strict": "true"}"#;
1576        let opts: CompilerOptions = serde_json::from_str(json).unwrap();
1577        assert_eq!(opts.strict, Some(true));
1578    }
1579
1580    #[test]
1581    fn test_parse_invalid_string() {
1582        let json = r#"{"strict": "invalid"}"#;
1583        let result: Result<CompilerOptions, _> = serde_json::from_str(json);
1584        assert!(result.is_err());
1585    }
1586
1587    #[test]
1588    fn test_parse_module_resolution_list_value() {
1589        let json =
1590            r#"{"compilerOptions":{"moduleResolution":"node16,nodenext","module":"commonjs"}} "#;
1591        let config: TsConfig = serde_json::from_str(json).unwrap();
1592        let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
1593        assert_eq!(
1594            resolved.module_resolution,
1595            Some(ModuleResolutionKind::Node16)
1596        );
1597    }
1598}