Skip to content

Commit e7e0f6c

Browse files
authored
feat(js_analyze): implement useRegexpExec (#8034)
1 parent a226b28 commit e7e0f6c

File tree

16 files changed

+369
-30
lines changed

16 files changed

+369
-30
lines changed

.changeset/big-shoes-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useRegexpExec`](https://biomejs.dev/linter/rules/use-regexp-exec/). Enforce `RegExp#exec` over `String#match` if no global flag is provided.

crates/biome_analyze/CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -893,7 +893,7 @@ impl Rule for ForLoopCountReferences {
893893
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
894894
let node = ctx.query();
895895

896-
// The model holds all informations about the semantic, like scopes and declarations
896+
// The model holds all information about the semantic, like scopes and declarations
897897
let model = ctx.model();
898898

899899
// Here we are extracting the `let i = 0;` declaration in for loop

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

Lines changed: 24 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: 49 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/domain_selector.rs

Lines changed: 1 addition & 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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ define_categories! {
217217
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",
218218
"lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage",
219219
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
220+
"lint/nursery/useRegexpExec": "https://biomejs.dev/linter/rules/use-regexp-exec",
220221
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
221222
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
222223
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",

crates/biome_js_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@ pub mod use_find;
4141
pub mod use_max_params;
4242
pub mod use_qwik_method_usage;
4343
pub mod use_qwik_valid_lexical_scope;
44+
pub mod use_regexp_exec;
4445
pub mod use_sorted_classes;
4546
pub mod use_spread;
4647
pub mod use_vue_define_macros_order;
4748
pub mod use_vue_multi_word_component_names;
48-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , 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_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , 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_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , 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_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
49+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , 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_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , 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_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , 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_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , 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_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use crate::services::typed::Typed;
2+
use biome_analyze::{
3+
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
4+
};
5+
use biome_console::markup;
6+
use biome_js_syntax::JsCallExpression;
7+
use biome_js_type_info::{Literal, ResolvedTypeData, TypeData};
8+
use biome_rowan::{AstNode, AstSeparatedList};
9+
use biome_rule_options::use_regexp_exec::UseRegexpExecOptions;
10+
11+
declare_lint_rule! {
12+
/// Enforce `RegExp#exec` over `String#match` if no global flag is provided.
13+
///
14+
/// String#match is defined to work the same as RegExp#exec when the regular expression does not include the g flag.
15+
/// Keeping to consistently using one of the two can help improve code readability.
16+
///
17+
/// RegExp#exec may also be slightly faster than String#match; this is the reason to choose it as the preferred usage.
18+
///
19+
/// ## Examples
20+
///
21+
/// ### Invalid
22+
///
23+
/// ```ts,file=invalid.ts,expect_diagnostic
24+
/// 'something'.match(/thing/);
25+
/// ```
26+
///
27+
/// ### Valid
28+
///
29+
/// ```ts,file=valid.ts
30+
/// /thing/.exec('something');
31+
/// ```
32+
///
33+
pub UseRegexpExec {
34+
version: "next",
35+
name: "useRegexpExec",
36+
language: "js",
37+
recommended: false,
38+
sources: &[RuleSource::EslintTypeScript("prefer-regexp-exec").same(), RuleSource::EslintRegexp("prefer-regexp-exec").same()],
39+
domains: &[RuleDomain::Project],
40+
}
41+
}
42+
43+
impl Rule for UseRegexpExec {
44+
type Query = Typed<JsCallExpression>;
45+
type State = ();
46+
type Signals = Option<Self::State>;
47+
type Options = UseRegexpExecOptions;
48+
49+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
50+
let node = ctx.query();
51+
52+
let binding = node.callee().ok()?.omit_parentheses();
53+
let callee = binding.as_js_static_member_expression()?;
54+
55+
let call_object = callee.object().ok()?;
56+
if !ctx
57+
.type_of_expression(&call_object)
58+
.is_string_or_string_literal()
59+
{
60+
return None;
61+
}
62+
63+
let call_name = callee.member().ok()?.as_js_name()?.to_trimmed_text();
64+
if call_name != "match" {
65+
return None;
66+
}
67+
68+
let args = node.arguments().ok()?.args();
69+
let first_arg = args.first()?.ok()?;
70+
let express = first_arg.as_any_js_expression()?;
71+
72+
let value_type = ctx.type_of_expression(express);
73+
74+
if value_type
75+
.resolved_data()
76+
.map(ResolvedTypeData::as_raw_data)
77+
.is_some_and(|ty| match ty {
78+
TypeData::Literal(literal) => match literal.as_ref() {
79+
Literal::RegExp(literal) => !literal.flags.contains('g'),
80+
_ => false,
81+
},
82+
_ => false,
83+
})
84+
{
85+
return Some(());
86+
}
87+
88+
None
89+
}
90+
91+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
92+
let node = ctx.query();
93+
Some(
94+
RuleDiagnostic::new(
95+
rule_category!(),
96+
node.range(),
97+
markup! {
98+
"Prefer "<Emphasis>"RegExp#exec()"</Emphasis>" over "<Emphasis>"String#match()"</Emphasis>" when searching within a string."
99+
},
100+
)
101+
.note(markup! {
102+
"Use "<Emphasis>"RegExp#exec()"</Emphasis>" instead of "<Emphasis>"String#match()"</Emphasis>" for consistent and slightly faster regex matching."
103+
}),
104+
)
105+
}
106+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'something'.match(/thing/);
2+
3+
'some things are just things'.match(/thing/);
4+
5+
'something'.match(new RegExp(/thing/));
6+
7+
const text = 'something';
8+
const search = /thing/;
9+
text.match(search);
10+
11+
const text1 = 'something';
12+
const search1 = new RegExp(/thing/);
13+
text1.match(search1);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
source: crates/biome_js_analyze/tests/spec_tests.rs
3+
expression: invalid.js
4+
---
5+
# Input
6+
```js
7+
'something'.match(/thing/);
8+
9+
'some things are just things'.match(/thing/);
10+
11+
'something'.match(new RegExp(/thing/));
12+
13+
const text = 'something';
14+
const search = /thing/;
15+
text.match(search);
16+
17+
const text1 = 'something';
18+
const search1 = new RegExp(/thing/);
19+
text1.match(search1);
20+
21+
```
22+
23+
# Diagnostics
24+
```
25+
invalid.js:1:1 lint/nursery/useRegexpExec ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26+
27+
i Prefer RegExp#exec() over String#match() when searching within a string.
28+
29+
> 1 │ 'something'.match(/thing/);
30+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
31+
2 │
32+
3 │ 'some things are just things'.match(/thing/);
33+
34+
i Use RegExp#exec() instead of String#match() for consistent and slightly faster regex matching.
35+
36+
37+
```
38+
39+
```
40+
invalid.js:3:1 lint/nursery/useRegexpExec ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41+
42+
i Prefer RegExp#exec() over String#match() when searching within a string.
43+
44+
1 │ 'something'.match(/thing/);
45+
2 │
46+
> 3 │ 'some things are just things'.match(/thing/);
47+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
48+
4 │
49+
5 │ 'something'.match(new RegExp(/thing/));
50+
51+
i Use RegExp#exec() instead of String#match() for consistent and slightly faster regex matching.
52+
53+
54+
```
55+
56+
```
57+
invalid.js:9:1 lint/nursery/useRegexpExec ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58+
59+
i Prefer RegExp#exec() over String#match() when searching within a string.
60+
61+
7 │ const text = 'something';
62+
8 │ const search = /thing/;
63+
> 9 │ text.match(search);
64+
│ ^^^^^^^^^^^^^^^^^^
65+
10 │
66+
11 │ const text1 = 'something';
67+
68+
i Use RegExp#exec() instead of String#match() for consistent and slightly faster regex matching.
69+
70+
71+
```

0 commit comments

Comments
 (0)