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