Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/big-shoes-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

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.
2 changes: 1 addition & 1 deletion crates/biome_analyze/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -893,7 +893,7 @@ impl Rule for ForLoopCountReferences {
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

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

// Here we are extracting the `let i = 0;` declaration in for loop
Expand Down
24 changes: 24 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 28 additions & 7 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ define_categories! {
"lint/nursery/useMaxParams": "https://biomejs.dev/linter/rules/use-max-params",
"lint/nursery/useQwikMethodUsage": "https://biomejs.dev/linter/rules/use-qwik-method-usage",
"lint/nursery/useQwikValidLexicalScope": "https://biomejs.dev/linter/rules/use-qwik-valid-lexical-scope",
"lint/nursery/useRegexpExec": "https://biomejs.dev/linter/rules/use-regexp-exec",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useVueDefineMacrosOrder": "https://biomejs.dev/linter/rules/use-vue-define-macros-order",
"lint/nursery/useVueMultiWordComponentNames": "https://biomejs.dev/linter/rules/use-vue-multi-word-component-names",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pub mod use_explicit_type;
pub mod use_max_params;
pub mod use_qwik_method_usage;
pub mod use_qwik_valid_lexical_scope;
pub mod use_regexp_exec;
pub mod use_sorted_classes;
pub mod use_vue_define_macros_order;
pub mod use_vue_multi_word_component_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , 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_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , 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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_empty_source :: NoEmptySource , self :: no_floating_promises :: NoFloatingPromises , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_misused_promises :: NoMisusedPromises , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_shadow :: NoShadow , 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_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , 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_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } }
105 changes: 105 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery/use_regexp_exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::services::typed::Typed;
use biome_analyze::{
Rule, RuleDiagnostic, RuleDomain, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_js_syntax::{AnyJsExpression, JsCallExpression};
use biome_rowan::{AstNode, AstSeparatedList};
use biome_rule_options::use_regexp_exec::UseRegexpExecOptions;

declare_lint_rule! {
/// Enforce RegExp#exec over String#match if no global flag is provided.
///
/// String#match is defined to work the same as RegExp#exec when the regular expression does not include the g flag.
/// Keeping to consistently using one of the two can help improve code readability.
///
/// RegExp#exec may also be slightly faster than String#match; this is the reason to choose it as the preferred usage.
///
/// ## Examples
///
/// ### Invalid
///
/// ```ts,expect_diagnostic
/// 'something'.match(/thing/);
/// ```
///
/// ### Valid
///
/// ```ts
/// /thing/.exec('something');
/// ```
///
pub UseRegexpExec {
version: "next",
name: "useRegexpExec",
language: "js",
recommended: false,
sources: &[RuleSource::EslintTypeScript("prefer-regexp-exec").same(), RuleSource::EslintRegexp("prefer-regexp-exec").same()],
domains: &[RuleDomain::Project],
}
}

impl Rule for UseRegexpExec {
type Query = Typed<JsCallExpression>;
type State = ();
type Signals = Option<Self::State>;
type Options = UseRegexpExecOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

let binding = node.callee().ok()?.omit_parentheses();
let callee = binding.as_js_static_member_expression()?;

let call_object = callee.object().ok()?;
if !ctx
.type_of_expression(&call_object)
.is_string_or_string_literal()
{
return None;
}

let call_name = callee.member().ok()?.as_js_name()?.to_trimmed_text();
if call_name != "match" {
return None;
}

let args = node.arguments().ok()?.args();
let first_arg = args.first()?.ok()?;
let express = first_arg.as_any_js_expression()?;

let regex_arg = match express {
AnyJsExpression::AnyJsLiteralExpression(express) => {
express.as_js_regex_literal_expression()
}
AnyJsExpression::JsIdentifierExpression(identifier) => {
// TODO: get static regexp value
return None;
}
Comment on lines +75 to +78
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how how to resolve the value of a reference.

Could also use the type of the expression, then check the flags within the regex type? Not sure if that's valid?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@Netail Netail Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They seem to call an Eslint API to get the value of a given node if it can be decided statically. (not through types) But not sure if biome provides such API?

https://eslint-community.github.io/eslint-utils/api/ast-utils.html#getstaticvalue

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the semantic model and the control flow graph, but iirc typed rules shouldn't be using those because they use the module graph. @arendjr, I'd appreciate your input here, you have more context on the type system and the module graph than I do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typed rules can also use the semantic model and the control flow graph, but the semantic model is largely redundant with functionality that is already provided by the typed service, which provides similar things, but with a higher level of abstraction that includes types and cross-module inference.

So far the abstract way of looking at it :)

Unfortunately we don't really have an API yet that can determine static values (AFAIK), but the typed service actually does try to determine static values when it can determine them. Simple example:

const foo = 1;
foo;

The type system will be able to determine that the reference to foo on the second line resolves to the concrete value 1, because in TypeScript concrete values can be used as types as well and we try to infer as specific as we can. So yes, I think using the type system is probably the way to go here, but some more work will be necessary. I don't think we have any specific detection for RegExp yet, and I haven't looked yet into what flags need to be checked, but that might require some custom logic too?

If you like, I might be interested to look into this next week.

Copy link
Member Author

@Netail Netail Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be awesome. For now I only need to check on the global flag. But some API to the regexp type could be to check if it has any given flag could be nice

_ => None,
}?;

let (_, flags) = regex_arg.decompose().unwrap();
if flags.contains('g') {
return None;
}

Some(())
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Prefer "<Emphasis>"Regexp#exec()"</Emphasis>" over "<Emphasis>"String#match()"</Emphasis>" when searching within a string."
},
)
.note(markup! {
"This note will give you more information."
}),
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'something'.match(/thing/);

'some things are just things'.match(/thing/);

const text = 'something';
const search = /thing/;
text.match(search);
17 changes: 17 additions & 0 deletions crates/biome_js_analyze/tests/specs/nursery/useRegexpExec/valid.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* should not generate diagnostics */
/thing/.exec('something');

'some things are just things'.match(/thing/g);

const text = 'something';
const search = /thing/;
search.exec(text);

const text1 = 'something';
const search1 = /thing/g;
text1.match(search1);

const obj = {
match: () => { }
}
obj.match(/thing/)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
source: crates/biome_js_analyze/tests/spec_tests.rs
expression: valid.js
---
# Input
```js
/* should not generate diagnostics */
/thing/.exec('something');

'some things are just things'.match(/thing/g);

const text = 'something';
const search = /thing/;
search.exec(text);

const text1 = 'something';
const search1 = /thing/g;
text1.match(search1);

const obj = {
match: () => { }
}
obj.match(/thing/)

```
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ pub mod use_qwik_valid_lexical_scope;
pub mod use_react_function_components;
pub mod use_readonly_class_properties;
pub mod use_regex_literals;
pub mod use_regexp_exec;
pub mod use_self_closing_elements;
pub mod use_semantic_elements;
pub mod use_shorthand_assign;
Expand Down
6 changes: 6 additions & 0 deletions crates/biome_rule_options/src/use_regexp_exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use biome_deserialize_macros::{Deserializable, Merge};
use serde::{Deserialize, Serialize};
#[derive(Default, Clone, Debug, Deserialize, Deserializable, Eq, Merge, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "camelCase", deny_unknown_fields, default)]
pub struct UseRegexpExecOptions {}
19 changes: 19 additions & 0 deletions packages/@biomejs/backend-jsonrpc/src/workspace.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions packages/@biomejs/biome/configuration_schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading