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::checker::diagnostics::Diagnostic;
11use crate::emitter::{ModuleKind, PrinterOptions, ScriptTarget};
12use tsz_common::diagnostics::data::{diagnostic_codes, diagnostic_messages};
13use tsz_common::diagnostics::format_message;
14
15/// Custom deserializer for boolean options that accepts both bool and string values.
16/// This handles cases where tsconfig.json contains `"strict": "true"` instead of `"strict": true`.
17fn deserialize_bool_or_string<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
18where
19    D: Deserializer<'de>,
20{
21    use serde::de::Error;
22
23    // Use a helper enum to deserialize either a bool or a string
24    #[derive(Deserialize)]
25    #[serde(untagged)]
26    enum BoolOrString {
27        Bool(bool),
28        String(String),
29    }
30
31    match Option::<BoolOrString>::deserialize(deserializer)? {
32        None => Ok(None),
33        Some(BoolOrString::Bool(b)) => Ok(Some(b)),
34        Some(BoolOrString::String(s)) => {
35            // Parse common string representations of boolean values
36            let normalized = s.trim().to_lowercase();
37            match normalized.as_str() {
38                "true" | "1" | "yes" | "on" => Ok(Some(true)),
39                "false" | "0" | "no" | "off" => Ok(Some(false)),
40                _ => {
41                    // Invalid boolean string - return error with helpful message
42                    Err(Error::custom(format!(
43                        "invalid boolean value: '{s}'. Expected true, false, 'true', or 'false'",
44                    )))
45                }
46            }
47        }
48    }
49}
50
51#[derive(Debug, Clone, Deserialize, Default)]
52#[serde(rename_all = "camelCase")]
53pub struct TsConfig {
54    #[serde(default)]
55    pub extends: Option<String>,
56    #[serde(default)]
57    pub compiler_options: Option<CompilerOptions>,
58    #[serde(default)]
59    pub include: Option<Vec<String>>,
60    #[serde(default)]
61    pub exclude: Option<Vec<String>>,
62    #[serde(default)]
63    pub files: Option<Vec<String>>,
64}
65
66#[derive(Debug, Clone, Deserialize, Default)]
67#[serde(rename_all = "camelCase")]
68pub struct CompilerOptions {
69    #[serde(default)]
70    pub target: Option<String>,
71    #[serde(default)]
72    pub module: Option<String>,
73    #[serde(default)]
74    pub module_resolution: Option<String>,
75    /// Use the package.json 'exports' field when resolving package imports.
76    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
77    pub resolve_package_json_exports: Option<bool>,
78    /// Use the package.json 'imports' field when resolving imports.
79    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
80    pub resolve_package_json_imports: Option<bool>,
81    /// List of file name suffixes to search when resolving a module.
82    #[serde(default)]
83    pub module_suffixes: Option<Vec<String>>,
84    /// Enable importing .json files.
85    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
86    pub resolve_json_module: Option<bool>,
87    /// Enable importing files with any extension, provided a declaration file is present.
88    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
89    pub allow_arbitrary_extensions: Option<bool>,
90    /// Allow imports to include TypeScript file extensions.
91    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
92    pub allow_importing_ts_extensions: Option<bool>,
93    /// Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths.
94    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
95    pub rewrite_relative_import_extensions: Option<bool>,
96    #[serde(default)]
97    pub types_versions_compiler_version: Option<String>,
98    #[serde(default)]
99    pub types: Option<Vec<String>>,
100    #[serde(default)]
101    pub type_roots: Option<Vec<String>>,
102    #[serde(default)]
103    pub jsx: Option<String>,
104    #[serde(default)]
105    #[serde(rename = "jsxFactory")]
106    pub jsx_factory: Option<String>,
107    #[serde(default)]
108    #[serde(rename = "jsxFragmentFactory")]
109    pub jsx_fragment_factory: Option<String>,
110    #[serde(default)]
111    #[serde(rename = "reactNamespace")]
112    pub react_namespace: Option<String>,
113
114    #[serde(default)]
115    pub lib: Option<Vec<String>>,
116    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
117    pub no_lib: Option<bool>,
118    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
119    pub no_types_and_symbols: Option<bool>,
120    #[serde(default)]
121    pub base_url: Option<String>,
122    #[serde(default)]
123    pub paths: Option<FxHashMap<String, Vec<String>>>,
124    #[serde(default)]
125    pub root_dir: Option<String>,
126    #[serde(default)]
127    pub out_dir: Option<String>,
128    #[serde(default)]
129    pub out_file: Option<String>,
130    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
131    pub declaration: Option<bool>,
132    #[serde(default)]
133    pub declaration_dir: Option<String>,
134    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
135    pub source_map: Option<bool>,
136    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
137    pub declaration_map: Option<bool>,
138    #[serde(default)]
139    pub ts_build_info_file: Option<String>,
140    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
141    pub incremental: Option<bool>,
142    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
143    pub strict: Option<bool>,
144    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
145    pub no_emit: Option<bool>,
146    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
147    pub no_resolve: Option<bool>,
148    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
149    pub no_emit_on_error: Option<bool>,
150    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
151    pub isolated_modules: Option<bool>,
152    /// Custom conditions for package.json exports resolution
153    #[serde(default)]
154    pub custom_conditions: Option<Vec<String>>,
155    /// Emit additional JavaScript to ease support for importing CommonJS modules
156    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
157    pub es_module_interop: Option<bool>,
158    /// Allow 'import x from y' when a module doesn't have a default export
159    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
160    pub allow_synthetic_default_imports: Option<bool>,
161    /// Enable experimental support for legacy experimental decorators
162    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
163    pub experimental_decorators: Option<bool>,
164    /// Import emit helpers from tslib instead of inlining them per-file
165    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
166    pub import_helpers: Option<bool>,
167    /// Allow JavaScript files to be a part of your program
168    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
169    pub allow_js: Option<bool>,
170    /// Enable error reporting in type-checked JavaScript files
171    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
172    pub check_js: Option<bool>,
173    /// Skip type checking of declaration files (.d.ts)
174    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
175    pub skip_lib_check: Option<bool>,
176    /// Parse in strict mode and emit "use strict" for each source file
177    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
178    pub always_strict: Option<bool>,
179    /// Use `Object.defineProperty` semantics for class fields when downleveling.
180    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
181    pub use_define_for_class_fields: Option<bool>,
182    /// Raise error on expressions and declarations with an implied 'any' type
183    #[serde(
184        default,
185        alias = "noImplicitAny",
186        deserialize_with = "deserialize_bool_or_string"
187    )]
188    pub no_implicit_any: Option<bool>,
189    /// Enable error reporting when a function doesn't explicitly return in all code paths
190    #[serde(
191        default,
192        alias = "noImplicitReturns",
193        deserialize_with = "deserialize_bool_or_string"
194    )]
195    pub no_implicit_returns: Option<bool>,
196    /// Enable strict null checks
197    #[serde(
198        default,
199        alias = "strictNullChecks",
200        deserialize_with = "deserialize_bool_or_string"
201    )]
202    pub strict_null_checks: Option<bool>,
203    /// Enable strict checking of function types
204    #[serde(
205        default,
206        alias = "strictFunctionTypes",
207        deserialize_with = "deserialize_bool_or_string"
208    )]
209    pub strict_function_types: Option<bool>,
210    /// Check for class properties that are declared but not set in the constructor
211    #[serde(
212        default,
213        alias = "strictPropertyInitialization",
214        deserialize_with = "deserialize_bool_or_string"
215    )]
216    pub strict_property_initialization: Option<bool>,
217    /// Raise error on 'this' expressions with an implied 'any' type
218    #[serde(
219        default,
220        alias = "noImplicitThis",
221        deserialize_with = "deserialize_bool_or_string"
222    )]
223    pub no_implicit_this: Option<bool>,
224    /// Default catch clause variables as 'unknown' instead of 'any'
225    #[serde(
226        default,
227        alias = "useUnknownInCatchVariables",
228        deserialize_with = "deserialize_bool_or_string"
229    )]
230    pub use_unknown_in_catch_variables: Option<bool>,
231    /// Add 'undefined' to a type when accessed using an index
232    #[serde(
233        default,
234        alias = "noUncheckedIndexedAccess",
235        deserialize_with = "deserialize_bool_or_string"
236    )]
237    pub no_unchecked_indexed_access: Option<bool>,
238    /// Check that the arguments for 'bind', 'call', and 'apply' methods match the original function
239    #[serde(
240        default,
241        alias = "strictBindCallApply",
242        deserialize_with = "deserialize_bool_or_string"
243    )]
244    pub strict_bind_call_apply: Option<bool>,
245    /// Report errors on unused local variables
246    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
247    pub no_unused_locals: Option<bool>,
248    /// Report errors on unused parameters
249    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
250    pub no_unused_parameters: Option<bool>,
251    /// Do not report errors on unreachable code
252    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
253    pub allow_unreachable_code: Option<bool>,
254    /// Check side-effect imports for module resolution errors
255    #[serde(default, deserialize_with = "deserialize_bool_or_string")]
256    pub no_unchecked_side_effect_imports: Option<bool>,
257    /// Require 'override' modifier on members that override base class members
258    #[serde(
259        default,
260        alias = "noImplicitOverride",
261        deserialize_with = "deserialize_bool_or_string"
262    )]
263    pub no_implicit_override: Option<bool>,
264    /// Control what method is used to detect module-format JS files.
265    #[serde(default)]
266    pub module_detection: Option<String>,
267}
268
269// Re-export CheckerOptions from checker::context for unified API
270pub use crate::checker::context::CheckerOptions;
271
272#[derive(Debug, Clone, Default)]
273pub struct ResolvedCompilerOptions {
274    pub printer: PrinterOptions,
275    pub checker: CheckerOptions,
276    pub jsx: Option<JsxEmit>,
277    pub lib_files: Vec<PathBuf>,
278    pub lib_is_default: bool,
279    pub module_resolution: Option<ModuleResolutionKind>,
280    pub resolve_package_json_exports: bool,
281    pub resolve_package_json_imports: bool,
282    pub module_suffixes: Vec<String>,
283    pub resolve_json_module: bool,
284    pub allow_arbitrary_extensions: bool,
285    pub allow_importing_ts_extensions: bool,
286    pub rewrite_relative_import_extensions: bool,
287    pub types_versions_compiler_version: Option<String>,
288    pub types: Option<Vec<String>>,
289    pub type_roots: Option<Vec<PathBuf>>,
290    pub base_url: Option<PathBuf>,
291    pub paths: Option<Vec<PathMapping>>,
292    pub root_dir: Option<PathBuf>,
293    pub out_dir: Option<PathBuf>,
294    pub out_file: Option<PathBuf>,
295    pub declaration_dir: Option<PathBuf>,
296    pub emit_declarations: bool,
297    pub source_map: bool,
298    pub declaration_map: bool,
299    pub ts_build_info_file: Option<PathBuf>,
300    pub incremental: bool,
301    pub no_emit: bool,
302    pub no_emit_on_error: bool,
303    /// Skip module graph expansion from imports/references when checking.
304    pub no_resolve: bool,
305    pub import_helpers: bool,
306    /// Disable full type checking (only parse and emit errors reported).
307    pub no_check: bool,
308    /// Custom conditions for package.json exports resolution
309    pub custom_conditions: Vec<String>,
310    /// Emit additional JavaScript to ease support for importing CommonJS modules
311    pub es_module_interop: bool,
312    /// Allow 'import x from y' when a module doesn't have a default export
313    pub allow_synthetic_default_imports: bool,
314    /// Allow JavaScript files to be part of the program
315    pub allow_js: bool,
316    /// Enable error reporting in type-checked JavaScript files
317    pub check_js: bool,
318    /// Skip type checking of declaration files (.d.ts)
319    pub skip_lib_check: bool,
320}
321
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum JsxEmit {
324    Preserve,
325    React,
326    ReactJsx,
327    ReactJsxDev,
328    ReactNative,
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332pub enum ModuleResolutionKind {
333    Classic,
334    Node,
335    Node16,
336    NodeNext,
337    Bundler,
338}
339
340#[derive(Debug, Clone)]
341pub struct PathMapping {
342    pub pattern: String,
343    pub(crate) prefix: String,
344    pub(crate) suffix: String,
345    pub targets: Vec<String>,
346}
347
348impl PathMapping {
349    pub fn match_specifier(&self, specifier: &str) -> Option<String> {
350        if !self.pattern.contains('*') {
351            return (self.pattern == specifier).then(String::new);
352        }
353
354        if !specifier.starts_with(&self.prefix) || !specifier.ends_with(&self.suffix) {
355            return None;
356        }
357
358        let start = self.prefix.len();
359        let end = specifier.len().saturating_sub(self.suffix.len());
360        if end < start {
361            return None;
362        }
363
364        Some(specifier[start..end].to_string())
365    }
366
367    pub const fn specificity(&self) -> usize {
368        self.prefix.len() + self.suffix.len()
369    }
370}
371
372impl ResolvedCompilerOptions {
373    pub const fn effective_module_resolution(&self) -> ModuleResolutionKind {
374        if let Some(resolution) = self.module_resolution {
375            return resolution;
376        }
377
378        // Match tsc's computed moduleResolution defaults exactly:
379        // None/AMD/UMD/System → Classic
380        // CommonJS → Node (node10)
381        // NodeNext → NodeNext
382        // Node16 → Node16
383        // Everything else (ES2015, ES2020, ES2022, ESNext, Preserve) → Bundler
384        match self.printer.module {
385            ModuleKind::None | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
386                ModuleResolutionKind::Classic
387            }
388            ModuleKind::CommonJS => ModuleResolutionKind::Node,
389            ModuleKind::NodeNext => ModuleResolutionKind::NodeNext,
390            ModuleKind::Node16 => ModuleResolutionKind::Node16,
391            _ => ModuleResolutionKind::Bundler,
392        }
393    }
394}
395
396pub fn resolve_compiler_options(
397    options: Option<&CompilerOptions>,
398) -> Result<ResolvedCompilerOptions> {
399    let mut resolved = ResolvedCompilerOptions::default();
400    let Some(options) = options else {
401        resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
402        resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
403        resolved.lib_is_default = true;
404        resolved.module_suffixes = vec![String::new()];
405        let default_resolution = resolved.effective_module_resolution();
406        resolved.resolve_package_json_exports = matches!(
407            default_resolution,
408            ModuleResolutionKind::Node16
409                | ModuleResolutionKind::NodeNext
410                | ModuleResolutionKind::Bundler
411        );
412        resolved.resolve_package_json_imports = resolved.resolve_package_json_exports;
413        return Ok(resolved);
414    };
415
416    if let Some(target) = options.target.as_deref() {
417        resolved.printer.target = parse_script_target(target)?;
418    }
419    resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
420
421    let module_explicitly_set = options.module.is_some();
422    if let Some(module) = options.module.as_deref() {
423        let kind = parse_module_kind(module)?;
424        resolved.printer.module = kind;
425        resolved.checker.module = kind;
426    } else {
427        // Match tsc: when --module is omitted, default depends on target.
428        // ES2015+ targets default to ES2015 modules; lower targets default to CommonJS.
429        let default_module = if resolved.printer.target.supports_es2015() {
430            ModuleKind::ES2015
431        } else {
432            ModuleKind::CommonJS
433        };
434        resolved.printer.module = default_module;
435        resolved.checker.module = default_module;
436    }
437    resolved.checker.module_explicitly_set = module_explicitly_set;
438
439    if let Some(module_resolution) = options.module_resolution.as_deref() {
440        let value = module_resolution.trim();
441        if !value.is_empty() {
442            resolved.module_resolution = Some(parse_module_resolution(value)?);
443        }
444    }
445
446    // When module is not explicitly set, infer it from moduleResolution (matches tsc behavior).
447    // tsc infers module: node16 when moduleResolution: node16, etc.
448    if !module_explicitly_set && let Some(mr) = resolved.module_resolution {
449        let inferred = match mr {
450            ModuleResolutionKind::Node16 => Some(ModuleKind::Node16),
451            ModuleResolutionKind::NodeNext => Some(ModuleKind::NodeNext),
452            _ => None,
453        };
454        if let Some(kind) = inferred {
455            resolved.printer.module = kind;
456            resolved.checker.module = kind;
457        }
458    }
459    let effective_resolution = resolved.effective_module_resolution();
460    resolved.resolve_package_json_exports = options.resolve_package_json_exports.unwrap_or({
461        matches!(
462            effective_resolution,
463            ModuleResolutionKind::Node16
464                | ModuleResolutionKind::NodeNext
465                | ModuleResolutionKind::Bundler
466        )
467    });
468    resolved.resolve_package_json_imports = options.resolve_package_json_imports.unwrap_or({
469        matches!(
470            effective_resolution,
471            ModuleResolutionKind::Node
472                | ModuleResolutionKind::Node16
473                | ModuleResolutionKind::NodeNext
474                | ModuleResolutionKind::Bundler
475        )
476    });
477    if let Some(module_suffixes) = options.module_suffixes.as_ref() {
478        resolved.module_suffixes = module_suffixes.clone();
479    } else {
480        resolved.module_suffixes = vec![String::new()];
481    }
482    if let Some(resolve_json_module) = options.resolve_json_module {
483        resolved.resolve_json_module = resolve_json_module;
484        resolved.checker.resolve_json_module = resolve_json_module;
485    }
486    if let Some(import_helpers) = options.import_helpers {
487        resolved.import_helpers = import_helpers;
488    }
489    if let Some(allow_arbitrary_extensions) = options.allow_arbitrary_extensions {
490        resolved.allow_arbitrary_extensions = allow_arbitrary_extensions;
491    }
492    if let Some(allow_importing_ts_extensions) = options.allow_importing_ts_extensions {
493        resolved.allow_importing_ts_extensions = allow_importing_ts_extensions;
494    }
495    if let Some(rewrite_relative_import_extensions) = options.rewrite_relative_import_extensions {
496        resolved.rewrite_relative_import_extensions = rewrite_relative_import_extensions;
497    }
498
499    if let Some(types_versions_compiler_version) =
500        options.types_versions_compiler_version.as_deref()
501    {
502        let value = types_versions_compiler_version.trim();
503        if !value.is_empty() {
504            resolved.types_versions_compiler_version = Some(value.to_string());
505        }
506    }
507
508    if let Some(types) = options.types.as_ref() {
509        let list: Vec<String> = types
510            .iter()
511            .filter_map(|value| {
512                let trimmed = value.trim();
513                if trimmed.is_empty() {
514                    None
515                } else {
516                    Some(trimmed.to_string())
517                }
518            })
519            .collect();
520        resolved.types = Some(list);
521    }
522
523    if let Some(type_roots) = options.type_roots.as_ref() {
524        let roots: Vec<PathBuf> = type_roots
525            .iter()
526            .filter_map(|value| {
527                let trimmed = value.trim();
528                if trimmed.is_empty() {
529                    None
530                } else {
531                    Some(PathBuf::from(trimmed))
532                }
533            })
534            .collect();
535        resolved.type_roots = Some(roots);
536    }
537
538    if let Some(factory) = options.jsx_factory.as_deref() {
539        resolved.checker.jsx_factory = factory.to_string();
540    } else if let Some(ns) = options.react_namespace.as_deref() {
541        resolved.checker.jsx_factory = format!("{ns}.createElement");
542    }
543    if let Some(frag) = options.jsx_fragment_factory.as_deref() {
544        resolved.checker.jsx_fragment_factory = frag.to_string();
545    }
546
547    if let Some(jsx) = options.jsx.as_deref() {
548        let jsx_emit = parse_jsx_emit(jsx)?;
549        resolved.jsx = Some(jsx_emit);
550        resolved.checker.jsx_mode = jsx_emit_to_mode(jsx_emit);
551    }
552
553    if let Some(no_lib) = options.no_lib {
554        resolved.checker.no_lib = no_lib;
555    }
556
557    if resolved.checker.no_lib && options.lib.is_some() {
558        return Err(anyhow::anyhow!(
559            "Option 'lib' cannot be specified with option 'noLib'."
560        ));
561    }
562
563    if let Some(no_types_and_symbols) = options.no_types_and_symbols {
564        resolved.checker.no_types_and_symbols = no_types_and_symbols;
565    }
566
567    if resolved.checker.no_lib && options.lib.is_some() {
568        bail!("Option 'lib' cannot be specified with option 'noLib'.");
569    }
570
571    if let Some(lib_list) = options.lib.as_ref() {
572        resolved.lib_files = resolve_lib_files(lib_list)?;
573        resolved.lib_is_default = false;
574    } else if !resolved.checker.no_lib && !resolved.checker.no_types_and_symbols {
575        resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
576        resolved.lib_is_default = true;
577    }
578
579    let base_url = options.base_url.as_deref().map(str::trim);
580    if let Some(base_url) = base_url
581        && !base_url.is_empty()
582    {
583        resolved.base_url = Some(PathBuf::from(base_url));
584    }
585
586    if let Some(paths) = options.paths.as_ref()
587        && !paths.is_empty()
588    {
589        resolved.paths = Some(build_path_mappings(paths));
590    }
591
592    if let Some(root_dir) = options.root_dir.as_deref()
593        && !root_dir.is_empty()
594    {
595        resolved.root_dir = Some(PathBuf::from(root_dir));
596    }
597
598    if let Some(out_dir) = options.out_dir.as_deref()
599        && !out_dir.is_empty()
600    {
601        resolved.out_dir = Some(PathBuf::from(out_dir));
602    }
603
604    if let Some(out_file) = options.out_file.as_deref()
605        && !out_file.is_empty()
606    {
607        resolved.out_file = Some(PathBuf::from(out_file));
608    }
609
610    if let Some(declaration_dir) = options.declaration_dir.as_deref()
611        && !declaration_dir.is_empty()
612    {
613        resolved.declaration_dir = Some(PathBuf::from(declaration_dir));
614    }
615
616    if let Some(declaration) = options.declaration {
617        resolved.emit_declarations = declaration;
618    }
619
620    if let Some(source_map) = options.source_map {
621        resolved.source_map = source_map;
622    }
623
624    if let Some(declaration_map) = options.declaration_map {
625        resolved.declaration_map = declaration_map;
626    }
627
628    if let Some(ts_build_info_file) = options.ts_build_info_file.as_deref()
629        && !ts_build_info_file.is_empty()
630    {
631        resolved.ts_build_info_file = Some(PathBuf::from(ts_build_info_file));
632    }
633
634    if let Some(incremental) = options.incremental {
635        resolved.incremental = incremental;
636    }
637
638    if let Some(strict) = options.strict {
639        resolved.checker.strict = strict;
640        if strict {
641            resolved.checker.no_implicit_any = true;
642            resolved.checker.strict_null_checks = true;
643            resolved.checker.strict_function_types = true;
644            resolved.checker.strict_bind_call_apply = true;
645            resolved.checker.strict_property_initialization = true;
646            resolved.checker.no_implicit_this = true;
647            resolved.checker.use_unknown_in_catch_variables = true;
648            resolved.checker.always_strict = true;
649            resolved.printer.always_strict = true;
650        } else {
651            resolved.checker.no_implicit_any = false;
652            resolved.checker.strict_null_checks = false;
653            resolved.checker.strict_function_types = false;
654            resolved.checker.strict_bind_call_apply = false;
655            resolved.checker.strict_property_initialization = false;
656            resolved.checker.no_implicit_this = false;
657            resolved.checker.use_unknown_in_catch_variables = false;
658            resolved.checker.always_strict = false;
659            resolved.printer.always_strict = false;
660        }
661    }
662
663    // When `strict` is not explicitly set, TypeScript defaults all strict-family
664    // options to false. Only an explicit `strict: true` enables them.
665    // This overrides the CheckerOptions::default() values (which use strict=true).
666    if options.strict.is_none() {
667        resolved.checker.strict = false;
668        resolved.checker.no_implicit_any = false;
669        resolved.checker.strict_null_checks = false;
670        resolved.checker.strict_function_types = false;
671        resolved.checker.strict_bind_call_apply = false;
672        resolved.checker.strict_property_initialization = false;
673        resolved.checker.no_implicit_this = false;
674        resolved.checker.use_unknown_in_catch_variables = false;
675        resolved.checker.always_strict = false;
676        resolved.printer.always_strict = false;
677    }
678
679    // Individual strict-family options (override strict if set explicitly)
680    if let Some(v) = options.no_implicit_any {
681        resolved.checker.no_implicit_any = v;
682    }
683    if let Some(v) = options.no_implicit_returns {
684        resolved.checker.no_implicit_returns = v;
685    }
686    if let Some(v) = options.strict_null_checks {
687        resolved.checker.strict_null_checks = v;
688    }
689    if let Some(v) = options.strict_function_types {
690        resolved.checker.strict_function_types = v;
691    }
692    if let Some(v) = options.strict_property_initialization {
693        resolved.checker.strict_property_initialization = v;
694    }
695    if let Some(v) = options.no_unchecked_indexed_access {
696        resolved.checker.no_unchecked_indexed_access = v;
697    }
698    if let Some(v) = options.no_implicit_this {
699        resolved.checker.no_implicit_this = v;
700    }
701    if let Some(v) = options.use_unknown_in_catch_variables {
702        resolved.checker.use_unknown_in_catch_variables = v;
703    }
704    if let Some(v) = options.strict_bind_call_apply {
705        resolved.checker.strict_bind_call_apply = v;
706    }
707    if let Some(v) = options.no_implicit_override {
708        resolved.checker.no_implicit_override = v;
709    }
710    if let Some(v) = options.no_unchecked_side_effect_imports {
711        resolved.checker.no_unchecked_side_effect_imports = v;
712    }
713
714    if let Some(no_emit) = options.no_emit {
715        resolved.no_emit = no_emit;
716    }
717    if let Some(no_resolve) = options.no_resolve {
718        resolved.no_resolve = no_resolve;
719        resolved.checker.no_resolve = no_resolve;
720    }
721
722    if let Some(no_emit_on_error) = options.no_emit_on_error {
723        resolved.no_emit_on_error = no_emit_on_error;
724    }
725
726    if let Some(isolated_modules) = options.isolated_modules {
727        resolved.checker.isolated_modules = isolated_modules;
728    }
729
730    if let Some(always_strict) = options.always_strict {
731        resolved.checker.always_strict = always_strict;
732        resolved.printer.always_strict = always_strict;
733    }
734
735    if let Some(use_define_for_class_fields) = options.use_define_for_class_fields {
736        resolved.printer.use_define_for_class_fields = use_define_for_class_fields;
737    }
738
739    if let Some(no_unused_locals) = options.no_unused_locals {
740        resolved.checker.no_unused_locals = no_unused_locals;
741    }
742
743    if let Some(no_unused_parameters) = options.no_unused_parameters {
744        resolved.checker.no_unused_parameters = no_unused_parameters;
745    }
746
747    if let Some(allow_unreachable_code) = options.allow_unreachable_code {
748        resolved.checker.allow_unreachable_code = Some(allow_unreachable_code);
749    }
750
751    if let Some(ref custom_conditions) = options.custom_conditions {
752        resolved.custom_conditions = custom_conditions.clone();
753    }
754
755    if let Some(es_module_interop) = options.es_module_interop {
756        resolved.es_module_interop = es_module_interop;
757        resolved.checker.es_module_interop = es_module_interop;
758        resolved.printer.es_module_interop = es_module_interop;
759        // esModuleInterop implies allowSyntheticDefaultImports
760        if es_module_interop {
761            resolved.allow_synthetic_default_imports = true;
762            resolved.checker.allow_synthetic_default_imports = true;
763        }
764    }
765
766    if let Some(allow_synthetic_default_imports) = options.allow_synthetic_default_imports {
767        resolved.allow_synthetic_default_imports = allow_synthetic_default_imports;
768        resolved.checker.allow_synthetic_default_imports = allow_synthetic_default_imports;
769    } else if !resolved.allow_synthetic_default_imports {
770        // TSC defaults allowSyntheticDefaultImports to true when:
771        // - esModuleInterop is true (already handled above)
772        // - module is "system"
773        // - moduleResolution is "bundler"
774        // Otherwise defaults to false.
775        let should_default_true = matches!(resolved.checker.module, ModuleKind::System)
776            || matches!(
777                resolved.module_resolution,
778                Some(ModuleResolutionKind::Bundler)
779            );
780        if should_default_true {
781            resolved.allow_synthetic_default_imports = true;
782            resolved.checker.allow_synthetic_default_imports = true;
783        }
784    }
785
786    if let Some(experimental_decorators) = options.experimental_decorators {
787        resolved.checker.experimental_decorators = experimental_decorators;
788        resolved.printer.legacy_decorators = experimental_decorators;
789    }
790
791    if let Some(allow_js) = options.allow_js {
792        resolved.allow_js = allow_js;
793    }
794
795    if let Some(check_js) = options.check_js {
796        resolved.check_js = check_js;
797    }
798    if let Some(skip_lib_check) = options.skip_lib_check {
799        resolved.skip_lib_check = skip_lib_check;
800    }
801
802    if let Some(ref module_detection) = options.module_detection
803        && module_detection.eq_ignore_ascii_case("force")
804    {
805        resolved.printer.module_detection_force = true;
806    }
807
808    Ok(resolved)
809}
810
811pub fn parse_tsconfig(source: &str) -> Result<TsConfig> {
812    let stripped = strip_jsonc(source);
813    let normalized = remove_trailing_commas(&stripped);
814    let config = serde_json::from_str(&normalized).context("failed to parse tsconfig JSON")?;
815    Ok(config)
816}
817
818/// Result of parsing a tsconfig.json with diagnostic collection.
819pub struct ParsedTsConfig {
820    pub config: TsConfig,
821    pub diagnostics: Vec<Diagnostic>,
822    /// Captured from removed option `suppressExcessPropertyErrors` before stripping.
823    /// tsc still honors its effect even after removal (TS5102).
824    pub suppress_excess_property_errors: bool,
825    /// Captured from removed option `suppressImplicitAnyIndexErrors` before stripping.
826    /// tsc still honors its effect even after removal (TS5102).
827    pub suppress_implicit_any_index_errors: bool,
828}
829
830/// Parse tsconfig.json source and collect diagnostics for unknown compiler options.
831///
832/// Unlike `parse_tsconfig`, this function:
833/// 1. Detects unknown/miscased compiler option keys in the JSON
834/// 2. Normalizes them to canonical casing so serde can deserialize them
835/// 3. Returns TS5025 diagnostics for any miscased or unknown options
836pub fn parse_tsconfig_with_diagnostics(source: &str, file_path: &str) -> Result<ParsedTsConfig> {
837    let stripped = strip_jsonc(source);
838    let normalized = remove_trailing_commas(&stripped);
839    let mut raw: serde_json::Value =
840        serde_json::from_str(&normalized).context("failed to parse tsconfig JSON")?;
841
842    let mut diagnostics = Vec::new();
843    let mut suppress_excess = false;
844    let mut suppress_any_index = false;
845
846    // Check compiler options for unknown/miscased keys
847    if let Some(obj) = raw.as_object_mut()
848        && let Some(serde_json::Value::Object(compiler_opts)) = obj.get_mut("compilerOptions")
849    {
850        let keys: Vec<String> = compiler_opts.keys().cloned().collect();
851        let mut renames: Vec<(String, String)> = Vec::new();
852
853        for key in &keys {
854            let key_lower = key.to_lowercase();
855            if let Some(canonical) = known_compiler_option(&key_lower) {
856                if key.as_str() != canonical {
857                    // Miscased option — emit TS5025 and schedule rename
858                    let start = find_key_offset_in_source(&stripped, key);
859                    let msg = format_message(
860                        diagnostic_messages::UNKNOWN_COMPILER_OPTION_DID_YOU_MEAN,
861                        &[key, canonical],
862                    );
863                    diagnostics.push(Diagnostic::error(
864                        file_path,
865                        start,
866                        key.len() as u32 + 2, // include quotes
867                        msg,
868                        diagnostic_codes::UNKNOWN_COMPILER_OPTION_DID_YOU_MEAN,
869                    ));
870                    renames.push((key.clone(), canonical.to_string()));
871                }
872                // else: exact match, no diagnostic needed
873            } else {
874                // Truly unknown option — emit TS5023
875                let start = find_key_offset_in_source(&stripped, key);
876                let msg = format_message(diagnostic_messages::UNKNOWN_COMPILER_OPTION, &[key]);
877                diagnostics.push(Diagnostic::error(
878                    file_path,
879                    start,
880                    key.len() as u32 + 2,
881                    msg,
882                    diagnostic_codes::UNKNOWN_COMPILER_OPTION,
883                ));
884            }
885        }
886
887        // Rename miscased keys to canonical casing so serde can deserialize them
888        for (old_key, new_key) in renames {
889            if let Some(value) = compiler_opts.remove(&old_key) {
890                compiler_opts.insert(new_key, value);
891            }
892        }
893
894        // Check for removed compiler options (TS5102)
895        // These options were deprecated in TS 5.0 and removed in TS 5.5/6.0.
896        // When ignoreDeprecations is set to "5.0", suppress TS5102 (tsc behavior).
897        let ignore_deprecations_valid = matches!(
898            compiler_opts.get("ignoreDeprecations"),
899            Some(serde_json::Value::String(v)) if v == "5.0"
900        );
901        let mut removed_keys: Vec<String> = Vec::new();
902        for key in compiler_opts.keys().cloned().collect::<Vec<_>>() {
903            if removed_compiler_option(&key).is_some() {
904                if !ignore_deprecations_valid {
905                    let value = compiler_opts.get(&key);
906                    // Only emit TS5102 if the option is actually set (non-null, non-default)
907                    let is_set = match value {
908                        Some(serde_json::Value::Bool(b)) => *b,
909                        Some(serde_json::Value::String(s)) => !s.is_empty(),
910                        Some(serde_json::Value::Null) | None => false,
911                        Some(_) => true,
912                    };
913                    if is_set {
914                        let start = find_key_offset_in_source(&stripped, &key);
915                        let msg = format_message(
916                            diagnostic_messages::OPTION_HAS_BEEN_REMOVED_PLEASE_REMOVE_IT_FROM_YOUR_CONFIGURATION,
917                            &[&key],
918                        );
919                        diagnostics.push(Diagnostic::error(
920                            file_path,
921                            start,
922                            key.len() as u32 + 2, // include quotes
923                            msg,
924                            diagnostic_codes::OPTION_HAS_BEEN_REMOVED_PLEASE_REMOVE_IT_FROM_YOUR_CONFIGURATION,
925                        ));
926                    }
927                }
928                removed_keys.push(key);
929            }
930        }
931        // Capture removed-but-still-honored suppress flags before stripping.
932        // tsc still honors these even after removal (TS5102 is emitted but suppression stays).
933        suppress_excess = matches!(
934            compiler_opts.get("suppressExcessPropertyErrors"),
935            Some(serde_json::Value::Bool(true))
936        );
937        suppress_any_index = matches!(
938            compiler_opts.get("suppressImplicitAnyIndexErrors"),
939            Some(serde_json::Value::Bool(true))
940        );
941
942        // Strip removed options so they don't reach serde or subsequent validation
943        for key in &removed_keys {
944            compiler_opts.remove(key);
945        }
946
947        // Check compiler option value types (TS5024)
948        // Collect keys that have type mismatches so we can remove them after iteration.
949        let keys_after_rename: Vec<String> = compiler_opts.keys().cloned().collect();
950        let mut bad_keys: Vec<String> = Vec::new();
951        for key in &keys_after_rename {
952            let expected_type = compiler_option_expected_type(key);
953            if expected_type.is_empty() {
954                continue; // Unknown option or no type constraint
955            }
956            let Some(value) = compiler_opts.get(key) else {
957                continue;
958            };
959            let type_ok = match expected_type {
960                "boolean" => value.is_boolean(),
961                "string" => value.is_string(),
962                "number" => value.is_number(),
963                "list" => value.is_array(),
964                "string or Array" => value.is_string() || value.is_array(),
965                "object" => value.is_object(),
966                _ => true,
967            };
968            if !type_ok {
969                let start = find_value_offset_in_source(&stripped, key);
970                let value_len = estimate_json_value_len(value);
971                let msg = format_message(
972                    diagnostic_messages::COMPILER_OPTION_REQUIRES_A_VALUE_OF_TYPE,
973                    &[key, expected_type],
974                );
975                diagnostics.push(Diagnostic::error(
976                    file_path,
977                    start,
978                    value_len,
979                    msg,
980                    diagnostic_codes::COMPILER_OPTION_REQUIRES_A_VALUE_OF_TYPE,
981                ));
982                bad_keys.push(key.clone());
983            }
984        }
985        // Remove invalid values so serde defaults them to None
986        for key in &bad_keys {
987            compiler_opts.remove(key);
988        }
989
990        // Check ignoreDeprecations value (TS5103)
991        // Only "5.0" is accepted as a valid value (tsc 6.0 does not accept "6.0").
992        if let Some(serde_json::Value::String(id_value)) = compiler_opts.get("ignoreDeprecations")
993            && id_value != "5.0"
994        {
995            let start = find_value_offset_in_source(&stripped, "ignoreDeprecations");
996            let value_len = id_value.len() as u32 + 2; // include quotes
997            diagnostics.push(Diagnostic::error(
998                file_path,
999                start,
1000                value_len,
1001                diagnostic_messages::INVALID_VALUE_FOR_IGNOREDEPRECATIONS.to_string(),
1002                diagnostic_codes::INVALID_VALUE_FOR_IGNOREDEPRECATIONS,
1003            ));
1004        }
1005
1006        // Check moduleResolution/module compatibility (TS5095)
1007        // `moduleResolution: "bundler"` requires `module` to be "preserve" or ES2015+.
1008        if let Some(serde_json::Value::String(mr_value)) = compiler_opts.get("moduleResolution") {
1009            let mr_normalized =
1010                normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
1011            if mr_normalized == "bundler" {
1012                let module_ok = if let Some(serde_json::Value::String(mod_value)) =
1013                    compiler_opts.get("module")
1014                {
1015                    let mod_normalized =
1016                        normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
1017                    matches!(
1018                        mod_normalized.as_str(),
1019                        "preserve"
1020                            | "es2015"
1021                            | "es6"
1022                            | "es2020"
1023                            | "es2022"
1024                            | "esnext"
1025                            | "node16"
1026                            | "node18"
1027                            | "node20"
1028                            | "nodenext"
1029                    )
1030                } else {
1031                    // module not set — default depends on target; ES2015+ targets
1032                    // default to es2015 which is compatible, but lower targets
1033                    // default to commonjs which is NOT compatible.
1034                    if let Some(serde_json::Value::String(target_value)) =
1035                        compiler_opts.get("target")
1036                    {
1037                        let target_normalized = normalize_option(
1038                            target_value
1039                                .split(',')
1040                                .next()
1041                                .unwrap_or(target_value)
1042                                .trim(),
1043                        );
1044                        matches!(
1045                            target_normalized.as_str(),
1046                            "es2015"
1047                                | "es6"
1048                                | "es2016"
1049                                | "es2017"
1050                                | "es2018"
1051                                | "es2019"
1052                                | "es2020"
1053                                | "es2021"
1054                                | "es2022"
1055                                | "es2023"
1056                                | "es2024"
1057                                | "esnext"
1058                        )
1059                    } else {
1060                        // No target set → default is ES3/ES5 → default module is CommonJS → not OK
1061                        false
1062                    }
1063                };
1064                if !module_ok {
1065                    let start = find_value_offset_in_source(&stripped, "moduleResolution");
1066                    let value_len = mr_value.len() as u32 + 2; // include quotes
1067                    // Use the exact message text that tsc 6.0 produces (differs slightly from
1068                    // the diagnosticMessages.json template which includes 'commonjs').
1069                    let msg = "Option 'bundler' can only be used when 'module' is set to 'preserve' or to 'es2015' or later.".to_string();
1070                    diagnostics.push(Diagnostic::error(
1071                        file_path,
1072                        start,
1073                        value_len,
1074                        msg,
1075                        diagnostic_codes::OPTION_CAN_ONLY_BE_USED_WHEN_MODULE_IS_SET_TO_PRESERVE_COMMONJS_OR_ES2015_OR_LAT,
1076                    ));
1077                }
1078            }
1079        }
1080
1081        // Check moduleResolution/module compatibility (TS5110)
1082        // When moduleResolution is node16/nodenext, module must also be node16/nodenext.
1083        if let Some(serde_json::Value::String(mr_value)) = compiler_opts.get("moduleResolution") {
1084            let mr_normalized =
1085                normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
1086            let is_node_mr = matches!(
1087                mr_normalized.as_str(),
1088                "node16" | "node18" | "node20" | "nodenext"
1089            );
1090            if is_node_mr {
1091                let module_ok = if let Some(serde_json::Value::String(mod_value)) =
1092                    compiler_opts.get("module")
1093                {
1094                    let mod_normalized =
1095                        normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
1096                    matches!(
1097                        mod_normalized.as_str(),
1098                        "node16" | "node18" | "node20" | "nodenext"
1099                    )
1100                } else {
1101                    true // module not explicitly set → tsc defaults it based on moduleResolution
1102                };
1103                if !module_ok {
1104                    let start = find_value_offset_in_source(&stripped, "module");
1105                    let value_len = compiler_opts
1106                        .get("module")
1107                        .and_then(|v| v.as_str())
1108                        .map_or(0, |s| s.len() as u32 + 2);
1109                    // tsc uses PascalCase for the option values in the message
1110                    let mr_display = match mr_normalized.as_str() {
1111                        "node16" => "Node16",
1112                        "node18" => "Node18",
1113                        "node20" => "Node20",
1114                        "nodenext" => "NodeNext",
1115                        _ => &mr_normalized,
1116                    };
1117                    let msg = format_message(
1118                        diagnostic_messages::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
1119                        &[mr_display, mr_display],
1120                    );
1121                    diagnostics.push(Diagnostic::error(
1122                        file_path,
1123                        start,
1124                        value_len,
1125                        msg,
1126                        diagnostic_codes::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
1127                    ));
1128                }
1129            }
1130        }
1131
1132        // TS6082: Only 'amd' and 'system' modules are supported alongside --outFile.
1133        // When outFile is set with a non-amd/system module, emit at both the module and outFile keys.
1134        if let Some(serde_json::Value::String(out_file_value)) = compiler_opts.get("outFile")
1135            && !out_file_value.is_empty()
1136            && !option_is_truthy(compiler_opts.get("emitDeclarationOnly"))
1137            && let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module")
1138        {
1139            let mod_normalized =
1140                normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
1141            if !matches!(mod_normalized.as_str(), "amd" | "system") {
1142                let msg = format_message(
1143                    diagnostic_messages::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
1144                    &["outFile"],
1145                );
1146                // Emit at the "module" key (matching tsc behavior)
1147                let start_module = find_key_offset_in_source(&stripped, "module");
1148                let module_key_len = "module".len() as u32 + 2; // include quotes
1149                diagnostics.push(Diagnostic::error(
1150                    file_path,
1151                    start_module,
1152                    module_key_len,
1153                    msg.clone(),
1154                    diagnostic_codes::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
1155                ));
1156                // Emit at the "outFile" key (matching tsc behavior)
1157                let start_outfile = find_key_offset_in_source(&stripped, "outFile");
1158                let outfile_key_len = "outFile".len() as u32 + 2;
1159                diagnostics.push(Diagnostic::error(
1160                    file_path,
1161                    start_outfile,
1162                    outfile_key_len,
1163                    msg,
1164                    diagnostic_codes::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
1165                ));
1166            }
1167        }
1168
1169        // TS5069: Option '{0}' cannot be specified without specifying option '{1}' or option '{2}'.
1170        let requires_decl_or_composite: &[&str] = &[
1171            "emitDeclarationOnly",
1172            "declarationMap",
1173            "isolatedDeclarations",
1174        ];
1175        for &opt in requires_decl_or_composite {
1176            if option_is_truthy(compiler_opts.get(opt))
1177                && !option_is_truthy(compiler_opts.get("declaration"))
1178                && !option_is_truthy(compiler_opts.get("composite"))
1179            {
1180                let start = find_key_offset_in_source(&stripped, opt);
1181                let key_len = opt.len() as u32 + 2; // include quotes
1182                let msg = format_message(
1183                    diagnostic_messages::OPTION_CANNOT_BE_SPECIFIED_WITHOUT_SPECIFYING_OPTION_OR_OPTION,
1184                    &[opt, "declaration", "composite"],
1185                );
1186                diagnostics.push(Diagnostic::error(
1187                    file_path,
1188                    start,
1189                    key_len,
1190                    msg,
1191                    diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITHOUT_SPECIFYING_OPTION_OR_OPTION,
1192                ));
1193            }
1194        }
1195
1196        // TS5053: Option '{0}' cannot be specified with option '{1}'.
1197        // tsc emits for each conflicting key, pointing at the key's position.
1198        // The message always names the pair (A, B) regardless of which key is pointed at.
1199        let conflicting_pairs: &[(&str, &str)] = &[
1200            ("sourceMap", "inlineSourceMap"),
1201            ("mapRoot", "inlineSourceMap"),
1202            ("reactNamespace", "jsxFactory"),
1203            ("allowJs", "isolatedDeclarations"),
1204        ];
1205        for &(opt_a, opt_b) in conflicting_pairs {
1206            if option_is_truthy(compiler_opts.get(opt_a))
1207                && option_is_truthy(compiler_opts.get(opt_b))
1208            {
1209                // Emit at opt_a's position
1210                let start = find_key_offset_in_source(&stripped, opt_a);
1211                let key_len = opt_a.len() as u32 + 2;
1212                let msg = format_message(
1213                    diagnostic_messages::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
1214                    &[opt_a, opt_b],
1215                );
1216                diagnostics.push(Diagnostic::error(
1217                    file_path,
1218                    start,
1219                    key_len,
1220                    msg.clone(),
1221                    diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
1222                ));
1223                // Emit at opt_b's position (same message, different location)
1224                let start_b = find_key_offset_in_source(&stripped, opt_b);
1225                let key_len_b = opt_b.len() as u32 + 2;
1226                diagnostics.push(Diagnostic::error(
1227                    file_path,
1228                    start_b,
1229                    key_len_b,
1230                    msg,
1231                    diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
1232                ));
1233            }
1234        }
1235
1236        // TS5070: Option '--resolveJsonModule' cannot be specified when 'moduleResolution' is set to 'classic'.
1237        // TS5071: Option '--resolveJsonModule' cannot be specified when 'module' is set to 'none', 'system', or 'umd'.
1238        if option_is_truthy(compiler_opts.get("resolveJsonModule")) {
1239            // Compute effective moduleResolution from raw JSON options
1240            let effective_mr = if let Some(serde_json::Value::String(mr_value)) =
1241                compiler_opts.get("moduleResolution")
1242            {
1243                normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim())
1244            } else {
1245                // Default moduleResolution based on module setting
1246                let effective_module = if let Some(serde_json::Value::String(mod_value)) =
1247                    compiler_opts.get("module")
1248                {
1249                    normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim())
1250                } else {
1251                    String::new() // no module set
1252                };
1253                match effective_module.as_str() {
1254                    "none" | "amd" | "umd" | "system" | "" => "classic".to_string(),
1255                    "commonjs" => "node".to_string(),
1256                    "node16" => "node16".to_string(),
1257                    "nodenext" => "nodenext".to_string(),
1258                    _ => "bundler".to_string(),
1259                }
1260            };
1261
1262            if effective_mr == "classic" {
1263                let start = find_key_offset_in_source(&stripped, "resolveJsonModule");
1264                let key_len = "resolveJsonModule".len() as u32 + 2;
1265                diagnostics.push(Diagnostic::error(
1266                    file_path,
1267                    start,
1268                    key_len,
1269                    diagnostic_messages::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULERESOLUTION_IS_SET_TO_CLA.to_string(),
1270                    diagnostic_codes::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULERESOLUTION_IS_SET_TO_CLA,
1271                ));
1272            }
1273
1274            // Check module setting for TS5071 (none/system/umd)
1275            if let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module") {
1276                let mod_normalized =
1277                    normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
1278                if matches!(mod_normalized.as_str(), "none" | "system" | "umd") {
1279                    let start = find_key_offset_in_source(&stripped, "resolveJsonModule");
1280                    let key_len = "resolveJsonModule".len() as u32 + 2;
1281                    diagnostics.push(Diagnostic::error(
1282                        file_path,
1283                        start,
1284                        key_len,
1285                        diagnostic_messages::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULE_IS_SET_TO_NONE_SYSTEM_O.to_string(),
1286                        diagnostic_codes::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULE_IS_SET_TO_NONE_SYSTEM_O,
1287                    ));
1288                }
1289            }
1290        }
1291
1292        // TS5098: Option '{0}' can only be used when 'moduleResolution' is set to 'node16', 'nodenext', or 'bundler'.
1293        let requires_modern_mr: &[&str] = &[
1294            "resolvePackageJsonExports",
1295            "resolvePackageJsonImports",
1296            "customConditions",
1297        ];
1298        let mr_is_modern = if let Some(serde_json::Value::String(mr_value)) =
1299            compiler_opts.get("moduleResolution")
1300        {
1301            let mr_normalized =
1302                normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
1303            matches!(mr_normalized.as_str(), "node16" | "nodenext" | "bundler")
1304        } else {
1305            // When moduleResolution is not set, the default depends on module.
1306            // For modern module settings (es2015+, preserve), default is bundler → OK.
1307            // For classic module settings (none, amd, umd, system, commonjs), default is classic/node → NOT OK.
1308            if let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module") {
1309                let mod_normalized =
1310                    normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
1311                !matches!(
1312                    mod_normalized.as_str(),
1313                    "none" | "amd" | "umd" | "system" | "commonjs"
1314                )
1315            } else {
1316                false // no module set → default is classic-ish
1317            }
1318        };
1319        if !mr_is_modern {
1320            for &opt in requires_modern_mr {
1321                if option_is_truthy(compiler_opts.get(opt)) {
1322                    let start = find_key_offset_in_source(&stripped, opt);
1323                    let key_len = opt.len() as u32 + 2;
1324                    let msg = format_message(
1325                        diagnostic_messages::OPTION_CAN_ONLY_BE_USED_WHEN_MODULERESOLUTION_IS_SET_TO_NODE16_NODENEXT_OR_BUNDL,
1326                        &[opt],
1327                    );
1328                    diagnostics.push(Diagnostic::error(
1329                        file_path,
1330                        start,
1331                        key_len,
1332                        msg,
1333                        diagnostic_codes::OPTION_CAN_ONLY_BE_USED_WHEN_MODULERESOLUTION_IS_SET_TO_NODE16_NODENEXT_OR_BUNDL,
1334                    ));
1335                }
1336            }
1337        }
1338    }
1339
1340    let config: TsConfig = serde_json::from_value(raw).context("failed to parse tsconfig JSON")?;
1341
1342    Ok(ParsedTsConfig {
1343        config,
1344        diagnostics,
1345        suppress_excess_property_errors: suppress_excess,
1346        suppress_implicit_any_index_errors: suppress_any_index,
1347    })
1348}
1349
1350/// Check whether a JSON value represents a truthy compiler option.
1351/// Returns true for `true` booleans, non-empty strings, and non-null values
1352/// that aren't `false`. Returns false for `None`, `null`, and `false`.
1353const fn option_is_truthy(value: Option<&serde_json::Value>) -> bool {
1354    match value {
1355        None | Some(serde_json::Value::Null) => false,
1356        Some(serde_json::Value::Bool(b)) => *b,
1357        // String options (like jsxFactory, reactNamespace) are truthy when present
1358        Some(_) => true,
1359    }
1360}
1361
1362/// Find the byte offset of a JSON key within the source text.
1363/// Searches for `"key"` after `compilerOptions`.
1364fn find_key_offset_in_source(source: &str, key: &str) -> u32 {
1365    let search = format!("\"{key}\"");
1366    // Look for the key after "compilerOptions" to avoid matching in other sections
1367    let compiler_opts_pos = source.find("compilerOptions").unwrap_or(0);
1368    if let Some(pos) = source[compiler_opts_pos..].find(&search) {
1369        // Point at the opening quote of the key, matching tsc behavior
1370        (compiler_opts_pos + pos) as u32
1371    } else {
1372        0
1373    }
1374}
1375
1376/// Find the byte offset of a JSON value within the source text.
1377/// Searches for `"key":` after `compilerOptions`, then finds the value start.
1378fn find_value_offset_in_source(source: &str, key: &str) -> u32 {
1379    let search = format!("\"{key}\"");
1380    let compiler_opts_pos = source.find("compilerOptions").unwrap_or(0);
1381    if let Some(key_pos) = source[compiler_opts_pos..].find(&search) {
1382        let after_key = compiler_opts_pos + key_pos + search.len();
1383        // Skip whitespace and colon to find value start
1384        let rest = &source[after_key..];
1385        if let Some(colon_pos) = rest.find(':') {
1386            let after_colon = after_key + colon_pos + 1;
1387            let value_rest = &source[after_colon..];
1388            // Skip whitespace to find value
1389            let trimmed_offset = value_rest.len() - value_rest.trim_start().len();
1390            return (after_colon + trimmed_offset) as u32;
1391        }
1392    }
1393    0
1394}
1395
1396/// Estimate the display length of a JSON value for diagnostic span.
1397fn estimate_json_value_len(value: &serde_json::Value) -> u32 {
1398    match value {
1399        serde_json::Value::String(s) => s.len() as u32 + 2, // include quotes
1400        serde_json::Value::Bool(b) => {
1401            if *b {
1402                4
1403            } else {
1404                5
1405            }
1406        }
1407        serde_json::Value::Number(n) => n.to_string().len() as u32,
1408        serde_json::Value::Null => 4,
1409        serde_json::Value::Array(_) | serde_json::Value::Object(_) => serde_json::to_string(value)
1410            .map(|s| s.len() as u32)
1411            .unwrap_or(2),
1412    }
1413}
1414
1415/// Return the expected JSON value type for a compiler option.
1416/// Returns "" for unknown/unvalidated options.
1417fn compiler_option_expected_type(key: &str) -> &'static str {
1418    match key {
1419        // Boolean options
1420        "allowArbitraryExtensions"
1421        | "allowImportingTsExtensions"
1422        | "allowJs"
1423        | "allowSyntheticDefaultImports"
1424        | "allowUmdGlobalAccess"
1425        | "allowUnreachableCode"
1426        | "allowUnusedLabels"
1427        | "alwaysStrict"
1428        | "checkJs"
1429        | "composite"
1430        | "declaration"
1431        | "declarationMap"
1432        | "disableReferencedProjectLoad"
1433        | "disableSizeLimit"
1434        | "disableSolutionSearching"
1435        | "disableSourceOfProjectReferenceRedirect"
1436        | "downlevelIteration"
1437        | "emitBOM"
1438        | "emitDeclarationOnly"
1439        | "emitDecoratorMetadata"
1440        | "esModuleInterop"
1441        | "exactOptionalPropertyTypes"
1442        | "experimentalDecorators"
1443        | "forceConsistentCasingInFileNames"
1444        | "importHelpers"
1445        | "incremental"
1446        | "inlineSourceMap"
1447        | "inlineSources"
1448        | "isolatedDeclarations"
1449        | "isolatedModules"
1450        | "keyofStringsOnly"
1451        | "noEmit"
1452        | "noEmitHelpers"
1453        | "noEmitOnError"
1454        | "noErrorTruncation"
1455        | "noFallthroughCasesInSwitch"
1456        | "noImplicitAny"
1457        | "noImplicitOverride"
1458        | "noImplicitReturns"
1459        | "noImplicitThis"
1460        | "noImplicitUseStrict"
1461        | "noLib"
1462        | "noPropertyAccessFromIndexSignature"
1463        | "noResolve"
1464        | "noStrictGenericChecks"
1465        | "noUncheckedIndexedAccess"
1466        | "noUncheckedSideEffectImports"
1467        | "noUnusedLocals"
1468        | "noUnusedParameters"
1469        | "preserveConstEnums"
1470        | "preserveSymlinks"
1471        | "preserveValueImports"
1472        | "pretty"
1473        | "removeComments"
1474        | "resolveJsonModule"
1475        | "resolvePackageJsonExports"
1476        | "resolvePackageJsonImports"
1477        | "rewriteRelativeImportExtensions"
1478        | "skipDefaultLibCheck"
1479        | "skipLibCheck"
1480        | "sourceMap"
1481        | "strict"
1482        | "strictBindCallApply"
1483        | "strictBuiltinIteratorReturn"
1484        | "strictFunctionTypes"
1485        | "strictNullChecks"
1486        | "strictPropertyInitialization"
1487        | "stripInternal"
1488        | "suppressExcessPropertyErrors"
1489        | "suppressImplicitAnyIndexErrors"
1490        | "useDefineForClassFields"
1491        | "useUnknownInCatchVariables"
1492        | "verbatimModuleSyntax" => "boolean",
1493        // String options
1494        "baseUrl" | "charset" | "declarationDir" | "jsx" | "jsxFactory" | "jsxFragmentFactory"
1495        | "jsxImportSource" | "mapRoot" | "module" | "moduleDetection" | "moduleResolution"
1496        | "newLine" | "out" | "outDir" | "outFile" | "reactNamespace" | "rootDir"
1497        | "sourceRoot" | "target" | "tsBuildInfoFile" | "ignoreDeprecations" => "string",
1498        // List options (arrays)
1499        "lib" | "types" | "typeRoots" | "rootDirs" | "moduleSuffixes" | "customConditions" => {
1500            "list"
1501        }
1502        // Object options
1503        "paths" => "object",
1504        _ => "",
1505    }
1506}
1507
1508/// Check if a compiler option has been removed in TypeScript 5.5.
1509/// Returns `Some(use_instead)` if removed, where `use_instead` is "" or a replacement name.
1510/// These options were deprecated in TS 5.0 and removed in TS 5.5.
1511fn removed_compiler_option(key: &str) -> Option<&'static str> {
1512    match key {
1513        "noImplicitUseStrict"
1514        | "keyofStringsOnly"
1515        | "suppressExcessPropertyErrors"
1516        | "suppressImplicitAnyIndexErrors"
1517        | "noStrictGenericChecks"
1518        | "charset" => Some(""),
1519        "importsNotUsedAsValues" | "preserveValueImports" => Some("verbatimModuleSyntax"),
1520        "out" => Some("outFile"),
1521        _ => None,
1522    }
1523}
1524
1525/// Comprehensive map of all known TypeScript compiler options.
1526/// Maps lowercase name → canonical camelCase name.
1527fn known_compiler_option(key_lower: &str) -> Option<&'static str> {
1528    match key_lower {
1529        "allowarbitraryextensions" => Some("allowArbitraryExtensions"),
1530        "allowimportingtsextensions" => Some("allowImportingTsExtensions"),
1531        "allowjs" => Some("allowJs"),
1532        "allowsyntheticdefaultimports" => Some("allowSyntheticDefaultImports"),
1533        "allowumdglobalaccess" => Some("allowUmdGlobalAccess"),
1534        "allowunreachablecode" => Some("allowUnreachableCode"),
1535        "allowunusedlabels" => Some("allowUnusedLabels"),
1536        "alwaysstrict" => Some("alwaysStrict"),
1537        "baseurl" => Some("baseUrl"),
1538        "charset" => Some("charset"),
1539        "checkjs" => Some("checkJs"),
1540        "composite" => Some("composite"),
1541        "customconditions" => Some("customConditions"),
1542        "declaration" => Some("declaration"),
1543        "declarationdir" => Some("declarationDir"),
1544        "declarationmap" => Some("declarationMap"),
1545        "diagnostics" => Some("diagnostics"),
1546        "disablereferencedprojectload" => Some("disableReferencedProjectLoad"),
1547        "disablesizelimt" => Some("disableSizeLimit"),
1548        "disablesolutiontypecheck" => Some("disableSolutionTypeCheck"),
1549        "disablesolutioncaching" => Some("disableSolutionCaching"),
1550        "disablesolutiontypechecking" => Some("disableSolutionTypeChecking"),
1551        "disablesourceofreferencedprojectload" => Some("disableSourceOfReferencedProjectLoad"),
1552        "downleveliteration" => Some("downlevelIteration"),
1553        "emitbom" => Some("emitBOM"),
1554        "emitdeclarationonly" => Some("emitDeclarationOnly"),
1555        "emitdecoratormetadata" => Some("emitDecoratorMetadata"),
1556        "erasablesyntaxonly" => Some("erasableSyntaxOnly"),
1557        "esmoduleinterop" => Some("esModuleInterop"),
1558        "exactoptionalpropertytypes" => Some("exactOptionalPropertyTypes"),
1559        "experimentaldecorators" => Some("experimentalDecorators"),
1560        "extendeddiagnostics" => Some("extendedDiagnostics"),
1561        "forceconsecinferfaces" | "forceconsistentcasinginfilenames" => {
1562            Some("forceConsistentCasingInFileNames")
1563        }
1564        "generatecputrace" | "generatecpuprofile" => Some("generateCpuProfile"),
1565        "generatetrace" => Some("generateTrace"),
1566        "ignoredeprecations" => Some("ignoreDeprecations"),
1567        "importhelpers" => Some("importHelpers"),
1568        "importsnotusedasvalues" => Some("importsNotUsedAsValues"),
1569        "incremental" => Some("incremental"),
1570        "inlineconstants" => Some("inlineConstants"),
1571        "inlinesourcemap" => Some("inlineSourceMap"),
1572        "inlinesources" => Some("inlineSources"),
1573        "isolateddeclarations" => Some("isolatedDeclarations"),
1574        "isolatedmodules" => Some("isolatedModules"),
1575        "jsx" => Some("jsx"),
1576        "jsxfactory" => Some("jsxFactory"),
1577        "jsxfragmentfactory" => Some("jsxFragmentFactory"),
1578        "jsximportsource" => Some("jsxImportSource"),
1579        "keyofstringsonly" => Some("keyofStringsOnly"),
1580        "lib" => Some("lib"),
1581        "libreplacement" => Some("libReplacement"),
1582        "listemittedfiles" => Some("listEmittedFiles"),
1583        "listfiles" => Some("listFiles"),
1584        "listfilesonly" => Some("listFilesOnly"),
1585        "locale" => Some("locale"),
1586        "maproot" => Some("mapRoot"),
1587        "maxnodemodulejsdepth" => Some("maxNodeModuleJsDepth"),
1588        "module" => Some("module"),
1589        "moduledetection" => Some("moduleDetection"),
1590        "moduleresolution" => Some("moduleResolution"),
1591        "modulesuffixes" => Some("moduleSuffixes"),
1592        "newline" => Some("newLine"),
1593        "nocheck" => Some("noCheck"),
1594        "noemit" => Some("noEmit"),
1595        "noemithelpers" => Some("noEmitHelpers"),
1596        "noemitonerror" => Some("noEmitOnError"),
1597        "noerrortruncation" => Some("noErrorTruncation"),
1598        "nofallthroughcasesinswitch" => Some("noFallthroughCasesInSwitch"),
1599        "noimplicitany" => Some("noImplicitAny"),
1600        "noimplicitoverride" => Some("noImplicitOverride"),
1601        "noimplicitreturns" => Some("noImplicitReturns"),
1602        "noimplicitthis" => Some("noImplicitThis"),
1603        "noimplicitusestrict" => Some("noImplicitUseStrict"),
1604        "nolib" => Some("noLib"),
1605        "nopropertyaccessfromindexsignature" => Some("noPropertyAccessFromIndexSignature"),
1606        "noresolve" => Some("noResolve"),
1607        "nostrictgenericchecks" => Some("noStrictGenericChecks"),
1608        "notypesandsymbols" => Some("noTypesAndSymbols"),
1609        "nouncheckedindexedaccess" => Some("noUncheckedIndexedAccess"),
1610        "nouncheckedsideeffectimports" => Some("noUncheckedSideEffectImports"),
1611        "nounusedlocals" => Some("noUnusedLocals"),
1612        "nounusedparameters" => Some("noUnusedParameters"),
1613        "out" => Some("out"),
1614        "outdir" => Some("outDir"),
1615        "outfile" => Some("outFile"),
1616        "paths" => Some("paths"),
1617        "plugins" => Some("plugins"),
1618        "preserveconstenums" => Some("preserveConstEnums"),
1619        "preservesymlinks" => Some("preserveSymlinks"),
1620        "preservevalueimports" => Some("preserveValueImports"),
1621        "preservewatchoutput" => Some("preserveWatchOutput"),
1622        "pretty" => Some("pretty"),
1623        "reactnamespace" => Some("reactNamespace"),
1624        "removecomments" => Some("removeComments"),
1625        "resolvejsonmodule" => Some("resolveJsonModule"),
1626        "resolvepackagejsonexports" => Some("resolvePackageJsonExports"),
1627        "resolvepackagejsonimports" => Some("resolvePackageJsonImports"),
1628        "rewriterelativeimportextensions" => Some("rewriteRelativeImportExtensions"),
1629        "rootdir" => Some("rootDir"),
1630        "rootdirs" => Some("rootDirs"),
1631        "skipdefaultlibcheck" => Some("skipDefaultLibCheck"),
1632        "skiplibcheck" => Some("skipLibCheck"),
1633        "sourcemap" => Some("sourceMap"),
1634        "sourceroot" => Some("sourceRoot"),
1635        "strict" => Some("strict"),
1636        "strictbindcallapply" => Some("strictBindCallApply"),
1637        "strictbuiltiniteratorreturn" => Some("strictBuiltinIteratorReturn"),
1638        "strictfunctiontypes" => Some("strictFunctionTypes"),
1639        "strictnullchecks" => Some("strictNullChecks"),
1640        "strictpropertyinitialization" => Some("strictPropertyInitialization"),
1641        "stripinternal" => Some("stripInternal"),
1642        "suppressexcesspropertyerrors" => Some("suppressExcessPropertyErrors"),
1643        "suppressimplicitanyindexerrors" => Some("suppressImplicitAnyIndexErrors"),
1644        "target" => Some("target"),
1645        "traceresolution" => Some("traceResolution"),
1646        "tsbuildinfofile" => Some("tsBuildInfoFile"),
1647        "typeroots" => Some("typeRoots"),
1648        "types" => Some("types"),
1649        "usedefineforclassfields" => Some("useDefineForClassFields"),
1650        "useunknownincatchvariables" => Some("useUnknownInCatchVariables"),
1651        "verbatimmodulesyntax" => Some("verbatimModuleSyntax"),
1652        _ => None,
1653    }
1654}
1655
1656pub fn load_tsconfig(path: &Path) -> Result<TsConfig> {
1657    let mut visited = FxHashSet::default();
1658    load_tsconfig_inner(path, &mut visited)
1659}
1660
1661/// Load tsconfig.json and collect config-level diagnostics.
1662pub fn load_tsconfig_with_diagnostics(path: &Path) -> Result<ParsedTsConfig> {
1663    let mut visited = FxHashSet::default();
1664    load_tsconfig_inner_with_diagnostics(path, &mut visited)
1665}
1666
1667fn load_tsconfig_inner(path: &Path, visited: &mut FxHashSet<PathBuf>) -> Result<TsConfig> {
1668    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1669    if !visited.insert(canonical.clone()) {
1670        bail!("tsconfig extends cycle detected at {}", canonical.display());
1671    }
1672
1673    let source = std::fs::read_to_string(path)
1674        .with_context(|| format!("failed to read tsconfig: {}", path.display()))?;
1675    let mut config = parse_tsconfig(&source)
1676        .with_context(|| format!("failed to parse tsconfig: {}", path.display()))?;
1677
1678    let extends = config.extends.take();
1679    if let Some(extends_path) = extends {
1680        let base_path = resolve_extends_path(path, &extends_path)?;
1681        let base_config = load_tsconfig_inner(&base_path, visited)?;
1682        config = merge_configs(base_config, config);
1683    }
1684
1685    visited.remove(&canonical);
1686    Ok(config)
1687}
1688
1689fn load_tsconfig_inner_with_diagnostics(
1690    path: &Path,
1691    visited: &mut FxHashSet<PathBuf>,
1692) -> Result<ParsedTsConfig> {
1693    let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
1694    if !visited.insert(canonical.clone()) {
1695        bail!("tsconfig extends cycle detected at {}", canonical.display());
1696    }
1697
1698    let source = std::fs::read_to_string(path)
1699        .with_context(|| format!("failed to read tsconfig: {}", path.display()))?;
1700    let file_display = path.display().to_string();
1701    let mut parsed = parse_tsconfig_with_diagnostics(&source, &file_display)
1702        .with_context(|| format!("failed to parse tsconfig: {}", path.display()))?;
1703
1704    let extends = parsed.config.extends.take();
1705    if let Some(extends_path) = extends {
1706        let base_path = resolve_extends_path(path, &extends_path)?;
1707        let base_config = load_tsconfig_inner(&base_path, visited)?;
1708        parsed.config = merge_configs(base_config, parsed.config);
1709    }
1710
1711    visited.remove(&canonical);
1712    Ok(parsed)
1713}
1714
1715fn resolve_extends_path(current_path: &Path, extends: &str) -> Result<PathBuf> {
1716    let base_dir = current_path
1717        .parent()
1718        .ok_or_else(|| anyhow!("tsconfig has no parent directory"))?;
1719    let mut candidate = PathBuf::from(extends);
1720    if candidate.extension().is_none() {
1721        candidate.set_extension("json");
1722    }
1723
1724    if candidate.is_absolute() {
1725        Ok(candidate)
1726    } else {
1727        Ok(base_dir.join(candidate))
1728    }
1729}
1730
1731fn merge_configs(base: TsConfig, mut child: TsConfig) -> TsConfig {
1732    let merged_compiler_options = match (base.compiler_options, child.compiler_options.take()) {
1733        (Some(base_opts), Some(child_opts)) => Some(merge_compiler_options(base_opts, child_opts)),
1734        (Some(base_opts), None) => Some(base_opts),
1735        (None, Some(child_opts)) => Some(child_opts),
1736        (None, None) => None,
1737    };
1738
1739    TsConfig {
1740        extends: None,
1741        compiler_options: merged_compiler_options,
1742        include: child.include.or(base.include),
1743        exclude: child.exclude.or(base.exclude),
1744        files: child.files.or(base.files),
1745    }
1746}
1747
1748/// Merge two `CompilerOptions` structs, preferring child values over base.
1749/// Every `Option` field in `CompilerOptions` uses `.or()` — child wins when present.
1750macro_rules! merge_options {
1751    ($child:expr, $base:expr, $Struct:ident { $($field:ident),* $(,)? }) => {
1752        $Struct { $( $field: $child.$field.or($base.$field), )* }
1753    };
1754}
1755
1756fn merge_compiler_options(base: CompilerOptions, child: CompilerOptions) -> CompilerOptions {
1757    merge_options!(
1758        child,
1759        base,
1760        CompilerOptions {
1761            target,
1762            module,
1763            module_resolution,
1764            resolve_package_json_exports,
1765            resolve_package_json_imports,
1766            module_suffixes,
1767            resolve_json_module,
1768            allow_arbitrary_extensions,
1769            allow_importing_ts_extensions,
1770            rewrite_relative_import_extensions,
1771            types_versions_compiler_version,
1772            types,
1773            type_roots,
1774            jsx,
1775            jsx_factory,
1776            jsx_fragment_factory,
1777            react_namespace,
1778
1779            lib,
1780            no_lib,
1781            no_types_and_symbols,
1782            base_url,
1783            paths,
1784            root_dir,
1785            out_dir,
1786            out_file,
1787            declaration,
1788            declaration_dir,
1789            source_map,
1790            declaration_map,
1791            ts_build_info_file,
1792            incremental,
1793            strict,
1794            no_emit,
1795            no_emit_on_error,
1796            isolated_modules,
1797            custom_conditions,
1798            es_module_interop,
1799            allow_synthetic_default_imports,
1800            experimental_decorators,
1801            import_helpers,
1802            allow_js,
1803            check_js,
1804            skip_lib_check,
1805            always_strict,
1806            use_define_for_class_fields,
1807            no_implicit_any,
1808            no_implicit_returns,
1809            strict_null_checks,
1810            strict_function_types,
1811            strict_property_initialization,
1812            no_implicit_this,
1813            use_unknown_in_catch_variables,
1814            strict_bind_call_apply,
1815            no_unchecked_indexed_access,
1816            no_unused_locals,
1817            no_unused_parameters,
1818            allow_unreachable_code,
1819            no_resolve,
1820            no_unchecked_side_effect_imports,
1821            no_implicit_override,
1822            module_detection,
1823        }
1824    )
1825}
1826
1827fn parse_script_target(value: &str) -> Result<ScriptTarget> {
1828    // Strip trailing comma — multi-target test directives like `esnext, es2022`
1829    // can pass `esnext,` through the tsconfig pipeline.
1830    let cleaned = value.trim_end_matches(',');
1831    let normalized = normalize_option(cleaned);
1832    let target = match normalized.as_str() {
1833        "es3" => ScriptTarget::ES3,
1834        "es5" => ScriptTarget::ES5,
1835        "es6" | "es2015" => ScriptTarget::ES2015,
1836        "es2016" => ScriptTarget::ES2016,
1837        "es2017" => ScriptTarget::ES2017,
1838        "es2018" => ScriptTarget::ES2018,
1839        "es2019" => ScriptTarget::ES2019,
1840        "es2020" => ScriptTarget::ES2020,
1841        "es2021" => ScriptTarget::ES2021,
1842        "es2022" | "es2023" | "es2024" => ScriptTarget::ES2022,
1843        "esnext" => ScriptTarget::ESNext,
1844        _ => bail!("unsupported compilerOptions.target '{value}'"),
1845    };
1846
1847    Ok(target)
1848}
1849
1850fn parse_module_kind(value: &str) -> Result<ModuleKind> {
1851    let cleaned = value.split(',').next().unwrap_or(value).trim();
1852    let normalized = normalize_option(cleaned);
1853    let module = match normalized.as_str() {
1854        "none" => ModuleKind::None,
1855        "commonjs" => ModuleKind::CommonJS,
1856        "amd" => ModuleKind::AMD,
1857        "umd" => ModuleKind::UMD,
1858        "system" => ModuleKind::System,
1859        "es6" | "es2015" => ModuleKind::ES2015,
1860        "es2020" => ModuleKind::ES2020,
1861        "es2022" => ModuleKind::ES2022,
1862        "esnext" => ModuleKind::ESNext,
1863        "node16" | "node18" | "node20" => ModuleKind::Node16,
1864        "nodenext" => ModuleKind::NodeNext,
1865        "preserve" => ModuleKind::Preserve,
1866        _ => bail!("unsupported compilerOptions.module '{value}'"),
1867    };
1868
1869    Ok(module)
1870}
1871
1872fn parse_module_resolution(value: &str) -> Result<ModuleResolutionKind> {
1873    let cleaned = value.split(',').next().unwrap_or(value).trim();
1874    let normalized = normalize_option(cleaned);
1875    let resolution = match normalized.as_str() {
1876        "classic" => ModuleResolutionKind::Classic,
1877        "node" | "node10" => ModuleResolutionKind::Node,
1878        "node16" => ModuleResolutionKind::Node16,
1879        "nodenext" => ModuleResolutionKind::NodeNext,
1880        "bundler" => ModuleResolutionKind::Bundler,
1881        _ => bail!("unsupported compilerOptions.moduleResolution '{value}'"),
1882    };
1883
1884    Ok(resolution)
1885}
1886
1887fn parse_jsx_emit(value: &str) -> Result<JsxEmit> {
1888    let normalized = normalize_option(value);
1889    let jsx = match normalized.as_str() {
1890        "preserve" => JsxEmit::Preserve,
1891        "react" => JsxEmit::React,
1892        "react-jsx" | "reactjsx" => JsxEmit::ReactJsx,
1893        "react-jsxdev" | "reactjsxdev" => JsxEmit::ReactJsxDev,
1894        "reactnative" | "react-native" => JsxEmit::ReactNative,
1895        _ => bail!("unsupported compilerOptions.jsx '{value}'"),
1896    };
1897
1898    Ok(jsx)
1899}
1900
1901const fn jsx_emit_to_mode(emit: JsxEmit) -> tsz_common::checker_options::JsxMode {
1902    use tsz_common::checker_options::JsxMode;
1903    match emit {
1904        JsxEmit::Preserve => JsxMode::Preserve,
1905        JsxEmit::React => JsxMode::React,
1906        JsxEmit::ReactJsx => JsxMode::ReactJsx,
1907        JsxEmit::ReactJsxDev => JsxMode::ReactJsxDev,
1908        JsxEmit::ReactNative => JsxMode::ReactNative,
1909    }
1910}
1911
1912fn build_path_mappings(paths: &FxHashMap<String, Vec<String>>) -> Vec<PathMapping> {
1913    let mut mappings = Vec::new();
1914    for (pattern, targets) in paths {
1915        if targets.is_empty() {
1916            continue;
1917        }
1918        let pattern = normalize_path_pattern(pattern);
1919        let targets = targets
1920            .iter()
1921            .map(|target| normalize_path_pattern(target))
1922            .collect();
1923        let (prefix, suffix) = split_path_pattern(&pattern);
1924        mappings.push(PathMapping {
1925            pattern,
1926            prefix,
1927            suffix,
1928            targets,
1929        });
1930    }
1931    mappings.sort_by(|left, right| {
1932        right
1933            .specificity()
1934            .cmp(&left.specificity())
1935            .then_with(|| right.pattern.len().cmp(&left.pattern.len()))
1936            .then_with(|| left.pattern.cmp(&right.pattern))
1937    });
1938    mappings
1939}
1940
1941fn normalize_path_pattern(value: &str) -> String {
1942    value.trim().replace('\\', "/")
1943}
1944
1945fn split_path_pattern(pattern: &str) -> (String, String) {
1946    match pattern.find('*') {
1947        Some(star_idx) => {
1948            let (prefix, rest) = pattern.split_at(star_idx);
1949            (prefix.to_string(), rest[1..].to_string())
1950        }
1951        None => (pattern.to_string(), String::new()),
1952    }
1953}
1954
1955/// Resolve lib files from names, optionally following `/// <reference lib="..." />` directives.
1956///
1957/// When `follow_references` is true, each lib file is scanned for reference directives
1958/// and those referenced libs are also loaded. When false, only the explicitly listed
1959/// libs are loaded without following their internal references.
1960///
1961/// TypeScript always follows `/// <reference lib="..." />` directives when loading libs.
1962/// For example, `lib.dom.d.ts` references `es2015` and `es2018.asynciterable`, so even
1963/// `--target es5` (which loads lib.d.ts -> dom) transitively loads ES2015 features.
1964/// Verified with `tsc 6.0.0-dev --target es5 --listFiles`.
1965pub fn resolve_lib_files_with_options(
1966    lib_list: &[String],
1967    follow_references: bool,
1968) -> Result<Vec<PathBuf>> {
1969    if lib_list.is_empty() {
1970        return Ok(Vec::new());
1971    }
1972
1973    let lib_dir = default_lib_dir()?;
1974    resolve_lib_files_from_dir_with_options(lib_list, follow_references, &lib_dir)
1975}
1976
1977pub fn resolve_lib_files_from_dir_with_options(
1978    lib_list: &[String],
1979    follow_references: bool,
1980    lib_dir: &Path,
1981) -> Result<Vec<PathBuf>> {
1982    if lib_list.is_empty() {
1983        return Ok(Vec::new());
1984    }
1985
1986    let lib_map = build_lib_map(lib_dir)?;
1987    let mut resolved = Vec::new();
1988    let mut pending: VecDeque<String> = lib_list
1989        .iter()
1990        .map(|value| normalize_lib_name(value))
1991        .collect();
1992    let mut visited = FxHashSet::default();
1993
1994    while let Some(lib_name) = pending.pop_front() {
1995        if lib_name.is_empty() || !visited.insert(lib_name.clone()) {
1996            continue;
1997        }
1998
1999        let path = match lib_map.get(&lib_name) {
2000            Some(path) => path.clone(),
2001            None => {
2002                // Handle tsc compatibility aliases:
2003                // - "lib" refers to lib.d.ts which is equivalent to es5.full.d.ts
2004                // - "es6" refers to lib.es6.d.ts which is equivalent to es2015.full.d.ts
2005                // - "es7" refers to lib.es2016.d.ts which is equivalent to es2016.d.ts
2006                let alias = match lib_name.as_str() {
2007                    "lib" => Some("es5.full"),
2008                    "es6" => Some("es2015.full"),
2009                    "es7" => Some("es2016"),
2010                    _ => None,
2011                };
2012                let Some(alias) = alias else {
2013                    return Err(anyhow!(
2014                        "unsupported compilerOptions.lib '{}' (not found in {})",
2015                        lib_name,
2016                        lib_dir.display()
2017                    ));
2018                };
2019                lib_map.get(alias).cloned().ok_or_else(|| {
2020                    anyhow!(
2021                        "unsupported compilerOptions.lib '{}' (alias '{}' not found in {})",
2022                        lib_name,
2023                        alias,
2024                        lib_dir.display()
2025                    )
2026                })?
2027            }
2028        };
2029        resolved.push(path.clone());
2030
2031        // Only follow /// <reference lib="..." /> directives if requested
2032        if follow_references {
2033            let contents = std::fs::read_to_string(&path)
2034                .with_context(|| format!("failed to read lib file {}", path.display()))?;
2035            for reference in extract_lib_references(&contents) {
2036                pending.push_back(reference);
2037            }
2038        }
2039    }
2040
2041    Ok(resolved)
2042}
2043
2044/// Resolve lib files from names, following `/// <reference lib="..." />` directives.
2045/// This is used when explicitly specifying libs via `--lib`.
2046pub fn resolve_lib_files(lib_list: &[String]) -> Result<Vec<PathBuf>> {
2047    resolve_lib_files_with_options(lib_list, true)
2048}
2049
2050pub fn resolve_lib_files_from_dir(lib_list: &[String], lib_dir: &Path) -> Result<Vec<PathBuf>> {
2051    resolve_lib_files_from_dir_with_options(lib_list, true, lib_dir)
2052}
2053
2054/// Resolve default lib files for a given target.
2055///
2056/// Matches tsc's behavior exactly:
2057/// 1. Get the root lib file for the target (e.g., "lib" for ES5, "es6" for ES2015)
2058/// 2. Follow ALL `/// <reference lib="..." />` directives recursively
2059///
2060/// This means `--target es5` loads lib.d.ts -> dom -> es2015 (transitively),
2061/// which is exactly what tsc does (verified with `tsc --target es5 --listFiles`).
2062pub fn resolve_default_lib_files(target: ScriptTarget) -> Result<Vec<PathBuf>> {
2063    let lib_dir = default_lib_dir()?;
2064    resolve_default_lib_files_from_dir(target, &lib_dir)
2065}
2066
2067pub fn resolve_default_lib_files_from_dir(
2068    target: ScriptTarget,
2069    lib_dir: &Path,
2070) -> Result<Vec<PathBuf>> {
2071    let root_lib = default_lib_name_for_target(target);
2072    resolve_lib_files_from_dir(&[root_lib.to_string()], lib_dir)
2073}
2074
2075/// Get the default lib name for a target.
2076///
2077/// This matches tsc's default behavior exactly:
2078/// - Each target loads the corresponding `.full` lib which includes:
2079///   - The ES version libs (e.g., es5, es2015.promise, etc.)
2080///   - DOM types (document, window, console, fetch, etc.)
2081///   - `ScriptHost` types
2082///
2083/// The mapping matches TypeScript's `getDefaultLibFileName()` in utilitiesPublic.ts:
2084/// - ES3/ES5 → lib.d.ts (equivalent to es5.full.d.ts in source tree)
2085/// - ES2015  → lib.es6.d.ts (equivalent to es2015.full.d.ts in source tree)
2086/// - ES2016+ → lib.es20XX.full.d.ts
2087/// - `ESNext`  → lib.esnext.full.d.ts
2088///
2089/// Note: The source tree uses `es5.full.d.ts` naming, while built TypeScript uses `lib.d.ts`.
2090/// We use the source tree naming since that's what exists in TypeScript/src/lib.
2091pub const fn default_lib_name_for_target(target: ScriptTarget) -> &'static str {
2092    match target {
2093        // ES3/ES5 -> lib.d.ts (ES5 + DOM + ScriptHost)
2094        ScriptTarget::ES3 | ScriptTarget::ES5 => "lib",
2095        // ES2015 -> lib.es6.d.ts (ES2015 + DOM + DOM.Iterable + ScriptHost)
2096        // Note: NOT "es2015.full" (doesn't exist), use "es6" per tsc convention
2097        ScriptTarget::ES2015 => "es6",
2098        // ES2016+ use .full variants (ES + DOM + ScriptHost + others)
2099        ScriptTarget::ES2016 => "es2016.full",
2100        ScriptTarget::ES2017 => "es2017.full",
2101        ScriptTarget::ES2018 => "es2018.full",
2102        ScriptTarget::ES2019 => "es2019.full",
2103        ScriptTarget::ES2020 => "es2020.full",
2104        ScriptTarget::ES2021 => "es2021.full",
2105        ScriptTarget::ES2022 => "es2022.full",
2106        ScriptTarget::ES2023
2107        | ScriptTarget::ES2024
2108        | ScriptTarget::ES2025
2109        | ScriptTarget::ESNext => "esnext.full",
2110    }
2111}
2112
2113/// Get the core lib name for a target (without DOM/ScriptHost).
2114///
2115/// This is useful for conformance testing where:
2116/// 1. Tests don't need DOM types
2117/// 2. Core libs are smaller and faster to load
2118/// 3. Tests that need DOM should specify @lib: dom explicitly
2119pub const fn core_lib_name_for_target(target: ScriptTarget) -> &'static str {
2120    match target {
2121        ScriptTarget::ES3 | ScriptTarget::ES5 => "es5",
2122        ScriptTarget::ES2015 => "es2015",
2123        ScriptTarget::ES2016 => "es2016",
2124        ScriptTarget::ES2017 => "es2017",
2125        ScriptTarget::ES2018 => "es2018",
2126        ScriptTarget::ES2019 => "es2019",
2127        ScriptTarget::ES2020 => "es2020",
2128        ScriptTarget::ES2021 => "es2021",
2129        ScriptTarget::ES2022 => "es2022",
2130        ScriptTarget::ES2023
2131        | ScriptTarget::ES2024
2132        | ScriptTarget::ES2025
2133        | ScriptTarget::ESNext => "esnext",
2134    }
2135}
2136
2137/// Get the default lib directory.
2138///
2139/// Searches in order:
2140/// 1. `TSZ_LIB_DIR` environment variable
2141/// 2. Relative to the executable
2142/// 3. Relative to current working directory
2143/// 4. TypeScript/src/lib in the source tree
2144pub fn default_lib_dir() -> Result<PathBuf> {
2145    if let Some(dir) = env::var_os("TSZ_LIB_DIR") {
2146        let dir = PathBuf::from(dir);
2147        if !dir.is_dir() {
2148            bail!(
2149                "TSZ_LIB_DIR does not point to a directory: {}",
2150                dir.display()
2151            );
2152        }
2153        return Ok(canonicalize_or_owned(&dir));
2154    }
2155
2156    if let Some(dir) = lib_dir_from_exe() {
2157        return Ok(dir);
2158    }
2159
2160    if let Some(dir) = lib_dir_from_cwd() {
2161        return Ok(dir);
2162    }
2163
2164    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
2165    if let Some(dir) = lib_dir_from_root(manifest_dir) {
2166        return Ok(dir);
2167    }
2168
2169    bail!("lib directory not found under {}", manifest_dir.display());
2170}
2171
2172fn lib_dir_from_exe() -> Option<PathBuf> {
2173    let exe = env::current_exe().ok()?;
2174    let exe_dir = exe.parent()?;
2175    let candidate = exe_dir.join("lib");
2176    if candidate.is_dir() {
2177        return Some(canonicalize_or_owned(&candidate));
2178    }
2179    lib_dir_from_root(exe_dir)
2180}
2181
2182fn lib_dir_from_cwd() -> Option<PathBuf> {
2183    let cwd = env::current_dir().ok()?;
2184    lib_dir_from_root(&cwd)
2185}
2186
2187fn lib_dir_from_root(root: &Path) -> Option<PathBuf> {
2188    let candidates = [
2189        // Built/compiled libs from tsc build output (highest priority)
2190        root.join("TypeScript").join("built").join("local"),
2191        root.join("TypeScript").join("lib"),
2192        // npm-installed TypeScript libs (self-contained, matching tsc's shipped format).
2193        // Prefer these over TypeScript/src/lib which has source-format files with
2194        // cross-module /// <reference lib> directives that pull in ES2015+ content
2195        // even for ES5 targets (e.g., dom.generated.d.ts references es2015.symbol.d.ts).
2196        root.join("node_modules").join("typescript").join("lib"),
2197        root.join("scripts")
2198            .join("node_modules")
2199            .join("typescript")
2200            .join("lib"),
2201        root.join("scripts")
2202            .join("emit")
2203            .join("node_modules")
2204            .join("typescript")
2205            .join("lib"),
2206        root.join("TypeScript").join("src").join("lib"),
2207        root.join("TypeScript")
2208            .join("node_modules")
2209            .join("typescript")
2210            .join("lib"),
2211        root.join("tests").join("lib"),
2212    ];
2213
2214    for candidate in candidates {
2215        if candidate.is_dir() {
2216            return Some(canonicalize_or_owned(&candidate));
2217        }
2218    }
2219
2220    None
2221}
2222
2223fn build_lib_map(lib_dir: &Path) -> Result<FxHashMap<String, PathBuf>> {
2224    let mut map = FxHashMap::default();
2225    for entry in std::fs::read_dir(lib_dir)
2226        .with_context(|| format!("failed to read lib directory {}", lib_dir.display()))?
2227    {
2228        let entry = entry?;
2229        let path = entry.path();
2230        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
2231            continue;
2232        };
2233        if !file_name.ends_with(".d.ts") {
2234            continue;
2235        }
2236
2237        let stem = file_name.trim_end_matches(".d.ts");
2238        let stem = stem.strip_suffix(".generated").unwrap_or(stem);
2239        let key = normalize_lib_name(stem);
2240        map.insert(key, canonicalize_or_owned(&path));
2241    }
2242
2243    Ok(map)
2244}
2245
2246/// Extract /// <reference lib="..." /> directives from a lib file source.
2247/// Returns a list of referenced lib names.
2248pub(crate) fn extract_lib_references(source: &str) -> Vec<String> {
2249    let mut refs = Vec::new();
2250    let mut in_block_comment = false;
2251    for line in source.lines() {
2252        let line = line.trim_start();
2253        if in_block_comment {
2254            if line.contains("*/") {
2255                in_block_comment = false;
2256            }
2257            continue;
2258        }
2259        if line.starts_with("/*") {
2260            if !line.contains("*/") {
2261                in_block_comment = true;
2262            }
2263            continue;
2264        }
2265        if line.is_empty() {
2266            continue;
2267        }
2268        if line.starts_with("///") {
2269            if let Some(value) = parse_reference_lib_value(line) {
2270                refs.push(normalize_lib_name(value));
2271            }
2272            continue;
2273        }
2274        if line.starts_with("//") {
2275            continue;
2276        }
2277        break;
2278    }
2279    refs
2280}
2281
2282fn parse_reference_lib_value(line: &str) -> Option<&str> {
2283    let mut offset = 0;
2284    let bytes = line.as_bytes();
2285    while let Some(idx) = line[offset..].find("lib=") {
2286        let start = offset + idx;
2287        if start > 0 {
2288            let prev = bytes[start - 1];
2289            if !prev.is_ascii_whitespace() && prev != b'<' {
2290                offset = start + 4;
2291                continue;
2292            }
2293        }
2294        let quote = *bytes.get(start + 4)?;
2295        if quote != b'"' && quote != b'\'' {
2296            offset = start + 4;
2297            continue;
2298        }
2299        let rest = &line[start + 5..];
2300        let end = rest.find(quote as char)?;
2301        return Some(&rest[..end]);
2302    }
2303    None
2304}
2305
2306fn normalize_lib_name(value: &str) -> String {
2307    let normalized = value.trim().to_ascii_lowercase();
2308    normalized
2309        .strip_prefix("lib.")
2310        .unwrap_or(normalized.as_str())
2311        .to_string()
2312}
2313
2314/// Convert emitter `ScriptTarget` to checker `ScriptTarget`.
2315/// The emitter has more variants (`ES2021`, `ES2022`) which map to `ESNext` in the checker.
2316pub const fn checker_target_from_emitter(target: ScriptTarget) -> CheckerScriptTarget {
2317    match target {
2318        ScriptTarget::ES3 => CheckerScriptTarget::ES3,
2319        ScriptTarget::ES5 => CheckerScriptTarget::ES5,
2320        ScriptTarget::ES2015 => CheckerScriptTarget::ES2015,
2321        ScriptTarget::ES2016 => CheckerScriptTarget::ES2016,
2322        ScriptTarget::ES2017 => CheckerScriptTarget::ES2017,
2323        ScriptTarget::ES2018 => CheckerScriptTarget::ES2018,
2324        ScriptTarget::ES2019 => CheckerScriptTarget::ES2019,
2325        ScriptTarget::ES2020 => CheckerScriptTarget::ES2020,
2326        ScriptTarget::ES2021
2327        | ScriptTarget::ES2022
2328        | ScriptTarget::ES2023
2329        | ScriptTarget::ES2024
2330        | ScriptTarget::ES2025
2331        | ScriptTarget::ESNext => CheckerScriptTarget::ESNext,
2332    }
2333}
2334
2335fn canonicalize_or_owned(path: &Path) -> PathBuf {
2336    std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
2337}
2338
2339fn normalize_option(value: &str) -> String {
2340    let mut normalized = String::with_capacity(value.len());
2341    for ch in value.chars() {
2342        if ch == '-' || ch == '_' || ch.is_whitespace() {
2343            continue;
2344        }
2345        normalized.push(ch.to_ascii_lowercase());
2346    }
2347    normalized
2348}
2349
2350fn strip_jsonc(input: &str) -> String {
2351    let mut out = String::with_capacity(input.len());
2352    let mut chars = input.chars().peekable();
2353    let mut in_string = false;
2354    let mut escape = false;
2355    let mut in_line_comment = false;
2356    let mut in_block_comment = false;
2357
2358    while let Some(ch) = chars.next() {
2359        if in_line_comment {
2360            if ch == '\n' {
2361                in_line_comment = false;
2362                out.push(ch);
2363            }
2364            continue;
2365        }
2366
2367        if in_block_comment {
2368            if ch == '*' {
2369                if let Some('/') = chars.peek().copied() {
2370                    chars.next();
2371                    in_block_comment = false;
2372                }
2373            } else if ch == '\n' {
2374                out.push(ch);
2375            }
2376            continue;
2377        }
2378
2379        if in_string {
2380            out.push(ch);
2381            if escape {
2382                escape = false;
2383            } else if ch == '\\' {
2384                escape = true;
2385            } else if ch == '"' {
2386                in_string = false;
2387            }
2388            continue;
2389        }
2390
2391        if ch == '"' {
2392            in_string = true;
2393            out.push(ch);
2394            continue;
2395        }
2396
2397        if ch == '/'
2398            && let Some(&next) = chars.peek()
2399        {
2400            if next == '/' {
2401                chars.next();
2402                in_line_comment = true;
2403                continue;
2404            }
2405            if next == '*' {
2406                chars.next();
2407                in_block_comment = true;
2408                continue;
2409            }
2410        }
2411
2412        out.push(ch);
2413    }
2414
2415    out
2416}
2417
2418fn remove_trailing_commas(input: &str) -> String {
2419    let mut out = String::with_capacity(input.len());
2420    let mut chars = input.chars().peekable();
2421    let mut in_string = false;
2422    let mut escape = false;
2423
2424    while let Some(ch) = chars.next() {
2425        if in_string {
2426            out.push(ch);
2427            if escape {
2428                escape = false;
2429            } else if ch == '\\' {
2430                escape = true;
2431            } else if ch == '"' {
2432                in_string = false;
2433            }
2434            continue;
2435        }
2436
2437        if ch == '"' {
2438            in_string = true;
2439            out.push(ch);
2440            continue;
2441        }
2442
2443        if ch == ',' {
2444            let mut lookahead = chars.clone();
2445            while let Some(next) = lookahead.peek().copied() {
2446                if next.is_whitespace() {
2447                    lookahead.next();
2448                    continue;
2449                }
2450                if next == '}' || next == ']' {
2451                    break;
2452                }
2453                break;
2454            }
2455
2456            if let Some(next) = lookahead.peek().copied()
2457                && (next == '}' || next == ']')
2458            {
2459                continue;
2460            }
2461        }
2462
2463        out.push(ch);
2464    }
2465
2466    out
2467}
2468
2469#[cfg(test)]
2470mod tests {
2471    use super::*;
2472
2473    #[test]
2474    fn test_parse_boolean_true() {
2475        let json = r#"{"strict": true}"#;
2476        let opts: CompilerOptions = serde_json::from_str(json).unwrap();
2477        assert_eq!(opts.strict, Some(true));
2478    }
2479
2480    #[test]
2481    fn test_parse_string_true() {
2482        let json = r#"{"strict": "true"}"#;
2483        let opts: CompilerOptions = serde_json::from_str(json).unwrap();
2484        assert_eq!(opts.strict, Some(true));
2485    }
2486
2487    #[test]
2488    fn test_parse_invalid_string() {
2489        let json = r#"{"strict": "invalid"}"#;
2490        let result: Result<CompilerOptions, _> = serde_json::from_str(json);
2491        assert!(result.is_err());
2492    }
2493
2494    #[test]
2495    fn test_parse_module_resolution_list_value() {
2496        let json =
2497            r#"{"compilerOptions":{"moduleResolution":"node16,nodenext","module":"commonjs"}} "#;
2498        let config: TsConfig = serde_json::from_str(json).unwrap();
2499        let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
2500        assert_eq!(
2501            resolved.module_resolution,
2502            Some(ModuleResolutionKind::Node16)
2503        );
2504    }
2505
2506    #[test]
2507    fn test_module_explicitly_set_when_specified() {
2508        let json = r#"{"compilerOptions":{"module":"es2015"}}"#;
2509        let config: TsConfig = serde_json::from_str(json).unwrap();
2510        let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
2511        assert!(resolved.checker.module_explicitly_set);
2512        assert!(resolved.checker.module.is_es_module());
2513    }
2514
2515    #[test]
2516    fn test_module_explicitly_set_commonjs() {
2517        let json = r#"{"compilerOptions":{"module":"commonjs"}}"#;
2518        let config: TsConfig = serde_json::from_str(json).unwrap();
2519        let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
2520        assert!(resolved.checker.module_explicitly_set);
2521        assert!(!resolved.checker.module.is_es_module());
2522    }
2523
2524    #[test]
2525    fn test_module_not_explicitly_set_defaults_from_target() {
2526        // When module is not specified, it's computed from target.
2527        // module_explicitly_set should be false so TS1202 is suppressed.
2528        let json = r#"{"compilerOptions":{"target":"es2015"}}"#;
2529        let config: TsConfig = serde_json::from_str(json).unwrap();
2530        let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
2531        assert!(!resolved.checker.module_explicitly_set);
2532        // Module defaults to ES2015 for es2015+ targets
2533        assert!(resolved.checker.module.is_es_module());
2534    }
2535
2536    #[test]
2537    fn test_module_not_explicitly_set_no_options() {
2538        // When no options at all, module_explicitly_set should be false.
2539        let resolved = resolve_compiler_options(None).unwrap();
2540        assert!(!resolved.checker.module_explicitly_set);
2541    }
2542
2543    #[test]
2544    fn test_removed_compiler_option_lookup() {
2545        assert!(removed_compiler_option("noImplicitUseStrict").is_some());
2546        assert!(removed_compiler_option("keyofStringsOnly").is_some());
2547        assert!(removed_compiler_option("suppressExcessPropertyErrors").is_some());
2548        assert!(removed_compiler_option("suppressImplicitAnyIndexErrors").is_some());
2549        assert!(removed_compiler_option("noStrictGenericChecks").is_some());
2550        assert!(removed_compiler_option("charset").is_some());
2551        assert!(removed_compiler_option("out").is_some());
2552        assert_eq!(
2553            removed_compiler_option("importsNotUsedAsValues"),
2554            Some("verbatimModuleSyntax")
2555        );
2556        assert_eq!(
2557            removed_compiler_option("preserveValueImports"),
2558            Some("verbatimModuleSyntax")
2559        );
2560        // Non-removed options return None
2561        assert!(removed_compiler_option("strict").is_none());
2562        assert!(removed_compiler_option("target").is_none());
2563    }
2564
2565    #[test]
2566    fn test_ts5102_emitted_for_removed_option() {
2567        let source = r#"{"compilerOptions":{"noImplicitUseStrict":true}}"#;
2568        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2569        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2570        assert!(
2571            codes.contains(&5102),
2572            "Expected TS5102 for removed option noImplicitUseStrict, got: {codes:?}"
2573        );
2574    }
2575
2576    #[test]
2577    fn test_ts5102_not_emitted_for_false_removed_option() {
2578        // When a removed boolean option is set to false, tsc doesn't emit TS5102
2579        let source = r#"{"compilerOptions":{"noImplicitUseStrict":false}}"#;
2580        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2581        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2582        assert!(
2583            !codes.contains(&5102),
2584            "Should NOT emit TS5102 for false-valued removed option, got: {codes:?}"
2585        );
2586    }
2587
2588    #[test]
2589    fn test_ts5102_emitted_for_string_removed_option() {
2590        let source = r#"{"compilerOptions":{"importsNotUsedAsValues":"error"}}"#;
2591        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2592        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2593        assert!(
2594            codes.contains(&5102),
2595            "Expected TS5102 for removed option importsNotUsedAsValues, got: {codes:?}"
2596        );
2597    }
2598
2599    #[test]
2600    fn test_ts5102_suppressed_with_ignore_deprecations() {
2601        // When ignoreDeprecations: "5.0" is set, tsc suppresses TS5102 for removed options
2602        let source =
2603            r#"{"compilerOptions":{"ignoreDeprecations":"5.0","noImplicitUseStrict":true}}"#;
2604        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2605        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2606        assert!(
2607            !codes.contains(&5102),
2608            "Should NOT emit TS5102 when ignoreDeprecations is '5.0', got: {codes:?}"
2609        );
2610    }
2611
2612    #[test]
2613    fn test_ts5102_not_suppressed_with_invalid_ignore_deprecations() {
2614        // Invalid ignoreDeprecations value should NOT suppress TS5102
2615        let source =
2616            r#"{"compilerOptions":{"ignoreDeprecations":"6.0","noImplicitUseStrict":true}}"#;
2617        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2618        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2619        assert!(
2620            codes.contains(&5102),
2621            "Should emit TS5102 when ignoreDeprecations is invalid, got: {codes:?}"
2622        );
2623        assert!(
2624            codes.contains(&5103),
2625            "Should also emit TS5103 for invalid ignoreDeprecations, got: {codes:?}"
2626        );
2627    }
2628
2629    #[test]
2630    fn test_ts5102_not_emitted_for_valid_option() {
2631        let source = r#"{"compilerOptions":{"strict":true}}"#;
2632        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2633        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2634        assert!(
2635            !codes.contains(&5102),
2636            "Should NOT emit TS5102 for valid option 'strict', got: {codes:?}"
2637        );
2638    }
2639
2640    #[test]
2641    fn test_ts5095_bundler_with_commonjs() {
2642        let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"bundler"}}"#;
2643        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2644        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2645        assert!(
2646            codes.contains(&5095),
2647            "Expected TS5095 for bundler+commonjs, got: {codes:?}"
2648        );
2649    }
2650
2651    #[test]
2652    fn test_ts5095_bundler_with_none() {
2653        let source = r#"{"compilerOptions":{"module":"none","moduleResolution":"bundler"}}"#;
2654        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2655        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2656        assert!(
2657            codes.contains(&5095),
2658            "Expected TS5095 for bundler+none, got: {codes:?}"
2659        );
2660    }
2661
2662    #[test]
2663    fn test_ts5095_bundler_with_amd() {
2664        let source = r#"{"compilerOptions":{"module":"amd","moduleResolution":"bundler"}}"#;
2665        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2666        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2667        assert!(
2668            codes.contains(&5095),
2669            "Expected TS5095 for bundler+amd, got: {codes:?}"
2670        );
2671    }
2672
2673    #[test]
2674    fn test_ts5095_bundler_with_system() {
2675        let source = r#"{"compilerOptions":{"module":"system","moduleResolution":"bundler"}}"#;
2676        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2677        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2678        assert!(
2679            codes.contains(&5095),
2680            "Expected TS5095 for bundler+system, got: {codes:?}"
2681        );
2682    }
2683
2684    #[test]
2685    fn test_ts5095_not_emitted_for_bundler_with_es2015() {
2686        let source = r#"{"compilerOptions":{"module":"es2015","moduleResolution":"bundler"}}"#;
2687        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2688        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2689        assert!(
2690            !codes.contains(&5095),
2691            "Should NOT emit TS5095 for bundler+es2015, got: {codes:?}"
2692        );
2693    }
2694
2695    #[test]
2696    fn test_ts5095_not_emitted_for_bundler_with_esnext() {
2697        let source = r#"{"compilerOptions":{"module":"esnext","moduleResolution":"bundler"}}"#;
2698        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2699        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2700        assert!(
2701            !codes.contains(&5095),
2702            "Should NOT emit TS5095 for bundler+esnext, got: {codes:?}"
2703        );
2704    }
2705
2706    #[test]
2707    fn test_ts5095_not_emitted_for_bundler_with_preserve() {
2708        let source = r#"{"compilerOptions":{"module":"preserve","moduleResolution":"bundler"}}"#;
2709        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2710        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2711        assert!(
2712            !codes.contains(&5095),
2713            "Should NOT emit TS5095 for bundler+preserve, got: {codes:?}"
2714        );
2715    }
2716
2717    #[test]
2718    fn test_ts5095_not_emitted_for_bundler_with_node16() {
2719        let source = r#"{"compilerOptions":{"module":"node16","moduleResolution":"bundler"}}"#;
2720        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2721        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2722        assert!(
2723            !codes.contains(&5095),
2724            "Should NOT emit TS5095 for bundler+node16, got: {codes:?}"
2725        );
2726    }
2727
2728    #[test]
2729    fn test_ts5095_not_emitted_for_bundler_with_node18() {
2730        let source = r#"{"compilerOptions":{"module":"node18","moduleResolution":"bundler"}}"#;
2731        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2732        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2733        assert!(
2734            !codes.contains(&5095),
2735            "Should NOT emit TS5095 for bundler+node18, got: {codes:?}"
2736        );
2737    }
2738
2739    #[test]
2740    fn test_ts5095_not_emitted_for_bundler_with_nodenext() {
2741        let source = r#"{"compilerOptions":{"module":"nodenext","moduleResolution":"bundler"}}"#;
2742        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2743        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2744        assert!(
2745            !codes.contains(&5095),
2746            "Should NOT emit TS5095 for bundler+nodenext, got: {codes:?}"
2747        );
2748    }
2749
2750    #[test]
2751    fn test_ts5095_not_emitted_for_node16_resolution() {
2752        let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"node16"}}"#;
2753        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2754        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2755        assert!(
2756            !codes.contains(&5095),
2757            "Should NOT emit TS5095 for node16 resolution, got: {codes:?}"
2758        );
2759    }
2760
2761    #[test]
2762    fn test_ts5103_emitted_for_invalid_ignore_deprecations() {
2763        let source = r#"{"compilerOptions":{"ignoreDeprecations":"6.0"}}"#;
2764        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2765        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2766        assert!(
2767            codes.contains(&5103),
2768            "Expected TS5103 for ignoreDeprecations='6.0', got: {codes:?}"
2769        );
2770    }
2771
2772    #[test]
2773    fn test_ts5103_emitted_for_wrong_version() {
2774        let source = r#"{"compilerOptions":{"ignoreDeprecations":"5.1"}}"#;
2775        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2776        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2777        assert!(
2778            codes.contains(&5103),
2779            "Expected TS5103 for ignoreDeprecations='5.1', got: {codes:?}"
2780        );
2781    }
2782
2783    #[test]
2784    fn test_ts5103_not_emitted_for_valid_value() {
2785        let source = r#"{"compilerOptions":{"ignoreDeprecations":"5.0"}}"#;
2786        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2787        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2788        assert!(
2789            !codes.contains(&5103),
2790            "Should NOT emit TS5103 for valid ignoreDeprecations='5.0', got: {codes:?}"
2791        );
2792    }
2793
2794    #[test]
2795    fn test_ts5103_not_emitted_when_absent() {
2796        let source = r#"{"compilerOptions":{"strict":true}}"#;
2797        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2798        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2799        assert!(
2800            !codes.contains(&5103),
2801            "Should NOT emit TS5103 when ignoreDeprecations is absent, got: {codes:?}"
2802        );
2803    }
2804
2805    #[test]
2806    fn test_ts5110_node16_resolution_with_commonjs_module() {
2807        let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"node16"}}"#;
2808        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2809        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2810        assert!(
2811            codes.contains(&5110),
2812            "Should emit TS5110 for node16 resolution with commonjs module, got: {codes:?}"
2813        );
2814    }
2815
2816    #[test]
2817    fn test_ts5110_nodenext_resolution_with_es2022_module() {
2818        let source = r#"{"compilerOptions":{"module":"es2022","moduleResolution":"nodenext"}}"#;
2819        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2820        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2821        assert!(
2822            codes.contains(&5110),
2823            "Should emit TS5110 for nodenext resolution with es2022 module, got: {codes:?}"
2824        );
2825    }
2826
2827    #[test]
2828    fn test_ts5110_not_emitted_for_matching_node16() {
2829        let source = r#"{"compilerOptions":{"module":"node16","moduleResolution":"node16"}}"#;
2830        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2831        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2832        assert!(
2833            !codes.contains(&5110),
2834            "Should NOT emit TS5110 when module matches moduleResolution, got: {codes:?}"
2835        );
2836    }
2837
2838    #[test]
2839    fn test_ts5110_not_emitted_for_matching_nodenext() {
2840        let source = r#"{"compilerOptions":{"module":"nodenext","moduleResolution":"nodenext"}}"#;
2841        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2842        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2843        assert!(
2844            !codes.contains(&5110),
2845            "Should NOT emit TS5110 when module matches moduleResolution, got: {codes:?}"
2846        );
2847    }
2848
2849    #[test]
2850    fn test_ts5069_emit_declaration_only_without_declaration() {
2851        let source = r#"{"compilerOptions":{"emitDeclarationOnly":true}}"#;
2852        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2853        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2854        assert!(
2855            codes.contains(&5069),
2856            "Expected TS5069 for emitDeclarationOnly without declaration, got: {:?}",
2857            codes
2858        );
2859    }
2860
2861    #[test]
2862    fn test_ts5069_not_emitted_with_declaration() {
2863        let source = r#"{"compilerOptions":{"emitDeclarationOnly":true,"declaration":true}}"#;
2864        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2865        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2866        assert!(
2867            !codes.contains(&5069),
2868            "Should NOT emit TS5069 when declaration is true, got: {:?}",
2869            codes
2870        );
2871    }
2872
2873    #[test]
2874    fn test_ts5069_not_emitted_with_composite() {
2875        let source = r#"{"compilerOptions":{"emitDeclarationOnly":true,"composite":true}}"#;
2876        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2877        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2878        assert!(
2879            !codes.contains(&5069),
2880            "Should NOT emit TS5069 when composite is true, got: {:?}",
2881            codes
2882        );
2883    }
2884
2885    #[test]
2886    fn test_ts5069_declaration_map_without_declaration() {
2887        let source = r#"{"compilerOptions":{"declarationMap":true}}"#;
2888        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2889        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2890        assert!(
2891            codes.contains(&5069),
2892            "Expected TS5069 for declarationMap without declaration, got: {:?}",
2893            codes
2894        );
2895    }
2896
2897    #[test]
2898    fn test_ts5053_sourcemap_with_inline_sourcemap() {
2899        let source = r#"{"compilerOptions":{"sourceMap":true,"inlineSourceMap":true}}"#;
2900        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2901        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2902        assert!(
2903            codes.contains(&5053),
2904            "Expected TS5053 for sourceMap with inlineSourceMap, got: {:?}",
2905            codes
2906        );
2907        // tsc emits twice (at each key position)
2908        let count = codes.iter().filter(|&&c| c == 5053).count();
2909        assert_eq!(
2910            count, 2,
2911            "Expected 2 TS5053 diagnostics (one per key), got: {}",
2912            count
2913        );
2914    }
2915
2916    #[test]
2917    fn test_ts5053_not_emitted_without_conflict() {
2918        let source = r#"{"compilerOptions":{"sourceMap":true}}"#;
2919        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2920        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2921        assert!(
2922            !codes.contains(&5053),
2923            "Should NOT emit TS5053 for sourceMap alone, got: {:?}",
2924            codes
2925        );
2926    }
2927
2928    #[test]
2929    fn test_ts5053_allow_js_with_isolated_declarations() {
2930        let source = r#"{"compilerOptions":{"allowJs":true,"isolatedDeclarations":true,"declaration":true}}"#;
2931        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2932        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2933        assert!(
2934            codes.contains(&5053),
2935            "Expected TS5053 for allowJs with isolatedDeclarations, got: {:?}",
2936            codes
2937        );
2938    }
2939
2940    #[test]
2941    fn test_ts5070_resolve_json_module_with_classic_module_resolution() {
2942        let source =
2943            r#"{"compilerOptions":{"resolveJsonModule":true,"moduleResolution":"classic"}}"#;
2944        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2945        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2946        assert!(
2947            codes.contains(&5070),
2948            "Expected TS5070 for resolveJsonModule with classic moduleResolution, got: {:?}",
2949            codes
2950        );
2951    }
2952
2953    #[test]
2954    fn test_ts5070_resolve_json_module_with_amd_module() {
2955        // module=amd defaults to moduleResolution=classic
2956        let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"amd"}}"#;
2957        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2958        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2959        assert!(
2960            codes.contains(&5070),
2961            "Expected TS5070 for resolveJsonModule with module=amd (implies classic), got: {:?}",
2962            codes
2963        );
2964    }
2965
2966    #[test]
2967    fn test_ts5071_resolve_json_module_with_system_module() {
2968        let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"system"}}"#;
2969        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2970        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2971        assert!(
2972            codes.contains(&5071),
2973            "Expected TS5071 for resolveJsonModule with module=system, got: {:?}",
2974            codes
2975        );
2976    }
2977
2978    #[test]
2979    fn test_ts5071_resolve_json_module_with_none_module() {
2980        let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"none"}}"#;
2981        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2982        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2983        assert!(
2984            codes.contains(&5071),
2985            "Expected TS5071 for resolveJsonModule with module=none, got: {:?}",
2986            codes
2987        );
2988    }
2989
2990    #[test]
2991    fn test_ts5098_resolve_package_json_with_classic() {
2992        let source = r#"{"compilerOptions":{"resolvePackageJsonExports":true,"moduleResolution":"classic"}}"#;
2993        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
2994        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
2995        assert!(
2996            codes.contains(&5098),
2997            "Expected TS5098 for resolvePackageJsonExports with classic moduleResolution, got: {:?}",
2998            codes
2999        );
3000    }
3001
3002    #[test]
3003    fn test_ts5098_not_emitted_with_bundler() {
3004        let source = r#"{"compilerOptions":{"resolvePackageJsonExports":true,"moduleResolution":"bundler"}}"#;
3005        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3006        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3007        assert!(
3008            !codes.contains(&5098),
3009            "Should NOT emit TS5098 with bundler moduleResolution, got: {:?}",
3010            codes
3011        );
3012    }
3013
3014    #[test]
3015    fn test_ts6082_outfile_with_commonjs() {
3016        let source = r#"{"compilerOptions":{"module":"commonjs","outFile":"all.js"}}"#;
3017        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3018        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3019        assert!(
3020            codes.contains(&6082),
3021            "Expected TS6082 for outFile+commonjs, got: {codes:?}"
3022        );
3023        // Should emit twice — once at "module" key, once at "outFile" key
3024        let count = codes.iter().filter(|&&c| c == 6082).count();
3025        assert_eq!(
3026            count, 2,
3027            "Expected two TS6082 diagnostics (module + outFile keys), got {count}"
3028        );
3029    }
3030
3031    #[test]
3032    fn test_ts6082_outfile_with_umd() {
3033        let source = r#"{"compilerOptions":{"module":"umd","outFile":"all.js"}}"#;
3034        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3035        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3036        assert!(
3037            codes.contains(&6082),
3038            "Expected TS6082 for outFile+umd, got: {codes:?}"
3039        );
3040    }
3041
3042    #[test]
3043    fn test_ts6082_outfile_with_es6() {
3044        let source = r#"{"compilerOptions":{"module":"es6","outFile":"all.js"}}"#;
3045        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3046        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3047        assert!(
3048            codes.contains(&6082),
3049            "Expected TS6082 for outFile+es6, got: {codes:?}"
3050        );
3051    }
3052
3053    #[test]
3054    fn test_ts6082_not_emitted_for_amd() {
3055        let source = r#"{"compilerOptions":{"module":"amd","outFile":"all.js"}}"#;
3056        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3057        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3058        assert!(
3059            !codes.contains(&6082),
3060            "Should NOT emit TS6082 for outFile+amd, got: {codes:?}"
3061        );
3062    }
3063
3064    #[test]
3065    fn test_ts6082_not_emitted_for_system() {
3066        let source = r#"{"compilerOptions":{"module":"system","outFile":"all.js"}}"#;
3067        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3068        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3069        assert!(
3070            !codes.contains(&6082),
3071            "Should NOT emit TS6082 for outFile+system, got: {codes:?}"
3072        );
3073    }
3074
3075    #[test]
3076    fn test_ts6082_not_emitted_with_emit_declaration_only() {
3077        let source = r#"{"compilerOptions":{"module":"commonjs","outFile":"all.js","emitDeclarationOnly":true,"declaration":true}}"#;
3078        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3079        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3080        assert!(
3081            !codes.contains(&6082),
3082            "Should NOT emit TS6082 when emitDeclarationOnly is true, got: {codes:?}"
3083        );
3084    }
3085
3086    #[test]
3087    fn test_ts6082_not_emitted_without_outfile() {
3088        let source = r#"{"compilerOptions":{"module":"commonjs"}}"#;
3089        let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
3090        let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
3091        assert!(
3092            !codes.contains(&6082),
3093            "Should NOT emit TS6082 without outFile, got: {codes:?}"
3094        );
3095    }
3096}