use anyhow::{Context, Result, anyhow, bail};
use serde::{Deserialize, Deserializer};
use std::collections::VecDeque;
use rustc_hash::{FxHashMap, FxHashSet};
use std::env;
use std::path::{Path, PathBuf};
use crate::checker::context::ScriptTarget as CheckerScriptTarget;
use crate::checker::diagnostics::Diagnostic;
use crate::emitter::{ModuleKind, PrinterOptions, ScriptTarget};
use tsz_common::diagnostics::data::{diagnostic_codes, diagnostic_messages};
use tsz_common::diagnostics::format_message;
fn deserialize_bool_or_string<'de, D>(deserializer: D) -> Result<Option<bool>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum BoolOrString {
Bool(bool),
String(String),
}
match Option::<BoolOrString>::deserialize(deserializer)? {
None => Ok(None),
Some(BoolOrString::Bool(b)) => Ok(Some(b)),
Some(BoolOrString::String(s)) => {
let normalized = s.trim().to_lowercase();
match normalized.as_str() {
"true" | "1" | "yes" | "on" => Ok(Some(true)),
"false" | "0" | "no" | "off" => Ok(Some(false)),
_ => {
Err(Error::custom(format!(
"invalid boolean value: '{s}'. Expected true, false, 'true', or 'false'",
)))
}
}
}
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct TsConfig {
#[serde(default)]
pub extends: Option<String>,
#[serde(default)]
pub compiler_options: Option<CompilerOptions>,
#[serde(default)]
pub include: Option<Vec<String>>,
#[serde(default)]
pub exclude: Option<Vec<String>>,
#[serde(default)]
pub files: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CompilerOptions {
#[serde(default)]
pub target: Option<String>,
#[serde(default)]
pub module: Option<String>,
#[serde(default)]
pub module_resolution: Option<String>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub resolve_package_json_exports: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub resolve_package_json_imports: Option<bool>,
#[serde(default)]
pub module_suffixes: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub resolve_json_module: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub allow_arbitrary_extensions: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub allow_importing_ts_extensions: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub rewrite_relative_import_extensions: Option<bool>,
#[serde(default)]
pub types_versions_compiler_version: Option<String>,
#[serde(default)]
pub types: Option<Vec<String>>,
#[serde(default)]
pub type_roots: Option<Vec<String>>,
#[serde(default)]
pub jsx: Option<String>,
#[serde(default)]
#[serde(rename = "jsxFactory")]
pub jsx_factory: Option<String>,
#[serde(default)]
#[serde(rename = "jsxFragmentFactory")]
pub jsx_fragment_factory: Option<String>,
#[serde(default)]
#[serde(rename = "reactNamespace")]
pub react_namespace: Option<String>,
#[serde(default)]
pub lib: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_lib: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_types_and_symbols: Option<bool>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub paths: Option<FxHashMap<String, Vec<String>>>,
#[serde(default)]
pub root_dir: Option<String>,
#[serde(default)]
pub out_dir: Option<String>,
#[serde(default)]
pub out_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub declaration: Option<bool>,
#[serde(default)]
pub declaration_dir: Option<String>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub source_map: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub declaration_map: Option<bool>,
#[serde(default)]
pub ts_build_info_file: Option<String>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub incremental: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub strict: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_emit: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_resolve: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_emit_on_error: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub isolated_modules: Option<bool>,
#[serde(default)]
pub custom_conditions: Option<Vec<String>>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub es_module_interop: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub allow_synthetic_default_imports: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub experimental_decorators: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub import_helpers: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub allow_js: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub check_js: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub skip_lib_check: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub always_strict: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub use_define_for_class_fields: Option<bool>,
#[serde(
default,
alias = "noImplicitAny",
deserialize_with = "deserialize_bool_or_string"
)]
pub no_implicit_any: Option<bool>,
#[serde(
default,
alias = "noImplicitReturns",
deserialize_with = "deserialize_bool_or_string"
)]
pub no_implicit_returns: Option<bool>,
#[serde(
default,
alias = "strictNullChecks",
deserialize_with = "deserialize_bool_or_string"
)]
pub strict_null_checks: Option<bool>,
#[serde(
default,
alias = "strictFunctionTypes",
deserialize_with = "deserialize_bool_or_string"
)]
pub strict_function_types: Option<bool>,
#[serde(
default,
alias = "strictPropertyInitialization",
deserialize_with = "deserialize_bool_or_string"
)]
pub strict_property_initialization: Option<bool>,
#[serde(
default,
alias = "noImplicitThis",
deserialize_with = "deserialize_bool_or_string"
)]
pub no_implicit_this: Option<bool>,
#[serde(
default,
alias = "useUnknownInCatchVariables",
deserialize_with = "deserialize_bool_or_string"
)]
pub use_unknown_in_catch_variables: Option<bool>,
#[serde(
default,
alias = "noUncheckedIndexedAccess",
deserialize_with = "deserialize_bool_or_string"
)]
pub no_unchecked_indexed_access: Option<bool>,
#[serde(
default,
alias = "strictBindCallApply",
deserialize_with = "deserialize_bool_or_string"
)]
pub strict_bind_call_apply: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_unused_locals: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_unused_parameters: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub allow_unreachable_code: Option<bool>,
#[serde(default, deserialize_with = "deserialize_bool_or_string")]
pub no_unchecked_side_effect_imports: Option<bool>,
#[serde(
default,
alias = "noImplicitOverride",
deserialize_with = "deserialize_bool_or_string"
)]
pub no_implicit_override: Option<bool>,
#[serde(default)]
pub module_detection: Option<String>,
}
pub use crate::checker::context::CheckerOptions;
#[derive(Debug, Clone, Default)]
pub struct ResolvedCompilerOptions {
pub printer: PrinterOptions,
pub checker: CheckerOptions,
pub jsx: Option<JsxEmit>,
pub lib_files: Vec<PathBuf>,
pub lib_is_default: bool,
pub module_resolution: Option<ModuleResolutionKind>,
pub resolve_package_json_exports: bool,
pub resolve_package_json_imports: bool,
pub module_suffixes: Vec<String>,
pub resolve_json_module: bool,
pub allow_arbitrary_extensions: bool,
pub allow_importing_ts_extensions: bool,
pub rewrite_relative_import_extensions: bool,
pub types_versions_compiler_version: Option<String>,
pub types: Option<Vec<String>>,
pub type_roots: Option<Vec<PathBuf>>,
pub base_url: Option<PathBuf>,
pub paths: Option<Vec<PathMapping>>,
pub root_dir: Option<PathBuf>,
pub out_dir: Option<PathBuf>,
pub out_file: Option<PathBuf>,
pub declaration_dir: Option<PathBuf>,
pub emit_declarations: bool,
pub source_map: bool,
pub declaration_map: bool,
pub ts_build_info_file: Option<PathBuf>,
pub incremental: bool,
pub no_emit: bool,
pub no_emit_on_error: bool,
pub no_resolve: bool,
pub import_helpers: bool,
pub no_check: bool,
pub custom_conditions: Vec<String>,
pub es_module_interop: bool,
pub allow_synthetic_default_imports: bool,
pub allow_js: bool,
pub check_js: bool,
pub skip_lib_check: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JsxEmit {
Preserve,
React,
ReactJsx,
ReactJsxDev,
ReactNative,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleResolutionKind {
Classic,
Node,
Node16,
NodeNext,
Bundler,
}
#[derive(Debug, Clone)]
pub struct PathMapping {
pub pattern: String,
pub(crate) prefix: String,
pub(crate) suffix: String,
pub targets: Vec<String>,
}
impl PathMapping {
pub fn match_specifier(&self, specifier: &str) -> Option<String> {
if !self.pattern.contains('*') {
return (self.pattern == specifier).then(String::new);
}
if !specifier.starts_with(&self.prefix) || !specifier.ends_with(&self.suffix) {
return None;
}
let start = self.prefix.len();
let end = specifier.len().saturating_sub(self.suffix.len());
if end < start {
return None;
}
Some(specifier[start..end].to_string())
}
pub const fn specificity(&self) -> usize {
self.prefix.len() + self.suffix.len()
}
}
impl ResolvedCompilerOptions {
pub const fn effective_module_resolution(&self) -> ModuleResolutionKind {
if let Some(resolution) = self.module_resolution {
return resolution;
}
match self.printer.module {
ModuleKind::None | ModuleKind::AMD | ModuleKind::UMD | ModuleKind::System => {
ModuleResolutionKind::Classic
}
ModuleKind::CommonJS => ModuleResolutionKind::Node,
ModuleKind::NodeNext => ModuleResolutionKind::NodeNext,
ModuleKind::Node16 => ModuleResolutionKind::Node16,
_ => ModuleResolutionKind::Bundler,
}
}
}
pub fn resolve_compiler_options(
options: Option<&CompilerOptions>,
) -> Result<ResolvedCompilerOptions> {
let mut resolved = ResolvedCompilerOptions::default();
let Some(options) = options else {
resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
resolved.lib_is_default = true;
resolved.module_suffixes = vec![String::new()];
let default_resolution = resolved.effective_module_resolution();
resolved.resolve_package_json_exports = matches!(
default_resolution,
ModuleResolutionKind::Node16
| ModuleResolutionKind::NodeNext
| ModuleResolutionKind::Bundler
);
resolved.resolve_package_json_imports = resolved.resolve_package_json_exports;
return Ok(resolved);
};
if let Some(target) = options.target.as_deref() {
resolved.printer.target = parse_script_target(target)?;
}
resolved.checker.target = checker_target_from_emitter(resolved.printer.target);
let module_explicitly_set = options.module.is_some();
if let Some(module) = options.module.as_deref() {
let kind = parse_module_kind(module)?;
resolved.printer.module = kind;
resolved.checker.module = kind;
} else {
let default_module = if resolved.printer.target.supports_es2015() {
ModuleKind::ES2015
} else {
ModuleKind::CommonJS
};
resolved.printer.module = default_module;
resolved.checker.module = default_module;
}
resolved.checker.module_explicitly_set = module_explicitly_set;
if let Some(module_resolution) = options.module_resolution.as_deref() {
let value = module_resolution.trim();
if !value.is_empty() {
resolved.module_resolution = Some(parse_module_resolution(value)?);
}
}
if !module_explicitly_set && let Some(mr) = resolved.module_resolution {
let inferred = match mr {
ModuleResolutionKind::Node16 => Some(ModuleKind::Node16),
ModuleResolutionKind::NodeNext => Some(ModuleKind::NodeNext),
_ => None,
};
if let Some(kind) = inferred {
resolved.printer.module = kind;
resolved.checker.module = kind;
}
}
let effective_resolution = resolved.effective_module_resolution();
resolved.resolve_package_json_exports = options.resolve_package_json_exports.unwrap_or({
matches!(
effective_resolution,
ModuleResolutionKind::Node16
| ModuleResolutionKind::NodeNext
| ModuleResolutionKind::Bundler
)
});
resolved.resolve_package_json_imports = options.resolve_package_json_imports.unwrap_or({
matches!(
effective_resolution,
ModuleResolutionKind::Node
| ModuleResolutionKind::Node16
| ModuleResolutionKind::NodeNext
| ModuleResolutionKind::Bundler
)
});
if let Some(module_suffixes) = options.module_suffixes.as_ref() {
resolved.module_suffixes = module_suffixes.clone();
} else {
resolved.module_suffixes = vec![String::new()];
}
if let Some(resolve_json_module) = options.resolve_json_module {
resolved.resolve_json_module = resolve_json_module;
resolved.checker.resolve_json_module = resolve_json_module;
}
if let Some(import_helpers) = options.import_helpers {
resolved.import_helpers = import_helpers;
}
if let Some(allow_arbitrary_extensions) = options.allow_arbitrary_extensions {
resolved.allow_arbitrary_extensions = allow_arbitrary_extensions;
}
if let Some(allow_importing_ts_extensions) = options.allow_importing_ts_extensions {
resolved.allow_importing_ts_extensions = allow_importing_ts_extensions;
}
if let Some(rewrite_relative_import_extensions) = options.rewrite_relative_import_extensions {
resolved.rewrite_relative_import_extensions = rewrite_relative_import_extensions;
}
if let Some(types_versions_compiler_version) =
options.types_versions_compiler_version.as_deref()
{
let value = types_versions_compiler_version.trim();
if !value.is_empty() {
resolved.types_versions_compiler_version = Some(value.to_string());
}
}
if let Some(types) = options.types.as_ref() {
let list: Vec<String> = types
.iter()
.filter_map(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
})
.collect();
resolved.types = Some(list);
}
if let Some(type_roots) = options.type_roots.as_ref() {
let roots: Vec<PathBuf> = type_roots
.iter()
.filter_map(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
None
} else {
Some(PathBuf::from(trimmed))
}
})
.collect();
resolved.type_roots = Some(roots);
}
if let Some(factory) = options.jsx_factory.as_deref() {
resolved.checker.jsx_factory = factory.to_string();
} else if let Some(ns) = options.react_namespace.as_deref() {
resolved.checker.jsx_factory = format!("{ns}.createElement");
}
if let Some(frag) = options.jsx_fragment_factory.as_deref() {
resolved.checker.jsx_fragment_factory = frag.to_string();
}
if let Some(jsx) = options.jsx.as_deref() {
let jsx_emit = parse_jsx_emit(jsx)?;
resolved.jsx = Some(jsx_emit);
resolved.checker.jsx_mode = jsx_emit_to_mode(jsx_emit);
}
if let Some(no_lib) = options.no_lib {
resolved.checker.no_lib = no_lib;
}
if resolved.checker.no_lib && options.lib.is_some() {
return Err(anyhow::anyhow!(
"Option 'lib' cannot be specified with option 'noLib'."
));
}
if let Some(no_types_and_symbols) = options.no_types_and_symbols {
resolved.checker.no_types_and_symbols = no_types_and_symbols;
}
if resolved.checker.no_lib && options.lib.is_some() {
bail!("Option 'lib' cannot be specified with option 'noLib'.");
}
if let Some(lib_list) = options.lib.as_ref() {
resolved.lib_files = resolve_lib_files(lib_list)?;
resolved.lib_is_default = false;
} else if !resolved.checker.no_lib && !resolved.checker.no_types_and_symbols {
resolved.lib_files = resolve_default_lib_files(resolved.printer.target)?;
resolved.lib_is_default = true;
}
let base_url = options.base_url.as_deref().map(str::trim);
if let Some(base_url) = base_url
&& !base_url.is_empty()
{
resolved.base_url = Some(PathBuf::from(base_url));
}
if let Some(paths) = options.paths.as_ref()
&& !paths.is_empty()
{
resolved.paths = Some(build_path_mappings(paths));
}
if let Some(root_dir) = options.root_dir.as_deref()
&& !root_dir.is_empty()
{
resolved.root_dir = Some(PathBuf::from(root_dir));
}
if let Some(out_dir) = options.out_dir.as_deref()
&& !out_dir.is_empty()
{
resolved.out_dir = Some(PathBuf::from(out_dir));
}
if let Some(out_file) = options.out_file.as_deref()
&& !out_file.is_empty()
{
resolved.out_file = Some(PathBuf::from(out_file));
}
if let Some(declaration_dir) = options.declaration_dir.as_deref()
&& !declaration_dir.is_empty()
{
resolved.declaration_dir = Some(PathBuf::from(declaration_dir));
}
if let Some(declaration) = options.declaration {
resolved.emit_declarations = declaration;
}
if let Some(source_map) = options.source_map {
resolved.source_map = source_map;
}
if let Some(declaration_map) = options.declaration_map {
resolved.declaration_map = declaration_map;
}
if let Some(ts_build_info_file) = options.ts_build_info_file.as_deref()
&& !ts_build_info_file.is_empty()
{
resolved.ts_build_info_file = Some(PathBuf::from(ts_build_info_file));
}
if let Some(incremental) = options.incremental {
resolved.incremental = incremental;
}
if let Some(strict) = options.strict {
resolved.checker.strict = strict;
if strict {
resolved.checker.no_implicit_any = true;
resolved.checker.strict_null_checks = true;
resolved.checker.strict_function_types = true;
resolved.checker.strict_bind_call_apply = true;
resolved.checker.strict_property_initialization = true;
resolved.checker.no_implicit_this = true;
resolved.checker.use_unknown_in_catch_variables = true;
resolved.checker.always_strict = true;
resolved.printer.always_strict = true;
} else {
resolved.checker.no_implicit_any = false;
resolved.checker.strict_null_checks = false;
resolved.checker.strict_function_types = false;
resolved.checker.strict_bind_call_apply = false;
resolved.checker.strict_property_initialization = false;
resolved.checker.no_implicit_this = false;
resolved.checker.use_unknown_in_catch_variables = false;
resolved.checker.always_strict = false;
resolved.printer.always_strict = false;
}
}
if options.strict.is_none() {
resolved.checker.strict = false;
resolved.checker.no_implicit_any = false;
resolved.checker.strict_null_checks = false;
resolved.checker.strict_function_types = false;
resolved.checker.strict_bind_call_apply = false;
resolved.checker.strict_property_initialization = false;
resolved.checker.no_implicit_this = false;
resolved.checker.use_unknown_in_catch_variables = false;
resolved.checker.always_strict = false;
resolved.printer.always_strict = false;
}
if let Some(v) = options.no_implicit_any {
resolved.checker.no_implicit_any = v;
}
if let Some(v) = options.no_implicit_returns {
resolved.checker.no_implicit_returns = v;
}
if let Some(v) = options.strict_null_checks {
resolved.checker.strict_null_checks = v;
}
if let Some(v) = options.strict_function_types {
resolved.checker.strict_function_types = v;
}
if let Some(v) = options.strict_property_initialization {
resolved.checker.strict_property_initialization = v;
}
if let Some(v) = options.no_unchecked_indexed_access {
resolved.checker.no_unchecked_indexed_access = v;
}
if let Some(v) = options.no_implicit_this {
resolved.checker.no_implicit_this = v;
}
if let Some(v) = options.use_unknown_in_catch_variables {
resolved.checker.use_unknown_in_catch_variables = v;
}
if let Some(v) = options.strict_bind_call_apply {
resolved.checker.strict_bind_call_apply = v;
}
if let Some(v) = options.no_implicit_override {
resolved.checker.no_implicit_override = v;
}
if let Some(v) = options.no_unchecked_side_effect_imports {
resolved.checker.no_unchecked_side_effect_imports = v;
}
if let Some(no_emit) = options.no_emit {
resolved.no_emit = no_emit;
}
if let Some(no_resolve) = options.no_resolve {
resolved.no_resolve = no_resolve;
resolved.checker.no_resolve = no_resolve;
}
if let Some(no_emit_on_error) = options.no_emit_on_error {
resolved.no_emit_on_error = no_emit_on_error;
}
if let Some(isolated_modules) = options.isolated_modules {
resolved.checker.isolated_modules = isolated_modules;
}
if let Some(always_strict) = options.always_strict {
resolved.checker.always_strict = always_strict;
resolved.printer.always_strict = always_strict;
}
if let Some(use_define_for_class_fields) = options.use_define_for_class_fields {
resolved.printer.use_define_for_class_fields = use_define_for_class_fields;
}
if let Some(no_unused_locals) = options.no_unused_locals {
resolved.checker.no_unused_locals = no_unused_locals;
}
if let Some(no_unused_parameters) = options.no_unused_parameters {
resolved.checker.no_unused_parameters = no_unused_parameters;
}
if let Some(allow_unreachable_code) = options.allow_unreachable_code {
resolved.checker.allow_unreachable_code = Some(allow_unreachable_code);
}
if let Some(ref custom_conditions) = options.custom_conditions {
resolved.custom_conditions = custom_conditions.clone();
}
if let Some(es_module_interop) = options.es_module_interop {
resolved.es_module_interop = es_module_interop;
resolved.checker.es_module_interop = es_module_interop;
resolved.printer.es_module_interop = es_module_interop;
if es_module_interop {
resolved.allow_synthetic_default_imports = true;
resolved.checker.allow_synthetic_default_imports = true;
}
}
if let Some(allow_synthetic_default_imports) = options.allow_synthetic_default_imports {
resolved.allow_synthetic_default_imports = allow_synthetic_default_imports;
resolved.checker.allow_synthetic_default_imports = allow_synthetic_default_imports;
} else if !resolved.allow_synthetic_default_imports {
let should_default_true = matches!(resolved.checker.module, ModuleKind::System)
|| matches!(
resolved.module_resolution,
Some(ModuleResolutionKind::Bundler)
);
if should_default_true {
resolved.allow_synthetic_default_imports = true;
resolved.checker.allow_synthetic_default_imports = true;
}
}
if let Some(experimental_decorators) = options.experimental_decorators {
resolved.checker.experimental_decorators = experimental_decorators;
resolved.printer.legacy_decorators = experimental_decorators;
}
if let Some(allow_js) = options.allow_js {
resolved.allow_js = allow_js;
}
if let Some(check_js) = options.check_js {
resolved.check_js = check_js;
}
if let Some(skip_lib_check) = options.skip_lib_check {
resolved.skip_lib_check = skip_lib_check;
}
if let Some(ref module_detection) = options.module_detection
&& module_detection.eq_ignore_ascii_case("force")
{
resolved.printer.module_detection_force = true;
}
Ok(resolved)
}
pub fn parse_tsconfig(source: &str) -> Result<TsConfig> {
let stripped = strip_jsonc(source);
let normalized = remove_trailing_commas(&stripped);
let config = serde_json::from_str(&normalized).context("failed to parse tsconfig JSON")?;
Ok(config)
}
pub struct ParsedTsConfig {
pub config: TsConfig,
pub diagnostics: Vec<Diagnostic>,
pub suppress_excess_property_errors: bool,
pub suppress_implicit_any_index_errors: bool,
}
pub fn parse_tsconfig_with_diagnostics(source: &str, file_path: &str) -> Result<ParsedTsConfig> {
let stripped = strip_jsonc(source);
let normalized = remove_trailing_commas(&stripped);
let mut raw: serde_json::Value =
serde_json::from_str(&normalized).context("failed to parse tsconfig JSON")?;
let mut diagnostics = Vec::new();
let mut suppress_excess = false;
let mut suppress_any_index = false;
if let Some(obj) = raw.as_object_mut()
&& let Some(serde_json::Value::Object(compiler_opts)) = obj.get_mut("compilerOptions")
{
let keys: Vec<String> = compiler_opts.keys().cloned().collect();
let mut renames: Vec<(String, String)> = Vec::new();
for key in &keys {
let key_lower = key.to_lowercase();
if let Some(canonical) = known_compiler_option(&key_lower) {
if key.as_str() != canonical {
let start = find_key_offset_in_source(&stripped, key);
let msg = format_message(
diagnostic_messages::UNKNOWN_COMPILER_OPTION_DID_YOU_MEAN,
&[key, canonical],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
key.len() as u32 + 2, msg,
diagnostic_codes::UNKNOWN_COMPILER_OPTION_DID_YOU_MEAN,
));
renames.push((key.clone(), canonical.to_string()));
}
} else {
let start = find_key_offset_in_source(&stripped, key);
let msg = format_message(diagnostic_messages::UNKNOWN_COMPILER_OPTION, &[key]);
diagnostics.push(Diagnostic::error(
file_path,
start,
key.len() as u32 + 2,
msg,
diagnostic_codes::UNKNOWN_COMPILER_OPTION,
));
}
}
for (old_key, new_key) in renames {
if let Some(value) = compiler_opts.remove(&old_key) {
compiler_opts.insert(new_key, value);
}
}
let ignore_deprecations_valid = matches!(
compiler_opts.get("ignoreDeprecations"),
Some(serde_json::Value::String(v)) if v == "5.0"
);
let mut removed_keys: Vec<String> = Vec::new();
for key in compiler_opts.keys().cloned().collect::<Vec<_>>() {
if removed_compiler_option(&key).is_some() {
if !ignore_deprecations_valid {
let value = compiler_opts.get(&key);
let is_set = match value {
Some(serde_json::Value::Bool(b)) => *b,
Some(serde_json::Value::String(s)) => !s.is_empty(),
Some(serde_json::Value::Null) | None => false,
Some(_) => true,
};
if is_set {
let start = find_key_offset_in_source(&stripped, &key);
let msg = format_message(
diagnostic_messages::OPTION_HAS_BEEN_REMOVED_PLEASE_REMOVE_IT_FROM_YOUR_CONFIGURATION,
&[&key],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
key.len() as u32 + 2, msg,
diagnostic_codes::OPTION_HAS_BEEN_REMOVED_PLEASE_REMOVE_IT_FROM_YOUR_CONFIGURATION,
));
}
}
removed_keys.push(key);
}
}
suppress_excess = matches!(
compiler_opts.get("suppressExcessPropertyErrors"),
Some(serde_json::Value::Bool(true))
);
suppress_any_index = matches!(
compiler_opts.get("suppressImplicitAnyIndexErrors"),
Some(serde_json::Value::Bool(true))
);
for key in &removed_keys {
compiler_opts.remove(key);
}
let keys_after_rename: Vec<String> = compiler_opts.keys().cloned().collect();
let mut bad_keys: Vec<String> = Vec::new();
for key in &keys_after_rename {
let expected_type = compiler_option_expected_type(key);
if expected_type.is_empty() {
continue; }
let Some(value) = compiler_opts.get(key) else {
continue;
};
let type_ok = match expected_type {
"boolean" => value.is_boolean(),
"string" => value.is_string(),
"number" => value.is_number(),
"list" => value.is_array(),
"string or Array" => value.is_string() || value.is_array(),
"object" => value.is_object(),
_ => true,
};
if !type_ok {
let start = find_value_offset_in_source(&stripped, key);
let value_len = estimate_json_value_len(value);
let msg = format_message(
diagnostic_messages::COMPILER_OPTION_REQUIRES_A_VALUE_OF_TYPE,
&[key, expected_type],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
value_len,
msg,
diagnostic_codes::COMPILER_OPTION_REQUIRES_A_VALUE_OF_TYPE,
));
bad_keys.push(key.clone());
}
}
for key in &bad_keys {
compiler_opts.remove(key);
}
if let Some(serde_json::Value::String(id_value)) = compiler_opts.get("ignoreDeprecations")
&& id_value != "5.0"
{
let start = find_value_offset_in_source(&stripped, "ignoreDeprecations");
let value_len = id_value.len() as u32 + 2; diagnostics.push(Diagnostic::error(
file_path,
start,
value_len,
diagnostic_messages::INVALID_VALUE_FOR_IGNOREDEPRECATIONS.to_string(),
diagnostic_codes::INVALID_VALUE_FOR_IGNOREDEPRECATIONS,
));
}
if let Some(serde_json::Value::String(mr_value)) = compiler_opts.get("moduleResolution") {
let mr_normalized =
normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
if mr_normalized == "bundler" {
let module_ok = if let Some(serde_json::Value::String(mod_value)) =
compiler_opts.get("module")
{
let mod_normalized =
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
matches!(
mod_normalized.as_str(),
"preserve"
| "es2015"
| "es6"
| "es2020"
| "es2022"
| "esnext"
| "node16"
| "node18"
| "node20"
| "nodenext"
)
} else {
if let Some(serde_json::Value::String(target_value)) =
compiler_opts.get("target")
{
let target_normalized = normalize_option(
target_value
.split(',')
.next()
.unwrap_or(target_value)
.trim(),
);
matches!(
target_normalized.as_str(),
"es2015"
| "es6"
| "es2016"
| "es2017"
| "es2018"
| "es2019"
| "es2020"
| "es2021"
| "es2022"
| "es2023"
| "es2024"
| "esnext"
)
} else {
false
}
};
if !module_ok {
let start = find_value_offset_in_source(&stripped, "moduleResolution");
let value_len = mr_value.len() as u32 + 2; let msg = "Option 'bundler' can only be used when 'module' is set to 'preserve' or to 'es2015' or later.".to_string();
diagnostics.push(Diagnostic::error(
file_path,
start,
value_len,
msg,
diagnostic_codes::OPTION_CAN_ONLY_BE_USED_WHEN_MODULE_IS_SET_TO_PRESERVE_COMMONJS_OR_ES2015_OR_LAT,
));
}
}
}
if let Some(serde_json::Value::String(mr_value)) = compiler_opts.get("moduleResolution") {
let mr_normalized =
normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
let is_node_mr = matches!(
mr_normalized.as_str(),
"node16" | "node18" | "node20" | "nodenext"
);
if is_node_mr {
let module_ok = if let Some(serde_json::Value::String(mod_value)) =
compiler_opts.get("module")
{
let mod_normalized =
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
matches!(
mod_normalized.as_str(),
"node16" | "node18" | "node20" | "nodenext"
)
} else {
true };
if !module_ok {
let start = find_value_offset_in_source(&stripped, "module");
let value_len = compiler_opts
.get("module")
.and_then(|v| v.as_str())
.map_or(0, |s| s.len() as u32 + 2);
let mr_display = match mr_normalized.as_str() {
"node16" => "Node16",
"node18" => "Node18",
"node20" => "Node20",
"nodenext" => "NodeNext",
_ => &mr_normalized,
};
let msg = format_message(
diagnostic_messages::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
&[mr_display, mr_display],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
value_len,
msg,
diagnostic_codes::OPTION_MODULE_MUST_BE_SET_TO_WHEN_OPTION_MODULERESOLUTION_IS_SET_TO,
));
}
}
}
if let Some(serde_json::Value::String(out_file_value)) = compiler_opts.get("outFile")
&& !out_file_value.is_empty()
&& !option_is_truthy(compiler_opts.get("emitDeclarationOnly"))
&& let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module")
{
let mod_normalized =
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
if !matches!(mod_normalized.as_str(), "amd" | "system") {
let msg = format_message(
diagnostic_messages::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
&["outFile"],
);
let start_module = find_key_offset_in_source(&stripped, "module");
let module_key_len = "module".len() as u32 + 2; diagnostics.push(Diagnostic::error(
file_path,
start_module,
module_key_len,
msg.clone(),
diagnostic_codes::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
));
let start_outfile = find_key_offset_in_source(&stripped, "outFile");
let outfile_key_len = "outFile".len() as u32 + 2;
diagnostics.push(Diagnostic::error(
file_path,
start_outfile,
outfile_key_len,
msg,
diagnostic_codes::ONLY_AMD_AND_SYSTEM_MODULES_ARE_SUPPORTED_ALONGSIDE,
));
}
}
let requires_decl_or_composite: &[&str] = &[
"emitDeclarationOnly",
"declarationMap",
"isolatedDeclarations",
];
for &opt in requires_decl_or_composite {
if option_is_truthy(compiler_opts.get(opt))
&& !option_is_truthy(compiler_opts.get("declaration"))
&& !option_is_truthy(compiler_opts.get("composite"))
{
let start = find_key_offset_in_source(&stripped, opt);
let key_len = opt.len() as u32 + 2; let msg = format_message(
diagnostic_messages::OPTION_CANNOT_BE_SPECIFIED_WITHOUT_SPECIFYING_OPTION_OR_OPTION,
&[opt, "declaration", "composite"],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
key_len,
msg,
diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITHOUT_SPECIFYING_OPTION_OR_OPTION,
));
}
}
let conflicting_pairs: &[(&str, &str)] = &[
("sourceMap", "inlineSourceMap"),
("mapRoot", "inlineSourceMap"),
("reactNamespace", "jsxFactory"),
("allowJs", "isolatedDeclarations"),
];
for &(opt_a, opt_b) in conflicting_pairs {
if option_is_truthy(compiler_opts.get(opt_a))
&& option_is_truthy(compiler_opts.get(opt_b))
{
let start = find_key_offset_in_source(&stripped, opt_a);
let key_len = opt_a.len() as u32 + 2;
let msg = format_message(
diagnostic_messages::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
&[opt_a, opt_b],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
key_len,
msg.clone(),
diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
));
let start_b = find_key_offset_in_source(&stripped, opt_b);
let key_len_b = opt_b.len() as u32 + 2;
diagnostics.push(Diagnostic::error(
file_path,
start_b,
key_len_b,
msg,
diagnostic_codes::OPTION_CANNOT_BE_SPECIFIED_WITH_OPTION,
));
}
}
if option_is_truthy(compiler_opts.get("resolveJsonModule")) {
let effective_mr = if let Some(serde_json::Value::String(mr_value)) =
compiler_opts.get("moduleResolution")
{
normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim())
} else {
let effective_module = if let Some(serde_json::Value::String(mod_value)) =
compiler_opts.get("module")
{
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim())
} else {
String::new() };
match effective_module.as_str() {
"none" | "amd" | "umd" | "system" | "" => "classic".to_string(),
"commonjs" => "node".to_string(),
"node16" => "node16".to_string(),
"nodenext" => "nodenext".to_string(),
_ => "bundler".to_string(),
}
};
if effective_mr == "classic" {
let start = find_key_offset_in_source(&stripped, "resolveJsonModule");
let key_len = "resolveJsonModule".len() as u32 + 2;
diagnostics.push(Diagnostic::error(
file_path,
start,
key_len,
diagnostic_messages::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULERESOLUTION_IS_SET_TO_CLA.to_string(),
diagnostic_codes::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULERESOLUTION_IS_SET_TO_CLA,
));
}
if let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module") {
let mod_normalized =
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
if matches!(mod_normalized.as_str(), "none" | "system" | "umd") {
let start = find_key_offset_in_source(&stripped, "resolveJsonModule");
let key_len = "resolveJsonModule".len() as u32 + 2;
diagnostics.push(Diagnostic::error(
file_path,
start,
key_len,
diagnostic_messages::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULE_IS_SET_TO_NONE_SYSTEM_O.to_string(),
diagnostic_codes::OPTION_RESOLVEJSONMODULE_CANNOT_BE_SPECIFIED_WHEN_MODULE_IS_SET_TO_NONE_SYSTEM_O,
));
}
}
}
let requires_modern_mr: &[&str] = &[
"resolvePackageJsonExports",
"resolvePackageJsonImports",
"customConditions",
];
let mr_is_modern = if let Some(serde_json::Value::String(mr_value)) =
compiler_opts.get("moduleResolution")
{
let mr_normalized =
normalize_option(mr_value.split(',').next().unwrap_or(mr_value).trim());
matches!(mr_normalized.as_str(), "node16" | "nodenext" | "bundler")
} else {
if let Some(serde_json::Value::String(mod_value)) = compiler_opts.get("module") {
let mod_normalized =
normalize_option(mod_value.split(',').next().unwrap_or(mod_value).trim());
!matches!(
mod_normalized.as_str(),
"none" | "amd" | "umd" | "system" | "commonjs"
)
} else {
false }
};
if !mr_is_modern {
for &opt in requires_modern_mr {
if option_is_truthy(compiler_opts.get(opt)) {
let start = find_key_offset_in_source(&stripped, opt);
let key_len = opt.len() as u32 + 2;
let msg = format_message(
diagnostic_messages::OPTION_CAN_ONLY_BE_USED_WHEN_MODULERESOLUTION_IS_SET_TO_NODE16_NODENEXT_OR_BUNDL,
&[opt],
);
diagnostics.push(Diagnostic::error(
file_path,
start,
key_len,
msg,
diagnostic_codes::OPTION_CAN_ONLY_BE_USED_WHEN_MODULERESOLUTION_IS_SET_TO_NODE16_NODENEXT_OR_BUNDL,
));
}
}
}
}
let config: TsConfig = serde_json::from_value(raw).context("failed to parse tsconfig JSON")?;
Ok(ParsedTsConfig {
config,
diagnostics,
suppress_excess_property_errors: suppress_excess,
suppress_implicit_any_index_errors: suppress_any_index,
})
}
const fn option_is_truthy(value: Option<&serde_json::Value>) -> bool {
match value {
None | Some(serde_json::Value::Null) => false,
Some(serde_json::Value::Bool(b)) => *b,
Some(_) => true,
}
}
fn find_key_offset_in_source(source: &str, key: &str) -> u32 {
let search = format!("\"{key}\"");
let compiler_opts_pos = source.find("compilerOptions").unwrap_or(0);
if let Some(pos) = source[compiler_opts_pos..].find(&search) {
(compiler_opts_pos + pos) as u32
} else {
0
}
}
fn find_value_offset_in_source(source: &str, key: &str) -> u32 {
let search = format!("\"{key}\"");
let compiler_opts_pos = source.find("compilerOptions").unwrap_or(0);
if let Some(key_pos) = source[compiler_opts_pos..].find(&search) {
let after_key = compiler_opts_pos + key_pos + search.len();
let rest = &source[after_key..];
if let Some(colon_pos) = rest.find(':') {
let after_colon = after_key + colon_pos + 1;
let value_rest = &source[after_colon..];
let trimmed_offset = value_rest.len() - value_rest.trim_start().len();
return (after_colon + trimmed_offset) as u32;
}
}
0
}
fn estimate_json_value_len(value: &serde_json::Value) -> u32 {
match value {
serde_json::Value::String(s) => s.len() as u32 + 2, serde_json::Value::Bool(b) => {
if *b {
4
} else {
5
}
}
serde_json::Value::Number(n) => n.to_string().len() as u32,
serde_json::Value::Null => 4,
serde_json::Value::Array(_) | serde_json::Value::Object(_) => serde_json::to_string(value)
.map(|s| s.len() as u32)
.unwrap_or(2),
}
}
fn compiler_option_expected_type(key: &str) -> &'static str {
match key {
"allowArbitraryExtensions"
| "allowImportingTsExtensions"
| "allowJs"
| "allowSyntheticDefaultImports"
| "allowUmdGlobalAccess"
| "allowUnreachableCode"
| "allowUnusedLabels"
| "alwaysStrict"
| "checkJs"
| "composite"
| "declaration"
| "declarationMap"
| "disableReferencedProjectLoad"
| "disableSizeLimit"
| "disableSolutionSearching"
| "disableSourceOfProjectReferenceRedirect"
| "downlevelIteration"
| "emitBOM"
| "emitDeclarationOnly"
| "emitDecoratorMetadata"
| "esModuleInterop"
| "exactOptionalPropertyTypes"
| "experimentalDecorators"
| "forceConsistentCasingInFileNames"
| "importHelpers"
| "incremental"
| "inlineSourceMap"
| "inlineSources"
| "isolatedDeclarations"
| "isolatedModules"
| "keyofStringsOnly"
| "noEmit"
| "noEmitHelpers"
| "noEmitOnError"
| "noErrorTruncation"
| "noFallthroughCasesInSwitch"
| "noImplicitAny"
| "noImplicitOverride"
| "noImplicitReturns"
| "noImplicitThis"
| "noImplicitUseStrict"
| "noLib"
| "noPropertyAccessFromIndexSignature"
| "noResolve"
| "noStrictGenericChecks"
| "noUncheckedIndexedAccess"
| "noUncheckedSideEffectImports"
| "noUnusedLocals"
| "noUnusedParameters"
| "preserveConstEnums"
| "preserveSymlinks"
| "preserveValueImports"
| "pretty"
| "removeComments"
| "resolveJsonModule"
| "resolvePackageJsonExports"
| "resolvePackageJsonImports"
| "rewriteRelativeImportExtensions"
| "skipDefaultLibCheck"
| "skipLibCheck"
| "sourceMap"
| "strict"
| "strictBindCallApply"
| "strictBuiltinIteratorReturn"
| "strictFunctionTypes"
| "strictNullChecks"
| "strictPropertyInitialization"
| "stripInternal"
| "suppressExcessPropertyErrors"
| "suppressImplicitAnyIndexErrors"
| "useDefineForClassFields"
| "useUnknownInCatchVariables"
| "verbatimModuleSyntax" => "boolean",
"baseUrl" | "charset" | "declarationDir" | "jsx" | "jsxFactory" | "jsxFragmentFactory"
| "jsxImportSource" | "mapRoot" | "module" | "moduleDetection" | "moduleResolution"
| "newLine" | "out" | "outDir" | "outFile" | "reactNamespace" | "rootDir"
| "sourceRoot" | "target" | "tsBuildInfoFile" | "ignoreDeprecations" => "string",
"lib" | "types" | "typeRoots" | "rootDirs" | "moduleSuffixes" | "customConditions" => {
"list"
}
"paths" => "object",
_ => "",
}
}
fn removed_compiler_option(key: &str) -> Option<&'static str> {
match key {
"noImplicitUseStrict"
| "keyofStringsOnly"
| "suppressExcessPropertyErrors"
| "suppressImplicitAnyIndexErrors"
| "noStrictGenericChecks"
| "charset" => Some(""),
"importsNotUsedAsValues" | "preserveValueImports" => Some("verbatimModuleSyntax"),
"out" => Some("outFile"),
_ => None,
}
}
fn known_compiler_option(key_lower: &str) -> Option<&'static str> {
match key_lower {
"allowarbitraryextensions" => Some("allowArbitraryExtensions"),
"allowimportingtsextensions" => Some("allowImportingTsExtensions"),
"allowjs" => Some("allowJs"),
"allowsyntheticdefaultimports" => Some("allowSyntheticDefaultImports"),
"allowumdglobalaccess" => Some("allowUmdGlobalAccess"),
"allowunreachablecode" => Some("allowUnreachableCode"),
"allowunusedlabels" => Some("allowUnusedLabels"),
"alwaysstrict" => Some("alwaysStrict"),
"baseurl" => Some("baseUrl"),
"charset" => Some("charset"),
"checkjs" => Some("checkJs"),
"composite" => Some("composite"),
"customconditions" => Some("customConditions"),
"declaration" => Some("declaration"),
"declarationdir" => Some("declarationDir"),
"declarationmap" => Some("declarationMap"),
"diagnostics" => Some("diagnostics"),
"disablereferencedprojectload" => Some("disableReferencedProjectLoad"),
"disablesizelimt" => Some("disableSizeLimit"),
"disablesolutiontypecheck" => Some("disableSolutionTypeCheck"),
"disablesolutioncaching" => Some("disableSolutionCaching"),
"disablesolutiontypechecking" => Some("disableSolutionTypeChecking"),
"disablesourceofreferencedprojectload" => Some("disableSourceOfReferencedProjectLoad"),
"downleveliteration" => Some("downlevelIteration"),
"emitbom" => Some("emitBOM"),
"emitdeclarationonly" => Some("emitDeclarationOnly"),
"emitdecoratormetadata" => Some("emitDecoratorMetadata"),
"erasablesyntaxonly" => Some("erasableSyntaxOnly"),
"esmoduleinterop" => Some("esModuleInterop"),
"exactoptionalpropertytypes" => Some("exactOptionalPropertyTypes"),
"experimentaldecorators" => Some("experimentalDecorators"),
"extendeddiagnostics" => Some("extendedDiagnostics"),
"forceconsecinferfaces" | "forceconsistentcasinginfilenames" => {
Some("forceConsistentCasingInFileNames")
}
"generatecputrace" | "generatecpuprofile" => Some("generateCpuProfile"),
"generatetrace" => Some("generateTrace"),
"ignoredeprecations" => Some("ignoreDeprecations"),
"importhelpers" => Some("importHelpers"),
"importsnotusedasvalues" => Some("importsNotUsedAsValues"),
"incremental" => Some("incremental"),
"inlineconstants" => Some("inlineConstants"),
"inlinesourcemap" => Some("inlineSourceMap"),
"inlinesources" => Some("inlineSources"),
"isolateddeclarations" => Some("isolatedDeclarations"),
"isolatedmodules" => Some("isolatedModules"),
"jsx" => Some("jsx"),
"jsxfactory" => Some("jsxFactory"),
"jsxfragmentfactory" => Some("jsxFragmentFactory"),
"jsximportsource" => Some("jsxImportSource"),
"keyofstringsonly" => Some("keyofStringsOnly"),
"lib" => Some("lib"),
"libreplacement" => Some("libReplacement"),
"listemittedfiles" => Some("listEmittedFiles"),
"listfiles" => Some("listFiles"),
"listfilesonly" => Some("listFilesOnly"),
"locale" => Some("locale"),
"maproot" => Some("mapRoot"),
"maxnodemodulejsdepth" => Some("maxNodeModuleJsDepth"),
"module" => Some("module"),
"moduledetection" => Some("moduleDetection"),
"moduleresolution" => Some("moduleResolution"),
"modulesuffixes" => Some("moduleSuffixes"),
"newline" => Some("newLine"),
"nocheck" => Some("noCheck"),
"noemit" => Some("noEmit"),
"noemithelpers" => Some("noEmitHelpers"),
"noemitonerror" => Some("noEmitOnError"),
"noerrortruncation" => Some("noErrorTruncation"),
"nofallthroughcasesinswitch" => Some("noFallthroughCasesInSwitch"),
"noimplicitany" => Some("noImplicitAny"),
"noimplicitoverride" => Some("noImplicitOverride"),
"noimplicitreturns" => Some("noImplicitReturns"),
"noimplicitthis" => Some("noImplicitThis"),
"noimplicitusestrict" => Some("noImplicitUseStrict"),
"nolib" => Some("noLib"),
"nopropertyaccessfromindexsignature" => Some("noPropertyAccessFromIndexSignature"),
"noresolve" => Some("noResolve"),
"nostrictgenericchecks" => Some("noStrictGenericChecks"),
"notypesandsymbols" => Some("noTypesAndSymbols"),
"nouncheckedindexedaccess" => Some("noUncheckedIndexedAccess"),
"nouncheckedsideeffectimports" => Some("noUncheckedSideEffectImports"),
"nounusedlocals" => Some("noUnusedLocals"),
"nounusedparameters" => Some("noUnusedParameters"),
"out" => Some("out"),
"outdir" => Some("outDir"),
"outfile" => Some("outFile"),
"paths" => Some("paths"),
"plugins" => Some("plugins"),
"preserveconstenums" => Some("preserveConstEnums"),
"preservesymlinks" => Some("preserveSymlinks"),
"preservevalueimports" => Some("preserveValueImports"),
"preservewatchoutput" => Some("preserveWatchOutput"),
"pretty" => Some("pretty"),
"reactnamespace" => Some("reactNamespace"),
"removecomments" => Some("removeComments"),
"resolvejsonmodule" => Some("resolveJsonModule"),
"resolvepackagejsonexports" => Some("resolvePackageJsonExports"),
"resolvepackagejsonimports" => Some("resolvePackageJsonImports"),
"rewriterelativeimportextensions" => Some("rewriteRelativeImportExtensions"),
"rootdir" => Some("rootDir"),
"rootdirs" => Some("rootDirs"),
"skipdefaultlibcheck" => Some("skipDefaultLibCheck"),
"skiplibcheck" => Some("skipLibCheck"),
"sourcemap" => Some("sourceMap"),
"sourceroot" => Some("sourceRoot"),
"strict" => Some("strict"),
"strictbindcallapply" => Some("strictBindCallApply"),
"strictbuiltiniteratorreturn" => Some("strictBuiltinIteratorReturn"),
"strictfunctiontypes" => Some("strictFunctionTypes"),
"strictnullchecks" => Some("strictNullChecks"),
"strictpropertyinitialization" => Some("strictPropertyInitialization"),
"stripinternal" => Some("stripInternal"),
"suppressexcesspropertyerrors" => Some("suppressExcessPropertyErrors"),
"suppressimplicitanyindexerrors" => Some("suppressImplicitAnyIndexErrors"),
"target" => Some("target"),
"traceresolution" => Some("traceResolution"),
"tsbuildinfofile" => Some("tsBuildInfoFile"),
"typeroots" => Some("typeRoots"),
"types" => Some("types"),
"usedefineforclassfields" => Some("useDefineForClassFields"),
"useunknownincatchvariables" => Some("useUnknownInCatchVariables"),
"verbatimmodulesyntax" => Some("verbatimModuleSyntax"),
_ => None,
}
}
pub fn load_tsconfig(path: &Path) -> Result<TsConfig> {
let mut visited = FxHashSet::default();
load_tsconfig_inner(path, &mut visited)
}
pub fn load_tsconfig_with_diagnostics(path: &Path) -> Result<ParsedTsConfig> {
let mut visited = FxHashSet::default();
load_tsconfig_inner_with_diagnostics(path, &mut visited)
}
fn load_tsconfig_inner(path: &Path, visited: &mut FxHashSet<PathBuf>) -> Result<TsConfig> {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if !visited.insert(canonical.clone()) {
bail!("tsconfig extends cycle detected at {}", canonical.display());
}
let source = std::fs::read_to_string(path)
.with_context(|| format!("failed to read tsconfig: {}", path.display()))?;
let mut config = parse_tsconfig(&source)
.with_context(|| format!("failed to parse tsconfig: {}", path.display()))?;
let extends = config.extends.take();
if let Some(extends_path) = extends {
let base_path = resolve_extends_path(path, &extends_path)?;
let base_config = load_tsconfig_inner(&base_path, visited)?;
config = merge_configs(base_config, config);
}
visited.remove(&canonical);
Ok(config)
}
fn load_tsconfig_inner_with_diagnostics(
path: &Path,
visited: &mut FxHashSet<PathBuf>,
) -> Result<ParsedTsConfig> {
let canonical = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
if !visited.insert(canonical.clone()) {
bail!("tsconfig extends cycle detected at {}", canonical.display());
}
let source = std::fs::read_to_string(path)
.with_context(|| format!("failed to read tsconfig: {}", path.display()))?;
let file_display = path.display().to_string();
let mut parsed = parse_tsconfig_with_diagnostics(&source, &file_display)
.with_context(|| format!("failed to parse tsconfig: {}", path.display()))?;
let extends = parsed.config.extends.take();
if let Some(extends_path) = extends {
let base_path = resolve_extends_path(path, &extends_path)?;
let base_config = load_tsconfig_inner(&base_path, visited)?;
parsed.config = merge_configs(base_config, parsed.config);
}
visited.remove(&canonical);
Ok(parsed)
}
fn resolve_extends_path(current_path: &Path, extends: &str) -> Result<PathBuf> {
let base_dir = current_path
.parent()
.ok_or_else(|| anyhow!("tsconfig has no parent directory"))?;
let mut candidate = PathBuf::from(extends);
if candidate.extension().is_none() {
candidate.set_extension("json");
}
if candidate.is_absolute() {
Ok(candidate)
} else {
Ok(base_dir.join(candidate))
}
}
fn merge_configs(base: TsConfig, mut child: TsConfig) -> TsConfig {
let merged_compiler_options = match (base.compiler_options, child.compiler_options.take()) {
(Some(base_opts), Some(child_opts)) => Some(merge_compiler_options(base_opts, child_opts)),
(Some(base_opts), None) => Some(base_opts),
(None, Some(child_opts)) => Some(child_opts),
(None, None) => None,
};
TsConfig {
extends: None,
compiler_options: merged_compiler_options,
include: child.include.or(base.include),
exclude: child.exclude.or(base.exclude),
files: child.files.or(base.files),
}
}
macro_rules! merge_options {
($child:expr, $base:expr, $Struct:ident { $($field:ident),* $(,)? }) => {
$Struct { $( $field: $child.$field.or($base.$field), )* }
};
}
fn merge_compiler_options(base: CompilerOptions, child: CompilerOptions) -> CompilerOptions {
merge_options!(
child,
base,
CompilerOptions {
target,
module,
module_resolution,
resolve_package_json_exports,
resolve_package_json_imports,
module_suffixes,
resolve_json_module,
allow_arbitrary_extensions,
allow_importing_ts_extensions,
rewrite_relative_import_extensions,
types_versions_compiler_version,
types,
type_roots,
jsx,
jsx_factory,
jsx_fragment_factory,
react_namespace,
lib,
no_lib,
no_types_and_symbols,
base_url,
paths,
root_dir,
out_dir,
out_file,
declaration,
declaration_dir,
source_map,
declaration_map,
ts_build_info_file,
incremental,
strict,
no_emit,
no_emit_on_error,
isolated_modules,
custom_conditions,
es_module_interop,
allow_synthetic_default_imports,
experimental_decorators,
import_helpers,
allow_js,
check_js,
skip_lib_check,
always_strict,
use_define_for_class_fields,
no_implicit_any,
no_implicit_returns,
strict_null_checks,
strict_function_types,
strict_property_initialization,
no_implicit_this,
use_unknown_in_catch_variables,
strict_bind_call_apply,
no_unchecked_indexed_access,
no_unused_locals,
no_unused_parameters,
allow_unreachable_code,
no_resolve,
no_unchecked_side_effect_imports,
no_implicit_override,
module_detection,
}
)
}
fn parse_script_target(value: &str) -> Result<ScriptTarget> {
let cleaned = value.trim_end_matches(',');
let normalized = normalize_option(cleaned);
let target = match normalized.as_str() {
"es3" => ScriptTarget::ES3,
"es5" => ScriptTarget::ES5,
"es6" | "es2015" => ScriptTarget::ES2015,
"es2016" => ScriptTarget::ES2016,
"es2017" => ScriptTarget::ES2017,
"es2018" => ScriptTarget::ES2018,
"es2019" => ScriptTarget::ES2019,
"es2020" => ScriptTarget::ES2020,
"es2021" => ScriptTarget::ES2021,
"es2022" | "es2023" | "es2024" => ScriptTarget::ES2022,
"esnext" => ScriptTarget::ESNext,
_ => bail!("unsupported compilerOptions.target '{value}'"),
};
Ok(target)
}
fn parse_module_kind(value: &str) -> Result<ModuleKind> {
let cleaned = value.split(',').next().unwrap_or(value).trim();
let normalized = normalize_option(cleaned);
let module = match normalized.as_str() {
"none" => ModuleKind::None,
"commonjs" => ModuleKind::CommonJS,
"amd" => ModuleKind::AMD,
"umd" => ModuleKind::UMD,
"system" => ModuleKind::System,
"es6" | "es2015" => ModuleKind::ES2015,
"es2020" => ModuleKind::ES2020,
"es2022" => ModuleKind::ES2022,
"esnext" => ModuleKind::ESNext,
"node16" | "node18" | "node20" => ModuleKind::Node16,
"nodenext" => ModuleKind::NodeNext,
"preserve" => ModuleKind::Preserve,
_ => bail!("unsupported compilerOptions.module '{value}'"),
};
Ok(module)
}
fn parse_module_resolution(value: &str) -> Result<ModuleResolutionKind> {
let cleaned = value.split(',').next().unwrap_or(value).trim();
let normalized = normalize_option(cleaned);
let resolution = match normalized.as_str() {
"classic" => ModuleResolutionKind::Classic,
"node" | "node10" => ModuleResolutionKind::Node,
"node16" => ModuleResolutionKind::Node16,
"nodenext" => ModuleResolutionKind::NodeNext,
"bundler" => ModuleResolutionKind::Bundler,
_ => bail!("unsupported compilerOptions.moduleResolution '{value}'"),
};
Ok(resolution)
}
fn parse_jsx_emit(value: &str) -> Result<JsxEmit> {
let normalized = normalize_option(value);
let jsx = match normalized.as_str() {
"preserve" => JsxEmit::Preserve,
"react" => JsxEmit::React,
"react-jsx" | "reactjsx" => JsxEmit::ReactJsx,
"react-jsxdev" | "reactjsxdev" => JsxEmit::ReactJsxDev,
"reactnative" | "react-native" => JsxEmit::ReactNative,
_ => bail!("unsupported compilerOptions.jsx '{value}'"),
};
Ok(jsx)
}
const fn jsx_emit_to_mode(emit: JsxEmit) -> tsz_common::checker_options::JsxMode {
use tsz_common::checker_options::JsxMode;
match emit {
JsxEmit::Preserve => JsxMode::Preserve,
JsxEmit::React => JsxMode::React,
JsxEmit::ReactJsx => JsxMode::ReactJsx,
JsxEmit::ReactJsxDev => JsxMode::ReactJsxDev,
JsxEmit::ReactNative => JsxMode::ReactNative,
}
}
fn build_path_mappings(paths: &FxHashMap<String, Vec<String>>) -> Vec<PathMapping> {
let mut mappings = Vec::new();
for (pattern, targets) in paths {
if targets.is_empty() {
continue;
}
let pattern = normalize_path_pattern(pattern);
let targets = targets
.iter()
.map(|target| normalize_path_pattern(target))
.collect();
let (prefix, suffix) = split_path_pattern(&pattern);
mappings.push(PathMapping {
pattern,
prefix,
suffix,
targets,
});
}
mappings.sort_by(|left, right| {
right
.specificity()
.cmp(&left.specificity())
.then_with(|| right.pattern.len().cmp(&left.pattern.len()))
.then_with(|| left.pattern.cmp(&right.pattern))
});
mappings
}
fn normalize_path_pattern(value: &str) -> String {
value.trim().replace('\\', "/")
}
fn split_path_pattern(pattern: &str) -> (String, String) {
match pattern.find('*') {
Some(star_idx) => {
let (prefix, rest) = pattern.split_at(star_idx);
(prefix.to_string(), rest[1..].to_string())
}
None => (pattern.to_string(), String::new()),
}
}
pub fn resolve_lib_files_with_options(
lib_list: &[String],
follow_references: bool,
) -> Result<Vec<PathBuf>> {
if lib_list.is_empty() {
return Ok(Vec::new());
}
let lib_dir = default_lib_dir()?;
resolve_lib_files_from_dir_with_options(lib_list, follow_references, &lib_dir)
}
pub fn resolve_lib_files_from_dir_with_options(
lib_list: &[String],
follow_references: bool,
lib_dir: &Path,
) -> Result<Vec<PathBuf>> {
if lib_list.is_empty() {
return Ok(Vec::new());
}
let lib_map = build_lib_map(lib_dir)?;
let mut resolved = Vec::new();
let mut pending: VecDeque<String> = lib_list
.iter()
.map(|value| normalize_lib_name(value))
.collect();
let mut visited = FxHashSet::default();
while let Some(lib_name) = pending.pop_front() {
if lib_name.is_empty() || !visited.insert(lib_name.clone()) {
continue;
}
let path = match lib_map.get(&lib_name) {
Some(path) => path.clone(),
None => {
let alias = match lib_name.as_str() {
"lib" => Some("es5.full"),
"es6" => Some("es2015.full"),
"es7" => Some("es2016"),
_ => None,
};
let Some(alias) = alias else {
return Err(anyhow!(
"unsupported compilerOptions.lib '{}' (not found in {})",
lib_name,
lib_dir.display()
));
};
lib_map.get(alias).cloned().ok_or_else(|| {
anyhow!(
"unsupported compilerOptions.lib '{}' (alias '{}' not found in {})",
lib_name,
alias,
lib_dir.display()
)
})?
}
};
resolved.push(path.clone());
if follow_references {
let contents = std::fs::read_to_string(&path)
.with_context(|| format!("failed to read lib file {}", path.display()))?;
for reference in extract_lib_references(&contents) {
pending.push_back(reference);
}
}
}
Ok(resolved)
}
pub fn resolve_lib_files(lib_list: &[String]) -> Result<Vec<PathBuf>> {
resolve_lib_files_with_options(lib_list, true)
}
pub fn resolve_lib_files_from_dir(lib_list: &[String], lib_dir: &Path) -> Result<Vec<PathBuf>> {
resolve_lib_files_from_dir_with_options(lib_list, true, lib_dir)
}
pub fn resolve_default_lib_files(target: ScriptTarget) -> Result<Vec<PathBuf>> {
let lib_dir = default_lib_dir()?;
resolve_default_lib_files_from_dir(target, &lib_dir)
}
pub fn resolve_default_lib_files_from_dir(
target: ScriptTarget,
lib_dir: &Path,
) -> Result<Vec<PathBuf>> {
let root_lib = default_lib_name_for_target(target);
resolve_lib_files_from_dir(&[root_lib.to_string()], lib_dir)
}
pub const fn default_lib_name_for_target(target: ScriptTarget) -> &'static str {
match target {
ScriptTarget::ES3 | ScriptTarget::ES5 => "lib",
ScriptTarget::ES2015 => "es6",
ScriptTarget::ES2016 => "es2016.full",
ScriptTarget::ES2017 => "es2017.full",
ScriptTarget::ES2018 => "es2018.full",
ScriptTarget::ES2019 => "es2019.full",
ScriptTarget::ES2020 => "es2020.full",
ScriptTarget::ES2021 => "es2021.full",
ScriptTarget::ES2022 => "es2022.full",
ScriptTarget::ES2023
| ScriptTarget::ES2024
| ScriptTarget::ES2025
| ScriptTarget::ESNext => "esnext.full",
}
}
pub const fn core_lib_name_for_target(target: ScriptTarget) -> &'static str {
match target {
ScriptTarget::ES3 | ScriptTarget::ES5 => "es5",
ScriptTarget::ES2015 => "es2015",
ScriptTarget::ES2016 => "es2016",
ScriptTarget::ES2017 => "es2017",
ScriptTarget::ES2018 => "es2018",
ScriptTarget::ES2019 => "es2019",
ScriptTarget::ES2020 => "es2020",
ScriptTarget::ES2021 => "es2021",
ScriptTarget::ES2022 => "es2022",
ScriptTarget::ES2023
| ScriptTarget::ES2024
| ScriptTarget::ES2025
| ScriptTarget::ESNext => "esnext",
}
}
pub fn default_lib_dir() -> Result<PathBuf> {
if let Some(dir) = env::var_os("TSZ_LIB_DIR") {
let dir = PathBuf::from(dir);
if !dir.is_dir() {
bail!(
"TSZ_LIB_DIR does not point to a directory: {}",
dir.display()
);
}
return Ok(canonicalize_or_owned(&dir));
}
if let Some(dir) = lib_dir_from_exe() {
return Ok(dir);
}
if let Some(dir) = lib_dir_from_cwd() {
return Ok(dir);
}
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
if let Some(dir) = lib_dir_from_root(manifest_dir) {
return Ok(dir);
}
bail!("lib directory not found under {}", manifest_dir.display());
}
fn lib_dir_from_exe() -> Option<PathBuf> {
let exe = env::current_exe().ok()?;
let exe_dir = exe.parent()?;
let candidate = exe_dir.join("lib");
if candidate.is_dir() {
return Some(canonicalize_or_owned(&candidate));
}
lib_dir_from_root(exe_dir)
}
fn lib_dir_from_cwd() -> Option<PathBuf> {
let cwd = env::current_dir().ok()?;
lib_dir_from_root(&cwd)
}
fn lib_dir_from_root(root: &Path) -> Option<PathBuf> {
let candidates = [
root.join("TypeScript").join("built").join("local"),
root.join("TypeScript").join("lib"),
root.join("node_modules").join("typescript").join("lib"),
root.join("scripts")
.join("node_modules")
.join("typescript")
.join("lib"),
root.join("scripts")
.join("emit")
.join("node_modules")
.join("typescript")
.join("lib"),
root.join("TypeScript").join("src").join("lib"),
root.join("TypeScript")
.join("node_modules")
.join("typescript")
.join("lib"),
root.join("tests").join("lib"),
];
for candidate in candidates {
if candidate.is_dir() {
return Some(canonicalize_or_owned(&candidate));
}
}
None
}
fn build_lib_map(lib_dir: &Path) -> Result<FxHashMap<String, PathBuf>> {
let mut map = FxHashMap::default();
for entry in std::fs::read_dir(lib_dir)
.with_context(|| format!("failed to read lib directory {}", lib_dir.display()))?
{
let entry = entry?;
let path = entry.path();
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if !file_name.ends_with(".d.ts") {
continue;
}
let stem = file_name.trim_end_matches(".d.ts");
let stem = stem.strip_suffix(".generated").unwrap_or(stem);
let key = normalize_lib_name(stem);
map.insert(key, canonicalize_or_owned(&path));
}
Ok(map)
}
pub(crate) fn extract_lib_references(source: &str) -> Vec<String> {
let mut refs = Vec::new();
let mut in_block_comment = false;
for line in source.lines() {
let line = line.trim_start();
if in_block_comment {
if line.contains("*/") {
in_block_comment = false;
}
continue;
}
if line.starts_with("/*") {
if !line.contains("*/") {
in_block_comment = true;
}
continue;
}
if line.is_empty() {
continue;
}
if line.starts_with("///") {
if let Some(value) = parse_reference_lib_value(line) {
refs.push(normalize_lib_name(value));
}
continue;
}
if line.starts_with("//") {
continue;
}
break;
}
refs
}
fn parse_reference_lib_value(line: &str) -> Option<&str> {
let mut offset = 0;
let bytes = line.as_bytes();
while let Some(idx) = line[offset..].find("lib=") {
let start = offset + idx;
if start > 0 {
let prev = bytes[start - 1];
if !prev.is_ascii_whitespace() && prev != b'<' {
offset = start + 4;
continue;
}
}
let quote = *bytes.get(start + 4)?;
if quote != b'"' && quote != b'\'' {
offset = start + 4;
continue;
}
let rest = &line[start + 5..];
let end = rest.find(quote as char)?;
return Some(&rest[..end]);
}
None
}
fn normalize_lib_name(value: &str) -> String {
let normalized = value.trim().to_ascii_lowercase();
normalized
.strip_prefix("lib.")
.unwrap_or(normalized.as_str())
.to_string()
}
pub const fn checker_target_from_emitter(target: ScriptTarget) -> CheckerScriptTarget {
match target {
ScriptTarget::ES3 => CheckerScriptTarget::ES3,
ScriptTarget::ES5 => CheckerScriptTarget::ES5,
ScriptTarget::ES2015 => CheckerScriptTarget::ES2015,
ScriptTarget::ES2016 => CheckerScriptTarget::ES2016,
ScriptTarget::ES2017 => CheckerScriptTarget::ES2017,
ScriptTarget::ES2018 => CheckerScriptTarget::ES2018,
ScriptTarget::ES2019 => CheckerScriptTarget::ES2019,
ScriptTarget::ES2020 => CheckerScriptTarget::ES2020,
ScriptTarget::ES2021
| ScriptTarget::ES2022
| ScriptTarget::ES2023
| ScriptTarget::ES2024
| ScriptTarget::ES2025
| ScriptTarget::ESNext => CheckerScriptTarget::ESNext,
}
}
fn canonicalize_or_owned(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn normalize_option(value: &str) -> String {
let mut normalized = String::with_capacity(value.len());
for ch in value.chars() {
if ch == '-' || ch == '_' || ch.is_whitespace() {
continue;
}
normalized.push(ch.to_ascii_lowercase());
}
normalized
}
fn strip_jsonc(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut escape = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
while let Some(ch) = chars.next() {
if in_line_comment {
if ch == '\n' {
in_line_comment = false;
out.push(ch);
}
continue;
}
if in_block_comment {
if ch == '*' {
if let Some('/') = chars.peek().copied() {
chars.next();
in_block_comment = false;
}
} else if ch == '\n' {
out.push(ch);
}
continue;
}
if in_string {
out.push(ch);
if escape {
escape = false;
} else if ch == '\\' {
escape = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
if ch == '"' {
in_string = true;
out.push(ch);
continue;
}
if ch == '/'
&& let Some(&next) = chars.peek()
{
if next == '/' {
chars.next();
in_line_comment = true;
continue;
}
if next == '*' {
chars.next();
in_block_comment = true;
continue;
}
}
out.push(ch);
}
out
}
fn remove_trailing_commas(input: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut chars = input.chars().peekable();
let mut in_string = false;
let mut escape = false;
while let Some(ch) = chars.next() {
if in_string {
out.push(ch);
if escape {
escape = false;
} else if ch == '\\' {
escape = true;
} else if ch == '"' {
in_string = false;
}
continue;
}
if ch == '"' {
in_string = true;
out.push(ch);
continue;
}
if ch == ',' {
let mut lookahead = chars.clone();
while let Some(next) = lookahead.peek().copied() {
if next.is_whitespace() {
lookahead.next();
continue;
}
if next == '}' || next == ']' {
break;
}
break;
}
if let Some(next) = lookahead.peek().copied()
&& (next == '}' || next == ']')
{
continue;
}
}
out.push(ch);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_boolean_true() {
let json = r#"{"strict": true}"#;
let opts: CompilerOptions = serde_json::from_str(json).unwrap();
assert_eq!(opts.strict, Some(true));
}
#[test]
fn test_parse_string_true() {
let json = r#"{"strict": "true"}"#;
let opts: CompilerOptions = serde_json::from_str(json).unwrap();
assert_eq!(opts.strict, Some(true));
}
#[test]
fn test_parse_invalid_string() {
let json = r#"{"strict": "invalid"}"#;
let result: Result<CompilerOptions, _> = serde_json::from_str(json);
assert!(result.is_err());
}
#[test]
fn test_parse_module_resolution_list_value() {
let json =
r#"{"compilerOptions":{"moduleResolution":"node16,nodenext","module":"commonjs"}} "#;
let config: TsConfig = serde_json::from_str(json).unwrap();
let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
assert_eq!(
resolved.module_resolution,
Some(ModuleResolutionKind::Node16)
);
}
#[test]
fn test_module_explicitly_set_when_specified() {
let json = r#"{"compilerOptions":{"module":"es2015"}}"#;
let config: TsConfig = serde_json::from_str(json).unwrap();
let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
assert!(resolved.checker.module_explicitly_set);
assert!(resolved.checker.module.is_es_module());
}
#[test]
fn test_module_explicitly_set_commonjs() {
let json = r#"{"compilerOptions":{"module":"commonjs"}}"#;
let config: TsConfig = serde_json::from_str(json).unwrap();
let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
assert!(resolved.checker.module_explicitly_set);
assert!(!resolved.checker.module.is_es_module());
}
#[test]
fn test_module_not_explicitly_set_defaults_from_target() {
let json = r#"{"compilerOptions":{"target":"es2015"}}"#;
let config: TsConfig = serde_json::from_str(json).unwrap();
let resolved = resolve_compiler_options(config.compiler_options.as_ref()).unwrap();
assert!(!resolved.checker.module_explicitly_set);
assert!(resolved.checker.module.is_es_module());
}
#[test]
fn test_module_not_explicitly_set_no_options() {
let resolved = resolve_compiler_options(None).unwrap();
assert!(!resolved.checker.module_explicitly_set);
}
#[test]
fn test_removed_compiler_option_lookup() {
assert!(removed_compiler_option("noImplicitUseStrict").is_some());
assert!(removed_compiler_option("keyofStringsOnly").is_some());
assert!(removed_compiler_option("suppressExcessPropertyErrors").is_some());
assert!(removed_compiler_option("suppressImplicitAnyIndexErrors").is_some());
assert!(removed_compiler_option("noStrictGenericChecks").is_some());
assert!(removed_compiler_option("charset").is_some());
assert!(removed_compiler_option("out").is_some());
assert_eq!(
removed_compiler_option("importsNotUsedAsValues"),
Some("verbatimModuleSyntax")
);
assert_eq!(
removed_compiler_option("preserveValueImports"),
Some("verbatimModuleSyntax")
);
assert!(removed_compiler_option("strict").is_none());
assert!(removed_compiler_option("target").is_none());
}
#[test]
fn test_ts5102_emitted_for_removed_option() {
let source = r#"{"compilerOptions":{"noImplicitUseStrict":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5102),
"Expected TS5102 for removed option noImplicitUseStrict, got: {codes:?}"
);
}
#[test]
fn test_ts5102_not_emitted_for_false_removed_option() {
let source = r#"{"compilerOptions":{"noImplicitUseStrict":false}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5102),
"Should NOT emit TS5102 for false-valued removed option, got: {codes:?}"
);
}
#[test]
fn test_ts5102_emitted_for_string_removed_option() {
let source = r#"{"compilerOptions":{"importsNotUsedAsValues":"error"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5102),
"Expected TS5102 for removed option importsNotUsedAsValues, got: {codes:?}"
);
}
#[test]
fn test_ts5102_suppressed_with_ignore_deprecations() {
let source =
r#"{"compilerOptions":{"ignoreDeprecations":"5.0","noImplicitUseStrict":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5102),
"Should NOT emit TS5102 when ignoreDeprecations is '5.0', got: {codes:?}"
);
}
#[test]
fn test_ts5102_not_suppressed_with_invalid_ignore_deprecations() {
let source =
r#"{"compilerOptions":{"ignoreDeprecations":"6.0","noImplicitUseStrict":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5102),
"Should emit TS5102 when ignoreDeprecations is invalid, got: {codes:?}"
);
assert!(
codes.contains(&5103),
"Should also emit TS5103 for invalid ignoreDeprecations, got: {codes:?}"
);
}
#[test]
fn test_ts5102_not_emitted_for_valid_option() {
let source = r#"{"compilerOptions":{"strict":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5102),
"Should NOT emit TS5102 for valid option 'strict', got: {codes:?}"
);
}
#[test]
fn test_ts5095_bundler_with_commonjs() {
let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5095),
"Expected TS5095 for bundler+commonjs, got: {codes:?}"
);
}
#[test]
fn test_ts5095_bundler_with_none() {
let source = r#"{"compilerOptions":{"module":"none","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5095),
"Expected TS5095 for bundler+none, got: {codes:?}"
);
}
#[test]
fn test_ts5095_bundler_with_amd() {
let source = r#"{"compilerOptions":{"module":"amd","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5095),
"Expected TS5095 for bundler+amd, got: {codes:?}"
);
}
#[test]
fn test_ts5095_bundler_with_system() {
let source = r#"{"compilerOptions":{"module":"system","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5095),
"Expected TS5095 for bundler+system, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_es2015() {
let source = r#"{"compilerOptions":{"module":"es2015","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+es2015, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_esnext() {
let source = r#"{"compilerOptions":{"module":"esnext","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+esnext, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_preserve() {
let source = r#"{"compilerOptions":{"module":"preserve","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+preserve, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_node16() {
let source = r#"{"compilerOptions":{"module":"node16","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+node16, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_node18() {
let source = r#"{"compilerOptions":{"module":"node18","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+node18, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_bundler_with_nodenext() {
let source = r#"{"compilerOptions":{"module":"nodenext","moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for bundler+nodenext, got: {codes:?}"
);
}
#[test]
fn test_ts5095_not_emitted_for_node16_resolution() {
let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"node16"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5095),
"Should NOT emit TS5095 for node16 resolution, got: {codes:?}"
);
}
#[test]
fn test_ts5103_emitted_for_invalid_ignore_deprecations() {
let source = r#"{"compilerOptions":{"ignoreDeprecations":"6.0"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5103),
"Expected TS5103 for ignoreDeprecations='6.0', got: {codes:?}"
);
}
#[test]
fn test_ts5103_emitted_for_wrong_version() {
let source = r#"{"compilerOptions":{"ignoreDeprecations":"5.1"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5103),
"Expected TS5103 for ignoreDeprecations='5.1', got: {codes:?}"
);
}
#[test]
fn test_ts5103_not_emitted_for_valid_value() {
let source = r#"{"compilerOptions":{"ignoreDeprecations":"5.0"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5103),
"Should NOT emit TS5103 for valid ignoreDeprecations='5.0', got: {codes:?}"
);
}
#[test]
fn test_ts5103_not_emitted_when_absent() {
let source = r#"{"compilerOptions":{"strict":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5103),
"Should NOT emit TS5103 when ignoreDeprecations is absent, got: {codes:?}"
);
}
#[test]
fn test_ts5110_node16_resolution_with_commonjs_module() {
let source = r#"{"compilerOptions":{"module":"commonjs","moduleResolution":"node16"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5110),
"Should emit TS5110 for node16 resolution with commonjs module, got: {codes:?}"
);
}
#[test]
fn test_ts5110_nodenext_resolution_with_es2022_module() {
let source = r#"{"compilerOptions":{"module":"es2022","moduleResolution":"nodenext"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5110),
"Should emit TS5110 for nodenext resolution with es2022 module, got: {codes:?}"
);
}
#[test]
fn test_ts5110_not_emitted_for_matching_node16() {
let source = r#"{"compilerOptions":{"module":"node16","moduleResolution":"node16"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5110),
"Should NOT emit TS5110 when module matches moduleResolution, got: {codes:?}"
);
}
#[test]
fn test_ts5110_not_emitted_for_matching_nodenext() {
let source = r#"{"compilerOptions":{"module":"nodenext","moduleResolution":"nodenext"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5110),
"Should NOT emit TS5110 when module matches moduleResolution, got: {codes:?}"
);
}
#[test]
fn test_ts5069_emit_declaration_only_without_declaration() {
let source = r#"{"compilerOptions":{"emitDeclarationOnly":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5069),
"Expected TS5069 for emitDeclarationOnly without declaration, got: {:?}",
codes
);
}
#[test]
fn test_ts5069_not_emitted_with_declaration() {
let source = r#"{"compilerOptions":{"emitDeclarationOnly":true,"declaration":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5069),
"Should NOT emit TS5069 when declaration is true, got: {:?}",
codes
);
}
#[test]
fn test_ts5069_not_emitted_with_composite() {
let source = r#"{"compilerOptions":{"emitDeclarationOnly":true,"composite":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5069),
"Should NOT emit TS5069 when composite is true, got: {:?}",
codes
);
}
#[test]
fn test_ts5069_declaration_map_without_declaration() {
let source = r#"{"compilerOptions":{"declarationMap":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5069),
"Expected TS5069 for declarationMap without declaration, got: {:?}",
codes
);
}
#[test]
fn test_ts5053_sourcemap_with_inline_sourcemap() {
let source = r#"{"compilerOptions":{"sourceMap":true,"inlineSourceMap":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5053),
"Expected TS5053 for sourceMap with inlineSourceMap, got: {:?}",
codes
);
let count = codes.iter().filter(|&&c| c == 5053).count();
assert_eq!(
count, 2,
"Expected 2 TS5053 diagnostics (one per key), got: {}",
count
);
}
#[test]
fn test_ts5053_not_emitted_without_conflict() {
let source = r#"{"compilerOptions":{"sourceMap":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5053),
"Should NOT emit TS5053 for sourceMap alone, got: {:?}",
codes
);
}
#[test]
fn test_ts5053_allow_js_with_isolated_declarations() {
let source = r#"{"compilerOptions":{"allowJs":true,"isolatedDeclarations":true,"declaration":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5053),
"Expected TS5053 for allowJs with isolatedDeclarations, got: {:?}",
codes
);
}
#[test]
fn test_ts5070_resolve_json_module_with_classic_module_resolution() {
let source =
r#"{"compilerOptions":{"resolveJsonModule":true,"moduleResolution":"classic"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5070),
"Expected TS5070 for resolveJsonModule with classic moduleResolution, got: {:?}",
codes
);
}
#[test]
fn test_ts5070_resolve_json_module_with_amd_module() {
let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"amd"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5070),
"Expected TS5070 for resolveJsonModule with module=amd (implies classic), got: {:?}",
codes
);
}
#[test]
fn test_ts5071_resolve_json_module_with_system_module() {
let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"system"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5071),
"Expected TS5071 for resolveJsonModule with module=system, got: {:?}",
codes
);
}
#[test]
fn test_ts5071_resolve_json_module_with_none_module() {
let source = r#"{"compilerOptions":{"resolveJsonModule":true,"module":"none"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5071),
"Expected TS5071 for resolveJsonModule with module=none, got: {:?}",
codes
);
}
#[test]
fn test_ts5098_resolve_package_json_with_classic() {
let source = r#"{"compilerOptions":{"resolvePackageJsonExports":true,"moduleResolution":"classic"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&5098),
"Expected TS5098 for resolvePackageJsonExports with classic moduleResolution, got: {:?}",
codes
);
}
#[test]
fn test_ts5098_not_emitted_with_bundler() {
let source = r#"{"compilerOptions":{"resolvePackageJsonExports":true,"moduleResolution":"bundler"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&5098),
"Should NOT emit TS5098 with bundler moduleResolution, got: {:?}",
codes
);
}
#[test]
fn test_ts6082_outfile_with_commonjs() {
let source = r#"{"compilerOptions":{"module":"commonjs","outFile":"all.js"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&6082),
"Expected TS6082 for outFile+commonjs, got: {codes:?}"
);
let count = codes.iter().filter(|&&c| c == 6082).count();
assert_eq!(
count, 2,
"Expected two TS6082 diagnostics (module + outFile keys), got {count}"
);
}
#[test]
fn test_ts6082_outfile_with_umd() {
let source = r#"{"compilerOptions":{"module":"umd","outFile":"all.js"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&6082),
"Expected TS6082 for outFile+umd, got: {codes:?}"
);
}
#[test]
fn test_ts6082_outfile_with_es6() {
let source = r#"{"compilerOptions":{"module":"es6","outFile":"all.js"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
codes.contains(&6082),
"Expected TS6082 for outFile+es6, got: {codes:?}"
);
}
#[test]
fn test_ts6082_not_emitted_for_amd() {
let source = r#"{"compilerOptions":{"module":"amd","outFile":"all.js"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&6082),
"Should NOT emit TS6082 for outFile+amd, got: {codes:?}"
);
}
#[test]
fn test_ts6082_not_emitted_for_system() {
let source = r#"{"compilerOptions":{"module":"system","outFile":"all.js"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&6082),
"Should NOT emit TS6082 for outFile+system, got: {codes:?}"
);
}
#[test]
fn test_ts6082_not_emitted_with_emit_declaration_only() {
let source = r#"{"compilerOptions":{"module":"commonjs","outFile":"all.js","emitDeclarationOnly":true,"declaration":true}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&6082),
"Should NOT emit TS6082 when emitDeclarationOnly is true, got: {codes:?}"
);
}
#[test]
fn test_ts6082_not_emitted_without_outfile() {
let source = r#"{"compilerOptions":{"module":"commonjs"}}"#;
let parsed = parse_tsconfig_with_diagnostics(source, "tsconfig.json").unwrap();
let codes: Vec<u32> = parsed.diagnostics.iter().map(|d| d.code).collect();
assert!(
!codes.contains(&6082),
"Should NOT emit TS6082 without outFile, got: {codes:?}"
);
}
}