Skip to content

Commit fc74849

Browse files
committed
feat(linter): inherit rules via the extended config files (#9308)
- part of #9307 This gives the `extends` keyword some functionality: specifically to allow inheriting rules from other configuration files. Rules that are in the extended configuration will be used as the base, and then files in the config doing the extending will override the extended rules. So, you can, for example, specify a base configuration that defines rules that you generally want to be enabled. Then, in each nested configuration file, you could disable rules depending on the directory: ```text .oxlintrc.json // { "rules: { "no-useless-escape": "error" } } package1/ .oxlintrc.json // { "extends": ["../.oxlintrc.json"], "rules": { "no-useless-escape": "off" } } package2/ src // uses ../.oxlintrc.json with `no-useless-escape` enabled ``` As a side effect of this, building a config from a `.oxlintrc.json` file can result in config errors, even if all of the syntax and rules are configured correctly, because an extended configuration file might be incorrect or unparseable. For now, we simply raise an error if this occurs and stop execution, but in the future we could make this more graceful if we wanted and just ignore that file. We will also need to revisit performance here, as I think a global cache config might be necessary so that we never load a file more than once and parse it more than once.
1 parent 3fce826 commit fc74849

14 files changed

+324
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log("test");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["./invalid_config.json"]
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["./rules_config.json"]
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
invalid
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"rules": {
3+
"no-debugger": "off",
4+
"no-console": "error",
5+
"unicorn/no-null": "error"
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"rules": {
3+
"no-console": "off",
4+
"no-var": "off",
5+
"oxc/approx-constant": "off",
6+
"unicorn/no-null": "off",
7+
"unicorn/no-array-for-each": "off"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"rules": {
3+
"no-console": "error",
4+
"no-var": "error",
5+
"oxc/approx-constant": "error",
6+
"unicorn/no-null": "error",
7+
"unicorn/no-array-for-each": "error"
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"rules": {
3+
"no-console": "warn",
4+
"no-var": "warn",
5+
"oxc/approx-constant": "warn",
6+
"unicorn/no-null": "warn",
7+
"unicorn/no-array-for-each": "warn"
8+
}
9+
}

apps/oxlint/src/lint.rs

+45-5
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use std::{
77

88
use cow_utils::CowUtils;
99
use ignore::{gitignore::Gitignore, overrides::OverrideBuilder};
10-
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler};
10+
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic};
1111
use oxc_linter::{
1212
AllowWarnDeny, ConfigStore, ConfigStoreBuilder, InvalidFilterKind, LintFilter, LintOptions,
1313
LintService, LintServiceOptions, Linter, Oxlintrc, loader::LINT_PARTIAL_LOADER_EXT,
@@ -200,9 +200,28 @@ impl Runner for LintRunner {
200200

201201
// iterate over each config and build the ConfigStore
202202
for (dir, oxlintrc) in nested_oxlintrc {
203+
// TODO(refactor): clean up all of the error handling in this function
204+
let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc) {
205+
Ok(builder) => builder,
206+
Err(e) => {
207+
let handler = GraphicalReportHandler::new();
208+
let mut err = String::new();
209+
handler
210+
.render_report(&mut err, &OxcDiagnostic::error(e.to_string()))
211+
.unwrap();
212+
stdout
213+
.write_all(
214+
format!("Failed to parse configuration file.\n{err}\n").as_bytes(),
215+
)
216+
.or_else(Self::check_for_writer_error)
217+
.unwrap();
218+
stdout.flush().unwrap();
219+
220+
return CliRunResult::InvalidOptionConfig;
221+
}
222+
}
203223
// TODO(perf): figure out if we can avoid cloning `filter`
204-
let builder =
205-
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc).with_filters(filter.clone());
224+
.with_filters(filter.clone());
206225
match builder.build() {
207226
Ok(config) => nested_configs.insert(dir.to_path_buf(), config),
208227
Err(diagnostic) => {
@@ -230,8 +249,22 @@ impl Runner for LintRunner {
230249
} else {
231250
None
232251
};
233-
let config_builder =
234-
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc).with_filters(filter);
252+
let config_builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc) {
253+
Ok(builder) => builder,
254+
Err(e) => {
255+
let handler = GraphicalReportHandler::new();
256+
let mut err = String::new();
257+
handler.render_report(&mut err, &OxcDiagnostic::error(e.to_string())).unwrap();
258+
stdout
259+
.write_all(format!("Failed to parse configuration file.\n{err}\n").as_bytes())
260+
.or_else(Self::check_for_writer_error)
261+
.unwrap();
262+
stdout.flush().unwrap();
263+
264+
return CliRunResult::InvalidOptionConfig;
265+
}
266+
}
267+
.with_filters(filter);
235268

236269
if let Some(basic_config_file) = oxlintrc_for_print {
237270
let config_file = config_builder.resolve_final_config_file(basic_config_file);
@@ -982,4 +1015,11 @@ mod test {
9821015
];
9831016
Tester::new().with_cwd("fixtures/nested_config".into()).test_and_snapshot(args);
9841017
}
1018+
1019+
#[test]
1020+
fn test_extends_explicit_config() {
1021+
// Check that referencing a config file that extends other config files works as expected
1022+
let args = &["--config", "extends_rules_config.json", "console.js"];
1023+
Tester::new().with_cwd("fixtures/extends_config".into()).test_and_snapshot(args);
1024+
}
9851025
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
source: apps/oxlint/src/tester.rs
3+
---
4+
##########
5+
arguments: --config extends_rules_config.json console.js
6+
working directory: fixtures/extends_config
7+
----------
8+
9+
x ]8;;https://oxc.rs/docs/guide/usage/linter/rules/eslint/no-console.html\eslint(no-console)]8;;\: eslint(no-console): Unexpected console statement.
10+
,-[console.js:1:1]
11+
1 | console.log("test");
12+
: ^^^^^^^^^^^
13+
`----
14+
help: Delete this console statement.
15+
16+
Found 0 warnings and 1 error.
17+
Finished in <variable>ms on 1 file with 101 rules using 1 threads.
18+
----------
19+
CLI result: LintFoundErrors
20+
----------

crates/oxc_language_server/src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,7 @@ impl Backend {
497497
let config = Oxlintrc::from_file(&config_path)
498498
.expect("should have initialized linter with new options");
499499
let config_store = ConfigStoreBuilder::from_oxlintrc(true, config.clone())
500+
.expect("failed to build config")
500501
.build()
501502
.expect("failed to build config");
502503
*linter = ServerLinter::new_with_linter(

0 commit comments

Comments
 (0)