feat(es/lints): Implement simple validations (#2763)

swc_ecma_lint:
 - Implement `const-assign`. (Closes #2016)
 - Implement `duplicate-bindings`. (Closes #2795)
 - Implement `duplicate-exports`. (Closes #3056)

swc:
 - Use linter.
This commit is contained in:
Donny/강동윤 2021-12-19 21:07:51 +09:00 committed by GitHub
parent 23742e3b18
commit f21af5bcd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 638 additions and 3 deletions

15
Cargo.lock generated
View File

@ -2498,6 +2498,7 @@ dependencies = [
"swc_ecma_ast",
"swc_ecma_codegen",
"swc_ecma_ext_transforms",
"swc_ecma_lints",
"swc_ecma_loader",
"swc_ecma_minifier",
"swc_ecma_parser",
@ -2783,6 +2784,20 @@ dependencies = [
"swc_ecma_visit",
]
[[package]]
name = "swc_ecma_lints"
version = "0.1.0"
dependencies = [
"auto_impl 0.5.0",
"parking_lot 0.11.1",
"rayon",
"swc_atoms 0.2.9",
"swc_common",
"swc_ecma_ast",
"swc_ecma_utils",
"swc_ecma_visit",
]
[[package]]
name = "swc_ecma_loader"
version = "0.25.1"

View File

@ -4,6 +4,7 @@ members = [
"crates/node",
"crates/swc_css",
"crates/swc_ecmascript",
"crates/swc_ecma_lints",
"crates/swc_ecma_plugin_ast",
"crates/swc_estree_compat",
"crates/swc_plugin",

View File

@ -56,6 +56,7 @@ swc_common = {version = "0.15.0", path = "../swc_common", features = ["sourcemap
swc_ecma_ast = {version = "0.60.0", path = "../swc_ecma_ast"}
swc_ecma_codegen = {version = "0.84.0", path = "../swc_ecma_codegen"}
swc_ecma_ext_transforms = {version = "0.42.0", path = "../swc_ecma_ext_transforms"}
swc_ecma_lints = {version = "0.1.0", path = "../swc_ecma_lints"}
swc_ecma_loader = {version = "0.25.0", path = "../swc_ecma_loader", features = ["lru", "node", "tsc"]}
swc_ecma_minifier = {version = "0.60.0", path = "../swc_ecma_minifier"}
swc_ecma_parser = {version = "0.82.0", path = "../swc_ecma_parser"}

View File

@ -29,6 +29,7 @@ use swc_common::{
};
use swc_ecma_ast::{EsVersion, Expr, Program};
use swc_ecma_ext_transforms::jest;
use swc_ecma_lints::rules::lint_to_fold;
use swc_ecma_loader::resolvers::{
lru::CachingResolver, node::NodeModulesResolver, tsc::TsConfigResolver,
};
@ -241,7 +242,7 @@ impl Default for InputSourceMap {
}
impl Options {
/// `parss`: `(syntax, target, is_module)`
/// `parse`: `(syntax, target, is_module)`
pub fn build_as_input<'a, P>(
&self,
cm: &Arc<SourceMap>,
@ -383,6 +384,7 @@ impl Options {
),
syntax.typescript()
),
lint_to_fold(swc_ecma_lints::rules::all()),
crate::plugin::plugins(experimental.plugins),
custom_before_pass(&program),
// handle jsx

View File

@ -1,8 +1,9 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use swc::{
config::{IsModule, Options},
Compiler,
try_with_handler, Compiler,
};
use swc_common::{sync::Lrc, FilePathMapping, SourceMap};
use testing::{NormalizedOutput, Tester};
fn file(f: impl AsRef<Path>) -> NormalizedOutput {
@ -42,3 +43,37 @@ fn issue_1532() {
assert!(f.contains("unknown variant `esnext`"))
}
#[testing::fixture("tests/errors/**/input.js")]
fn fixture(input: PathBuf) {
let _log = testing::init();
let output_path = input.parent().unwrap().join("output.swc-stderr");
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let err = try_with_handler(cm.clone(), true, |handler| {
let c = Compiler::new(cm.clone());
let fm = cm.load_file(&input).expect("failed to load file");
match c.process_js_file(
fm,
&handler,
&Options {
swcrc: true,
is_module: IsModule::Bool(true),
..Default::default()
},
) {
Ok(..) => {}
Err(err) => return Err(err),
}
Ok(())
})
.expect_err("should fail");
let output = NormalizedOutput::from(format!("{:?}", err));
output.compare_to_file(&output_path).unwrap();
}

View File

@ -0,0 +1,5 @@
const a = 500;
a = 600;

View File

@ -0,0 +1,12 @@
error: Cannot reassign to a variable declared with `const`
|
5 | a = 600;
| ^
|
note: a was declared here
|
1 | const a = 500;
| ^

View File

@ -0,0 +1,2 @@
const a = 5;
let a = 2;

View File

@ -0,0 +1,12 @@
error: Duplicate binding
|
2 | let a = 2;
| ^
|
note: a was declared at here
|
1 | const a = 5;
| ^

View File

@ -0,0 +1,2 @@
import { hi } from 'foo';
import { hi } from 'foo';

View File

@ -0,0 +1,12 @@
error: Duplicate binding
|
2 | import { hi } from 'foo';
| ^^
|
note: hi was declared at here
|
1 | import { hi } from 'foo';
| ^^

View File

@ -0,0 +1,7 @@
export default () => {
let a = 2;
}
export default () => {
let b = 2;
}

View File

@ -0,0 +1,16 @@
error: Duplicate export
|
5 | / export default () => {
6 | | let b = 2;
7 | | }
| |_^
|
note: default was exported at here
|
1 | / export default () => {
2 | | let a = 2;
3 | | }
| |_^

View File

@ -0,0 +1,7 @@
const { a } = 1;
export { a as default };
export default () => {
let b = 2;
}

View File

@ -0,0 +1,14 @@
error: Duplicate export
|
5 | / export default () => {
6 | | let b = 2;
7 | | }
| |_^
|
note: default was exported at here
|
3 | export { a as default };
| ^^^^^^^

View File

@ -0,0 +1,19 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Linter for the swc project"
edition = "2018"
license = "Apache-2.0"
name = "swc_ecma_lints"
repository = "https://github.com/swc-project/swc.git"
version = "0.1.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
auto_impl = "0.5.0"
parking_lot = "0.11"
rayon = "1.5.1"
swc_atoms = {version = "0.2.9", path = "../swc_atoms"}
swc_common = {version = "0.15.1", path = "../swc_common"}
swc_ecma_ast = {version = "0.60.0", path = "../swc_ecma_ast"}
swc_ecma_utils = {version = "0.56.1", path = "../swc_ecma_utils"}
swc_ecma_visit = {version = "0.46.0", path = "../swc_ecma_visit"}

View File

@ -0,0 +1,2 @@
pub mod rule;
pub mod rules;

View File

@ -0,0 +1,102 @@
use auto_impl::auto_impl;
use parking_lot::Mutex;
use rayon::prelude::*;
use std::{fmt::Debug, sync::Arc};
use swc_common::errors::{Diagnostic, DiagnosticBuilder, Emitter, Handler, HANDLER};
use swc_ecma_ast::{Module, Script};
use swc_ecma_visit::{Visit, VisitWith};
/// A lint rule.
///
/// # Implementation notes
///
/// Must report error to [swc_common::HANDLER]
#[auto_impl(Box, &mut)]
pub trait Rule: Debug + Send + Sync {
fn lint_module(&mut self, program: &Module);
fn lint_script(&mut self, program: &Script);
}
macro_rules! for_vec {
($name:ident, $program:ident, $s:expr) => {{
let program = $program;
if cfg!(target_arch = "wasm32") {
for rule in $s {
rule.$name(program);
}
} else {
let errors = $s
.par_iter_mut()
.flat_map(|rule| {
let emitter = Capturing::default();
{
let handler = Handler::with_emitter(true, false, Box::new(emitter.clone()));
HANDLER.set(&handler, || {
rule.$name(program);
});
}
let errors = Arc::try_unwrap(emitter.errors).unwrap().into_inner();
errors
})
.collect::<Vec<_>>();
HANDLER.with(|handler| {
for error in errors {
DiagnosticBuilder::new_diagnostic(&handler, error).emit();
}
});
}
}};
}
/// This preserves the order of errors.
impl<R> Rule for Vec<R>
where
R: Rule,
{
fn lint_module(&mut self, program: &Module) {
for_vec!(lint_module, program, self)
}
fn lint_script(&mut self, program: &Script) {
for_vec!(lint_script, program, self)
}
}
#[derive(Default, Clone)]
struct Capturing {
errors: Arc<Mutex<Vec<Diagnostic>>>,
}
impl Emitter for Capturing {
fn emit(&mut self, db: &DiagnosticBuilder<'_>) {
self.errors.lock().push((**db).clone());
}
}
pub(crate) fn visitor_rule<V>(v: V) -> Box<dyn Rule>
where
V: 'static + Send + Sync + Visit + Default + Debug,
{
Box::new(VisitorRule(v))
}
#[derive(Debug)]
struct VisitorRule<V>(V)
where
V: Send + Sync + Visit;
impl<V> Rule for VisitorRule<V>
where
V: Send + Sync + Visit + Debug,
{
fn lint_module(&mut self, program: &Module) {
program.visit_with(&mut self.0);
}
fn lint_script(&mut self, program: &Script) {
program.visit_with(&mut self.0);
}
}

View File

@ -0,0 +1,127 @@
use crate::rule::{visitor_rule, Rule};
use swc_common::{collections::AHashMap, errors::HANDLER, Span};
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
pub fn const_assign() -> Box<dyn Rule> {
visitor_rule(ConstAssign::default())
}
#[derive(Debug, Default)]
struct ConstAssign {
const_vars: AHashMap<Id, Span>,
is_pat_decl: bool,
}
impl ConstAssign {
fn check(&mut self, id: &Ident) {
if self.is_pat_decl {
return;
}
if let Some(&decl_span) = self.const_vars.get(&id.to_id()) {
HANDLER.with(|handler| {
handler
.struct_span_err(
id.span,
"Cannot reassign to a variable declared with `const`",
)
.span_note(decl_span, &format!("{} was declared here", id.sym))
.emit();
});
}
}
}
impl Visit for ConstAssign {
noop_visit_type!();
fn visit_assign_pat_prop(&mut self, p: &AssignPatProp) {
p.visit_children_with(self);
self.check(&p.key);
}
fn visit_module(&mut self, program: &Module) {
program.visit_children_with(&mut Collector {
const_vars: &mut self.const_vars,
var_decl_kind: None,
});
program.visit_children_with(self);
}
fn visit_pat(&mut self, p: &Pat) {
p.visit_children_with(self);
match p {
Pat::Ident(p) => {
self.check(&p.id);
}
_ => {}
}
}
fn visit_script(&mut self, program: &Script) {
program.visit_children_with(&mut Collector {
const_vars: &mut self.const_vars,
var_decl_kind: None,
});
program.visit_children_with(self);
}
fn visit_var_decl(&mut self, var_decl: &VarDecl) {
let old_is_pat_decl = self.is_pat_decl;
self.is_pat_decl = true;
var_decl.visit_children_with(self);
self.is_pat_decl = old_is_pat_decl;
}
}
struct Collector<'a> {
const_vars: &'a mut AHashMap<Id, Span>,
var_decl_kind: Option<VarDeclKind>,
}
impl Visit for Collector<'_> {
fn visit_assign_pat_prop(&mut self, p: &AssignPatProp) {
p.visit_children_with(self);
if let Some(VarDeclKind::Const) = self.var_decl_kind {
*self.const_vars.entry(p.key.to_id()).or_default() = p.span;
}
}
fn visit_expr(&mut self, e: &Expr) {
let old_var_decl_kind = self.var_decl_kind;
self.var_decl_kind = None;
e.visit_children_with(self);
self.var_decl_kind = old_var_decl_kind;
}
fn visit_pat(&mut self, p: &Pat) {
p.visit_children_with(self);
if let Some(VarDeclKind::Const) = self.var_decl_kind {
if let Pat::Ident(i) = p {
*self.const_vars.entry(i.to_id()).or_default() = i.id.span;
}
}
}
fn visit_var_decl(&mut self, var_decl: &VarDecl) {
let old_var_decl_kind = self.var_decl_kind;
self.var_decl_kind = Some(var_decl.kind);
var_decl.visit_children_with(self);
self.var_decl_kind = old_var_decl_kind;
}
}

View File

@ -0,0 +1,129 @@
use std::collections::hash_map::Entry;
use swc_common::{collections::AHashMap, errors::HANDLER, Span};
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
use crate::rule::{visitor_rule, Rule};
pub fn duplicate_bindings() -> Box<dyn Rule> {
visitor_rule(DuplicateBindings::default())
}
#[derive(Debug, Default)]
struct DuplicateBindings {
bindings: AHashMap<Id, Span>,
var_decl_kind: Option<VarDeclKind>,
is_pat_decl: bool,
}
impl DuplicateBindings {
/// Add a binding.
fn add(&mut self, id: &Ident, check_for_var_kind: bool) {
if check_for_var_kind {
if let Some(VarDeclKind::Var) = self.var_decl_kind {
return;
}
}
match self.bindings.entry(id.to_id()) {
Entry::Occupied(mut prev) => {
HANDLER.with(|handler| {
handler
.struct_span_err(id.span, "Duplicate binding")
.span_note(*prev.get(), &format!("{} was declared at here", id.sym))
.emit();
});
// Next span.
*prev.get_mut() = id.span;
}
Entry::Vacant(e) => {
e.insert(id.span);
}
}
}
}
impl Visit for DuplicateBindings {
noop_visit_type!();
fn visit_assign_pat_prop(&mut self, p: &AssignPatProp) {
p.visit_children_with(self);
if self.is_pat_decl {
self.add(&p.key, true);
}
}
fn visit_class_decl(&mut self, d: &ClassDecl) {
self.add(&d.ident, false);
d.visit_children_with(self);
}
fn visit_expr(&mut self, e: &Expr) {
let old_var_decl_kind = self.var_decl_kind.take();
let old_is_pat_decl = self.is_pat_decl;
self.var_decl_kind = None;
self.is_pat_decl = false;
e.visit_children_with(self);
self.is_pat_decl = old_is_pat_decl;
self.var_decl_kind = old_var_decl_kind;
}
fn visit_fn_decl(&mut self, d: &FnDecl) {
self.add(&d.ident, false);
d.visit_children_with(self);
}
fn visit_import_default_specifier(&mut self, s: &ImportDefaultSpecifier) {
s.visit_children_with(self);
self.add(&s.local, false);
}
fn visit_import_named_specifier(&mut self, s: &ImportNamedSpecifier) {
s.visit_children_with(self);
self.add(&s.local, false);
}
fn visit_import_star_as_specifier(&mut self, s: &ImportStarAsSpecifier) {
s.visit_children_with(self);
self.add(&s.local, false);
}
fn visit_pat(&mut self, p: &Pat) {
p.visit_children_with(self);
match p {
Pat::Ident(p) => {
if self.is_pat_decl {
self.add(&p.id, true);
}
}
_ => {}
}
}
fn visit_var_decl(&mut self, d: &VarDecl) {
let old_var_decl_kind = self.var_decl_kind.take();
let old_is_pat_decl = self.is_pat_decl;
self.var_decl_kind = Some(d.kind);
self.is_pat_decl = true;
d.visit_children_with(self);
self.is_pat_decl = old_is_pat_decl;
self.var_decl_kind = old_var_decl_kind;
}
}

View File

@ -0,0 +1,66 @@
use std::collections::hash_map::Entry;
use swc_atoms::{js_word, JsWord};
use swc_common::{collections::AHashMap, errors::HANDLER, Span};
use swc_ecma_ast::*;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
use crate::rule::{visitor_rule, Rule};
pub fn duplicate_exports() -> Box<dyn Rule> {
visitor_rule(DuplicateExports::default())
}
#[derive(Debug, Default)]
struct DuplicateExports {
exports: AHashMap<JsWord, Span>,
}
impl DuplicateExports {
/// Add an export.
fn add(&mut self, id: &Ident) {
match self.exports.entry(id.sym.clone()) {
Entry::Occupied(mut prev) => {
HANDLER.with(|handler| {
handler
.struct_span_err(id.span, "Duplicate export")
.span_note(*prev.get(), &format!("{} was exported at here", id.sym))
.emit();
});
// Next span.
*prev.get_mut() = id.span;
}
Entry::Vacant(e) => {
e.insert(id.span);
}
}
}
}
impl Visit for DuplicateExports {
noop_visit_type!();
fn visit_export_default_decl(&mut self, d: &ExportDefaultDecl) {
d.visit_children_with(self);
self.add(&Ident::new(js_word!("default"), d.span));
}
fn visit_export_default_expr(&mut self, d: &ExportDefaultExpr) {
d.visit_children_with(self);
self.add(&Ident::new(js_word!("default"), d.span));
}
fn visit_export_default_specifier(&mut self, s: &ExportDefaultSpecifier) {
self.add(&s.exported);
}
fn visit_export_named_specifier(&mut self, s: &ExportNamedSpecifier) {
self.add(&s.exported.as_ref().unwrap_or(&s.orig));
}
fn visit_export_namespace_specifier(&mut self, s: &ExportNamespaceSpecifier) {
self.add(&s.name);
}
}

View File

@ -0,0 +1,47 @@
use crate::rule::Rule;
use swc_ecma_ast::*;
use swc_ecma_visit::{noop_fold_type, Fold};
mod const_assign;
mod duplicate_bindings;
mod duplicate_exports;
pub fn all() -> Vec<Box<dyn Rule>> {
vec![
const_assign::const_assign(),
duplicate_bindings::duplicate_bindings(),
duplicate_exports::duplicate_exports(),
]
}
pub fn lint_to_fold<R>(r: R) -> impl Fold
where
R: Rule,
{
LintFolder(r)
}
struct LintFolder<R>(R)
where
R: Rule;
impl<R> Fold for LintFolder<R>
where
R: Rule,
{
noop_fold_type!();
#[inline(always)]
fn fold_module(&mut self, program: Module) -> Module {
self.0.lint_module(&program);
program
}
#[inline(always)]
fn fold_script(&mut self, program: Script) -> Script {
self.0.lint_script(&program);
program
}
}