Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"options": {
"typeAware": true,
"typeCheck": false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"options": {
"typeAware": true,
"typeCheck": true
}
}
6 changes: 6 additions & 0 deletions apps/oxlint/src-js/package/config.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,12 @@ export interface OxlintOptions {
* Equivalent to passing `--type-aware` on the CLI.
*/
typeAware?: boolean | null;
/**
* Enable experimental type checking (includes TypeScript compiler diagnostics).
*
* Equivalent to passing `--type-check` on the CLI.
*/
typeCheck?: boolean | null;
}
export interface OxlintOverride {
/**
Expand Down
126 changes: 116 additions & 10 deletions apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,17 @@ impl<'a> ConfigLoader<'a> {
.and_then(|root| path.parent().map(|parent| parent == root))
.unwrap_or(false);

if builder.type_aware().is_some() && !is_root_config {
errors.push(ConfigLoadError::Diagnostic(nested_type_aware_not_supported(&path)));
continue;
if !is_root_config {
if builder.type_aware().is_some() {
errors
.push(ConfigLoadError::Diagnostic(nested_type_aware_not_supported(&path)));
continue;
}
if builder.type_check().is_some() {
errors
.push(ConfigLoadError::Diagnostic(nested_type_check_not_supported(&path)));
continue;
}
}

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

fn nested_type_check_not_supported(path: &Path) -> OxcDiagnostic {
OxcDiagnostic::error(format!(
"The `options.typeCheck` option is only supported in the root config, but it was found in {}.",
path.display()
))
.with_help("Move `options.typeCheck` to the root configuration file.")
}

#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};
Expand All @@ -655,10 +671,15 @@ mod test {
}

#[cfg(feature = "napi")]
fn make_js_config(path: PathBuf, type_aware: Option<bool>) -> JsConfigResult {
let mut config: oxc_linter::Oxlintrc =
serde_json::from_value(serde_json::json!({ "options": { "typeAware": type_aware } }))
.unwrap();
fn make_js_config(
path: PathBuf,
type_aware: Option<bool>,
type_check: Option<bool>,
) -> JsConfigResult {
let mut config: oxc_linter::Oxlintrc = serde_json::from_value(serde_json::json!({
"options": { "typeAware": type_aware, "typeCheck": type_check }
}))
.unwrap();
config.path = path.clone();
if let Some(config_dir) = path.parent() {
config.set_config_dir(config_dir);
Expand Down Expand Up @@ -773,7 +794,7 @@ mod test {
let js_loader = make_js_loader(move |paths| {
Ok(paths
.into_iter()
.map(|path| make_js_config(PathBuf::from(path), Some(true)))
.map(|path| make_js_config(PathBuf::from(path), Some(true), None))
.collect())
});
let loader = loader.with_js_config_loader(Some(&js_loader));
Expand All @@ -785,6 +806,31 @@ mod test {
assert_eq!(config.options.type_aware, Some(true));
}

#[cfg(feature = "napi")]
#[test]
fn test_root_oxlint_config_ts_allows_type_check() {
let root_dir = tempfile::tempdir().unwrap();
let root_path = root_dir.path().join("oxlint.config.ts");
std::fs::write(&root_path, "export default {};").unwrap();

let mut external_plugin_store = ExternalPluginStore::new(false);
let loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);

let js_loader = make_js_loader(move |paths| {
Ok(paths
.into_iter()
.map(|path| make_js_config(PathBuf::from(path), None, Some(true)))
.collect())
});
let loader = loader.with_js_config_loader(Some(&js_loader));

let config = loader
.load_root_config(root_dir.path(), Some(&PathBuf::from("oxlint.config.ts")))
.unwrap();

assert_eq!(config.options.type_check, Some(true));
}

#[cfg(feature = "napi")]
#[test]
fn test_nested_oxlint_config_ts_rejects_type_aware() {
Expand All @@ -799,7 +845,32 @@ mod test {
let js_loader = make_js_loader(move |paths| {
Ok(paths
.into_iter()
.map(|path| make_js_config(PathBuf::from(path), Some(false)))
.map(|path| make_js_config(PathBuf::from(path), Some(false), None))
.collect())
});
loader = loader.with_js_config_loader(Some(&js_loader));

let (_configs, errors) = loader
.load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Js(nested_path)]);
assert_eq!(errors.len(), 1);
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
}

#[cfg(feature = "napi")]
#[test]
fn test_nested_oxlint_config_ts_rejects_type_check() {
let root_dir = tempfile::tempdir().unwrap();
let nested_path = root_dir.path().join("nested/oxlint.config.ts");
std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap();
std::fs::write(&nested_path, "export default {};").unwrap();

let mut external_plugin_store = ExternalPluginStore::new(false);
let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);

let js_loader = make_js_loader(move |paths| {
Ok(paths
.into_iter()
.map(|path| make_js_config(PathBuf::from(path), None, Some(false)))
.collect())
});
loader = loader.with_js_config_loader(Some(&js_loader));
Expand All @@ -826,7 +897,7 @@ mod test {
.into_iter()
.map(|path| {
let path = PathBuf::from(path);
let mut config = make_js_config(path.clone(), None).config;
let mut config = make_js_config(path.clone(), None, None).config;
config.extends_configs = vec![
serde_json::from_value(
serde_json::json!({ "options": { "typeAware": true } }),
Expand All @@ -844,4 +915,39 @@ mod test {
assert_eq!(errors.len(), 1);
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
}

#[cfg(feature = "napi")]
#[test]
fn test_nested_oxlint_config_ts_rejects_type_check_from_extends() {
let root_dir = tempfile::tempdir().unwrap();
let nested_path = root_dir.path().join("nested/oxlint.config.ts");
std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap();
std::fs::write(&nested_path, "export default {};").unwrap();

let mut external_plugin_store = ExternalPluginStore::new(false);
let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);

let js_loader = make_js_loader(move |paths| {
Ok(paths
.into_iter()
.map(|path| {
let path = PathBuf::from(path);
let mut config = make_js_config(path.clone(), None, None).config;
config.extends_configs = vec![
serde_json::from_value(
serde_json::json!({ "options": { "typeCheck": true } }),
)
.unwrap(),
];
JsConfigResult { path, config }
})
.collect())
});
loader = loader.with_js_config_loader(Some(&js_loader));

let (_configs, errors) = loader
.load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Js(nested_path)]);
assert_eq!(errors.len(), 1);
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
}
}
24 changes: 23 additions & 1 deletion apps/oxlint/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ impl CliRunner {

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

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

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_type_check_via_config_file() {
let args = &["-c", "config-type-check.json"];
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_type_check_false_via_config_file() {
let args = &["-c", "config-type-check-false.json"];
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_type_check_false_overridden_by_cli_flag() {
let args = &["--type-check", "-c", "config-type-check-false.json"];
Tester::new().with_cwd("fixtures/tsgolint_type_error".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_no_typescript_files() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
source: apps/oxlint/src/tester.rs
---
##########
arguments: --type-check -c config-type-check-false.json
working directory: fixtures/tsgolint_type_error
----------

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.js:2:7]
1 | "use strict";
2 | const foo = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.ts:1:7]
1 | const foo: number = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

x typescript(TS2322): Type 'string' is not assignable to type 'number'.
,-[index.ts:1:7]
1 | const foo: number = "42";
: ^^^
`----

Found 2 warnings and 1 error.
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: apps/oxlint/src/tester.rs
---
##########
arguments: -c config-type-check-false.json
working directory: fixtures/tsgolint_type_error
----------

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.js:2:7]
1 | "use strict";
2 | const foo = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.ts:1:7]
1 | const foo: number = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

Found 2 warnings and 0 errors.
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
----------
CLI result: LintSucceeded
----------
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
source: apps/oxlint/src/tester.rs
---
##########
arguments: -c config-type-check.json
working directory: fixtures/tsgolint_type_error
----------

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.js:2:7]
1 | "use strict";
2 | const foo = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

! eslint(no-unused-vars): Variable 'foo' is declared but never used. Unused variables should start with a '_'.
,-[index.ts:1:7]
1 | const foo: number = "42";
: ^|^
: `-- 'foo' is declared here
`----
help: Consider removing this declaration.

x typescript(TS2322): Type 'string' is not assignable to type 'number'.
,-[index.ts:1:7]
1 | const foo: number = "42";
: ^^^
`----

Found 2 warnings and 1 error.
Finished in <variable>ms on 2 files with 107 rules using 1 threads.
----------
CLI result: LintFoundErrors
----------
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const floating = Promise.resolve("ok");
floating;

const value: number = "42";
void value;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Exit code
1

# stdout
```
x typescript-eslint(no-floating-promises): Promises must be awaited, add void operator to ignore.
,-[files/test.ts:2:1]
1 | const floating = Promise.resolve("ok");
2 | floating;
: ^^^^^^^^^
3 |
`----
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.
x typescript(TS2322): Type 'string' is not assignable to type 'number'.
,-[files/test.ts:4:7]
3 |
4 | const value: number = "42";
: ^^^^^
5 | void value;
`----
Found 0 warnings and 2 errors.
Finished in Xms on 1 file with 1 rules using X threads.
```

# stderr
```
```
Loading
Loading