Skip to content

Commit 48e6542

Browse files
committed
chore: document rule-group severity relation and update rules_check
1 parent ef45056 commit 48e6542

File tree

2 files changed

+98
-20
lines changed

2 files changed

+98
-20
lines changed

crates/biome_analyze/CONTRIBUTING.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,28 @@ declare_lint_rule! {
516516
}
517517
```
518518

519+
#### Rule group and severity
520+
521+
> [!NOTE]
522+
> This section is relevant to Biome maintainers when they want to move (promote) a rule to a group that is not `nursery`.
523+
524+
We try to maintain consistency in the default severity level and group membership of the rules.
525+
For legacy reasons, we have some rules that don't follow these constraints.
526+
527+
- `correctness`, `security`, and `a11y` rules **must** have a severity set to `error`.
528+
529+
If `error` is too strict for a rule, then it should certainly be in another group (for example `suspicious` instead of `correctness`).
530+
531+
- `style` rules **must** have a severity set to `info` or `warn`. If in doubt, choose `info`.
532+
533+
- `complexity` rules **must** have a severity set to `warn` or `info`. If in doubt, choose `info`.
534+
535+
- `suspicious` rules **must** have a severity set to `warn` or `error`. If in doubt, choose `warn`.
536+
537+
- `performance` rules **must** have a severity set to `warn`.
538+
539+
- Actions **must** have a severity set to `info`.
540+
519541
#### Rule domains
520542

521543
Domains are very specific ways to collect rules that belong to the same "concept". Domains are a way for users to opt-in/opt-out rules that belong to the same domain.
@@ -548,6 +570,7 @@ Instead, if the rule is **recommended** but _doesn't have domains_, the rule is
548570
> [!NOTE]
549571
> Before adding a new domain, please consult with the maintainers of the project.
550572
573+
551574
#### Rule Options
552575

553576
Some rules may allow customization [using per-rule options in `biome.json`](https://biomejs.dev/linter/#rule-options).

xtask/rules_check/src/lib.rs

Lines changed: 75 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,22 @@ use std::slice;
3232
use std::str::FromStr;
3333

3434
#[derive(Debug)]
35-
struct Errors(String);
36-
35+
struct Errors {
36+
message: String,
37+
}
3738
impl Errors {
38-
fn style_rule_error(rule_name: impl Display) -> Self {
39-
Self(format!(
40-
"The rule '{rule_name}' that belongs to the group 'style' can't have Severity::Error. Lower down the severity or change the group.",
41-
))
42-
}
43-
44-
fn action_error(rule_name: impl Display) -> Self {
45-
Self(format!(
46-
"The rule '{rule_name}' is an action, and it must have Severity::Information. Lower down the severity.",
47-
))
39+
const fn new(message: String) -> Self {
40+
Self { message }
4841
}
4942
}
50-
43+
impl std::error::Error for Errors {}
5144
impl Display for Errors {
5245
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
53-
f.write_str(self.0.as_str())
46+
let Self { message } = self;
47+
f.write_str(message)
5448
}
5549
}
5650

57-
impl std::error::Error for Errors {}
58-
5951
type Data = BTreeMap<&'static str, (RuleMetadata, RuleCategory)>;
6052
pub fn check_rules() -> anyhow::Result<()> {
6153
#[derive(Default)]
@@ -73,12 +65,75 @@ pub fn check_rules() -> anyhow::Result<()> {
7365
if !matches!(category, RuleCategory::Lint | RuleCategory::Action) {
7466
return;
7567
}
76-
if R::Group::NAME == "style" && R::METADATA.severity == Severity::Error {
77-
self.errors.push(Errors::style_rule_error(R::METADATA.name))
68+
let group = R::Group::NAME;
69+
let rule_name = R::METADATA.name;
70+
let rule_severity = R::METADATA.severity;
71+
if matches!(group, "a11y" | "correctness" | "security")
72+
&& rule_severity != Severity::Error
73+
&& !matches!(
74+
rule_name,
75+
// Legacy exceptions (severity set to 'warning')
76+
"noNodejsModules"
77+
| "noPrivateImports"
78+
| "noUnusedFunctionParameters"
79+
| "noUnusedImports"
80+
| "noUnusedLabels"
81+
| "noUnusedPrivateClassMembers"
82+
| "noUnusedVariables"
83+
| "useImportExtensions"
84+
// Legacy exceptions (severity set to 'info')
85+
| "noNoninteractiveElementInteractions"
86+
| "noGlobalDirnameFilename"
87+
| "noProcessGlobal"
88+
| "noReactPropAssignments"
89+
| "noRestrictedElements"
90+
| "noSolidDestructuredProps"
91+
| "useJsonImportAttributes"
92+
| "useParseIntRadix"
93+
| "useSingleJsDocAsterisk"
94+
)
95+
{
96+
self.errors.push(Errors::new(format!(
97+
"The rule '{rule_name}' belongs to the group '{group}' and has a severity set to '{rule_severity}'. Rules that belong to the group {group} must have a severity set to 'error'. Set the severity to 'error' or change the group of the rule."
98+
)));
99+
} else if matches!(group, "complexity" | "style") && rule_severity == Severity::Error {
100+
self.errors.push(Errors::new(format!(
101+
"The rule '{rule_name}' belongs to the group '{group}' and has a severity set to '{rule_severity}'. Rules that belong to the group '{group}' must not have a severity set to 'error'. Lower down the severity or change the group of the rule."
102+
)));
103+
} else if group == "performance"
104+
&& rule_severity != Severity::Warning
105+
&& !matches!(
106+
rule_name,
107+
// Legacy exceptions (severity set to 'info')
108+
"noAwaitInLoops" | "useGoogleFontPreconnect" | "useSolidForComponent"
109+
)
110+
{
111+
self.errors.push(Errors::new(format!(
112+
"The rule '{rule_name}' belongs to the group '{group}' and has a severity set to '{rule_severity}'. Rules that belong to the group '{group}' must have a severity set to 'warn'. Set the severity to 'warn' or change the group of the rule."
113+
)));
114+
} else if group == "suspicious"
115+
&& rule_severity == Severity::Information
116+
&& !matches!(
117+
rule_name,
118+
// Legacy exceptions (severity set to 'info')
119+
"noAlert"
120+
| "noBitwiseOperators"
121+
| "noConstantBinaryExpressions"
122+
| "noUnassignedVariables"
123+
| "useStaticResponseMethods"
124+
| "noQuickfixBiome"
125+
| "noDuplicateFields"
126+
)
127+
{
128+
self.errors.push(Errors::new(format!(
129+
"The rule '{rule_name}' belongs to the group '{group}' and has a severity set to '{rule_severity}'. Rules that belong to the group '{group}' must have a severity set to 'warn' or 'error'. Change the severity or change the group of the rule."
130+
)));
78131
} else if <R::Group as RuleGroup>::Category::CATEGORY == RuleCategory::Action
79-
&& R::METADATA.severity != Severity::Information
132+
&& rule_severity != Severity::Information
80133
{
81-
self.errors.push(Errors::action_error(R::METADATA.name));
134+
self.errors.push(Errors::new(format!(
135+
"The action '{rule_name}' has a severity set to '{rule_severity}'. Actions must have a severity set to 'info'. Set the severity of the rule to 'info'."
136+
)));
82137
} else {
83138
self.groups
84139
.entry((<R::Group as RuleGroup>::NAME, R::METADATA.language))

0 commit comments

Comments
 (0)