mirror of
https://github.com/swc-project/swc.git
synced 2024-10-04 04:07:18 +03:00
feat(es/lints): Implement dot-notation
rule (#3481)
This commit is contained in:
parent
8dbc949cfe
commit
5bb6bd71b6
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -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",
|
||||
|
12
crates/swc/tests/errors/lints/dot-notation/pattern/.swcrc
Normal file
12
crates/swc/tests/errors/lints/dot-notation/pattern/.swcrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"dotNotation": [
|
||||
"error",
|
||||
{
|
||||
"allowPattern": "^prefix.*"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
obj['prefix_a'];
|
||||
|
||||
obj['a'];
|
@ -0,0 +1,6 @@
|
||||
error: ['a'] is better written in dot notation
|
||||
|
||||
|
|
||||
3 | obj['a'];
|
||||
| ^^^^^
|
||||
|
@ -0,0 +1,12 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"dotNotation": [
|
||||
"error",
|
||||
{
|
||||
"allowKeywords": false
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
obj['break'];
|
@ -0,0 +1,6 @@
|
||||
error: ['break'] is better written in dot notation
|
||||
|
||||
|
|
||||
1 | obj['break'];
|
||||
| ^^^^^^^^^
|
||||
|
9
crates/swc/tests/errors/lints/dot-notation/simple/.swcrc
Normal file
9
crates/swc/tests/errors/lints/dot-notation/simple/.swcrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"dotNotation": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
15
crates/swc/tests/errors/lints/dot-notation/simple/input.js
Normal file
15
crates/swc/tests/errors/lints/dot-notation/simple/input.js
Normal file
@ -0,0 +1,15 @@
|
||||
obj["a"];
|
||||
|
||||
obj[obj2["b"]];
|
||||
|
||||
obj.key;
|
||||
|
||||
obj[prop];
|
||||
|
||||
obj['c'];
|
||||
|
||||
obj?.['d'];
|
||||
|
||||
(obj)['e'];
|
||||
|
||||
foo[call(my["x"])]
|
@ -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"])]
|
||||
| ^^^^^
|
||||
|
@ -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"]}
|
||||
|
@ -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>,
|
||||
|
145
crates/swc_ecma_lints/src/rules/dot_notation.rs
Normal file
145
crates/swc_ecma_lints/src/rules/dot_notation.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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 = "e.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) => {
|
||||
|
43
crates/swc_ecma_lints/src/rules/utils.rs
Normal file
43
crates/swc_ecma_lints/src/rules/utils.rs
Normal 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 = "e.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,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user