mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(linter): implement typescript/no-unused-expressions (#7498)
This commit is contained in:
parent
7da9a39b5d
commit
053bc081fb
3 changed files with 622 additions and 0 deletions
|
|
@ -180,6 +180,7 @@ mod typescript {
|
|||
pub mod no_unnecessary_type_constraint;
|
||||
pub mod no_unsafe_declaration_merging;
|
||||
pub mod no_unsafe_function_type;
|
||||
pub mod no_unused_expressions;
|
||||
pub mod no_useless_empty_export;
|
||||
pub mod no_var_requires;
|
||||
pub mod no_wrapper_object_types;
|
||||
|
|
@ -854,6 +855,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
typescript::consistent_type_definitions,
|
||||
typescript::consistent_type_imports,
|
||||
typescript::explicit_function_return_type,
|
||||
typescript::no_unused_expressions,
|
||||
typescript::no_inferrable_types,
|
||||
typescript::no_confusing_non_null_assertion,
|
||||
typescript::no_duplicate_enum_values,
|
||||
|
|
|
|||
418
crates/oxc_linter/src/rules/typescript/no_unused_expressions.rs
Normal file
418
crates/oxc_linter/src/rules/typescript/no_unused_expressions.rs
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
use oxc_ast::{
|
||||
ast::{ChainElement, Expression, UnaryOperator},
|
||||
AstKind,
|
||||
};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::Span;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
|
||||
fn no_unused_expressions_diagnostic(span: Span) -> OxcDiagnostic {
|
||||
OxcDiagnostic::warn("Disallow unused expressions")
|
||||
.with_help("Consider removing this expression")
|
||||
.with_label(span)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoUnusedExpressions(Box<NoUnusedExpressionsConfig>);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoUnusedExpressionsConfig {
|
||||
allow_short_circuit: bool,
|
||||
allow_ternary: bool,
|
||||
allow_tagged_templates: bool,
|
||||
enforce_for_jsx: bool,
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// This rule disallows unused expressions.
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// Unused expressions are usually a mistake. They can be a symptom of a bug or a misunderstanding of the code.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// Examples of **incorrect** code for this rule:
|
||||
/// ```ts
|
||||
/// Set<number>;
|
||||
/// 1 as number;
|
||||
/// window!;
|
||||
/// ```
|
||||
///
|
||||
/// Examples of **correct** code for this rule:
|
||||
/// ```ts
|
||||
/// const foo = new Set<number>();
|
||||
/// ```
|
||||
NoUnusedExpressions,
|
||||
restriction
|
||||
);
|
||||
|
||||
impl Rule for NoUnusedExpressions {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
let AstKind::ExpressionStatement(expression_stmt) = node.kind() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.is_disallowed(&expression_stmt.expression) {
|
||||
ctx.diagnostic(no_unused_expressions_diagnostic(expression_stmt.span));
|
||||
}
|
||||
}
|
||||
|
||||
fn from_configuration(value: serde_json::Value) -> Self {
|
||||
Self(Box::new(NoUnusedExpressionsConfig {
|
||||
allow_short_circuit: value
|
||||
.get(0)
|
||||
.and_then(|x| x.get("allowShortCircuit"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or_default(),
|
||||
allow_ternary: value
|
||||
.get(0)
|
||||
.and_then(|x| x.get("allowTernary"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or_default(),
|
||||
allow_tagged_templates: value
|
||||
.get(0)
|
||||
.and_then(|x| x.get("allowTaggedTemplates"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or_default(),
|
||||
enforce_for_jsx: value
|
||||
.get(0)
|
||||
.and_then(|x| x.get("enforceForJSX"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or_default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl NoUnusedExpressions {
|
||||
fn is_disallowed(&self, expr: &Expression) -> bool {
|
||||
match expr {
|
||||
Expression::BooleanLiteral(_)
|
||||
| Expression::NullLiteral(_)
|
||||
| Expression::NumericLiteral(_)
|
||||
| Expression::BigIntLiteral(_)
|
||||
| Expression::RegExpLiteral(_)
|
||||
| Expression::StringLiteral(_)
|
||||
| Expression::SequenceExpression(_)
|
||||
| Expression::FunctionExpression(_)
|
||||
| Expression::ArrayExpression(_)
|
||||
| Expression::ComputedMemberExpression(_)
|
||||
| Expression::MetaProperty(_)
|
||||
| Expression::ObjectExpression(_)
|
||||
| Expression::PrivateFieldExpression(_)
|
||||
| Expression::StaticMemberExpression(_)
|
||||
| Expression::TemplateLiteral(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::ClassExpression(_)
|
||||
| Expression::BinaryExpression(_)
|
||||
| Expression::PrivateInExpression(_)
|
||||
| Expression::ThisExpression(_)
|
||||
| Expression::Identifier(_) => true,
|
||||
Expression::ChainExpression(chain_expression) => match &chain_expression.expression {
|
||||
ChainElement::CallExpression(_) => false,
|
||||
ChainElement::TSNonNullExpression(ts_non_null_expression) => {
|
||||
self.is_disallowed(&ts_non_null_expression.expression)
|
||||
}
|
||||
ChainElement::ComputedMemberExpression(_)
|
||||
| ChainElement::StaticMemberExpression(_)
|
||||
| ChainElement::PrivateFieldExpression(_) => true,
|
||||
},
|
||||
Expression::AssignmentExpression(_)
|
||||
| Expression::AwaitExpression(_)
|
||||
| Expression::NewExpression(_)
|
||||
| Expression::ImportExpression(_)
|
||||
| Expression::Super(_)
|
||||
| Expression::CallExpression(_)
|
||||
| Expression::UpdateExpression(_)
|
||||
| Expression::YieldExpression(_) => false,
|
||||
Expression::ConditionalExpression(conditional_expression) => {
|
||||
if self.0.allow_ternary {
|
||||
return self.is_disallowed(&conditional_expression.alternate)
|
||||
|| self.is_disallowed(&conditional_expression.consequent);
|
||||
}
|
||||
true
|
||||
}
|
||||
Expression::LogicalExpression(logical_expression) => {
|
||||
if self.0.allow_short_circuit {
|
||||
return self.is_disallowed(&logical_expression.right);
|
||||
}
|
||||
true
|
||||
}
|
||||
Expression::ParenthesizedExpression(parenthesized_expression) => {
|
||||
self.is_disallowed(&parenthesized_expression.expression)
|
||||
}
|
||||
Expression::TaggedTemplateExpression(_) => !self.0.allow_tagged_templates,
|
||||
Expression::UnaryExpression(unary_expression) => {
|
||||
!matches!(unary_expression.operator, UnaryOperator::Delete | UnaryOperator::Void)
|
||||
}
|
||||
Expression::JSXElement(_) | Expression::JSXFragment(_) => self.0.enforce_for_jsx,
|
||||
Expression::TSAsExpression(ts_as_expression) => {
|
||||
self.is_disallowed(&ts_as_expression.expression)
|
||||
}
|
||||
Expression::TSSatisfiesExpression(ts_satisfies_expression) => {
|
||||
self.is_disallowed(&ts_satisfies_expression.expression)
|
||||
}
|
||||
Expression::TSTypeAssertion(ts_type_assertion) => {
|
||||
self.is_disallowed(&ts_type_assertion.expression)
|
||||
}
|
||||
Expression::TSNonNullExpression(ts_non_null_expression) => {
|
||||
self.is_disallowed(&ts_non_null_expression.expression)
|
||||
}
|
||||
Expression::TSInstantiationExpression(ts_instantiation_expression) => {
|
||||
self.is_disallowed(&ts_instantiation_expression.expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
(
|
||||
"
|
||||
test.age?.toLocaleString();
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
let a = (a?.b).c;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
let b = a?.['b'];
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
let c = one[2]?.[3][4];
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
one[2]?.[3][4]?.();
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a?.['b']?.c();
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
module Foo {
|
||||
'use strict';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
namespace Foo {
|
||||
'use strict';
|
||||
|
||||
export class Foo {}
|
||||
export class Bar {}
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
function foo() {
|
||||
'use strict';
|
||||
|
||||
return null;
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
import('./foo');
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
import('./foo').then(() => {});
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
class Foo<T> {}
|
||||
new Foo<string>();
|
||||
",
|
||||
None,
|
||||
),
|
||||
("foo && foo?.();", Some(serde_json::json!([{ "allowShortCircuit": true }]))),
|
||||
("foo && import('./foo');", Some(serde_json::json!([{ "allowShortCircuit": true }]))),
|
||||
(
|
||||
"foo ? import('./foo') : import('./bar');",
|
||||
Some(serde_json::json!([{ "allowTernary": true }])),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(
|
||||
"
|
||||
if (0) 0;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
f(0), {};
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a, b();
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a() &&
|
||||
function namedFunctionInExpressionContext() {
|
||||
f();
|
||||
};
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a?.b;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
(a?.b).c;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a?.['b'];
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
(a?.['b']).c;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
a?.b()?.c;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
(a?.b()).c;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
one[2]?.[3][4];
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
one.two?.three.four;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
module Foo {
|
||||
const foo = true;
|
||||
'use strict';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
namespace Foo {
|
||||
export class Foo {}
|
||||
export class Bar {}
|
||||
|
||||
'use strict';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
function foo() {
|
||||
const foo = true;
|
||||
|
||||
'use strict';
|
||||
}
|
||||
",
|
||||
None,
|
||||
),
|
||||
("foo && foo?.bar;", Some(serde_json::json!([{ "allowShortCircuit": true }]))),
|
||||
("foo ? foo?.bar : bar.baz;", Some(serde_json::json!([{ "allowTernary": true }]))),
|
||||
(
|
||||
"
|
||||
class Foo<T> {}
|
||||
Foo<string>;
|
||||
",
|
||||
None,
|
||||
),
|
||||
("Map<string, string>;", None),
|
||||
(
|
||||
"
|
||||
declare const foo: number | undefined;
|
||||
foo;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
declare const foo: number | undefined;
|
||||
foo as any;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
declare const foo: number | undefined;
|
||||
<any>foo;
|
||||
",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"
|
||||
declare const foo: number | undefined;
|
||||
foo!;
|
||||
",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(NoUnusedExpressions::NAME, NoUnusedExpressions::CATEGORY, pass, fail)
|
||||
.test_and_snapshot();
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:11]
|
||||
1 │
|
||||
2 │ if (0) 0;
|
||||
· ──
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ f(0), {};
|
||||
· ─────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ a, b();
|
||||
· ───────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ ╭─▶ a() &&
|
||||
3 │ │ function namedFunctionInExpressionContext() {
|
||||
4 │ │ f();
|
||||
5 │ ╰─▶ };
|
||||
6 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ a?.b;
|
||||
· ─────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ (a?.b).c;
|
||||
· ─────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ a?.['b'];
|
||||
· ─────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ (a?.['b']).c;
|
||||
· ─────────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ a?.b()?.c;
|
||||
· ──────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ (a?.b()).c;
|
||||
· ───────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ one[2]?.[3][4];
|
||||
· ───────────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:2:4]
|
||||
1 │
|
||||
2 │ one.two?.three.four;
|
||||
· ────────────────────
|
||||
3 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:4:6]
|
||||
3 │ const foo = true;
|
||||
4 │ 'use strict';
|
||||
· ─────────────
|
||||
5 │ }
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:6:6]
|
||||
5 │
|
||||
6 │ 'use strict';
|
||||
· ─────────────
|
||||
7 │ }
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:5:6]
|
||||
4 │
|
||||
5 │ 'use strict';
|
||||
· ─────────────
|
||||
6 │ }
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:1:1]
|
||||
1 │ foo && foo?.bar;
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:1:1]
|
||||
1 │ foo ? foo?.bar : bar.baz;
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:3:4]
|
||||
2 │ class Foo<T> {}
|
||||
3 │ Foo<string>;
|
||||
· ────────────
|
||||
4 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:1:1]
|
||||
1 │ Map<string, string>;
|
||||
· ────────────────────
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:3:4]
|
||||
2 │ declare const foo: number | undefined;
|
||||
3 │ foo;
|
||||
· ────
|
||||
4 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:3:4]
|
||||
2 │ declare const foo: number | undefined;
|
||||
3 │ foo as any;
|
||||
· ───────────
|
||||
4 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
|
||||
× Expected `<` but found `EOF`
|
||||
╭─[no_unused_expressions.tsx:4:10]
|
||||
3 │ <any>foo;
|
||||
4 │
|
||||
╰────
|
||||
|
||||
⚠ typescript-eslint(no-unused-expressions): Disallow unused expressions
|
||||
╭─[no_unused_expressions.tsx:3:4]
|
||||
2 │ declare const foo: number | undefined;
|
||||
3 │ foo!;
|
||||
· ─────
|
||||
4 │
|
||||
╰────
|
||||
help: Consider removing this expression
|
||||
Loading…
Reference in a new issue