From 6126b6b4cc7549635d28365f1ee28ce9478e3fb7 Mon Sep 17 00:00:00 2001 From: Kyle Cannon Date: Tue, 26 May 2026 22:35:56 -0700 Subject: [PATCH 1/3] feat(tsconfig): parse `strict` and `strictNullChecks` compiler options Add `strict` and `strictNullChecks` to `CompilerOptions`, inherit them independently through `extends`, and expose `effective_strict_null_checks()` which applies TypeScript's rule that an explicit `strictNullChecks` overrides the value implied by `strict` (returning `None` when neither is set, leaving the default to the consumer). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/tests/tsconfig_extends.rs | 97 +++++++++++++++++++++++++++++++++++ src/tsconfig.rs | 30 +++++++++++ 2 files changed, 127 insertions(+) diff --git a/src/tests/tsconfig_extends.rs b/src/tests/tsconfig_extends.rs index 2d4541219..cba93fe35 100644 --- a/src/tests/tsconfig_extends.rs +++ b/src/tests/tsconfig_extends.rs @@ -398,3 +398,100 @@ fn test_extend_tsconfig_via_symlink_relative() { let f = super::fixture_root().join("tsconfig/cases/extends-symlink"); assert_extends_symlink_resolves_to_canonical(&f.join("project/tsconfig.relative.json")); } + +#[test] +fn test_effective_strict_null_checks_resolution() { + let path = Path::new("/p/tsconfig.json"); + + // strict: true alone implies strictNullChecks + let cfg = TsConfig::parse( + true, + path, + path, + serde_json::json!({ "compilerOptions": { "strict": true } }).to_string(), + ) + .unwrap() + .build(); + assert_eq!(cfg.compiler_options.strict, Some(true)); + assert_eq!(cfg.compiler_options.strict_null_checks, None); + assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(true)); + + // explicit strictNullChecks: false overrides strict: true + let cfg = TsConfig::parse( + true, + path, + path, + serde_json::json!({ + "compilerOptions": { "strict": true, "strictNullChecks": false } + }) + .to_string(), + ) + .unwrap() + .build(); + assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(false)); + + // strict: false alone disables strictNullChecks (TypeScript's `strict` is the + // default for the flag, so an absent strictNullChecks resolves to `strict`). + let cfg = TsConfig::parse( + true, + path, + path, + serde_json::json!({ "compilerOptions": { "strict": false } }).to_string(), + ) + .unwrap() + .build(); + assert_eq!(cfg.compiler_options.strict, Some(false)); + assert_eq!(cfg.compiler_options.strict_null_checks, None); + assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(false)); + + // neither set -> None + let cfg = + TsConfig::parse(true, path, path, serde_json::json!({ "compilerOptions": {} }).to_string()) + .unwrap() + .build(); + assert_eq!(cfg.compiler_options.effective_strict_null_checks(), None); +} + +#[test] +fn test_extend_tsconfig_strict_null_checks() { + let parent_path = Path::new("/parent/tsconfig.json"); + let child_path = Path::new("/child/tsconfig.json"); + + // Child's explicit strictNullChecks: false survives inheriting parent strict: true. + let parent = TsConfig::parse( + true, + parent_path, + parent_path, + serde_json::json!({ "compilerOptions": { "strict": true } }).to_string(), + ) + .unwrap() + .build(); + let mut child = TsConfig::parse( + true, + child_path, + child_path, + serde_json::json!({ "compilerOptions": { "strictNullChecks": false } }).to_string(), + ) + .unwrap(); + child.extend_tsconfig(&parent); + let child = child.build(); + + assert_eq!(child.compiler_options.strict, Some(true)); // inherited + assert_eq!(child.compiler_options.strict_null_checks, Some(false)); // own value kept + assert_eq!(child.compiler_options.effective_strict_null_checks(), Some(false)); + + // Child sets neither -> inherits parent's strict, accessor implies true. + let mut child2 = TsConfig::parse( + true, + child_path, + child_path, + serde_json::json!({ "compilerOptions": {} }).to_string(), + ) + .unwrap(); + child2.extend_tsconfig(&parent); + let child2 = child2.build(); + + assert_eq!(child2.compiler_options.strict, Some(true)); + assert_eq!(child2.compiler_options.strict_null_checks, None); + assert_eq!(child2.compiler_options.effective_strict_null_checks(), Some(true)); +} diff --git a/src/tsconfig.rs b/src/tsconfig.rs index d9ae4065a..af605c2ff 100644 --- a/src/tsconfig.rs +++ b/src/tsconfig.rs @@ -243,6 +243,18 @@ impl TsConfig { compiler_options.emit_decorator_metadata = Some(*emit_decorator_metadata); } + if compiler_options.strict.is_none() + && let Some(strict) = &tsconfig.compiler_options.strict + { + compiler_options.strict = Some(*strict); + } + + if compiler_options.strict_null_checks.is_none() + && let Some(strict_null_checks) = &tsconfig.compiler_options.strict_null_checks + { + compiler_options.strict_null_checks = Some(*strict_null_checks); + } + if compiler_options.use_define_for_class_fields.is_none() && let Some(use_define_for_class_fields) = &tsconfig.compiler_options.use_define_for_class_fields @@ -488,6 +500,12 @@ pub struct CompilerOptions { /// pub emit_decorator_metadata: Option, + /// + pub strict: Option, + + /// + pub strict_null_checks: Option, + /// pub use_define_for_class_fields: Option, @@ -528,6 +546,18 @@ pub struct CompilerOptions { pub root_dirs: Option>, } +impl CompilerOptions { + /// Returns the effective `strictNullChecks` value. + /// + /// An explicit `strictNullChecks` always takes precedence over the `strict` + /// umbrella flag, in either direction. Returns `None` when neither is set; + /// the consumer is responsible for applying the appropriate default. + #[must_use] + pub fn effective_strict_null_checks(&self) -> Option { + self.strict_null_checks.or(self.strict) + } +} + /// Value for the "extends" field. /// /// From 6ac7f761beee993f4abaf2be981a8ff021d654a6 Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 27 May 2026 14:17:05 +0800 Subject: [PATCH 2/3] refactor(tsconfig): drop effective_strict_null_checks helper --- src/tests/tsconfig_extends.rs | 55 ----------------------------------- src/tsconfig.rs | 12 -------- 2 files changed, 67 deletions(-) diff --git a/src/tests/tsconfig_extends.rs b/src/tests/tsconfig_extends.rs index cba93fe35..044c2f775 100644 --- a/src/tests/tsconfig_extends.rs +++ b/src/tests/tsconfig_extends.rs @@ -399,59 +399,6 @@ fn test_extend_tsconfig_via_symlink_relative() { assert_extends_symlink_resolves_to_canonical(&f.join("project/tsconfig.relative.json")); } -#[test] -fn test_effective_strict_null_checks_resolution() { - let path = Path::new("/p/tsconfig.json"); - - // strict: true alone implies strictNullChecks - let cfg = TsConfig::parse( - true, - path, - path, - serde_json::json!({ "compilerOptions": { "strict": true } }).to_string(), - ) - .unwrap() - .build(); - assert_eq!(cfg.compiler_options.strict, Some(true)); - assert_eq!(cfg.compiler_options.strict_null_checks, None); - assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(true)); - - // explicit strictNullChecks: false overrides strict: true - let cfg = TsConfig::parse( - true, - path, - path, - serde_json::json!({ - "compilerOptions": { "strict": true, "strictNullChecks": false } - }) - .to_string(), - ) - .unwrap() - .build(); - assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(false)); - - // strict: false alone disables strictNullChecks (TypeScript's `strict` is the - // default for the flag, so an absent strictNullChecks resolves to `strict`). - let cfg = TsConfig::parse( - true, - path, - path, - serde_json::json!({ "compilerOptions": { "strict": false } }).to_string(), - ) - .unwrap() - .build(); - assert_eq!(cfg.compiler_options.strict, Some(false)); - assert_eq!(cfg.compiler_options.strict_null_checks, None); - assert_eq!(cfg.compiler_options.effective_strict_null_checks(), Some(false)); - - // neither set -> None - let cfg = - TsConfig::parse(true, path, path, serde_json::json!({ "compilerOptions": {} }).to_string()) - .unwrap() - .build(); - assert_eq!(cfg.compiler_options.effective_strict_null_checks(), None); -} - #[test] fn test_extend_tsconfig_strict_null_checks() { let parent_path = Path::new("/parent/tsconfig.json"); @@ -478,7 +425,6 @@ fn test_extend_tsconfig_strict_null_checks() { assert_eq!(child.compiler_options.strict, Some(true)); // inherited assert_eq!(child.compiler_options.strict_null_checks, Some(false)); // own value kept - assert_eq!(child.compiler_options.effective_strict_null_checks(), Some(false)); // Child sets neither -> inherits parent's strict, accessor implies true. let mut child2 = TsConfig::parse( @@ -493,5 +439,4 @@ fn test_extend_tsconfig_strict_null_checks() { assert_eq!(child2.compiler_options.strict, Some(true)); assert_eq!(child2.compiler_options.strict_null_checks, None); - assert_eq!(child2.compiler_options.effective_strict_null_checks(), Some(true)); } diff --git a/src/tsconfig.rs b/src/tsconfig.rs index af605c2ff..38fe4f5f9 100644 --- a/src/tsconfig.rs +++ b/src/tsconfig.rs @@ -546,18 +546,6 @@ pub struct CompilerOptions { pub root_dirs: Option>, } -impl CompilerOptions { - /// Returns the effective `strictNullChecks` value. - /// - /// An explicit `strictNullChecks` always takes precedence over the `strict` - /// umbrella flag, in either direction. Returns `None` when neither is set; - /// the consumer is responsible for applying the appropriate default. - #[must_use] - pub fn effective_strict_null_checks(&self) -> Option { - self.strict_null_checks.or(self.strict) - } -} - /// Value for the "extends" field. /// /// From d4e53e353f08c4c20ee8e3cd660ac7f29b1599d4 Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 27 May 2026 14:22:13 +0800 Subject: [PATCH 3/3] refactor(tsconfig): drop test_extend_tsconfig_strict_null_checks --- src/tests/tsconfig_extends.rs | 42 ----------------------------------- 1 file changed, 42 deletions(-) diff --git a/src/tests/tsconfig_extends.rs b/src/tests/tsconfig_extends.rs index 044c2f775..2d4541219 100644 --- a/src/tests/tsconfig_extends.rs +++ b/src/tests/tsconfig_extends.rs @@ -398,45 +398,3 @@ fn test_extend_tsconfig_via_symlink_relative() { let f = super::fixture_root().join("tsconfig/cases/extends-symlink"); assert_extends_symlink_resolves_to_canonical(&f.join("project/tsconfig.relative.json")); } - -#[test] -fn test_extend_tsconfig_strict_null_checks() { - let parent_path = Path::new("/parent/tsconfig.json"); - let child_path = Path::new("/child/tsconfig.json"); - - // Child's explicit strictNullChecks: false survives inheriting parent strict: true. - let parent = TsConfig::parse( - true, - parent_path, - parent_path, - serde_json::json!({ "compilerOptions": { "strict": true } }).to_string(), - ) - .unwrap() - .build(); - let mut child = TsConfig::parse( - true, - child_path, - child_path, - serde_json::json!({ "compilerOptions": { "strictNullChecks": false } }).to_string(), - ) - .unwrap(); - child.extend_tsconfig(&parent); - let child = child.build(); - - assert_eq!(child.compiler_options.strict, Some(true)); // inherited - assert_eq!(child.compiler_options.strict_null_checks, Some(false)); // own value kept - - // Child sets neither -> inherits parent's strict, accessor implies true. - let mut child2 = TsConfig::parse( - true, - child_path, - child_path, - serde_json::json!({ "compilerOptions": {} }).to_string(), - ) - .unwrap(); - child2.extend_tsconfig(&parent); - let child2 = child2.build(); - - assert_eq!(child2.compiler_options.strict, Some(true)); - assert_eq!(child2.compiler_options.strict_null_checks, None); -}