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,5 @@
{
"rules": {
"no-debugger": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"options": {
"typeAware": true
},
"rules": {
"typescript/no-floating-promises": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const promise = new Promise((resolve, _reject) => resolve("value"));
promise;

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const promise = new Promise((resolve, _reject) => resolve("value"));
promise;

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"options": {
"typeAware": true
},
"rules": {
"typescript/no-floating-promises": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const promise = new Promise((resolve, _reject) => resolve("value"));
promise;

export {};
4 changes: 4 additions & 0 deletions apps/oxlint/fixtures/lsp/tsgolint/type_aware_config/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const promise = new Promise((resolve, _reject) => resolve("value"));
promise;

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"options": {
"typeAware": false
},
"rules": {
"typescript/no-floating-promises": "off"
},
"overrides": [
{
"files": ["*.ts"],
"rules": {
"typescript/no-floating-promises": "error"
}
}
]
}
8 changes: 8 additions & 0 deletions apps/oxlint/fixtures/tsgolint/config-type-aware-false.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"options": {
"typeAware": false
},
"rules": {
"typescript/no-floating-promises": "error"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"options": {
"typeAware": true
},
"rules": {
"typescript/no-floating-promises": "off"
},
"overrides": [
{
"files": ["*.ts"],
"rules": {
"typescript/no-floating-promises": "error"
}
}
]
}
8 changes: 8 additions & 0 deletions apps/oxlint/fixtures/tsgolint/config-type-aware.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"options": {
"typeAware": true
},
"extends": [
"./extended-config.json"
]
}
15 changes: 15 additions & 0 deletions apps/oxlint/src-js/package/config.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ export interface Oxlintrc {
* ```
*/
jsPlugins?: null | ExternalPluginEntry[];
/**
* Oxlint config options.
*/
options?: OxlintOptions;
/**
* Add, remove, or otherwise reconfigure rules for specific files or groups of files.
*/
Expand Down Expand Up @@ -304,6 +308,17 @@ export interface OxlintEnv {
export interface OxlintGlobals {
[k: string]: GlobalValue;
}
/**
* Options for the linter.
*/
export interface OxlintOptions {
/**
* Enable rules that require type information.
*
* Equivalent to passing `--type-aware` on the CLI.
*/
typeAware?: boolean | null;
}
export interface OxlintOverride {
/**
* Environments enable and disable collections of global variables.
Expand Down
152 changes: 148 additions & 4 deletions apps/oxlint/src/config_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ impl<'a> ConfigLoader<'a> {
fn load_many(
&mut self,
paths: impl IntoIterator<Item = DiscoveredConfig>,
root_config_dir: Option<&Path>,
) -> (Vec<LoadedConfig>, Vec<ConfigLoadError>) {
let mut configs = Vec::new();
let mut errors = Vec::new();
Expand Down Expand Up @@ -371,6 +372,15 @@ impl<'a> ConfigLoader<'a> {
}
};

let is_root_config = root_config_dir
.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;
}

let extended_paths = builder.extended_paths.clone();

match builder
Expand All @@ -391,11 +401,12 @@ impl<'a> ConfigLoader<'a> {
(built_configs, errors)
}

pub(crate) fn load_discovered(
pub(crate) fn load_discovered_with_root_dir(
&mut self,
root_dir: &Path,
configs: impl IntoIterator<Item = DiscoveredConfig>,
) -> (Vec<LoadedConfig>, Vec<ConfigLoadError>) {
self.load_many(configs)
self.load_many(configs, Some(root_dir))
}

/// Try to load config from a specific directory.
Expand Down Expand Up @@ -544,7 +555,7 @@ impl<'a> ConfigLoader<'a> {
paths.iter().map(|p| Path::new(p.as_ref()).to_path_buf()).collect();
let discovered_configs = discover_configs_in_ancestors(&config_paths);

let (configs, errors) = self.load_many(discovered_configs);
let (configs, errors) = self.load_many(discovered_configs, Some(cwd));

// Fail if any config failed (CLI requires all configs to be valid)
if !errors.is_empty() {
Expand Down Expand Up @@ -614,13 +625,46 @@ fn is_js_config_path(path: &Path) -> bool {
)
}

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

#[cfg(test)]
mod test {
use std::path::{Path, PathBuf};

use oxc_linter::ExternalPluginStore;

use super::{ConfigLoader, is_js_config_path};
use super::{ConfigLoadError, ConfigLoader, DiscoveredConfig, is_js_config_path};
#[cfg(feature = "napi")]
use crate::js_config::{JsConfigLoaderCb, JsConfigResult};

#[cfg(feature = "napi")]
fn make_js_loader<F>(f: F) -> JsConfigLoaderCb
where
F: Fn(Vec<String>) -> Result<Vec<JsConfigResult>, Vec<oxc_diagnostics::OxcDiagnostic>>
+ Send
+ Sync
+ 'static,
{
Box::new(f)
}

#[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();
config.path = path.clone();
if let Some(config_dir) = path.parent() {
config.set_config_dir(config_dir);
}
JsConfigResult { path, config }
}

#[test]
fn test_config_path_with_parent_references() {
Expand Down Expand Up @@ -700,4 +744,104 @@ mod test {
assert!(is_js_config_path(Path::new("my-config.mts")));
assert!(!is_js_config_path(Path::new("oxlint.config.json")));
}

#[test]
fn test_nested_json_config_rejects_type_aware() {
let root_dir = tempfile::tempdir().unwrap();
let nested_path = root_dir.path().join("nested/.oxlintrc.json");
std::fs::create_dir_all(nested_path.parent().unwrap()).unwrap();
std::fs::write(&nested_path, r#"{ "options": { "typeAware": true } }"#).unwrap();

let mut external_plugin_store = ExternalPluginStore::new(false);
let mut loader = ConfigLoader::new(None, &mut external_plugin_store, &[], None);
let (_configs, errors) = loader
.load_discovered_with_root_dir(root_dir.path(), [DiscoveredConfig::Json(nested_path)]);
assert_eq!(errors.len(), 1);
assert!(matches!(errors[0], ConfigLoadError::Diagnostic(_)));
}

#[cfg(feature = "napi")]
#[test]
fn test_root_oxlint_config_ts_allows_type_aware() {
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), 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_aware, Some(true));
}

#[cfg(feature = "napi")]
#[test]
fn test_nested_oxlint_config_ts_rejects_type_aware() {
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), Some(false)))
.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_aware_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).config;
config.extends_configs = vec![
serde_json::from_value(
serde_json::json!({ "options": { "typeAware": 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(_)));
}
}
42 changes: 40 additions & 2 deletions apps/oxlint/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ impl CliRunner {
Self::get_diagnostic_service(&output_formatter, &warning_options, &misc_options);

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

// Send JS plugins config to JS side
if let Some(external_linter) = &external_linter {
Expand Down Expand Up @@ -393,12 +394,12 @@ impl CliRunner {
}
}

let number_of_rules = linter.number_of_rules(self.options.type_aware);
let number_of_rules = linter.number_of_rules(type_aware);

// Create the LintRunner
// 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(self.options.type_aware)
.with_type_aware(type_aware)
.with_type_check(self.options.type_check)
.with_silent(misc_options.silent)
.with_fix_kind(fix_options.fix_kind())
Expand Down Expand Up @@ -1244,6 +1245,43 @@ mod test {
Tester::new().with_cwd("fixtures/tsgolint".into()).test_and_snapshot(args);
}

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

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_config_type_aware_applies_to_overrides() {
let args = &["-c", "config-type-aware-with-overrides.json", "no-floating-promises.ts"];
Tester::new().with_cwd("fixtures/tsgolint".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_config_type_aware_false() {
let args = &["-c", "config-type-aware-false.json", "no-floating-promises.ts"];
Tester::new().with_cwd("fixtures/tsgolint".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_config_type_aware_false_overridden_by_cli_flag() {
let args =
&["--type-aware", "-c", "config-type-aware-false.json", "no-floating-promises.ts"];
Tester::new().with_cwd("fixtures/tsgolint".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_config_type_aware_false_disables_overrides() {
let args =
&["-c", "config-type-aware-false-with-overrides.json", "no-floating-promises.ts"];
Tester::new().with_cwd("fixtures/tsgolint".into()).test_and_snapshot(args);
}

#[test]
#[cfg(not(target_endian = "big"))]
fn test_tsgolint_type_error() {
Expand Down
Loading
Loading