feat(es/lints): Implement dot-notation rule (#3481)

This commit is contained in:
Artur 2022-02-09 14:24:58 +03:00 committed by GitHub
parent 8dbc949cfe
commit 5bb6bd71b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 314 additions and 35 deletions

3
Cargo.lock generated
View File

@ -2922,9 +2922,12 @@ dependencies = [
name = "swc_ecma_lints"
version = "0.14.1"
dependencies = [
"ahash",
"auto_impl",
"dashmap",
"parking_lot",
"rayon",
"regex",
"serde",
"swc_atoms",
"swc_common",

View File

@ -0,0 +1,12 @@
{
"jsc": {
"lints": {
"dotNotation": [
"error",
{
"allowPattern": "^prefix.*"
}
]
}
}
}

View File

@ -0,0 +1,3 @@
obj['prefix_a'];
obj['a'];

View File

@ -0,0 +1,6 @@
error: ['a'] is better written in dot notation
|
3 | obj['a'];
| ^^^^^

View File

@ -0,0 +1,12 @@
{
"jsc": {
"lints": {
"dotNotation": [
"error",
{
"allowKeywords": false
}
]
}
}
}

View File

@ -0,0 +1 @@
obj['break'];

View File

@ -0,0 +1,6 @@
error: ['break'] is better written in dot notation
|
1 | obj['break'];
| ^^^^^^^^^

View File

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

View File

@ -0,0 +1,15 @@
obj["a"];
obj[obj2["b"]];
obj.key;
obj[prop];
obj['c'];
obj?.['d'];
(obj)['e'];
foo[call(my["x"])]

View File

@ -0,0 +1,36 @@
error: ["a"] is better written in dot notation
|
1 | obj["a"];
| ^^^^^
error: ["b"] is better written in dot notation
|
3 | obj[obj2["b"]];
| ^^^^^
error: ['c'] is better written in dot notation
|
9 | obj['c'];
| ^^^^^
error: ['d'] is better written in dot notation
|
11 | obj?.['d'];
| ^^^^^
error: ['e'] is better written in dot notation
|
13 | (obj)['e'];
| ^^^^^
error: ["x"] is better written in dot notation
|
15 | foo[call(my["x"])]
| ^^^^^

View File

@ -12,6 +12,9 @@ version = "0.14.1"
auto_impl = "0.5.0"
parking_lot = "0.11"
rayon = "1.5.1"
regex = "1"
dashmap = "4.0.2"
ahash = "0.7"
serde = {version = "1.0.133", features = ["derive"]}
swc_atoms = {version = "0.2.9", path = "../swc_atoms"}
swc_common = {version = "0.17.0", path = "../swc_common", features = ["concurrent"]}

View File

@ -2,6 +2,8 @@ use std::fmt::Debug;
use serde::{Deserialize, Serialize};
#[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::dot_notation::DotNotationConfig;
#[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::no_console::NoConsoleConfig;
#[cfg(feature = "non_critical_lints")]
@ -59,6 +61,10 @@ pub struct LintConfig {
#[serde(default)]
pub no_debugger: RuleConfig<()>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub dot_notation: RuleConfig<DotNotationConfig>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub quotes: RuleConfig<QuotesConfig>,

View File

@ -0,0 +1,145 @@
use std::{
fmt::{self, Debug},
sync::Arc,
};
use dashmap::DashMap;
use regex::Regex;
use serde::{Deserialize, Serialize};
use swc_common::{errors::HANDLER, sync::Lazy, SourceMap, Span};
use swc_ecma_ast::*;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule},
rules::utils::{resolve_string_quote_type, QuotesType},
};
const INVALID_REGEX_MESSAGE: &str = "dotNotation: invalid regex pattern in allowPattern. Check syntax documentation https://docs.rs/regex/latest/regex/#syntax";
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DotNotationConfig {
allow_keywords: Option<bool>,
allow_pattern: Option<String>,
}
pub fn dot_notation(
program: &Program,
source_map: &Arc<SourceMap>,
config: &RuleConfig<DotNotationConfig>,
) -> Option<Box<dyn Rule>> {
match config.get_rule_reaction() {
LintRuleReaction::Off => None,
_ => Some(visitor_rule(DotNotation::new(
source_map.clone(),
program.is_module(),
config,
))),
}
}
#[derive(Default)]
struct DotNotation {
source_map: Arc<SourceMap>,
expected_reaction: LintRuleReaction,
allow_keywords: bool,
pattern: Option<String>,
is_module: bool,
}
impl Debug for DotNotation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DotNotation")
.field("expected_reaction", &self.expected_reaction)
.field("allow_keywords", &self.allow_keywords)
.field("pattern", &self.pattern)
.field("is_module", &self.is_module)
.finish()
}
}
impl DotNotation {
fn new(
source_map: Arc<SourceMap>,
is_module: bool,
config: &RuleConfig<DotNotationConfig>,
) -> Self {
let dot_notation_config = config.get_rule_config();
Self {
expected_reaction: *config.get_rule_reaction(),
allow_keywords: dot_notation_config.allow_keywords.unwrap_or(true),
source_map,
is_module,
pattern: dot_notation_config.allow_pattern.clone(),
}
}
fn emit_report(&self, span: Span, quote_type: QuotesType, prop: &str) {
let message = format!(
"[{quote}{prop}{quote}] is better written in dot notation",
prop = prop,
quote = quote_type.get_char()
);
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, span: Span, quote_type: QuotesType, prop_name: &str) {
if self.allow_keywords
&& (prop_name.is_reserved() || prop_name.is_reserved_in_strict_mode(self.is_module))
{
return;
}
if let Some(pattern) = &self.pattern {
static REGEX_CACHE: Lazy<DashMap<String, Regex, ahash::RandomState>> =
Lazy::new(Default::default);
if !REGEX_CACHE.contains_key(pattern) {
REGEX_CACHE.insert(
pattern.clone(),
Regex::new(pattern).expect(INVALID_REGEX_MESSAGE),
);
}
if REGEX_CACHE.get(pattern).unwrap().is_match(prop_name) {
return;
}
}
self.emit_report(span, quote_type, prop_name);
}
}
impl Visit for DotNotation {
noop_visit_type!();
fn visit_member_prop(&mut self, member: &MemberProp) {
if let MemberProp::Computed(prop) = member {
match &*prop.expr {
Expr::Lit(Lit::Str(lit_str)) => {
let quote_type = resolve_string_quote_type(&self.source_map, lit_str).unwrap();
self.check(prop.span, quote_type, &*lit_str.value);
}
Expr::Member(member) => {
member.visit_children_with(self);
}
_ => {
prop.visit_with(self);
}
}
}
}
}

View File

@ -9,10 +9,12 @@ use crate::{config::LintConfig, rule::Rule};
mod const_assign;
mod duplicate_bindings;
mod duplicate_exports;
mod utils;
#[cfg(feature = "non_critical_lints")]
#[path = ""]
pub(crate) mod non_critical_lints {
pub mod dot_notation;
pub mod no_alert;
pub mod no_console;
pub mod no_debugger;
@ -70,6 +72,12 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
top_level_ctxt,
es_version,
));
rules.extend(dot_notation::dot_notation(
program,
&source_map,
&lint_config.dot_notation,
));
}
rules

View File

@ -11,26 +11,13 @@ use swc_ecma_visit::{noop_visit_type, Visit};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule},
rules::utils::{resolve_string_quote_type, QuotesType},
};
const MUST_USE_SINGLE_QUOTES_MESSAGE: &str = "String must use singlequotes";
const MUST_USE_DOUBLE_QUOTES_MESSAGE: &str = "String must use doublequotes";
const MUST_USE_BACKTICK_QUOTES_MESSAGE: &str = "String must use backtick quotes";
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum QuotesType {
Single,
Double,
Backtick,
}
impl Default for QuotesType {
fn default() -> Self {
Self::Double
}
}
#[derive(Debug, Clone, Default, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuotesConfig {
@ -101,26 +88,8 @@ impl Quotes {
});
}
fn resolve_quote_type(&self, span: &Span) -> Option<QuotesType> {
let quote = self.source_map.lookup_byte_offset(span.lo);
let quote_index = quote.pos.0;
let src = &quote.sf.src;
let byte = src.as_bytes()[quote_index as usize];
match byte {
b'\'' => Some(QuotesType::Single),
b'"' => Some(QuotesType::Double),
b'`' => Some(QuotesType::Backtick),
_ => None,
}
}
fn is_mirroring_escape(&self, value: &str) -> bool {
let quote = match &self.prefer {
QuotesType::Single => '\'',
QuotesType::Double => '"',
QuotesType::Backtick => '`',
};
let quote = self.prefer.get_char();
for ch in value.chars() {
if ch == quote {
@ -131,8 +100,10 @@ impl Quotes {
false
}
fn check_str(&self, is_method_key_check: bool, Str { span, value, .. }: &Str) {
let found_quote_type = self.resolve_quote_type(span).unwrap();
fn check_str(&self, is_method_key_check: bool, lit_str: &Str) {
let found_quote_type = resolve_string_quote_type(&self.source_map, lit_str).unwrap();
let Str { span, value, .. } = lit_str;
match (&self.prefer, &found_quote_type) {
(QuotesType::Double, QuotesType::Single) => {

View File

@ -0,0 +1,43 @@
use std::{fmt::Debug, sync::Arc};
use serde::{Deserialize, Serialize};
use swc_common::SourceMap;
use swc_ecma_ast::Str;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum QuotesType {
Single,
Double,
Backtick,
}
impl Default for QuotesType {
fn default() -> Self {
Self::Double
}
}
impl QuotesType {
pub fn get_char(&self) -> char {
match self {
QuotesType::Backtick => '`',
QuotesType::Double => '"',
QuotesType::Single => '\'',
}
}
}
pub fn resolve_string_quote_type(source_map: &Arc<SourceMap>, lit_str: &Str) -> Option<QuotesType> {
let quote = source_map.lookup_byte_offset(lit_str.span.lo);
let quote_index = quote.pos.0;
let src = &quote.sf.src;
let byte = src.as_bytes()[quote_index as usize];
match byte {
b'\'' => Some(QuotesType::Single),
b'"' => Some(QuotesType::Double),
b'`' => Some(QuotesType::Backtick),
_ => None,
}
}