feat(config): per-rule configuration with severity and options - #1534
feat(config): per-rule configuration with severity and options#1534bartlomieju wants to merge 1 commit into
Conversation
|
Verdict: LGTM on the design & implementation — needs maintainer sign-off on the architecture direction (it's a foundational change) + a rebase. Not auto-merging (public API/behavior + it's a design prototype). One consistency nit below. Clean separation of rule definition ( Correctness — verified
Consistency nit (worth fixing before scaling to ~120 rules)
Perf / scaling (already flagged in your module docs)
Housekeeping: No blocking bugs. Recommend: align the empty-options normalization across all |
Introduces a per-rule configuration mechanism — severity plus rule-specific
options — and demonstrates it on three existing rules. This is a design
prototype: it establishes the machinery and proves it end-to-end, but only
three rules are wired up so far and the config-file/CLI parsing that would
populate it is intentionally out of scope.
The core idea is to stop conflating a rule's definition with a
configured, runnable instance. Today a rule is a zero-sized unit struct
registered as a pre-built
Box<dyn LintRule>, so there is nowhere to putoptions and no object-safe way to add a
from_configurationconstructor tothe trait. eslint (
{ meta, create }) and oxlint (a rule descriptor plusfrom_configuration) both keep the two halves apart, and this changereintroduces that split in a new
src/rules/config.rs:RuleDefis the definition —&'staticmetadata (code, tags, defaultseverity) plus a
configure_optionsfunction pointer that builds arunnable rule from optional JSON options. The registry can eventually hold
these instead of pre-built instances.
ConfiguredRuleis a runnableLintRuleplus theLintDiagnosticSeverityits diagnostics should carry.
RuleConfig { severity, options }models eslint's[severity, options].configure_rulesresolves a whole registry against user data keyed bycode, and
split_configuredadapts the result into the two inputs thelinter consumes.
Severity is unified with enablement the way eslint does it: a rule resolving
to
Offis never constructed, whileWarn/Errormap onto the existingLintDiagnostic.severityfield.LinterOptionsgains arule_severitiesmap, and the linter stamps each diagnostic's severity in a post-pass that is
skipped entirely when no severities are configured, so callers that don't opt
in keep the current always-error behavior.
Three rules are retrofitted to exercise three different option shapes, each
matching its eslint counterpart:
no-consolegainsallow(a string list):console.warnis permittedwhen listed, other methods are still flagged.
no-emptygainsallowEmptyCatch(a bool): an emptycatch {}is allowedwhile other empty blocks are still flagged.
eqeqeqgains a"always"/"smart"mode (a bare string enum):"smart"permits comparing against null, evaluating typeof, and comparing two
literals.
All three default to their previous behavior, so existing rules and tests are
unchanged; the other rules keep running through the current registry
untouched. New unit tests cover each option shape, the severity resolution
(including off-excludes-the-rule and unknown-rule errors), and an end-to-end
linter test proving a rule configured to warn emits a warning rather than the
default error.
A few things are deliberately left as fast-follows, and are called out in the
module documentation: hand-writing a
RuleDefper rule will not scale to thefull rule set and wants a
declare_rules!macro or build-time codegen; theregistry can then become a zero-allocation
&'static [RuleDef]; and theper-run severity stamping could move to emit time. Performance and generated
code size were a stated concern, so the design keeps descriptors to static
data plus a function pointer, shares a single monomorphic path for
option-less rules, and gates the one new per-run cost behind an emptiness
check.