Skip to content

Commit 16bc305

Browse files
committed
feat(linter): JS custom rules config
1 parent 94796a0 commit 16bc305

File tree

19 files changed

+605
-120
lines changed

19 files changed

+605
-120
lines changed

apps/oxlint/src/lint.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ use std::{
1010

1111
use cow_utils::CowUtils;
1212
use ignore::{gitignore::Gitignore, overrides::OverrideBuilder};
13+
use napi::tokio;
1314
use oxc_allocator::AllocatorPool;
1415
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic};
1516
use oxc_linter::{
16-
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, InvalidFilterKind,
17-
LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
17+
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore,
18+
InvalidFilterKind, LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
1819
};
1920
use rustc_hash::{FxHashMap, FxHashSet};
2021
use serde_json::Value;
@@ -178,13 +179,22 @@ impl Runner for LintRunner {
178179
GraphicalReportHandler::new()
179180
};
180181

182+
let mut external_plugin_store = ExternalPluginStore::default();
183+
181184
let search_for_nested_configs = !disable_nested_config &&
182185
// If the `--config` option is explicitly passed, we should not search for nested config files
183186
// as the passed config file takes absolute precedence.
184187
basic_options.config.is_none();
185188

186189
let nested_configs = if search_for_nested_configs {
187-
match Self::get_nested_configs(stdout, &handler, &filters, &paths, external_linter) {
190+
match Self::get_nested_configs(
191+
stdout,
192+
&handler,
193+
&filters,
194+
&paths,
195+
external_linter,
196+
&mut external_plugin_store,
197+
) {
188198
Ok(v) => v,
189199
Err(v) => return v,
190200
}
@@ -203,21 +213,25 @@ impl Runner for LintRunner {
203213
} else {
204214
None
205215
};
206-
let config_builder =
207-
match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter) {
208-
Ok(builder) => builder,
209-
Err(e) => {
210-
print_and_flush_stdout(
211-
stdout,
212-
&format!(
213-
"Failed to parse configuration file.\n{}\n",
214-
render_report(&handler, &OxcDiagnostic::error(e.to_string()))
215-
),
216-
);
217-
return CliRunResult::InvalidOptionConfig;
218-
}
216+
let config_builder = match ConfigStoreBuilder::from_oxlintrc(
217+
false,
218+
oxlintrc,
219+
external_linter,
220+
&mut external_plugin_store,
221+
) {
222+
Ok(builder) => builder,
223+
Err(e) => {
224+
print_and_flush_stdout(
225+
stdout,
226+
&format!(
227+
"Failed to parse configuration file.\n{}\n",
228+
render_report(&handler, &OxcDiagnostic::error(e.to_string()))
229+
),
230+
);
231+
return CliRunResult::InvalidOptionConfig;
219232
}
220-
.with_filters(&filters);
233+
}
234+
.with_filters(&filters);
221235

222236
if let Some(basic_config_file) = oxlintrc_for_print {
223237
let config_file = config_builder.resolve_final_config_file(basic_config_file);
@@ -270,10 +284,14 @@ impl Runner for LintRunner {
270284
_ => None,
271285
};
272286

273-
let linter =
274-
Linter::new(LintOptions::default(), ConfigStore::new(lint_config, nested_configs))
275-
.with_fix(fix_options.fix_kind())
276-
.with_report_unused_directives(report_unused_directives);
287+
let linter = Linter::new(
288+
LintOptions::default(),
289+
ConfigStore::new(lint_config, nested_configs, external_plugin_store),
290+
self.external_linter
291+
.map(|external_linter| (external_linter, tokio::runtime::Handle::current())),
292+
)
293+
.with_fix(fix_options.fix_kind())
294+
.with_report_unused_directives(report_unused_directives);
277295

278296
let tsconfig = basic_options.tsconfig;
279297
if let Some(path) = tsconfig.as_ref() {
@@ -406,6 +424,7 @@ impl LintRunner {
406424
filters: &Vec<LintFilter>,
407425
paths: &Vec<Arc<OsStr>>,
408426
external_linter: Option<&ExternalLinter>,
427+
external_plugin_store: &mut ExternalPluginStore,
409428
) -> Result<FxHashMap<PathBuf, Config>, CliRunResult> {
410429
// TODO(perf): benchmark whether or not it is worth it to store the configurations on a
411430
// per-file or per-directory basis, to avoid calling `.parent()` on every path.
@@ -436,8 +455,12 @@ impl LintRunner {
436455
// iterate over each config and build the ConfigStore
437456
for (dir, oxlintrc) in nested_oxlintrc {
438457
// TODO(refactor): clean up all of the error handling in this function
439-
let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter)
440-
{
458+
let builder = match ConfigStoreBuilder::from_oxlintrc(
459+
false,
460+
oxlintrc,
461+
external_linter,
462+
external_plugin_store,
463+
) {
441464
Ok(builder) => builder,
442465
Err(e) => {
443466
print_and_flush_stdout(

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ use rustc_hash::FxHashMap;
1010
use oxc_span::{CompactStr, format_compact_str};
1111

1212
use crate::{
13-
AllowWarnDeny, LintConfig, LintFilter, LintFilterKind, Oxlintrc, RuleCategory, RuleEnum,
13+
AllowWarnDeny, ExternalPluginStore, LintConfig, LintFilter, LintFilterKind, Oxlintrc,
14+
RuleCategory, RuleEnum,
1415
config::{
1516
ESLintRule, LintPlugins, OxlintOverrides, OxlintRules, overrides::OxlintOverride,
1617
plugins::BuiltinLintPlugins,
1718
},
1819
external_linter::ExternalLinter,
20+
external_plugin_store::{ExternalRuleId, ExternalRuleLookupError},
1921
rules::RULES,
2022
};
2123

@@ -24,6 +26,7 @@ use super::{Config, categories::OxlintCategories};
2426
#[must_use = "You dropped your builder without building a Linter! Did you mean to call .build()?"]
2527
pub struct ConfigStoreBuilder {
2628
pub(super) rules: FxHashMap<RuleEnum, AllowWarnDeny>,
29+
pub(super) external_rules: FxHashMap<ExternalRuleId, AllowWarnDeny>,
2730
config: LintConfig,
2831
categories: OxlintCategories,
2932
overrides: OxlintOverrides,
@@ -47,11 +50,12 @@ impl ConfigStoreBuilder {
4750
pub fn empty() -> Self {
4851
let config = LintConfig::default();
4952
let rules = FxHashMap::default();
53+
let external_rules = FxHashMap::default();
5054
let categories: OxlintCategories = OxlintCategories::default();
5155
let overrides = OxlintOverrides::default();
5256
let extended_paths = Vec::new();
5357

54-
Self { rules, config, categories, overrides, extended_paths }
58+
Self { rules, external_rules, config, categories, overrides, extended_paths }
5559
}
5660

5761
/// Warn on all rules in all plugins and categories, including those in `nursery`.
@@ -64,8 +68,9 @@ impl ConfigStoreBuilder {
6468
let overrides = OxlintOverrides::default();
6569
let categories: OxlintCategories = OxlintCategories::default();
6670
let rules = RULES.iter().map(|rule| (rule.clone(), AllowWarnDeny::Warn)).collect();
71+
let external_rules = FxHashMap::default();
6772
let extended_paths = Vec::new();
68-
Self { rules, config, categories, overrides, extended_paths }
73+
Self { rules, external_rules, config, categories, overrides, extended_paths }
6974
}
7075

7176
/// Create a [`ConfigStoreBuilder`] from a loaded or manually built [`Oxlintrc`].
@@ -90,6 +95,7 @@ impl ConfigStoreBuilder {
9095
start_empty: bool,
9196
oxlintrc: Oxlintrc,
9297
external_linter: Option<&ExternalLinter>,
98+
external_plugin_store: &mut ExternalPluginStore,
9399
) -> Result<Self, ConfigBuilderError> {
94100
// TODO: this can be cached to avoid re-computing the same oxlintrc
95101
fn resolve_oxlintrc_config(
@@ -146,13 +152,13 @@ impl ConfigStoreBuilder {
146152
let resolver = oxc_resolver::Resolver::new(ResolveOptions::default());
147153
#[expect(clippy::missing_panics_doc, reason = "infallible")]
148154
let oxlintrc_dir_path = oxlintrc.path.parent().unwrap();
149-
150155
for plugin_specifier in &plugins.external {
151156
Self::load_external_plugin(
152157
oxlintrc_dir_path,
153158
plugin_specifier,
154159
external_linter,
155160
&resolver,
161+
external_plugin_store,
156162
)?;
157163
}
158164
}
@@ -179,8 +185,14 @@ impl ConfigStoreBuilder {
179185
path: Some(oxlintrc.path),
180186
};
181187

182-
let mut builder =
183-
Self { rules, config, categories, overrides: oxlintrc.overrides, extended_paths };
188+
let mut builder = Self {
189+
rules,
190+
external_rules: FxHashMap::default(),
191+
config,
192+
categories,
193+
overrides: oxlintrc.overrides,
194+
extended_paths,
195+
};
184196

185197
for filter in oxlintrc.categories.filters() {
186198
builder = builder.with_filter(&filter);
@@ -189,7 +201,15 @@ impl ConfigStoreBuilder {
189201
{
190202
let all_rules = builder.get_all_rules();
191203

192-
oxlintrc.rules.override_rules(&mut builder.rules, &all_rules);
204+
oxlintrc
205+
.rules
206+
.override_rules(
207+
&mut builder.rules,
208+
&mut builder.external_rules,
209+
&all_rules,
210+
external_plugin_store,
211+
)
212+
.map_err(ConfigBuilderError::ExternalRuleLookupError)?;
193213
}
194214

195215
Ok(builder)
@@ -346,7 +366,12 @@ impl ConfigStoreBuilder {
346366
.filter(|(r, _)| plugins.contains(r.plugin_name().into()))
347367
.collect();
348368
rules.sort_unstable_by_key(|(r, _)| r.id());
349-
Config::new(rules, self.categories, self.config, self.overrides)
369+
370+
let mut external_rules: Vec<_> = self.external_rules.into_iter().collect();
371+
external_rules.sort_unstable_by_key(|(r, _)| *r);
372+
373+
// TODO: fixme
374+
Config::new(rules, external_rules, self.categories, self.config, self.overrides)
350375
}
351376

352377
/// Warn for all correctness rules in the given set of plugins.
@@ -399,6 +424,7 @@ impl ConfigStoreBuilder {
399424
_plugin_specifier: &str,
400425
_external_linter: &ExternalLinter,
401426
_resolver: &Resolver,
427+
_external_plugin_store: &mut ExternalPluginStore,
402428
) -> Result<(), ConfigBuilderError> {
403429
unreachable!()
404430
}
@@ -409,6 +435,7 @@ impl ConfigStoreBuilder {
409435
plugin_specifier: &str,
410436
external_linter: &ExternalLinter,
411437
resolver: &Resolver,
438+
external_plugin_store: &mut ExternalPluginStore,
412439
) -> Result<(), ConfigBuilderError> {
413440
use crate::PluginLoadResult;
414441

@@ -421,16 +448,27 @@ impl ConfigStoreBuilder {
421448
// TODO: We should support paths which are not valid UTF-8. How?
422449
let plugin_path = resolved.full_path().to_str().unwrap().to_string();
423450

424-
let result = tokio::task::block_in_place(move || {
425-
tokio::runtime::Handle::current().block_on((external_linter.load_plugin)(plugin_path))
426-
})
427-
.map_err(|e| ConfigBuilderError::PluginLoadFailed {
428-
plugin_specifier: plugin_specifier.to_string(),
429-
error: e.to_string(),
430-
})?;
451+
if external_plugin_store.is_plugin_registered(&plugin_path) {
452+
return Ok(());
453+
}
454+
455+
let result = {
456+
let plugin_path = plugin_path.clone();
457+
tokio::task::block_in_place(move || {
458+
tokio::runtime::Handle::current()
459+
.block_on((external_linter.load_plugin)(plugin_path))
460+
})
461+
.map_err(|e| ConfigBuilderError::PluginLoadFailed {
462+
plugin_specifier: plugin_specifier.to_string(),
463+
error: e.to_string(),
464+
})
465+
}?;
431466

432467
match result {
433-
PluginLoadResult::Success => Ok(()),
468+
PluginLoadResult::Success { name, offset, rules } => {
469+
external_plugin_store.register_plugin(plugin_path, name, offset, rules);
470+
Ok(())
471+
}
434472
PluginLoadResult::Failure(e) => Err(ConfigBuilderError::PluginLoadFailed {
435473
plugin_specifier: plugin_specifier.to_string(),
436474
error: e,
@@ -452,7 +490,8 @@ impl TryFrom<Oxlintrc> for ConfigStoreBuilder {
452490

453491
#[inline]
454492
fn try_from(oxlintrc: Oxlintrc) -> Result<Self, Self::Error> {
455-
Self::from_oxlintrc(false, oxlintrc, None)
493+
let mut external_plugin_store = ExternalPluginStore::default();
494+
Self::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
456495
}
457496
}
458497

@@ -481,6 +520,7 @@ pub enum ConfigBuilderError {
481520
plugin_specifier: String,
482521
error: String,
483522
},
523+
ExternalRuleLookupError(ExternalRuleLookupError),
484524
NoExternalLinterConfigured,
485525
}
486526

@@ -509,6 +549,7 @@ impl Display for ConfigBuilderError {
509549
f.write_str("Failed to load external plugin because no external linter was configured. This means the Oxlint binary was executed directly rather than via napi bindings.")?;
510550
Ok(())
511551
}
552+
ConfigBuilderError::ExternalRuleLookupError(e) => std::fmt::Display::fmt(&e, f),
512553
}
513554
}
514555
}
@@ -743,7 +784,11 @@ mod test {
743784
"#,
744785
)
745786
.unwrap();
746-
let builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap();
787+
let builder = {
788+
let mut external_plugin_store = ExternalPluginStore::default();
789+
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
790+
.unwrap()
791+
};
747792
for (rule, severity) in &builder.rules {
748793
let name = rule.name();
749794
let plugin = rule.plugin_name();
@@ -912,14 +957,18 @@ mod test {
912957

913958
#[test]
914959
fn test_extends_invalid() {
915-
let invalid_config = ConfigStoreBuilder::from_oxlintrc(
916-
true,
917-
Oxlintrc::from_file(&PathBuf::from(
918-
"fixtures/extends_config/extends_invalid_config.json",
919-
))
920-
.unwrap(),
921-
None,
922-
);
960+
let invalid_config = {
961+
let mut external_plugin_store = ExternalPluginStore::default();
962+
ConfigStoreBuilder::from_oxlintrc(
963+
true,
964+
Oxlintrc::from_file(&PathBuf::from(
965+
"fixtures/extends_config/extends_invalid_config.json",
966+
))
967+
.unwrap(),
968+
None,
969+
&mut external_plugin_store,
970+
)
971+
};
923972
let err = invalid_config.unwrap_err();
924973
assert!(matches!(err, ConfigBuilderError::InvalidConfigFile { .. }));
925974
if let ConfigBuilderError::InvalidConfigFile { file, reason } = err {
@@ -1072,18 +1121,26 @@ mod test {
10721121
}
10731122

10741123
fn config_store_from_path(path: &str) -> Config {
1124+
let mut external_plugin_store = ExternalPluginStore::default();
10751125
ConfigStoreBuilder::from_oxlintrc(
10761126
true,
10771127
Oxlintrc::from_file(&PathBuf::from(path)).unwrap(),
10781128
None,
1129+
&mut external_plugin_store,
10791130
)
10801131
.unwrap()
10811132
.build()
10821133
}
10831134

10841135
fn config_store_from_str(s: &str) -> Config {
1085-
ConfigStoreBuilder::from_oxlintrc(true, serde_json::from_str(s).unwrap(), None)
1086-
.unwrap()
1087-
.build()
1136+
let mut external_plugin_store = ExternalPluginStore::default();
1137+
ConfigStoreBuilder::from_oxlintrc(
1138+
true,
1139+
serde_json::from_str(s).unwrap(),
1140+
None,
1141+
&mut external_plugin_store,
1142+
)
1143+
.unwrap()
1144+
.build()
10881145
}
10891146
}

0 commit comments

Comments
 (0)