feat(es/lints): Add no-alert and a feature gate (#3394)

swc_ecma_lints:
 - Implement `no-alert`
 - Add a feature gate for non-critical rules.
This commit is contained in:
Artur 2022-02-02 17:43:46 +03:00 committed by GitHub
parent b509341fea
commit 5cbe4fe512
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 509 additions and 28 deletions

View File

@ -40,3 +40,4 @@ swc_node_base = {path = "../swc_node_base"}
swc_node_bundler = {path = "../swc_node_bundler"} swc_node_bundler = {path = "../swc_node_bundler"}
tracing = {version = "0.1.28", features = ["release_max_level_info"]} tracing = {version = "0.1.28", features = ["release_max_level_info"]}
tracing-subscriber = {version = "0.3.4", features = ["env-filter"]} tracing-subscriber = {version = "0.3.4", features = ["env-filter"]}
swc_ecma_lints = {version = "0.11.1", path = "../swc_ecma_lints", features = ["non_critical_lints"]}

View File

@ -91,6 +91,7 @@ rayon = "1"
swc_node_base = {version = "0.5.0", path = "../swc_node_base"} swc_node_base = {version = "0.5.0", path = "../swc_node_base"}
testing = {version = "0.18.0", path = "../testing"} testing = {version = "0.18.0", path = "../testing"}
walkdir = "2" walkdir = "2"
swc_ecma_lints = {version = "0.11.1", path = "../swc_ecma_lints", features = ["non_critical_lints"]}
[[example]] [[example]]
name = "usage" name = "usage"

View File

@ -29,7 +29,10 @@ use swc_common::{
}; };
use swc_ecma_ast::{EsVersion, Expr, Program}; use swc_ecma_ast::{EsVersion, Expr, Program};
use swc_ecma_ext_transforms::jest; use swc_ecma_ext_transforms::jest;
use swc_ecma_lints::{config::LintConfig, rules::lint_to_fold}; use swc_ecma_lints::{
config::LintConfig,
rules::{lint_to_fold, LintParams},
};
use swc_ecma_loader::resolvers::{ use swc_ecma_loader::resolvers::{
lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver, lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
}; };
@ -296,11 +299,11 @@ impl Options {
.global_mark .global_mark
.unwrap_or_else(|| Mark::fresh(Mark::root())); .unwrap_or_else(|| Mark::fresh(Mark::root()));
let target = target.unwrap_or_default(); let es_version = target.unwrap_or_default();
let syntax = syntax.unwrap_or_default(); let syntax = syntax.unwrap_or_default();
let mut program = parse(syntax, target, is_module)?; let mut program = parse(syntax, es_version, is_module)?;
// Do a resolver pass before everything. // Do a resolver pass before everything.
// //
@ -394,7 +397,7 @@ impl Options {
); );
let pass = PassBuilder::new(cm, handler, loose, assumptions, top_level_mark, pass) let pass = PassBuilder::new(cm, handler, loose, assumptions, top_level_mark, pass)
.target(target) .target(es_version)
.skip_helper_injection(self.skip_helper_injection) .skip_helper_injection(self.skip_helper_injection)
.minify(js_minify) .minify(js_minify)
.hygiene(if self.disable_hygiene { .hygiene(if self.disable_hygiene {
@ -439,7 +442,12 @@ impl Options {
), ),
syntax.typescript() syntax.typescript()
), ),
lint_to_fold(swc_ecma_lints::rules::all(&lints, top_level_ctxt)), lint_to_fold(swc_ecma_lints::rules::all(LintParams {
program: &program,
lint_config: &lints,
top_level_ctxt,
es_version,
})),
crate::plugin::plugins(experimental), crate::plugin::plugins(experimental),
custom_before_pass(&program), custom_before_pass(&program),
// handle jsx // handle jsx
@ -457,7 +465,7 @@ impl Options {
pass, pass,
external_helpers, external_helpers,
syntax, syntax,
target, target: es_version,
is_module, is_module,
source_maps: source_maps.unwrap_or(SourceMapsConfig::Bool(false)), source_maps: source_maps.unwrap_or(SourceMapsConfig::Bool(false)),
inline_sources_content: config.inline_sources_content, inline_sources_content: config.inline_sources_content,

View File

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

View File

@ -0,0 +1,5 @@
alert();
const alert = () => {};
window.alert();

View File

@ -0,0 +1,6 @@
error: Unexpected alert
|
5 | window.alert();
| ^^^^^^^^^^^^^^

View File

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

View File

@ -0,0 +1,4 @@
// Test cases taken form https://github.com/eslint/eslint/blob/5769cc23eca7197bb5993a0201cc269a056d4dfd/tests/lib/rules/no-alert.js
function alert() {}
window.alert(foo);

View File

@ -0,0 +1,6 @@
error: Unexpected alert
|
4 | window.alert(foo);
| ^^^^^^^^^^^^^^^^^

View File

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

View File

@ -0,0 +1,10 @@
// Test cases taken form https://github.com/eslint/eslint/blob/5769cc23eca7197bb5993a0201cc269a056d4dfd/tests/lib/rules/no-alert.js
globalThis.alert();
function foo() {
var globalThis = bar;
globalThis.alert();
}
globalThis.alert();
window?.alert(foo);
(window?.alert)(foo);

View File

@ -0,0 +1,24 @@
error: Unexpected alert
|
3 | globalThis.alert();
| ^^^^^^^^^^^^^^^^^^
error: Unexpected alert
|
8 | globalThis.alert();
| ^^^^^^^^^^^^^^^^^^
error: Unexpected alert
|
9 | window?.alert(foo);
| ^^^^^^^^^^^^^^^^^^
error: Unexpected alert
|
10 | (window?.alert)(foo);
| ^^^^^^^^^^^^^^^^^^^^

View File

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

View File

@ -0,0 +1,54 @@
// Test cases taken form https://github.com/eslint/eslint/blob/5769cc23eca7197bb5993a0201cc269a056d4dfd/tests/lib/rules/no-alert.js
alert();
window.alert();
window["alert"]();
confirm();
window.confirm();
window["confirm"]();
prompt();
window.prompt();
window["prompt"]();
function foo1(alert) {
window.alert();
}
function foo2() {
alert();
}
function foo3() {
var alert = function () {};
}
alert();
// currently unsupported
// this.alert(foo)
// this['alert'](foo)
function foo4() {
var window = bar;
window.alert();
}
window.alert();
function foo5() {
alert();
const alert = () => {};
}
alert(() => alert("foo"));
(() => {
const obj = {
alert,
};
})();
(() => {
alert = () => {};
})();
(() => {
console.log(alert);
})();
fu(() => alert(""));

View File

@ -0,0 +1,96 @@
error: Unexpected alert
|
3 | alert();
| ^^^^^^^
error: Unexpected alert
|
4 | window.alert();
| ^^^^^^^^^^^^^^
error: Unexpected alert
|
5 | window["alert"]();
| ^^^^^^^^^^^^^^^^^
error: Unexpected confirm
|
6 | confirm();
| ^^^^^^^^^
error: Unexpected confirm
|
7 | window.confirm();
| ^^^^^^^^^^^^^^^^
error: Unexpected confirm
|
8 | window["confirm"]();
| ^^^^^^^^^^^^^^^^^^^
error: Unexpected prompt
|
9 | prompt();
| ^^^^^^^^
error: Unexpected prompt
|
10 | window.prompt();
| ^^^^^^^^^^^^^^^
error: Unexpected prompt
|
11 | window["prompt"]();
| ^^^^^^^^^^^^^^^^^^
error: Unexpected alert
|
13 | window.alert();
| ^^^^^^^^^^^^^^
error: Unexpected alert
|
16 | alert();
| ^^^^^^^
error: Unexpected alert
|
21 | alert();
| ^^^^^^^
error: Unexpected alert
|
31 | window.alert();
| ^^^^^^^^^^^^^^
error: Unexpected alert
|
38 | alert(() => alert("foo"));
| ^^^^^^^^^^^^^^^^^^^^^^^^^
error: Unexpected alert
|
38 | alert(() => alert("foo"));
| ^^^^^^^^^^^^
error: Unexpected alert
|
54 | fu(() => alert(""));
| ^^^^^^^^^

View File

@ -24,3 +24,6 @@ swc_ecma_codegen = {version = "0.89.0", path = "../swc_ecma_codegen"}
swc_ecma_parser = {version = "0.87.0", path = "../swc_ecma_parser"} swc_ecma_parser = {version = "0.87.0", path = "../swc_ecma_parser"}
swc_ecma_transforms_base = {version = "0.57.0", path = "../swc_ecma_transforms_base"} swc_ecma_transforms_base = {version = "0.57.0", path = "../swc_ecma_transforms_base"}
testing = {version = "0.18.0", path = "../testing"} testing = {version = "0.18.0", path = "../testing"}
[features]
non_critical_lints = []

View File

@ -1,4 +1,5 @@
use crate::rules::no_console::NoConsoleConfig; #[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::no_console::NoConsoleConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@ -36,9 +37,15 @@ impl<T: Debug + Clone + Serialize + Default> RuleConfig<T> {
#[non_exhaustive] #[non_exhaustive]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct LintConfig { pub struct LintConfig {
#[cfg(feature = "non_critical_lints")]
#[serde(default)] #[serde(default)]
pub no_console: RuleConfig<NoConsoleConfig>, pub no_console: RuleConfig<NoConsoleConfig>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub no_alert: RuleConfig<()>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)] #[serde(default)]
pub no_debugger: RuleConfig<()>, pub no_debugger: RuleConfig<()>,
} }

View File

@ -6,22 +6,55 @@ use swc_ecma_visit::{noop_fold_type, Fold};
mod const_assign; mod const_assign;
mod duplicate_bindings; mod duplicate_bindings;
mod duplicate_exports; mod duplicate_exports;
pub mod no_console;
mod no_debugger;
pub fn all(lint_config: &LintConfig, top_level_ctxt: SyntaxContext) -> Vec<Box<dyn Rule>> { #[cfg(feature = "non_critical_lints")]
#[path = ""]
pub(crate) mod non_critical_lints {
pub mod no_alert;
pub mod no_console;
pub mod no_debugger;
}
#[cfg(feature = "non_critical_lints")]
use non_critical_lints::*;
pub struct LintParams<'a> {
pub program: &'a Program,
pub lint_config: &'a LintConfig,
pub top_level_ctxt: SyntaxContext,
pub es_version: EsVersion,
}
pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
let mut rules = vec![ let mut rules = vec![
const_assign::const_assign(), const_assign::const_assign(),
duplicate_bindings::duplicate_bindings(), duplicate_bindings::duplicate_bindings(),
duplicate_exports::duplicate_exports(), duplicate_exports::duplicate_exports(),
]; ];
rules.extend(no_console::no_console( #[cfg(feature = "non_critical_lints")]
&lint_config.no_console, {
top_level_ctxt, let LintParams {
)); program,
lint_config,
top_level_ctxt,
es_version,
} = lint_params;
rules.extend(no_debugger::no_debugger(&lint_config.no_debugger)); rules.extend(no_console::no_console(
&lint_config.no_console,
top_level_ctxt,
));
rules.extend(no_alert::no_alert(
program,
&lint_config.no_alert,
top_level_ctxt,
es_version,
));
rules.extend(no_debugger::no_debugger(&lint_config.no_debugger));
}
rules rules
} }

View File

@ -0,0 +1,184 @@
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule},
};
use swc_atoms::JsWord;
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 FN_NAMES: &[&str] = &["alert", "confirm", "prompt"];
const GLOBAL_THIS_PROP: &str = "globalThis";
const OBJ_NAMES: &[&str] = &["window", GLOBAL_THIS_PROP];
pub fn no_alert(
program: &Program,
config: &RuleConfig<()>,
top_level_ctxt: SyntaxContext,
es_version: EsVersion,
) -> Option<Box<dyn Rule>> {
let top_level_declared_vars: AHashSet<Id> = collect_decls_with_ctxt(program, top_level_ctxt);
let rule_reaction = config.get_rule_reaction();
match rule_reaction {
LintRuleReaction::Off => None,
_ => Some(visitor_rule(NoAlert::new(
*rule_reaction,
top_level_declared_vars,
top_level_ctxt,
es_version,
))),
}
}
#[derive(Debug, Default)]
struct NoAlert {
expected_reaction: LintRuleReaction,
top_level_ctxt: SyntaxContext,
top_level_declared_vars: AHashSet<Id>,
pass_call_on_global_this: bool,
inside_callee: bool,
obj: Option<JsWord>,
prop: Option<JsWord>,
}
impl NoAlert {
fn new(
expected_reaction: LintRuleReaction,
top_level_declared_vars: AHashSet<Id>,
top_level_ctxt: SyntaxContext,
es_version: EsVersion,
) -> Self {
Self {
expected_reaction,
top_level_ctxt,
top_level_declared_vars,
pass_call_on_global_this: es_version < EsVersion::Es2020,
inside_callee: false,
obj: None,
prop: None,
}
}
fn emit_report(&self, span: Span, fn_name: &str) {
let message = format!("Unexpected {}", fn_name);
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, call_span: Span, obj: &Option<JsWord>, prop: &JsWord) {
if let Some(obj) = obj {
let obj_name: &str = &*obj;
if self.pass_call_on_global_this && obj_name == GLOBAL_THIS_PROP {
return;
}
if !OBJ_NAMES.contains(&obj_name) {
return;
}
}
let fn_name: &str = &*prop;
if FN_NAMES.contains(&fn_name) {
self.emit_report(call_span, fn_name);
}
}
fn is_satisfying_indent(&self, ident: &Ident) -> bool {
if ident.span.ctxt != self.top_level_ctxt {
return false;
}
if self.top_level_declared_vars.contains(&ident.to_id()) {
return false;
}
true
}
fn handle_callee(&mut self, expr: &Expr) {
match expr {
Expr::Ident(ident) => {
if self.is_satisfying_indent(ident) {
self.prop = Some(ident.sym.clone());
}
}
Expr::Member(member_expr) => {
let MemberExpr { obj, prop, .. } = member_expr;
if let Expr::Ident(obj) = obj.as_ref() {
if !self.is_satisfying_indent(obj) {
return;
}
self.obj = Some(obj.sym.clone());
match prop {
MemberProp::Ident(Ident { sym, .. }) => {
self.prop = Some(sym.clone());
}
MemberProp::Computed(comp) => {
if let Expr::Lit(Lit::Str(Str { value, .. })) = comp.expr.as_ref() {
self.prop = Some(value.clone());
}
}
_ => {}
}
}
// TODO: handle call alert on "this"
}
Expr::OptChain(opt_chain) => {
opt_chain.visit_children_with(self);
}
Expr::Paren(paren) => {
paren.visit_children_with(self);
}
_ => {}
}
}
fn handle_call(&mut self, call_expr: &CallExpr) {
if let Some(callee) = call_expr.callee.as_expr() {
self.inside_callee = true;
callee.visit_with(self);
self.inside_callee = false;
}
if let Some(prop) = &self.prop {
self.check(call_expr.span, &self.obj, prop);
self.obj = None;
self.prop = None;
}
}
}
impl Visit for NoAlert {
noop_visit_type!();
fn visit_expr(&mut self, expr: &Expr) {
if self.inside_callee {
self.handle_callee(expr);
} else {
if let Expr::Call(call_expr) = expr {
self.handle_call(call_expr);
}
expr.visit_children_with(self);
}
}
}

View File

@ -23,10 +23,7 @@ pub fn no_console(
match rule_reaction { match rule_reaction {
LintRuleReaction::Off => None, LintRuleReaction::Off => None,
_ => Some(visitor_rule(NoConsole::new( _ => Some(visitor_rule(NoConsole::new(*rule_reaction, top_level_ctxt))),
rule_reaction.clone(),
top_level_ctxt,
))),
} }
} }

View File

@ -2,8 +2,7 @@ use crate::{
config::{LintRuleReaction, RuleConfig}, config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule}, rule::{visitor_rule, Rule},
}; };
use serde::{Deserialize, Serialize}; use swc_common::{errors::HANDLER, Span};
use swc_common::{collections::AHashSet, errors::HANDLER, Span, SyntaxContext};
use swc_ecma_ast::*; use swc_ecma_ast::*;
use swc_ecma_visit::{noop_visit_type, Visit}; use swc_ecma_visit::{noop_visit_type, Visit};

View File

@ -1,8 +1,11 @@
use std::path::PathBuf; use std::path::PathBuf;
use swc_common::{input::SourceFileInput, Mark, SyntaxContext}; use swc_common::{input::SourceFileInput, Mark, SyntaxContext};
use swc_ecma_ast::EsVersion; use swc_ecma_ast::{EsVersion, Program};
use swc_ecma_lints::{config::LintConfig, rules::all}; use swc_ecma_lints::{
config::LintConfig,
rules::{all, LintParams},
};
use swc_ecma_parser::{lexer::Lexer, Parser, Syntax}; use swc_ecma_parser::{lexer::Lexer, Parser, Syntax};
use swc_ecma_transforms_base::resolver::resolver_with_mark; use swc_ecma_transforms_base::resolver::resolver_with_mark;
use swc_ecma_utils::HANDLER; use swc_ecma_utils::HANDLER;
@ -13,6 +16,7 @@ use swc_ecma_visit::VisitMutWith;
fn pass(input: PathBuf) { fn pass(input: PathBuf) {
testing::run_test(false, |cm, handler| { testing::run_test(false, |cm, handler| {
let fm = cm.load_file(&input).unwrap(); let fm = cm.load_file(&input).unwrap();
let es_version = EsVersion::latest();
let lexer = Lexer::new( let lexer = Lexer::new(
if input.extension().unwrap() == "ts" { if input.extension().unwrap() == "ts" {
@ -29,7 +33,7 @@ fn pass(input: PathBuf) {
..Default::default() ..Default::default()
}) })
}, },
EsVersion::latest(), es_version,
SourceFileInput::from(&*fm), SourceFileInput::from(&*fm),
None, None,
); );
@ -43,13 +47,22 @@ fn pass(input: PathBuf) {
let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark); let top_level_ctxt = SyntaxContext::empty().apply_mark(top_level_mark);
let mut config = LintConfig::default(); let config = LintConfig::default();
let rules = all(&config, top_level_ctxt); let program = Program::Module(m);
let rules = all(LintParams {
program: &program,
lint_config: &config,
top_level_ctxt,
es_version,
});
HANDLER.set(handler, || { HANDLER.set(handler, || {
for mut rule in rules { if let Program::Module(m) = &program {
rule.lint_module(&m); for mut rule in rules {
rule.lint_module(&m);
}
} }
}); });

View File

@ -29,3 +29,4 @@ swc_common = {path = "../swc_common"}
swc_ecmascript = {path = "../swc_ecmascript"} swc_ecmascript = {path = "../swc_ecmascript"}
tracing = {version = "0.1.28", features = ["release_max_level_off"]} tracing = {version = "0.1.28", features = ["release_max_level_off"]}
wasm-bindgen = {version = "0.2", features = ["serde-serialize"]} wasm-bindgen = {version = "0.2", features = ["serde-serialize"]}
swc_ecma_lints = {version = "0.11.1", path = "../swc_ecma_lints", features = ["non_critical_lints"]}