feat(es/lints): Implement linter for quotes of string literals (#3443)

This commit is contained in:
Artur 2022-02-07 11:12:14 +03:00 committed by GitHub
parent 0ff58d83c6
commit 5d6143a53c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 444 additions and 2 deletions

View File

@ -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),

View File

@ -0,0 +1,11 @@
{
"jsc": {
"lints": {
"quotes": ["error", {
"prefer": "double",
"avoidEscape": false,
"allowTemplateLiterals": false
}]
}
}
}

View 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";

View File

@ -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"';
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

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

View 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';

View File

@ -0,0 +1,6 @@
error: String must use backtick quotes
|
1 | var s = "double quotes string";
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@ -0,0 +1,11 @@
{
"jsc": {
"lints": {
"quotes": ["error", {
"prefer": "single",
"avoidEscape": false,
"allowTemplateLiterals": false
}]
}
}
}

View 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";

View File

@ -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";
| ^^^^^^^^^^^^^^^^^^^^^^

View File

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

View File

@ -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'; }

View File

@ -0,0 +1,6 @@
error: String must use backtick quotes
|
1 | var s = "double quotes string";
| ^^^^^^^^^^^^^^^^^^^^^^

View File

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

View File

@ -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"() {} }

View File

@ -0,0 +1,6 @@
error: String must use doublequotes
|
2 | var s = 'double quotes string';
| ^^^^^^^^^^^^^^^^^^^^^^

View File

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

View File

@ -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";

View File

@ -0,0 +1,6 @@
error: String must use singlequotes
|
1 | var s = "double quotes string";
| ^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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>,
}

View File

@ -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,

View 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 = &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 => '`',
};
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);
}
}
}

View File

@ -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);
}
}
});