Skip to content

Commit f34f6fa

Browse files
committed
feat(linter): introduce typeCheck config option (#19764)
1 parent 95d5d66 commit f34f6fa

19 files changed

+434
-20
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"options": {
3+
"typeAware": true,
4+
"typeCheck": false
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"options": {
3+
"typeAware": true,
4+
"typeCheck": true
5+
}
6+
}

apps/oxlint/src-js/package/config.generated.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,12 @@ export interface OxlintOptions {
318318
* Equivalent to passing `--type-aware` on the CLI.
319319
*/
320320
typeAware?: boolean | null;
321+
/**
322+
* Enable experimental type checking (includes TypeScript compiler diagnostics).
323+
*
324+
* Equivalent to passing `--type-check` on the CLI.
325+
*/
326+
typeCheck?: boolean | null;
321327
}
322328
export interface OxlintOverride {
323329
/**

apps/oxlint/src/config_loader.rs

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,17 @@ impl<'a> ConfigLoader<'a> {
376376
.and_then(|root| path.parent().map(|parent| parent == root))
377377
.unwrap_or(false);
378378

379-
if builder.type_aware().is_some() && !is_root_config {
380-
errors.push(ConfigLoadError::Diagnostic(nested_type_aware_not_supported(&path)));
381-
continue;
379+
if !is_root_config {
380+
if builder.type_aware().is_some() {
381+
errors
382+
.push(ConfigLoadError::Diagnostic(nested_type_aware_not_supported(&path)));
383+
continue;
384+
}
385+
if builder.type_check().is_some() {
386+
errors
387+
.push(ConfigLoadError::Diagnostic(nested_type_check_not_supported(&path)));
388+
continue;
389+
}
382390
}
383391

384392
let extended_paths = builder.extended_paths.clone();
@@ -633,6 +641,14 @@ fn nested_type_aware_not_supported(path: &Path) -> OxcDiagnostic {
633641
.with_help("Move `options.typeAware` to the root configuration file.")
634642
}
635643

644+
fn nested_type_check_not_supported(path: &Path) -> OxcDiagnostic {
645+
OxcDiagnostic::error(format!(
646+
"The `options.typeCheck` option is only supported in the root config, but it was found in {}.",
647+
path.display()
648+
))
649+
.with_help("Move `options.typeCheck` to the root configuration file.")
650+
}
651+
636652
#[cfg(test)]
637653
mod test {
638654
use std::path::{Path, PathBuf};
@@ -655,10 +671,15 @@ mod test {
655671
}
656672

657673
#[cfg(feature = "napi")]
658-
fn make_js_config(path: PathBuf, type_aware: Option<bool>) -> JsConfigResult {
659-
let mut config: oxc_linter::Oxlintrc =
660-
serde_json::from_value(serde_json::json!({ "options": { "typeAware": type_aware } }))
661-
.unwrap();
674+
fn make_js_config(
675+
path: PathBuf,
676+
type_aware: Option<bool>,
677+
type_check: Option<bool>,
678+
) -> JsConfigResult {
679+
let mut config: oxc_linter::Oxlintrc = serde_json::from_value(serde_json::json!({
680+
"options": { "typeAware": type_aware, "typeCheck": type_check }
681+
}))
682+
.unwrap();
662683
config.path = path.clone();
663684
if let Some(config_dir) = path.parent() {
664685
config.set_config_dir(config_dir);
@@ -773,7 +794,7 @@ mod test {
773794
let js_loader = make_js_loader(move |paths| {
774795
Ok(paths
775796
.into_iter()
776-
.map(|path| make_js_config(PathBuf::from(path), Some(true)))
797+
.map(|path| make_js_config(PathBuf::from(path), Some(true), None))
777798
.collect())
778799
});
779800
let loader = loader.with_js_config_loader(Some(&js_loader));
@@ -785,6 +806,31 @@ mod test {
785806
assert_eq!(config.options.type_aware, Some(true));
786807
}
787808

809+
#[cfg(feature = "napi")]
810+
#[test]
811+
fn test_root_oxlint_config_ts_allows_type_check() {
812+
let root_dir = tempfile::tempdir().unwrap();
813+
let root_path = root_dir.path().join("oxlint.config.ts");
814+
std::fs::write(&root_path, "export default {};").unwrap();
815+
816+
let mut external_plugin_store = ExternalPluginStore::new(false);
817+
let loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);
818+
819+
let js_loader = make_js_loader(move |paths| {
820+
Ok(paths
821+
.into_iter()
822+
.map(|path| make_js_config(PathBuf::from(path), None, Some(true)))
823+
.collect())
824+
});
825+
let loader = loader.with_js_config_loader(Some(&js_loader));
826+
827+
let config = loader
828+
.load_root_config(root_dir.path(), Some(&PathBuf::from("oxlint.config.ts")))
829+
.unwrap();
830+
831+
assert_eq!(config.options.type_check, Some(true));
832+
}
833+
788834
#[cfg(feature = "napi")]
789835
#[test]
790836
fn test_nested_oxlint_config_ts_rejects_type_aware() {
@@ -799,7 +845,32 @@ mod test {
799845
let js_loader = make_js_loader(move |paths| {
800846
Ok(paths
801847
.into_iter()
802-
.map(|path| make_js_config(PathBuf::from(path), Some(false)))
848+
.map(|path| make_js_config(PathBuf::from(path), Some(false), None))
849+
.collect())
850+
});
851+
loader = loader.with_js_config_loader(Some(&js_loader));
852+
853+
let (_configs, errors) = loader
854+
.load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Js(nested_path)]);
855+
assert_eq!(errors.len(), 1);
856+
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
857+
}
858+
859+
#[cfg(feature = "napi")]
860+
#[test]
861+
fn test_nested_oxlint_config_ts_rejects_type_check() {
862+
let root_dir = tempfile::tempdir().unwrap();
863+
let nested_path = root_dir.path().join("nested/oxlint.config.ts");
864+
std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap();
865+
std::fs::write(&nested_path, "export default {};").unwrap();
866+
867+
let mut external_plugin_store = ExternalPluginStore::new(false);
868+
let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);
869+
870+
let js_loader = make_js_loader(move |paths| {
871+
Ok(paths
872+
.into_iter()
873+
.map(|path| make_js_config(PathBuf::from(path), None, Some(false)))
803874
.collect())
804875
});
805876
loader = loader.with_js_config_loader(Some(&js_loader));
@@ -826,7 +897,7 @@ mod test {
826897
.into_iter()
827898
.map(|path| {
828899
let path = PathBuf::from(path);
829-
let mut config = make_js_config(path.clone(), None).config;
900+
let mut config = make_js_config(path.clone(), None, None).config;
830901
config.extends_configs = vec![
831902
serde_json::from_value(
832903
serde_json::json!({ "options": { "typeAware": true } }),
@@ -844,4 +915,39 @@ mod test {
844915
assert_eq!(errors.len(), 1);
845916
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
846917
}
918+
919+
#[cfg(feature = "napi")]
920+
#[test]
921+
fn test_nested_oxlint_config_ts_rejects_type_check_from_extends() {
922+
let root_dir = tempfile::tempdir().unwrap();
923+
let nested_path = root_dir.path().join("nested/oxlint.config.ts");
924+
std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap();
925+
std::fs::write(&nested_path, "export default {};").unwrap();
926+
927+
let mut external_plugin_store = ExternalPluginStore::new(false);
928+
let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);
929+
930+
let js_loader = make_js_loader(move |paths| {
931+
Ok(paths
932+
.into_iter()
933+
.map(|path| {
934+
let path = PathBuf::from(path);
935+
let mut config = make_js_config(path.clone(), None, None).config;
936+
config.extends_configs = vec![
937+
serde_json::from_value(
938+
serde_json::json!({ "options": { "typeCheck": true } }),
939+
)
940+
.unwrap(),
941+
];
942+
JsConfigResult { path, config }
943+
})
944+
.collect())
945+
});
946+
loader = loader.with_js_config_loader(Some(&js_loader));
947+
948+
let (_configs, errors) = loader
949+
.load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Js(nested_path)]);
950+
assert_eq!(errors.len(), 1);
951+
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
952+
}
847953
}

apps/oxlint/src/lint.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ impl CliRunner {
348348

349349
let config_store = ConfigStore::new(lint_config, nested_configs, external_plugin_store);
350350
let type_aware = self.options.type_aware || config_store.type_aware_enabled();
351+
let type_check = self.options.type_check || config_store.type_check_enabled();
351352

352353
// Send JS plugins config to JS side
353354
if let Some(external_linter) = &external_linter {
@@ -400,7 +401,7 @@ impl CliRunner {
400401
// TODO: Add a warning message if `tsgolint` cannot be found, but type-aware rules are enabled
401402
let lint_runner = match LintRunner::builder(options, linter)
402403
.with_type_aware(type_aware)
403-
.with_type_check(self.options.type_check)
404+
.with_type_check(type_check)
404405
.with_silent(misc_options.silent)
405406
.with_fix_kind(fix_options.fix_kind())
406407
.build()
@@ -1289,6 +1290,27 @@ mod test {
12891290
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
12901291
}
12911292

1293+
#[test]
1294+
#[cfg(not(target_endian = "big"))]
1295+
fn test_tsgolint_type_check_via_config_file() {
1296+
let args = &["-c", "config-type-check.json"];
1297+
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
1298+
}
1299+
1300+
#[test]
1301+
#[cfg(not(target_endian = "big"))]
1302+
fn test_tsgolint_type_check_false_via_config_file() {
1303+
let args = &["-c", "config-type-check-false.json"];
1304+
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
1305+
}
1306+
1307+
#[test]
1308+
#[cfg(not(target_endian = "big"))]
1309+
fn test_tsgolint_type_check_false_overridden_by_cli_flag() {
1310+
let args = &["--type-check", "-c", "config-type-check-false.json"];
1311+
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
1312+
}
1313+
12921314
#[test]
12931315
#[cfg(not(target_endian = "big"))]
12941316
fn test_tsgolint_no_typescript_files() {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: --type-check -c config-type-check-false.json
6+
working directory: fixtures/tsgolint_type_error
7+
----------
8+
9+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
10+
,-[index.js:2:7]
11+
1 | "use strict";
12+
2 | const foo = "42";
13+
: ^|^
14+
: `-- 'foo' is declared here
15+
`----
16+
help: Consider removing this declaration.
17+
18+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
19+
,-[index.ts:1:7]
20+
1 | const foo: number = "42";
21+
: ^|^
22+
: `-- 'foo' is declared here
23+
`----
24+
help: Consider removing this declaration.
25+
26+
x typescript(TS2322): Type 'string' is not assignable to type 'number'.
27+
,-[index.ts:1:7]
28+
1 | const foo: number = "42";
29+
: ^^^
30+
`----
31+
32+
Found 2 warnings and 1 error.
33+
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
34+
----------
35+
CLI result: LintFoundErrors
36+
----------
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: -c config-type-check-false.json
6+
working directory: fixtures/tsgolint_type_error
7+
----------
8+
9+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
10+
,-[index.js:2:7]
11+
1 | "use strict";
12+
2 | const foo = "42";
13+
: ^|^
14+
: `-- 'foo' is declared here
15+
`----
16+
help: Consider removing this declaration.
17+
18+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
19+
,-[index.ts:1:7]
20+
1 | const foo: number = "42";
21+
: ^|^
22+
: `-- 'foo' is declared here
23+
`----
24+
help: Consider removing this declaration.
25+
26+
Found 2 warnings and 0 errors.
27+
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
28+
----------
29+
CLI result: LintSucceeded
30+
----------
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: -c config-type-check.json
6+
working directory: fixtures/tsgolint_type_error
7+
----------
8+
9+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
10+
,-[index.js:2:7]
11+
1 | "use strict";
12+
2 | const foo = "42";
13+
: ^|^
14+
: `-- 'foo' is declared here
15+
`----
16+
help: Consider removing this declaration.
17+
18+
! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
19+
,-[index.ts:1:7]
20+
1 | const foo: number = "42";
21+
: ^|^
22+
: `-- 'foo' is declared here
23+
`----
24+
help: Consider removing this declaration.
25+
26+
x typescript(TS2322): Type 'string' is not assignable to type 'number'.
27+
,-[index.ts:1:7]
28+
1 | const foo: number = "42";
29+
: ^^^
30+
`----
31+
32+
Found 2 warnings and 1 error.
33+
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
34+
----------
35+
CLI result: LintFoundErrors
36+
----------
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const floating = Promise.resolve("ok");
2+
floating;
3+
4+
const value: number = "42";
5+
void value;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Exit code
2+
1
3+
4+
# stdout
5+
```
6+
x typescript-eslint(no-floating-promises): Promises must be awaited, add void operator to ignore.
7+
,-[files/test.ts:2:1]
8+
1 | const floating = Promise.resolve("ok");
9+
2 | floating;
10+
: ^^^^^^^^^
11+
3 |
12+
`----
13+
help: The promise must end with a call to .catch, or end with a call to .then with a rejection handler, or be explicitly marked as ignored with the `void` operator.
14+
15+
x typescript(TS2322): Type 'string' is not assignable to type 'number'.
16+
,-[files/test.ts:4:7]
17+
3 |
18+
4 | const value: number = "42";
19+
: ^^^^^
20+
5 | void value;
21+
`----
22+
23+
Found 0 warnings and 2 errors.
24+
Finished in Xms on 1 file with 1 rules using X threads.
25+
```
26+
27+
# stderr
28+
```
29+
```

0 commit comments

Comments
 (0)