feat(es/lints): Implement no-use-before-define rule (#3456)

This commit is contained in:
Artur 2022-02-10 07:52:11 +03:00 committed by GitHub
parent 8b438ea024
commit 205b76e78d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 508 additions and 19 deletions

View File

@ -420,6 +420,13 @@ impl Options {
);
let pass = chain!(
lint_to_fold(swc_ecma_lints::rules::all(LintParams {
program: &program,
lint_config: &lints,
top_level_ctxt,
es_version,
source_map: cm.clone(),
})),
// Decorators may use type information
Optional::new(
decorators(decorators::Config {
@ -444,13 +451,6 @@ impl Options {
),
syntax.typescript()
),
lint_to_fold(swc_ecma_lints::rules::all(LintParams {
program: &program,
lint_config: &lints,
top_level_ctxt,
es_version,
source_map: cm.clone(),
})),
crate::plugin::plugins(experimental),
custom_before_pass(&program),
// handle jsx

View File

@ -46,6 +46,7 @@ fn issue_1532() {
}
#[testing::fixture("tests/errors/**/input.js")]
#[testing::fixture("tests/errors/**/input.ts")]
fn fixture(input: PathBuf) {
let _log = testing::init();
let output_path = input.parent().unwrap().join("output.swc-stderr");

View File

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

View File

@ -0,0 +1,9 @@
{
"jsc": {
"lints": {
"noUseBeforeDefine": ["error", {
"classes": true
}]
}
}
}

View File

@ -0,0 +1,3 @@
new C();
class C {}

View File

@ -0,0 +1,6 @@
error: 'C' was used before it was defined
|
1 | new C();
| ^

View File

@ -0,0 +1,16 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"decorators": true
},
"lints": {
"noUseBeforeDefine": [
"error",
{
"classes": true
}
]
}
}
}

View File

@ -0,0 +1,11 @@
// test case from node-swc/tests/issue-2546/input.ts
import { injectable } from 'inversify';
@injectable()
export default abstract class MyClass {}
// required to got error from test
console.log(a);
const a = 1;

View File

@ -0,0 +1,6 @@
error: 'a' was used before it was defined
|
10 | console.log(a);
| ^

View File

@ -0,0 +1,9 @@
{
"jsc": {
"lints": {
"noUseBeforeDefine": ["error", {
"functions": true
}]
}
}
}

View File

@ -0,0 +1,3 @@
foo();
function foo() {}

View File

@ -0,0 +1,6 @@
error: 'foo' was used before it was defined
|
1 | foo();
| ^^^

View File

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

View File

@ -0,0 +1,65 @@
alert(a);
console.log(a);
function f1() {
alert(a);
}
obj[a];
`${a}`;
[a];
var x = a + "";
var e = {
[a]: "",
};
const a = {};
// ----
function f2() {
alert(b);
const b = {};
}
// ----
console.log(a1);
const { a1, a2 } = {};
console.log(a2);
// ----
console.log(a3);
const { a3 = "a" } = {};
// ----
console.log(a4);
const {
n: { a4 },
} = {};
// ----
console.log(a5);
const { ...a5 } = {};
// ----
console.log(a6);
const [a6] = {};
// ----
console.log(a7);
const [_, ...a7] = {};
function b1(a) {
alert(a);
}

View File

@ -0,0 +1,84 @@
error: 'a' was used before it was defined
|
1 | alert(a);
| ^
error: 'a' was used before it was defined
|
3 | console.log(a);
| ^
error: 'a' was used before it was defined
|
9 | obj[a];
| ^
error: 'a' was used before it was defined
|
11 | `${a}`;
| ^
error: 'a' was used before it was defined
|
13 | [a];
| ^
error: 'a' was used before it was defined
|
15 | var x = a + "";
| ^
error: 'a' was used before it was defined
|
18 | [a]: "",
| ^
error: 'b' was used before it was defined
|
25 | alert(b);
| ^
error: 'a1' was used before it was defined
|
30 | console.log(a1);
| ^^
error: 'a3' was used before it was defined
|
37 | console.log(a3);
| ^^
error: 'a4' was used before it was defined
|
42 | console.log(a4);
| ^^
error: 'a5' was used before it was defined
|
49 | console.log(a5);
| ^^
error: 'a6' was used before it was defined
|
54 | console.log(a6);
| ^^
error: 'a7' was used before it was defined
|
59 | console.log(a7);
| ^^

View File

@ -0,0 +1,9 @@
{
"jsc": {
"lints": {
"noUseBeforeDefine": ["error", {
"variables": true
}]
}
}
}

View File

@ -0,0 +1,3 @@
alert(a);
var a = 10;

View File

@ -0,0 +1,6 @@
error: 'a' was used before it was defined
|
1 | alert(a);
| ^

View File

@ -3,13 +3,11 @@ 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")]
use crate::rules::non_critical_lints::prefer_regex_literals::PreferRegexLiteralsConfig;
#[cfg(feature = "non_critical_lints")]
use crate::rules::non_critical_lints::quotes::QuotesConfig;
use crate::rules::non_critical_lints::{
dot_notation::DotNotationConfig, no_console::NoConsoleConfig,
no_use_before_define::NoUseBeforeDefineConfig,
prefer_regex_literals::PreferRegexLiteralsConfig, quotes::QuotesConfig,
};
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
@ -61,6 +59,10 @@ pub struct LintConfig {
#[serde(default)]
pub no_debugger: RuleConfig<()>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub no_use_before_define: RuleConfig<NoUseBeforeDefineConfig>,
#[cfg(feature = "non_critical_lints")]
#[serde(default)]
pub dot_notation: RuleConfig<DotNotationConfig>,

View File

@ -18,6 +18,7 @@ pub(crate) mod non_critical_lints {
pub mod no_alert;
pub mod no_console;
pub mod no_debugger;
pub mod no_use_before_define;
pub mod prefer_regex_literals;
pub mod quotes;
}
@ -50,6 +51,10 @@ pub fn all(lint_params: LintParams) -> Vec<Box<dyn Rule>> {
source_map,
} = lint_params;
rules.extend(no_use_before_define::no_use_before_define(
&lint_params.lint_config.no_use_before_define,
));
rules.extend(no_console::no_console(
&lint_config.no_console,
top_level_ctxt,

View File

@ -0,0 +1,236 @@
use serde::{Deserialize, Serialize};
use swc_common::{
collections::{AHashMap, AHashSet},
errors::HANDLER,
Span, DUMMY_SP,
};
use swc_ecma_ast::*;
use swc_ecma_utils::ident::IdentLike;
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
use crate::{
config::{LintRuleReaction, RuleConfig},
rule::{visitor_rule, Rule},
};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoUseBeforeDefineConfig {
variables: Option<bool>,
functions: Option<bool>,
classes: Option<bool>,
}
pub fn no_use_before_define(config: &RuleConfig<NoUseBeforeDefineConfig>) -> Option<Box<dyn Rule>> {
match config.get_rule_reaction() {
LintRuleReaction::Off => None,
_ => Some(visitor_rule(NoUseBeforeDefine::new(config))),
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
struct SpansScopeId {
ident_id: Id,
block_span: Span,
}
#[derive(Debug, Default)]
struct NoUseBeforeDefine {
expected_reaction: LintRuleReaction,
// means variables defined with "var" keyword
check_vars: bool,
check_functions: bool,
check_classes: bool,
scoped_indents: AHashMap<Span, AHashSet<Id>>,
scope: Vec<Span>,
scoped_spans: AHashMap<SpansScopeId, AHashSet<Span>>,
}
impl NoUseBeforeDefine {
fn new(config: &RuleConfig<NoUseBeforeDefineConfig>) -> Self {
let rule_config = config.get_rule_config();
let root_scope = DUMMY_SP;
let mut scoped_indents: AHashMap<Span, AHashSet<Id>> = Default::default();
scoped_indents.insert(root_scope, Default::default());
Self {
expected_reaction: *config.get_rule_reaction(),
scoped_indents,
scope: vec![root_scope],
scoped_spans: Default::default(),
check_vars: rule_config.variables.unwrap_or(false),
check_functions: rule_config.functions.unwrap_or(false),
check_classes: rule_config.classes.unwrap_or(false),
}
}
fn insert_ident_to_current_scope(&mut self, id: Id, span: Span) {
let current_scope = self.scope.last().unwrap();
self.scoped_indents
.get_mut(current_scope)
.unwrap()
.insert(id.clone());
let spans_block_id = SpansScopeId {
ident_id: id,
block_span: *current_scope,
};
if let Some(spans) = self.scoped_spans.get_mut(&spans_block_id) {
spans.insert(span);
} else {
let mut spans: AHashSet<Span> = Default::default();
spans.insert(span);
self.scoped_spans.insert(spans_block_id, spans);
}
}
fn has_access_before_define(&self, id: &Id) -> bool {
let current_scope = self.scope.last().unwrap();
self.scoped_indents.get(current_scope).unwrap().contains(id)
}
fn get_ident_spans_in_current_scope(&self, id: &Id) -> &AHashSet<Span> {
let current_block = self.scope.last().unwrap();
let spans_block_id = SpansScopeId {
ident_id: id.clone(),
block_span: *current_block,
};
self.scoped_spans.get(&spans_block_id).unwrap()
}
fn emit_report(&self, es6_var_check: bool, span: Span, name: &str) {
let message = format!("'{}' was used before it was defined", name);
let expected_reaction = if es6_var_check {
LintRuleReaction::Error
} else {
self.expected_reaction
};
HANDLER.with(|handler| match expected_reaction {
LintRuleReaction::Error => {
handler.struct_span_err(span, &message).emit();
}
LintRuleReaction::Warning => {
handler.struct_span_warn(span, &message).emit();
}
_ => {}
});
}
fn check_ident(&self, es6_var_check: bool, ident: &Ident) {
let ident_id = ident.to_id();
if self.has_access_before_define(&ident_id) {
let sym = &*ident.sym;
let mut spans = self
.get_ident_spans_in_current_scope(&ident_id)
.iter()
.copied()
.collect::<Vec<Span>>();
spans.sort();
spans.into_iter().for_each(|span| {
self.emit_report(es6_var_check, span, sym);
});
}
}
fn check_pat(&self, es6_var_check: bool, pat: &Pat) {
match pat {
Pat::Ident(BindingIdent { id, .. }) => {
self.check_ident(es6_var_check, id);
}
Pat::Array(ArrayPat { elems, .. }) => {
elems.iter().for_each(|elem| {
if let Some(elem) = elem {
self.check_pat(es6_var_check, elem);
}
});
}
Pat::Object(ObjectPat { props, .. }) => {
props.iter().for_each(|prop| match prop {
ObjectPatProp::Assign(AssignPatProp { key, .. }) => {
self.check_ident(es6_var_check, key);
}
ObjectPatProp::KeyValue(KeyValuePatProp { value, .. }) => {
self.check_pat(es6_var_check, value.as_ref());
}
ObjectPatProp::Rest(RestPat { arg, .. }) => {
self.check_pat(es6_var_check, arg.as_ref());
}
});
}
Pat::Rest(RestPat { arg, .. }) => {
self.check_pat(es6_var_check, arg.as_ref());
}
Pat::Assign(AssignPat { left, .. }) => {
self.check_pat(es6_var_check, left.as_ref());
}
Pat::Invalid(_) => {}
Pat::Expr(_) => {}
}
}
}
impl Visit for NoUseBeforeDefine {
noop_visit_type!();
fn visit_ident(&mut self, ident: &Ident) {
self.insert_ident_to_current_scope(ident.to_id(), ident.span);
}
fn visit_block_stmt(&mut self, block: &BlockStmt) {
self.scoped_indents.insert(block.span, Default::default());
self.scope.push(block.span);
block.visit_children_with(self);
self.scoped_indents.remove(&block.span);
self.scope.pop();
}
fn visit_var_decl(&mut self, var_decl: &VarDecl) {
let es6_var_check = !matches!(&var_decl.kind, VarDeclKind::Var);
var_decl.decls.iter().for_each(|declarator| {
declarator.init.visit_with(self);
if let VarDeclKind::Var = var_decl.kind {
if !self.check_vars {
return;
}
}
self.check_pat(es6_var_check, &declarator.name);
});
}
fn visit_fn_decl(&mut self, function: &FnDecl) {
if self.check_functions {
self.check_ident(false, &function.ident);
}
function.visit_children_with(self);
}
fn visit_class_decl(&mut self, class: &ClassDecl) {
if self.check_classes {
self.check_ident(false, &class.ident);
}
class.visit_children_with(self);
}
}