mirror of
https://github.com/swc-project/swc.git
synced 2024-11-23 09:38:16 +03:00
feat(es/lints): Implement linter for quotes of string literals (#3443)
This commit is contained in:
parent
0ff58d83c6
commit
5d6143a53c
@ -447,6 +447,7 @@ impl Options {
|
||||
lint_config: &lints,
|
||||
top_level_ctxt,
|
||||
es_version,
|
||||
source_map: cm.clone(),
|
||||
})),
|
||||
crate::plugin::plugins(experimental),
|
||||
custom_before_pass(&program),
|
||||
|
11
crates/swc/tests/errors/lints/quotes/double/.swcrc
Normal file
11
crates/swc/tests/errors/lints/quotes/double/.swcrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "double",
|
||||
"avoidEscape": false,
|
||||
"allowTemplateLiterals": false
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
8
crates/swc/tests/errors/lints/quotes/double/input.js
Normal file
8
crates/swc/tests/errors/lints/quotes/double/input.js
Normal file
@ -0,0 +1,8 @@
|
||||
var s = `template string`;
|
||||
var s = `template string with "escape"`;
|
||||
var s = `now is ${new Date()}`;
|
||||
// prettier-ignore
|
||||
var s = 'single quotes string';
|
||||
// prettier-ignore
|
||||
var s = 'single quotes string with "escape"';
|
||||
var s = "double quotes string";
|
@ -0,0 +1,24 @@
|
||||
error: String must use doublequotes
|
||||
|
||||
|
|
||||
1 | var s = `template string`;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: String must use doublequotes
|
||||
|
||||
|
|
||||
2 | var s = `template string with "escape"`;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: String must use doublequotes
|
||||
|
||||
|
|
||||
5 | var s = 'single quotes string';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: String must use doublequotes
|
||||
|
||||
|
|
||||
7 | var s = 'single quotes string with "escape"';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
9
crates/swc/tests/errors/lints/quotes/imports/.swcrc
Normal file
9
crates/swc/tests/errors/lints/quotes/imports/.swcrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "backtick"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
14
crates/swc/tests/errors/lints/quotes/imports/input.js
Normal file
14
crates/swc/tests/errors/lints/quotes/imports/input.js
Normal file
@ -0,0 +1,14 @@
|
||||
var s = "double quotes string";
|
||||
|
||||
import "a";
|
||||
|
||||
// prettier-ignore
|
||||
import 'b';
|
||||
|
||||
import c from "c";
|
||||
|
||||
// prettier-ignore
|
||||
import d from 'd';
|
||||
|
||||
// prettier-ignore
|
||||
import { "e" as e, 'f' as f } from 'mod';
|
@ -0,0 +1,6 @@
|
||||
error: String must use backtick quotes
|
||||
|
||||
|
|
||||
1 | var s = "double quotes string";
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
11
crates/swc/tests/errors/lints/quotes/single/.swcrc
Normal file
11
crates/swc/tests/errors/lints/quotes/single/.swcrc
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "single",
|
||||
"avoidEscape": false,
|
||||
"allowTemplateLiterals": false
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
8
crates/swc/tests/errors/lints/quotes/single/input.js
Normal file
8
crates/swc/tests/errors/lints/quotes/single/input.js
Normal file
@ -0,0 +1,8 @@
|
||||
var s = `template string`;
|
||||
var s = `template string with "escape"`;
|
||||
var s = `now is ${new Date()}`;
|
||||
// prettier-ignore
|
||||
var s = 'single quotes string';
|
||||
// prettier-ignore
|
||||
var s = 'single quotes string with "escape"';
|
||||
var s = "double quotes string";
|
@ -0,0 +1,18 @@
|
||||
error: String must use singlequotes
|
||||
|
||||
|
|
||||
1 | var s = `template string`;
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: String must use singlequotes
|
||||
|
||||
|
|
||||
2 | var s = `template string with "escape"`;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
error: String must use singlequotes
|
||||
|
||||
|
|
||||
8 | var s = "double quotes string";
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "backtick"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
var s = "double quotes string";
|
||||
|
||||
// valid cases
|
||||
|
||||
var foo = `bar`;
|
||||
var foo = `bar 'baz'`;
|
||||
var foo = `bar \"baz\"`;
|
||||
var foo = 1;
|
||||
var foo = "a string containing `backtick` quotes";
|
||||
var foo = `bar 'foo' baz` + `bar`;
|
||||
|
||||
() => {
|
||||
"use strict";
|
||||
var foo = `backtick`;
|
||||
};
|
||||
|
||||
() => {
|
||||
"use strict";
|
||||
"use strong";
|
||||
"use asm";
|
||||
var foo = `backtick`;
|
||||
};
|
||||
|
||||
// prettier-ignore
|
||||
var obj = {"key0": 0, 'key1': 1};
|
||||
|
||||
// prettier-ignore
|
||||
class Foo1 { 'bar'(){} }
|
||||
|
||||
// prettier-ignore
|
||||
class Foo2 { static ''(){} }
|
||||
|
||||
// prettier-ignore
|
||||
class C { "double"; 'single'; }
|
@ -0,0 +1,6 @@
|
||||
error: String must use backtick quotes
|
||||
|
||||
|
|
||||
1 | var s = "double quotes string";
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "double"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
// prettier-ignore
|
||||
var s = 'double quotes string';
|
||||
|
||||
// valid cases
|
||||
|
||||
var foo = "bar";
|
||||
var foo = 1;
|
||||
var foo = '"';
|
||||
|
||||
// prettier-ignore
|
||||
class C { "f"; "m"() {} }
|
@ -0,0 +1,6 @@
|
||||
error: String must use doublequotes
|
||||
|
||||
|
|
||||
2 | var s = 'double quotes string';
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"jsc": {
|
||||
"lints": {
|
||||
"quotes": ["error", {
|
||||
"prefer": "single"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
var s = "double quotes string";
|
||||
|
||||
// valid cases
|
||||
|
||||
// prettier-ignore
|
||||
var foo = 'bar';
|
||||
var foo = 1;
|
||||
var foo = "'";
|
||||
|
||||
// prettier-ignore
|
||||
class C { 'f'; 'm'() {} }
|
||||
|
||||
var foo = `back\ntick`;
|
||||
var foo = `back\rtick`;
|
||||
var foo = `back\u2028tick`;
|
||||
var foo = `back\u2029tick`;
|
||||
var foo = `back\\\\\ntick`;
|
||||
var foo = `back\\\\\\\\\ntick`;
|
||||
var foo = `\n`;
|
||||
|
||||
// prettier-ignore
|
||||
var foo = `bar 'foo' baz` + 'bar';
|
||||
var foo = `back${x}tick`;
|
||||
var foo = tag`backtick`;
|
||||
var foo = `bar 'foo' baz` + "bar";
|
@ -0,0 +1,6 @@
|
||||
error: String must use singlequotes
|
||||
|
||||
|
|
||||
1 | var s = "double quotes string";
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
@ -2,6 +2,8 @@
|
||||
use crate::rules::non_critical_lints::no_console::NoConsoleConfig;
|
||||
#[cfg(feature = "non_critical_lints")]
|
||||
use crate::rules::non_critical_lints::prefer_regex_literals::PreferRegexLiteralsConfig;
|
||||
#[cfg(feature = "non_critical_lints")]
|
||||
use crate::rules::non_critical_lints::quotes::QuotesConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
||||
@ -54,4 +56,8 @@ pub struct LintConfig {
|
||||
#[cfg(feature = "non_critical_lints")]
|
||||
#[serde(default)]
|
||||
pub no_debugger: RuleConfig<()>,
|
||||
|
||||
#[cfg(feature = "non_critical_lints")]
|
||||
#[serde(default)]
|
||||
pub quotes: RuleConfig<QuotesConfig>,
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::{config::LintConfig, rule::Rule};
|
||||
use swc_common::SyntaxContext;
|
||||
use std::sync::Arc;
|
||||
use swc_common::{SourceMap, SyntaxContext};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_visit::{noop_fold_type, Fold};
|
||||
|
||||
@ -14,6 +15,7 @@ pub(crate) mod non_critical_lints {
|
||||
pub mod no_console;
|
||||
pub mod no_debugger;
|
||||
pub mod prefer_regex_literals;
|
||||
pub mod quotes;
|
||||
}
|
||||
|
||||
#[cfg(feature = "non_critical_lints")]
|
||||
@ -24,6 +26,7 @@ pub struct LintParams<'a> {
|
||||
pub lint_config: &'a LintConfig,
|
||||
pub top_level_ctxt: SyntaxContext,
|
||||
pub es_version: EsVersion,
|
||||
pub source_map: Arc<SourceMap>,
|
||||
}
|
||||
|
||||
pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
|
||||
@ -40,6 +43,7 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
|
||||
lint_config,
|
||||
top_level_ctxt,
|
||||
es_version,
|
||||
source_map,
|
||||
} = lint_params;
|
||||
|
||||
rules.extend(no_console::no_console(
|
||||
@ -56,6 +60,8 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
|
||||
|
||||
rules.extend(no_debugger::no_debugger(&lint_config.no_debugger));
|
||||
|
||||
rules.extend(quotes::quotes(&source_map, &lint_config.quotes));
|
||||
|
||||
rules.extend(prefer_regex_literals::prefer_regex_literals(
|
||||
program,
|
||||
&lint_config.prefer_regex_literals,
|
||||
|
204
crates/swc_ecma_lints/src/rules/quotes.rs
Normal file
204
crates/swc_ecma_lints/src/rules/quotes.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use crate::{
|
||||
config::{LintRuleReaction, RuleConfig},
|
||||
rule::{visitor_rule, Rule},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
use swc_common::{errors::HANDLER, SourceMap, Span};
|
||||
use swc_ecma_ast::*;
|
||||
use swc_ecma_visit::{noop_visit_type, Visit};
|
||||
|
||||
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 {
|
||||
#[serde(default)]
|
||||
prefer: QuotesType,
|
||||
avoid_escape: Option<bool>,
|
||||
allow_template_literals: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn quotes(
|
||||
source_map: &Arc<SourceMap>,
|
||||
config: &RuleConfig<QuotesConfig>,
|
||||
) -> Option<Box<dyn Rule>> {
|
||||
match config.get_rule_reaction() {
|
||||
LintRuleReaction::Off => None,
|
||||
_ => Some(visitor_rule(Quotes::new(source_map.clone(), config))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Quotes {
|
||||
expected_reaction: LintRuleReaction,
|
||||
prefer: QuotesType,
|
||||
avoid_escape: bool,
|
||||
allow_template_literals: bool,
|
||||
source_map: Arc<SourceMap>,
|
||||
}
|
||||
|
||||
impl Debug for Quotes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Quotes")
|
||||
.field("expected_reaction", &self.expected_reaction)
|
||||
.field("prefer", &self.prefer)
|
||||
.field("avoid_escape", &self.avoid_escape)
|
||||
.field("allow_template_literals", &self.allow_template_literals)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Quotes {
|
||||
fn new(source_map: Arc<SourceMap>, config: &RuleConfig<QuotesConfig>) -> Self {
|
||||
let quotes_config = config.get_rule_config();
|
||||
|
||||
Self {
|
||||
expected_reaction: *config.get_rule_reaction(),
|
||||
prefer: quotes_config.prefer,
|
||||
avoid_escape: quotes_config.avoid_escape.unwrap_or(true),
|
||||
allow_template_literals: quotes_config.allow_template_literals.unwrap_or(true),
|
||||
source_map,
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_report(&self, span: Span) {
|
||||
let message = match &self.prefer {
|
||||
QuotesType::Backtick => MUST_USE_BACKTICK_QUOTES_MESSAGE,
|
||||
QuotesType::Single => MUST_USE_SINGLE_QUOTES_MESSAGE,
|
||||
QuotesType::Double => MUST_USE_DOUBLE_QUOTES_MESSAGE,
|
||||
};
|
||||
|
||||
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 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 => '`',
|
||||
};
|
||||
|
||||
for ch in value.chars() {
|
||||
if ch == quote {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn check_str(&self, is_method_key_check: bool, Str { span, value, .. }: &Str) {
|
||||
let found_quote_type = self.resolve_quote_type(span).unwrap();
|
||||
|
||||
match (&self.prefer, &found_quote_type) {
|
||||
(QuotesType::Double, QuotesType::Single) => {
|
||||
if self.avoid_escape && self.is_mirroring_escape(&*value) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit_report(*span);
|
||||
}
|
||||
(QuotesType::Single, QuotesType::Double) => {
|
||||
if self.avoid_escape && self.is_mirroring_escape(&*value) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit_report(*span);
|
||||
}
|
||||
(QuotesType::Backtick, _) => {
|
||||
if is_method_key_check {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.avoid_escape && self.is_mirroring_escape(&*value) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit_report(*span);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_tpl_str(&self, tpl_str: &Tpl) {
|
||||
let Tpl { span, exprs, .. } = tpl_str;
|
||||
|
||||
if self.allow_template_literals {
|
||||
return;
|
||||
}
|
||||
|
||||
if let QuotesType::Backtick = &self.prefer {
|
||||
return;
|
||||
}
|
||||
|
||||
if !exprs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit_report(*span);
|
||||
}
|
||||
}
|
||||
|
||||
impl Visit for Quotes {
|
||||
noop_visit_type!();
|
||||
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
match expr {
|
||||
Expr::Tpl(tpl_str) => {
|
||||
self.check_tpl_str(tpl_str);
|
||||
}
|
||||
Expr::Lit(Lit::Str(lit_str)) => {
|
||||
self.check_str(false, lit_str);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_class_method(&mut self, class_method: &ClassMethod) {
|
||||
if let Some(lit_str) = class_method.key.as_str() {
|
||||
self.check_str(true, lit_str);
|
||||
}
|
||||
}
|
||||
}
|
@ -56,12 +56,13 @@ fn pass(input: PathBuf) {
|
||||
lint_config: &config,
|
||||
top_level_ctxt,
|
||||
es_version,
|
||||
source_map: cm,
|
||||
});
|
||||
|
||||
HANDLER.set(handler, || {
|
||||
if let Program::Module(m) = &program {
|
||||
for mut rule in rules {
|
||||
rule.lint_module(&m);
|
||||
rule.lint_module(m);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user