feat(es/lints): Implement prefer-regex-literals (#3399)

This commit is contained in:
Artur 2022-02-04 10:46:34 +03:00 committed by GitHub
parent d7c2d0561b
commit 8166275166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 418 additions and 0 deletions

View File

@ -0,0 +1,9 @@
{
"jsc": {
"lints": {
"preferRegexLiterals": ["error", {
"disallowRedundantWrapping": true
}]
}
}
}

View File

@ -0,0 +1,3 @@
new RegExp(/redundant/);
new RegExp(/redundant/, "g");

View File

@ -0,0 +1,12 @@
error: Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor
|
1 | new RegExp(/redundant/);
| ^^^^^^^^^^^^^^^^^^^^^^^
error: Use regular expression literal with flags instead of the 'RegExp' constructor
|
3 | new RegExp(/redundant/, "g");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,8 @@
{
"jsc": {
"target": "es2020",
"lints": {
"preferRegexLiterals": ["error"]
}
}
}

View File

@ -0,0 +1,3 @@
new globalThis.RegExp("a", "b");
new globalThis["RegExp"]("a", "b");
globalThis.RegExp("a", "b");

View File

@ -0,0 +1,18 @@
error: Use a regular expression literal instead of the 'RegExp' constructor
|
1 | new globalThis.RegExp("a", "b");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
2 | new globalThis["RegExp"]("a", "b");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
3 | globalThis.RegExp("a", "b");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,7 @@
{
"jsc": {
"lints": {
"preferRegexLiterals": ["error"]
}
}
}

View File

@ -0,0 +1,41 @@
new RegExp("abc");
new RegExp("abc", "u");
RegExp("abc");
RegExp("abc", "u");
new RegExp("\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d");
RegExp(`^\\d\\.$`);
new RegExp(String.raw`^\d\.$`);
/abc/;
/abc/u;
/\d\d\.\d\d\.\d\d\d\d/;
/^\d\.$/;
new RegExp(pattern);
RegExp("abc", flags);
new RegExp(prefix + "abc");
RegExp(`${prefix}abc`);
new RegExp(String.raw`^\d\. ${suffix}`);
function f1() {
class RegExp {
constructor(a, b) {}
}
new RegExp("a", "b");
}
new RegExp(/redundant/);
const obj = {
RegExp,
};
console.log(RegExp);
((a, b) => {
RegExp;
})("a", "b");
foo(() => {
new RegExp("a");
})("a", "b");
new RegExp("a", "b", "c");

View File

@ -0,0 +1,48 @@
error: Use a regular expression literal instead of the 'RegExp' constructor
|
1 | new RegExp("abc");
| ^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
2 | new RegExp("abc", "u");
| ^^^^^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
3 | RegExp("abc");
| ^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
4 | RegExp("abc", "u");
| ^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
5 | new RegExp("\\d\\d\\.\\d\\d\\.\\d\\d\\d\\d");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
6 | RegExp(`^\\d\\.$`);
| ^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
7 | new RegExp(String.raw`^\d\.$`);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: Use a regular expression literal instead of the 'RegExp' constructor
|
38 | new RegExp("a");
| ^^^^^^^^^^^^^^^

View File

@ -1,5 +1,7 @@
#[cfg(feature = "non_critical_lints")] #[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::no_console::NoConsoleConfig; use crate::rules::non_critical_lints::no_console::NoConsoleConfig;
#[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::prefer_regex_literals::PreferRegexLiteralsConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@ -41,6 +43,10 @@ pub struct LintConfig {
#[serde(default)] #[serde(default)]
pub no_console: RuleConfig<NoConsoleConfig>, pub no_console: RuleConfig<NoConsoleConfig>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub prefer_regex_literals: RuleConfig<PreferRegexLiteralsConfig>,
#[cfg(feature = "non_critical_lints")] #[cfg(feature = "non_critical_lints")]
#[serde(default)] #[serde(default)]
pub no_alert: RuleConfig<()>, pub no_alert: RuleConfig<()>,

View File

@ -13,6 +13,7 @@ pub(crate) mod non_critical_lints {
pub mod no_alert; pub mod no_alert;
pub mod no_console; pub mod no_console;
pub mod no_debugger; pub mod no_debugger;
pub mod prefer_regex_literals;
} }
#[cfg(feature = "non_critical_lints")] #[cfg(feature = "non_critical_lints")]
@ -54,6 +55,13 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
)); ));
rules.extend(no_debugger::no_debugger(&lint_config.no_debugger)); rules.extend(no_debugger::no_debugger(&lint_config.no_debugger));
rules.extend(prefer_regex_literals::prefer_regex_literals(
program,
&lint_config.prefer_regex_literals,
top_level_ctxt,
es_version,
));
} }
rules rules

View File

@ -0,0 +1,255 @@
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule},
};
use serde::{Deserialize, Serialize};
use swc_common::{collections::AHashSet, errors::HANDLER, Span, SyntaxContext};
use swc_ecma_ast::*;
use swc_ecma_utils::{collect_decls_with_ctxt, ident::IdentLike};
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
const UNEXPECTED_REG_EXP_MESSAGE: &str =
"Use a regular expression literal instead of the 'RegExp' constructor";
const UNEXPECTED_REDUNDANT_REG_EXP_MESSAGE: &str =
"Regular expression literal is unnecessarily wrapped within a 'RegExp' constructor";
const UNEXPECTED_REDUNDANT_REG_EXP_WITH_FLAGS_MESSAGE: &str =
"Use regular expression literal with flags instead of the 'RegExp' constructor";
const MAX_VALID_ARGS_COUNT: usize = 2;
const GLOBAL_THIS: &str = "globalThis";
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PreferRegexLiteralsConfig {
disallow_redundant_wrapping: Option<bool>,
}
pub fn prefer_regex_literals(
program: &Program,
config: &RuleConfig<PreferRegexLiteralsConfig>,
top_level_ctxt: SyntaxContext,
es_version: EsVersion,
) -> Option<Box<dyn Rule>> {
let rule_reaction = config.get_rule_reaction();
let disallow_redundant_wrapping = config
.get_rule_config()
.disallow_redundant_wrapping
.unwrap_or(false);
let top_level_declared_vars: AHashSet<Id> = collect_decls_with_ctxt(program, top_level_ctxt);
match rule_reaction {
LintRuleReaction::Off => None,
_ => Some(visitor_rule(PreferRegexLiterals::new(
*rule_reaction,
disallow_redundant_wrapping,
top_level_declared_vars,
top_level_ctxt,
es_version,
))),
}
}
#[derive(Debug)]
enum ArgValueType {
Str,
RegExp,
Another,
}
#[derive(Debug, Default)]
struct PreferRegexLiterals {
expected_reaction: LintRuleReaction,
disallow_redundant_wrapping: bool,
top_level_ctxt: SyntaxContext,
top_level_declared_vars: AHashSet<Id>,
allow_global_this: bool,
call_span: Option<Span>,
first_arg: Option<ArgValueType>,
second_arg: Option<ArgValueType>,
}
impl PreferRegexLiterals {
fn new(
expected_reaction: LintRuleReaction,
disallow_redundant_wrapping: bool,
top_level_declared_vars: AHashSet<Id>,
top_level_ctxt: SyntaxContext,
es_version: EsVersion,
) -> Self {
Self {
expected_reaction,
disallow_redundant_wrapping,
top_level_ctxt,
top_level_declared_vars,
allow_global_this: es_version < EsVersion::Es2020,
call_span: None,
first_arg: None,
second_arg: None,
}
}
fn extract_arg_value_type(expr: &Expr) -> ArgValueType {
match expr {
Expr::Lit(Lit::Str(_)) => ArgValueType::Str,
Expr::Lit(Lit::Regex(_)) => ArgValueType::RegExp,
Expr::Tpl(Tpl { exprs, .. }) => match exprs.len() {
0 => ArgValueType::Str,
_ => ArgValueType::Another,
},
Expr::TaggedTpl(TaggedTpl {
tpl: Tpl { exprs, .. },
..
}) => match exprs.len() {
0 => ArgValueType::Str,
_ => ArgValueType::Another,
},
_ => ArgValueType::Another,
}
}
fn reset_state(&mut self) {
self.call_span = None;
self.first_arg = None;
self.second_arg = None;
}
fn set_state(&mut self, call_span: Span, args: &[ExprOrSpread]) {
self.call_span = Some(call_span);
if let Some(ExprOrSpread { expr, .. }) = args.get(0) {
self.first_arg = Some(Self::extract_arg_value_type(expr.as_ref()));
}
if let Some(ExprOrSpread { expr, .. }) = args.get(1) {
self.second_arg = Some(Self::extract_arg_value_type(expr.as_ref()));
}
}
fn emit_report(&self, message: &str) {
let span = self.call_span.unwrap();
HANDLER.with(|handler| match self.expected_reaction {
LintRuleReaction::Error => {
handler.struct_span_err(span, message).emit();
}
LintRuleReaction::Warning => {
handler.struct_span_warn(span, message).emit();
}
_ => {}
});
}
fn check(&self, sym: &str) {
use ArgValueType::*;
if sym == "RegExp" {
match (self.first_arg.as_ref(), self.second_arg.as_ref()) {
(Some(Str), None) => self.emit_report(UNEXPECTED_REG_EXP_MESSAGE),
(Some(Str), Some(Str)) => self.emit_report(UNEXPECTED_REG_EXP_MESSAGE),
(Some(RegExp), None) => {
if self.disallow_redundant_wrapping {
self.emit_report(UNEXPECTED_REDUNDANT_REG_EXP_MESSAGE);
}
}
(Some(RegExp), Some(Str)) => {
if self.disallow_redundant_wrapping {
self.emit_report(UNEXPECTED_REDUNDANT_REG_EXP_WITH_FLAGS_MESSAGE);
}
}
_ => {}
}
}
}
}
impl Visit for PreferRegexLiterals {
noop_visit_type!();
fn visit_new_expr(&mut self, new_expr: &NewExpr) {
if let Some(args) = &new_expr.args {
if args.len() <= MAX_VALID_ARGS_COUNT {
self.set_state(new_expr.span, args);
}
}
new_expr.visit_children_with(self);
self.reset_state();
}
fn visit_call_expr(&mut self, call_expr: &CallExpr) {
if call_expr.args.len() <= MAX_VALID_ARGS_COUNT {
if let Some(expr) = call_expr.callee.as_expr() {
match expr.as_ref() {
Expr::Ident(_) => {
self.set_state(call_expr.span, &call_expr.args);
}
Expr::Member(_) => {
self.set_state(call_expr.span, &call_expr.args);
}
_ => {}
}
}
}
call_expr.visit_children_with(self);
self.reset_state();
}
fn visit_ident(&mut self, ident: &Ident) {
if ident.span.ctxt != self.top_level_ctxt {
return;
}
if self.top_level_declared_vars.contains(&ident.to_id()) {
return;
}
if self.call_span.is_none() {
return;
}
self.check(&*ident.sym);
}
fn visit_member_expr(&mut self, member_expr: &MemberExpr) {
if self.allow_global_this {
return;
}
if self.call_span.is_none() {
return;
}
if let Some(ident) = member_expr.obj.as_ident() {
if ident.span.ctxt != self.top_level_ctxt {
return;
}
if self.top_level_declared_vars.contains(&ident.to_id()) {
return;
}
if &*ident.sym != GLOBAL_THIS {
return;
}
} else {
return;
}
match &member_expr.prop {
MemberProp::Ident(ident) => {
self.check(&*ident.sym);
}
MemberProp::Computed(comp) => {
if let Expr::Lit(Lit::Str(Str { value, .. })) = comp.expr.as_ref() {
self.check(&*value);
}
}
_ => {}
}
}
}