Skip to content

Commit

Permalink
feat(lint/useRegexLiterals): implement the rule (#843)
Browse files Browse the repository at this point in the history
  • Loading branch information
Yuiki authored Nov 24, 2023
1 parent 98aeb72 commit a3e14da
Show file tree
Hide file tree
Showing 17 changed files with 6,988 additions and 9 deletions.
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 @@ -115,6 +115,7 @@ define_categories! {
"lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment",
"lint/nursery/useGroupedTypeImport": "https://biomejs.dev/linter/rules/use-grouped-type-import",
"lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions",
"lint/nursery/useRegexLiterals": "https://biomejs.dev/linter/rules/use-regex-literals",
"lint/nursery/useShorthandAssign": "https://biomejs.dev/linter/rules/use-shorthand-assign",
"lint/nursery/useValidAriaRole": "https://biomejs.dev/lint/rules/use-valid-aria-role",
"lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/analyzers/nursery.rs

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

244 changes: 244 additions & 0 deletions crates/biome_js_analyze/src/analyzers/nursery/use_regex_literals.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_factory::make::js_regex_literal_expression;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
global_identifier, static_value::StaticValue, AnyJsCallArgument, AnyJsExpression,
AnyJsLiteralExpression, JsCallArguments, JsCallExpression, JsComputedMemberExpression,
JsNewExpression, JsSyntaxKind, JsSyntaxToken,
};
use biome_rowan::{
declare_node_union, AstNode, AstSeparatedList, BatchMutationExt, SyntaxError, TokenText,
};

use crate::{semantic_services::Semantic, JsRuleAction};

declare_rule! {
/// Enforce the use of the regular expression literals instead of the RegExp constructor if possible.
///
/// There are two ways to create a regular expression:
/// - Regular expression literals, e.g., `/abc/u`.
/// - The RegExp constructor function, e.g., `new RegExp("abc", "u")` .
///
/// The constructor function is particularly useful when you want to dynamically generate the pattern,
/// because it takes string arguments.
///
/// Using regular expression literals avoids some escaping required in a string literal,
/// and are easier to analyze statically.
///
/// Source: https://eslint.org/docs/latest/rules/prefer-regex-literals/
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// new RegExp("abc", "u");
/// ```
///
/// ## Valid
///
/// ```js
/// /abc/u;
///
/// new RegExp("abc", flags);
/// ```
///
pub(crate) UseRegexLiterals {
version: "1.3.0",
name: "useRegexLiterals",
recommended: false,
fix_kind: FixKind::Unsafe,
}
}

declare_node_union! {
pub(crate) JsNewOrCallExpression = JsNewExpression | JsCallExpression
}

pub struct UseRegexLiteralsState {
pattern: String,
flags: Option<String>,
}

impl Rule for UseRegexLiterals {
type Query = Semantic<JsNewOrCallExpression>;
type State = UseRegexLiteralsState;
type Signals = Option<Self::State>;
type Options = ();

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

let (callee, arguments) = parse_node(node)?;
if !is_regexp_object(callee, model) {
return None;
}

let args = arguments.args();
if args.len() > 2 {
return None;
}
let mut args = args.iter();

let pattern = args.next()?;
let pattern = create_pattern(pattern, model)?;

let flags = match args.next() {
Some(flags) => {
let flags = create_flags(flags)?;
Some(flags)
}
None => None,
};
Some(UseRegexLiteralsState { pattern, flags })
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
Some(RuleDiagnostic::new(
rule_category!(),
ctx.query().range(),
markup! {
"Use a regular expression literal instead of the "<Emphasis>"RegExp"</Emphasis>" constructor."
},
).note(markup! {
"Regular expression literals avoid some escaping required in a string literal, and are easier to analyze statically."
}))
}

fn action(ctx: &RuleContext<Self>, state: &Self::State) -> Option<JsRuleAction> {
let prev = match ctx.query() {
JsNewOrCallExpression::JsNewExpression(node) => AnyJsExpression::from(node.clone()),
JsNewOrCallExpression::JsCallExpression(node) => AnyJsExpression::from(node.clone()),
};

let token = JsSyntaxToken::new_detached(
JsSyntaxKind::JS_REGEX_LITERAL,
&format!(
"/{}/{}",
state.pattern,
state.flags.as_deref().unwrap_or_default()
),
[],
[],
);
let next = AnyJsExpression::AnyJsLiteralExpression(AnyJsLiteralExpression::from(
js_regex_literal_expression(token),
));
let mut mutation = ctx.root().begin();
mutation.replace_node(prev, next);

Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::Always,
message: markup! {
"Use a "<Emphasis>"literal notation"</Emphasis>" instead."
}
.to_owned(),
mutation,
})
}
}

fn create_pattern(
pattern: Result<AnyJsCallArgument, SyntaxError>,
model: &SemanticModel,
) -> Option<String> {
let pattern = pattern.ok()?;
let expr = pattern.as_any_js_expression()?;
if let Some(expr) = expr.as_js_template_expression() {
if let Some(tag) = expr.tag() {
let (object, member) = match tag.omit_parentheses() {
AnyJsExpression::JsStaticMemberExpression(expr) => {
let object = expr.object().ok()?;
let member = expr.member().ok()?;
(object, member.value_token().ok()?.token_text_trimmed())
}
AnyJsExpression::JsComputedMemberExpression(expr) => {
let object = expr.object().ok()?;
let member = extract_inner_text(&expr)?;
(object, member)
}
_ => return None,
};
let (reference, name) = global_identifier(&object)?;
if model.binding(&reference).is_some() || name.text() != "String" || member != "raw" {
return None;
}
};
};
let pattern = extract_literal_string(pattern)?;
let pattern = pattern.replace("\\\\", "\\");

// If pattern is empty, (?:) is used instead.
if pattern.is_empty() {
return Some("(?:)".to_string());
}

// A repetition without quantifiers is invalid.
if pattern == "*" || pattern == "+" || pattern == "?" {
return None;
}
Some(pattern)
}

fn is_regexp_object(expr: AnyJsExpression, model: &SemanticModel) -> bool {
match global_identifier(&expr.omit_parentheses()) {
Some((reference, name)) => match model.binding(&reference) {
Some(_) if !reference.is_global_this() && !reference.has_name("window") => false,
_ => name.text() == "RegExp",
},
None => false,
}
}

fn parse_node(node: &JsNewOrCallExpression) -> Option<(AnyJsExpression, JsCallArguments)> {
match node {
JsNewOrCallExpression::JsNewExpression(node) => {
let callee = node.callee().ok()?;
let args = node.arguments()?;
Some((callee, args))
}
JsNewOrCallExpression::JsCallExpression(node) => {
let callee = node.callee().ok()?;
let args = node.arguments().ok()?;
Some((callee, args))
}
}
}

fn create_flags(flags: Result<AnyJsCallArgument, SyntaxError>) -> Option<String> {
let flags = flags.ok()?;
let flags = extract_literal_string(flags)?;
// u flag (Unicode mode) and v flag (unicodeSets mode) cannot be combined.
if flags == "uv" || flags == "vu" {
return None;
}
Some(flags)
}

fn extract_literal_string(from: AnyJsCallArgument) -> Option<String> {
let AnyJsCallArgument::AnyJsExpression(expr) = from else {
return None;
};
expr.omit_parentheses()
.as_static_value()
.and_then(|value| match value {
StaticValue::String(_) => Some(value.text().to_string().replace('\n', "\\n")),
StaticValue::EmptyString(_) => Some(String::new()),
_ => None,
})
}

fn extract_inner_text(expr: &JsComputedMemberExpression) -> Option<TokenText> {
expr.member()
.ok()?
.as_any_js_literal_expression()?
.as_js_string_literal_expression()?
.inner_string_text()
.ok()
}
Loading

0 comments on commit a3e14da

Please sign in to comment.