fix(linter): category filters not re-configuring already-enabled rules (#6085)

> AI generated because I'm busy
### TL;DR

Enhanced LinterBuilder functionality and improved rule configuration handling.

### What changed?

- Made `rules` field in `LinterBuilder` public within the crate.
- Added a `plugins()` method to `LinterBuilder` to access lint plugins.
- Improved rule configuration logic in `configure_rules` method to update existing rules' severity.
- Added comprehensive test cases for `LinterBuilder` functionality.
- Implemented `Borrow<RuleEnum>` for `RuleWithSeverity` to improve rule lookup efficiency.

### How to test?

1. Run the newly added test cases in the `test` module of `builder.rs`.
2. Verify that all tests pass, including:
   - Default builder configuration
   - Empty builder configuration
   - Rule filtering and severity changes
   - Plugin configuration and rule interactions

### Why make this change?

These changes improve the flexibility and robustness of the linter configuration process:

1. Allowing access to the `rules` field enables more advanced customization options.
2. The new `plugins()` method provides better visibility into configured plugins.
3. Updating existing rules' severity ensures consistent behavior when reconfiguring rules.
4. Comprehensive tests ensure the reliability of the `LinterBuilder` functionality.
5. Implementing `Borrow<RuleEnum>` for `RuleWithSeverity` optimizes rule lookup operations.

These enhancements contribute to a more maintainable and efficient linter system.
This commit is contained in:
DonIsaac 2024-09-26 20:18:18 +00:00
parent 55949ebd88
commit 87595286ce
2 changed files with 159 additions and 8 deletions

View file

@ -13,7 +13,7 @@ use crate::{
#[must_use = "You dropped your builder without building a Linter! Did you mean to call .build()?"]
pub struct LinterBuilder {
rules: FxHashSet<RuleWithSeverity>,
pub(super) rules: FxHashSet<RuleWithSeverity>,
options: LintOptions,
config: LintConfig,
cache: RulesCache,
@ -119,6 +119,11 @@ impl LinterBuilder {
self
}
#[inline]
pub fn plugins(&self) -> LintPlugins {
self.options.plugins
}
#[cfg(test)]
pub(crate) fn with_rule(mut self, rule: RuleWithSeverity) -> Self {
self.rules.insert(rule);
@ -139,12 +144,15 @@ impl LinterBuilder {
match severity {
AllowWarnDeny::Deny | AllowWarnDeny::Warn => match filter {
LintFilterKind::Category(category) => {
self.rules.extend(
all_rules
.iter()
.filter(|rule| rule.category() == category)
.map(|rule| RuleWithSeverity::new(rule.clone(), severity)),
);
let rules_to_configure = all_rules.iter().filter(|r| r.category() == category);
for rule in rules_to_configure {
if let Some(mut existing_rule) = self.rules.take(rule) {
existing_rule.severity = severity;
self.rules.insert(existing_rule);
} else {
self.rules.insert(RuleWithSeverity::new(rule.clone(), severity));
}
}
}
LintFilterKind::Rule(_, name) => {
self.rules.extend(
@ -292,3 +300,140 @@ impl RulesCache {
*self.0.borrow_mut() = Some(all_rules);
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_builder_default() {
let builder = LinterBuilder::default();
assert_eq!(builder.plugins(), LintPlugins::default());
// populated with all correctness-level ESLint rules at a "warn" severity
assert!(!builder.rules.is_empty());
for rule in &builder.rules {
assert_eq!(rule.category(), RuleCategory::Correctness);
assert_eq!(rule.severity, AllowWarnDeny::Warn);
let plugin = rule.rule.plugin_name();
let name = rule.name();
assert!(
builder.plugins().contains(plugin.into()),
"{plugin}/{name} is in the default rule set but its plugin is not enabled"
);
}
}
#[test]
fn test_builder_empty() {
let builder = LinterBuilder::empty();
assert_eq!(builder.plugins(), LintPlugins::default());
assert!(builder.rules.is_empty());
}
#[test]
fn test_filter_deny_on_default() {
let builder = LinterBuilder::default();
let initial_rule_count = builder.rules.len();
let builder = builder.with_filters([LintFilter::deny(RuleCategory::Correctness)]);
let rule_count_after_deny = builder.rules.len();
// By default, all correctness rules are set to warn. the above filter should only
// re-configure those rules, and not add/remove any others.
assert!(!builder.rules.is_empty());
assert_eq!(initial_rule_count, rule_count_after_deny);
for rule in &builder.rules {
assert_eq!(rule.category(), RuleCategory::Correctness);
assert_eq!(rule.severity, AllowWarnDeny::Deny);
let plugin = rule.plugin_name();
let name = rule.name();
assert!(
builder.plugins().contains(plugin.into()),
"{plugin}/{name} is in the default rule set but its plugin is not enabled"
);
}
}
#[test]
fn test_filter_allow_all_then_warn() {
let builder =
LinterBuilder::default()
.with_filters([LintFilter::new(AllowWarnDeny::Allow, "all").unwrap()]);
assert!(builder.rules.is_empty(), "Allowing all rules should empty out the rules list");
let builder = builder.with_filters([LintFilter::warn(RuleCategory::Correctness)]);
assert!(
!builder.rules.is_empty(),
"warning on categories after allowing all rules should populate the rules set"
);
for rule in &builder.rules {
let plugin = rule.plugin_name();
let name = rule.name();
assert_eq!(
rule.severity,
AllowWarnDeny::Warn,
"{plugin}/{name} should have a warning severity"
);
assert_eq!(
rule.category(),
RuleCategory::Correctness,
"{plugin}/{name} should not be enabled b/c we only enabled correctness rules"
);
}
}
#[test]
fn test_rules_after_plugin_added() {
let builder = LinterBuilder::default();
let initial_rule_count = builder.rules.len();
let builder = builder.and_plugins(LintPlugins::IMPORT, true);
assert_eq!(initial_rule_count, builder.rules.len(), "Enabling a plugin should not add any rules, since we don't know which categories to turn on.");
}
#[test]
fn test_plugin_configuration() {
let builder = LinterBuilder::default();
let initial_plugins = builder.plugins();
// ==========================================================================================
// Test LinterBuilder::and_plugins, which deltas the plugin list instead of overriding it
// ==========================================================================================
// Enable eslint plugin. Since it's already enabled, this does nothing.
assert!(initial_plugins.contains(LintPlugins::ESLINT)); // sanity check that eslint is
// enabled
let builder = builder.and_plugins(LintPlugins::ESLINT, true);
assert_eq!(initial_plugins, builder.plugins());
// Disable import plugin. Since it's not already enabled, this is also a no-op.
assert!(!builder.plugins().contains(LintPlugins::IMPORT)); // sanity check that it's not
// already enabled
let builder = builder.and_plugins(LintPlugins::IMPORT, false);
assert_eq!(initial_plugins, builder.plugins());
// Enable import plugin. Since it's not already enabled, this turns it on.
let builder = builder.and_plugins(LintPlugins::IMPORT, true);
assert_eq!(LintPlugins::default().union(LintPlugins::IMPORT), builder.plugins());
assert_ne!(initial_plugins, builder.plugins());
// Turn import back off, resetting plugins to the initial state
let builder = builder.and_plugins(LintPlugins::IMPORT, false);
assert_eq!(initial_plugins, builder.plugins());
// ==========================================================================================
// Test LinterBuilder::with_plugins, which _does_ override plugins
// ==========================================================================================
let builder = builder.with_plugins(LintPlugins::ESLINT);
assert_eq!(LintPlugins::ESLINT, builder.plugins());
let expected_plugins =
LintPlugins::ESLINT.union(LintPlugins::TYPESCRIPT).union(LintPlugins::NEXTJS);
let builder = builder.with_plugins(expected_plugins);
assert_eq!(expected_plugins, builder.plugins());
}
}

View file

@ -1,5 +1,5 @@
use std::{
borrow::Cow,
borrow::{Borrow, Cow},
fmt,
hash::{Hash, Hasher},
ops::Deref,
@ -261,6 +261,12 @@ impl Deref for RuleWithSeverity {
}
}
impl Borrow<RuleEnum> for RuleWithSeverity {
fn borrow(&self) -> &RuleEnum {
&self.rule
}
}
impl RuleWithSeverity {
pub fn new(rule: RuleEnum, severity: AllowWarnDeny) -> Self {
Self { rule, severity }