Skip to content

Commit 17ed9d3

Browse files
authored
feat(js_analyze): implement noDivRegex (#8757)
1 parent 72565bd commit 17ed9d3

14 files changed

Lines changed: 249 additions & 13 deletions

File tree

.changeset/fast-needles-hammer.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`noDivRegex`](https://biomejs.dev/linter/rules/no-div-regex). Disallow equal signs explicitly at the beginning of regular expressions.
6+
7+
**Invalid:**
8+
9+
```js
10+
var f = function() {
11+
return /=foo/;
12+
};
13+
```

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 7 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod no_ambiguous_anchor_text;
77
pub mod no_before_interactive_script_outside_document;
88
pub mod no_continue;
99
pub mod no_deprecated_imports;
10+
pub mod no_div_regex;
1011
pub mod no_duplicate_enum_values;
1112
pub mod no_duplicated_spread_props;
1213
pub mod no_empty_source;
@@ -62,4 +63,4 @@ pub mod use_spread;
6263
pub mod use_vue_consistent_define_props_declaration;
6364
pub mod use_vue_define_macros_order;
6465
pub mod use_vue_multi_word_component_names;
65-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
66+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use biome_analyze::{
2+
Ast, FixKind, Rule, RuleAction, RuleDiagnostic, RuleSource, context::RuleContext,
3+
declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_js_syntax::{JsRegexLiteralExpression, JsSyntaxKind, JsSyntaxToken};
7+
use biome_rowan::{AstNode, BatchMutationExt};
8+
use biome_rule_options::no_div_regex::NoDivRegexOptions;
9+
10+
use crate::JsRuleAction;
11+
12+
declare_lint_rule! {
13+
/// Disallow equal signs explicitly at the beginning of regular expressions.
14+
///
15+
/// This rule forbids equal signs (`=`) after the slash (`/`) at the beginning of a regular expression literal,
16+
/// because the characters `/=` can be confused with a division assignment operator.
17+
///
18+
/// ## Examples
19+
///
20+
/// ### Invalid
21+
///
22+
/// ```js,expect_diagnostic
23+
/// function bar() {
24+
/// return /=foo/;
25+
/// }
26+
/// ```
27+
///
28+
/// ### Valid
29+
///
30+
/// ```js
31+
/// function bar() {
32+
/// return /[=]foo/;
33+
/// }
34+
/// ```
35+
///
36+
pub NoDivRegex {
37+
version: "next",
38+
name: "noDivRegex",
39+
language: "js",
40+
recommended: false,
41+
sources: &[RuleSource::Eslint("no-div-regex").same()],
42+
fix_kind: FixKind::Safe,
43+
}
44+
}
45+
46+
impl Rule for NoDivRegex {
47+
type Query = Ast<JsRegexLiteralExpression>;
48+
type State = ();
49+
type Signals = Option<Self::State>;
50+
type Options = NoDivRegexOptions;
51+
52+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
53+
let node = ctx.query();
54+
55+
let value_token = node.value_token().ok()?;
56+
let text = value_token.text_trimmed();
57+
58+
if let Some(first_char) = text.chars().nth(1)
59+
&& first_char == '='
60+
{
61+
return Some(());
62+
}
63+
64+
None
65+
}
66+
67+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
68+
let node = ctx.query();
69+
Some(
70+
RuleDiagnostic::new(
71+
rule_category!(),
72+
node.range(),
73+
markup! {
74+
"Avoid using an equal sign directly after the slash at the beginning of a regular expression literal."
75+
},
76+
)
77+
.note(markup! {
78+
"The characters "<Emphasis>"/="</Emphasis>" can be confused with a division assignment operator. Replace the equal sign ("<Emphasis>"="</Emphasis>") with "<Emphasis>"[=]"</Emphasis>" to prevent confusion."
79+
}),
80+
)
81+
}
82+
83+
fn action(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<JsRuleAction> {
84+
let node = ctx.query();
85+
let mut mutation = ctx.root().begin();
86+
87+
let text = node.to_trimmed_string();
88+
let new_text = format!("/[=]{}", &text[2..]);
89+
90+
let new_node = node.clone().with_value_token(JsSyntaxToken::new_detached(
91+
JsSyntaxKind::JS_REGEX_LITERAL,
92+
new_text.as_str(),
93+
[],
94+
[],
95+
));
96+
97+
mutation.replace_node(node.clone(), new_node);
98+
99+
Some(RuleAction::new(
100+
ctx.metadata().action_category(ctx.category(), ctx.group()),
101+
ctx.metadata().applicability(),
102+
markup! {
103+
"Replace with "<Emphasis>"[=]"</Emphasis>"."
104+
}
105+
.to_owned(),
106+
mutation,
107+
))
108+
}
109+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* should generate diagnostics */
2+
var f = function() { return /=foo/; };
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```js
7+
/* should generate diagnostics */
8+
var f = function() { return /=foo/; };
9+
10+
```
11+
12+
# Diagnostics
13+
```
14+
invalid.js:2:29 lint/nursery/noDivRegex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15+
16+
i Avoid using an equal sign directly after the slash at the beginning of a regular expression literal.
17+
18+
1 │ /* should generate diagnostics */
19+
> 2 │ var f = function() { return /=foo/; };
20+
│ ^^^^^^
21+
3 │
22+
23+
i The characters /= can be confused with a division assignment operator. Replace the equal sign (=) with [=] to prevent confusion.
24+
25+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
26+
27+
i Safe fix: Replace with [=].
28+
29+
2 │ var·f·=·function()·{·return·/[=]foo/};
30+
│ + +
31+
32+
```
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/* should not generate diagnostics */
2+
var f = function() { return /foo/ig.test('bar'); };
3+
4+
var f = function() { return /\\=foo/; };
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: valid.js
4+
---
5+
# Input
6+
```js
7+
/* should not generate diagnostics */
8+
var f = function() { return /foo/ig.test('bar'); };
9+
10+
var f = function() { return /\\=foo/; };
11+
12+
```

0 commit comments

Comments
 (0)