Skip to content

Commit 6abae9e

Browse files
committed
feat(linter): JS custom rules config
1 parent 152e59d commit 6abae9e

File tree

13 files changed

+334
-79
lines changed

13 files changed

+334
-79
lines changed

apps/oxlint/src/lint.rs

Lines changed: 44 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use ignore::{gitignore::Gitignore, overrides::OverrideBuilder};
1313
use oxc_allocator::AllocatorPool;
1414
use oxc_diagnostics::{DiagnosticService, GraphicalReportHandler, OxcDiagnostic};
1515
use oxc_linter::{
16-
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, InvalidFilterKind,
17-
LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
16+
AllowWarnDeny, Config, ConfigStore, ConfigStoreBuilder, ExternalLinter, ExternalPluginStore,
17+
InvalidFilterKind, LintFilter, LintOptions, LintService, LintServiceOptions, Linter, Oxlintrc,
1818
};
1919
use rustc_hash::{FxHashMap, FxHashSet};
2020
use serde_json::Value;
@@ -178,13 +178,22 @@ impl Runner for LintRunner {
178178
GraphicalReportHandler::new()
179179
};
180180

181+
let mut external_plugin_store = ExternalPluginStore::default();
182+
181183
let search_for_nested_configs = !disable_nested_config &&
182184
// If the `--config` option is explicitly passed, we should not search for nested config files
183185
// as the passed config file takes absolute precedence.
184186
basic_options.config.is_none();
185187

186188
let nested_configs = if search_for_nested_configs {
187-
match Self::get_nested_configs(stdout, &handler, &filters, &paths, external_linter) {
189+
match Self::get_nested_configs(
190+
stdout,
191+
&handler,
192+
&filters,
193+
&paths,
194+
external_linter,
195+
&mut external_plugin_store,
196+
) {
188197
Ok(v) => v,
189198
Err(v) => return v,
190199
}
@@ -203,21 +212,25 @@ impl Runner for LintRunner {
203212
} else {
204213
None
205214
};
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-
}
215+
let config_builder = match ConfigStoreBuilder::from_oxlintrc(
216+
false,
217+
oxlintrc,
218+
external_linter,
219+
&mut external_plugin_store,
220+
) {
221+
Ok(builder) => builder,
222+
Err(e) => {
223+
print_and_flush_stdout(
224+
stdout,
225+
&format!(
226+
"Failed to parse configuration file.\n{}\n",
227+
render_report(&handler, &OxcDiagnostic::error(e.to_string()))
228+
),
229+
);
230+
return CliRunResult::InvalidOptionConfig;
219231
}
220-
.with_filters(&filters);
232+
}
233+
.with_filters(&filters);
221234

222235
if let Some(basic_config_file) = oxlintrc_for_print {
223236
let config_file = config_builder.resolve_final_config_file(basic_config_file);
@@ -273,10 +286,13 @@ impl Runner for LintRunner {
273286
_ => None,
274287
};
275288

276-
let linter =
277-
Linter::new(LintOptions::default(), ConfigStore::new(lint_config, nested_configs))
278-
.with_fix(fix_options.fix_kind())
279-
.with_report_unused_directives(report_unused_directives);
289+
let linter = Linter::new(
290+
LintOptions::default(),
291+
ConfigStore::new(lint_config, nested_configs, external_plugin_store),
292+
self.external_linter,
293+
)
294+
.with_fix(fix_options.fix_kind())
295+
.with_report_unused_directives(report_unused_directives);
280296

281297
let tsconfig = basic_options.tsconfig;
282298
if let Some(path) = tsconfig.as_ref() {
@@ -409,6 +425,7 @@ impl LintRunner {
409425
filters: &Vec<LintFilter>,
410426
paths: &Vec<Arc<OsStr>>,
411427
external_linter: Option<&ExternalLinter>,
428+
mut external_plugin_store: &mut ExternalPluginStore,
412429
) -> Result<FxHashMap<PathBuf, Config>, CliRunResult> {
413430
// TODO(perf): benchmark whether or not it is worth it to store the configurations on a
414431
// per-file or per-directory basis, to avoid calling `.parent()` on every path.
@@ -439,8 +456,12 @@ impl LintRunner {
439456
// iterate over each config and build the ConfigStore
440457
for (dir, oxlintrc) in nested_oxlintrc {
441458
// TODO(refactor): clean up all of the error handling in this function
442-
let builder = match ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, external_linter)
443-
{
459+
let builder = match ConfigStoreBuilder::from_oxlintrc(
460+
false,
461+
oxlintrc,
462+
external_linter,
463+
external_plugin_store,
464+
) {
444465
Ok(builder) => builder,
445466
Err(e) => {
446467
print_and_flush_stdout(

crates/oxc_linter/src/config/config_builder.rs

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ 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,
@@ -90,6 +91,7 @@ impl ConfigStoreBuilder {
9091
start_empty: bool,
9192
oxlintrc: Oxlintrc,
9293
external_linter: Option<&ExternalLinter>,
94+
external_plugin_store: &mut ExternalPluginStore,
9395
) -> Result<Self, ConfigBuilderError> {
9496
// TODO: this can be cached to avoid re-computing the same oxlintrc
9597
fn resolve_oxlintrc_config(
@@ -146,13 +148,13 @@ impl ConfigStoreBuilder {
146148
let resolver = oxc_resolver::Resolver::new(ResolveOptions::default());
147149
#[expect(clippy::missing_panics_doc, reason = "infallible")]
148150
let oxlintrc_dir_path = oxlintrc.path.parent().unwrap();
149-
150151
for plugin_specifier in &plugins.external {
151152
Self::load_external_plugin(
152153
oxlintrc_dir_path,
153154
plugin_specifier,
154155
external_linter,
155156
&resolver,
157+
external_plugin_store,
156158
)?;
157159
}
158160
}
@@ -399,6 +401,7 @@ impl ConfigStoreBuilder {
399401
_plugin_specifier: &str,
400402
_external_linter: &ExternalLinter,
401403
_resolver: &Resolver,
404+
_external_plugin_store: &mut ExternalPluginStore,
402405
) -> Result<(), ConfigBuilderError> {
403406
unreachable!()
404407
}
@@ -409,6 +412,7 @@ impl ConfigStoreBuilder {
409412
plugin_specifier: &str,
410413
external_linter: &ExternalLinter,
411414
resolver: &Resolver,
415+
external_plugin_store: &mut ExternalPluginStore,
412416
) -> Result<(), ConfigBuilderError> {
413417
use crate::PluginLoadResult;
414418

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

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-
})?;
428+
if external_plugin_store.is_plugin_registered(&plugin_path) {
429+
return Ok(());
430+
}
431+
432+
let result = {
433+
let plugin_path = plugin_path.clone();
434+
tokio::task::block_in_place(move || {
435+
tokio::runtime::Handle::current()
436+
.block_on((external_linter.load_plugin)(plugin_path))
437+
})
438+
.map_err(|e| ConfigBuilderError::PluginLoadFailed {
439+
plugin_specifier: plugin_specifier.to_string(),
440+
error: e.to_string(),
441+
})
442+
}?;
431443

432444
match result {
433-
PluginLoadResult::Success => Ok(()),
445+
PluginLoadResult::Success { name, offset, rules } => {
446+
external_plugin_store.register_plugin(plugin_path, name, offset, rules);
447+
Ok(())
448+
}
434449
PluginLoadResult::Failure(e) => Err(ConfigBuilderError::PluginLoadFailed {
435450
plugin_specifier: plugin_specifier.to_string(),
436451
error: e,
@@ -452,7 +467,8 @@ impl TryFrom<Oxlintrc> for ConfigStoreBuilder {
452467

453468
#[inline]
454469
fn try_from(oxlintrc: Oxlintrc) -> Result<Self, Self::Error> {
455-
Self::from_oxlintrc(false, oxlintrc, None)
470+
let mut external_plugin_store = ExternalPluginStore::default();
471+
Self::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
456472
}
457473
}
458474

@@ -743,7 +759,11 @@ mod test {
743759
"#,
744760
)
745761
.unwrap();
746-
let builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None).unwrap();
762+
let builder = {
763+
let mut external_plugin_store = ExternalPluginStore::default();
764+
ConfigStoreBuilder::from_oxlintrc(false, oxlintrc, None, &mut external_plugin_store)
765+
.unwrap()
766+
};
747767
for (rule, severity) in &builder.rules {
748768
let name = rule.name();
749769
let plugin = rule.plugin_name();

crates/oxc_linter/src/config/config_store.rs

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use super::{
1010
};
1111
use crate::{
1212
AllowWarnDeny, LintPlugins,
13+
external_plugin_store::{ExternalPluginStore, ExternalRuleId},
1314
rules::{RULES, RuleEnum},
1415
};
1516

@@ -19,11 +20,17 @@ pub struct ResolvedLinterState {
1920
// TODO: Arc + Vec -> SyncVec? It would save a pointer dereference.
2021
pub rules: Arc<[(RuleEnum, AllowWarnDeny)]>,
2122
pub config: Arc<LintConfig>,
23+
24+
pub external_rules: Vec<(ExternalRuleId, AllowWarnDeny)>,
2225
}
2326

2427
impl Clone for ResolvedLinterState {
2528
fn clone(&self) -> Self {
26-
Self { rules: Arc::clone(&self.rules), config: Arc::clone(&self.config) }
29+
Self {
30+
rules: Arc::clone(&self.rules),
31+
config: Arc::clone(&self.config),
32+
external_rules: self.external_rules.clone(),
33+
}
2734
}
2835
}
2936

@@ -64,6 +71,7 @@ impl Config {
6471
.into_boxed_slice(),
6572
),
6673
config: Arc::new(config),
74+
external_rules: vec![], // FIXME(camc314)
6775
},
6876
base_rules: rules,
6977
categories,
@@ -179,7 +187,11 @@ impl Config {
179187

180188
let rules =
181189
rules.into_iter().filter(|(_, severity)| severity.is_warn_deny()).collect::<Vec<_>>();
182-
ResolvedLinterState { rules: Arc::from(rules.into_boxed_slice()), config }
190+
ResolvedLinterState {
191+
rules: Arc::from(rules.into_boxed_slice()),
192+
config,
193+
external_rules: vec![], // FIXME(camc314)
194+
}
183195
}
184196
}
185197

@@ -192,11 +204,20 @@ impl Config {
192204
pub struct ConfigStore {
193205
base: Config,
194206
nested_configs: FxHashMap<PathBuf, Config>,
207+
external_plugin_store: Arc<ExternalPluginStore>,
195208
}
196209

197210
impl ConfigStore {
198-
pub fn new(base_config: Config, nested_configs: FxHashMap<PathBuf, Config>) -> Self {
199-
Self { base: base_config, nested_configs }
211+
pub fn new(
212+
base_config: Config,
213+
nested_configs: FxHashMap<PathBuf, Config>,
214+
external_plugin_store: ExternalPluginStore,
215+
) -> Self {
216+
Self {
217+
base: base_config,
218+
nested_configs,
219+
external_plugin_store: Arc::new(external_plugin_store),
220+
}
200221
}
201222

202223
pub fn number_of_rules(&self) -> Option<usize> {
@@ -212,6 +233,7 @@ impl ConfigStore {
212233
}
213234

214235
pub(crate) fn resolve(&self, path: &Path) -> ResolvedLinterState {
236+
dbg!(&self.external_plugin_store);
215237
let resolved_config = if self.nested_configs.is_empty() {
216238
&self.base
217239
} else if let Some(config) = self.get_nearest_config(path) {
@@ -243,7 +265,7 @@ mod test {
243265

244266
use super::{ConfigStore, OxlintOverrides};
245267
use crate::{
246-
AllowWarnDeny, BuiltinLintPlugins, LintPlugins, RuleEnum,
268+
AllowWarnDeny, BuiltinLintPlugins, ExternalPluginStore, LintPlugins, RuleEnum,
247269
config::{
248270
LintConfig, OxlintEnv, OxlintGlobals, OxlintSettings, categories::OxlintCategories,
249271
config_store::Config,
@@ -272,6 +294,7 @@ mod test {
272294
let store = ConfigStore::new(
273295
Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides),
274296
FxHashMap::default(),
297+
ExternalPluginStore::default(),
275298
);
276299

277300
let rules_for_source_file = store.resolve("App.tsx".as_ref());
@@ -294,6 +317,7 @@ mod test {
294317
let store = ConfigStore::new(
295318
Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides),
296319
FxHashMap::default(),
320+
ExternalPluginStore::default(),
297321
);
298322

299323
let rules_for_source_file = store.resolve("App.tsx".as_ref());
@@ -317,6 +341,7 @@ mod test {
317341
let store = ConfigStore::new(
318342
Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides),
319343
FxHashMap::default(),
344+
ExternalPluginStore::default(),
320345
);
321346
assert_eq!(store.number_of_rules(), Some(1));
322347

@@ -340,6 +365,7 @@ mod test {
340365
let store = ConfigStore::new(
341366
Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides),
342367
FxHashMap::default(),
368+
ExternalPluginStore::default(),
343369
);
344370
assert_eq!(store.number_of_rules(), Some(1));
345371

@@ -363,6 +389,7 @@ mod test {
363389
let store = ConfigStore::new(
364390
Config::new(base_rules, OxlintCategories::default(), LintConfig::default(), overrides),
365391
FxHashMap::default(),
392+
ExternalPluginStore::default(),
366393
);
367394
assert_eq!(store.number_of_rules(), Some(1));
368395

@@ -395,6 +422,7 @@ mod test {
395422
let store = ConfigStore::new(
396423
Config::new(vec![], OxlintCategories::default(), base_config, overrides),
397424
FxHashMap::default(),
425+
ExternalPluginStore::default(),
398426
);
399427

400428
assert_eq!(store.base.base.config.plugins.builtin, BuiltinLintPlugins::IMPORT);
@@ -438,6 +466,7 @@ mod test {
438466
let store = ConfigStore::new(
439467
Config::new(vec![], OxlintCategories::default(), base_config, overrides),
440468
FxHashMap::default(),
469+
ExternalPluginStore::default(),
441470
);
442471
assert!(!store.base.base.config.env.contains("React"));
443472

@@ -465,6 +494,7 @@ mod test {
465494
let store = ConfigStore::new(
466495
Config::new(vec![], OxlintCategories::default(), base_config, overrides),
467496
FxHashMap::default(),
497+
ExternalPluginStore::default(),
468498
);
469499
assert!(store.base.base.config.env.contains("es2024"));
470500

@@ -493,6 +523,7 @@ mod test {
493523
let store = ConfigStore::new(
494524
Config::new(vec![], OxlintCategories::default(), base_config, overrides),
495525
FxHashMap::default(),
526+
ExternalPluginStore::default(),
496527
);
497528
assert!(!store.base.base.config.globals.is_enabled("React"));
498529
assert!(!store.base.base.config.globals.is_enabled("Secret"));
@@ -526,6 +557,7 @@ mod test {
526557
let store = ConfigStore::new(
527558
Config::new(vec![], OxlintCategories::default(), base_config, overrides),
528559
FxHashMap::default(),
560+
ExternalPluginStore::default(),
529561
);
530562
assert!(store.base.base.config.globals.is_enabled("React"));
531563
assert!(store.base.base.config.globals.is_enabled("Secret"));

0 commit comments

Comments
 (0)