mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(linter): implement no_constant_binary_expression
This commit is contained in:
parent
5b8bdaabab
commit
6acb08f054
7 changed files with 960 additions and 1 deletions
|
|
@ -1,5 +1,6 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use num_bigint::BigUint;
|
||||
use oxc_allocator::{Box, Vec};
|
||||
use serde::Serialize;
|
||||
|
||||
|
|
@ -125,6 +126,24 @@ impl<'a> Expression<'a> {
|
|||
matches!(self, Self::Identifier(ident) if ident.name == "undefined")
|
||||
}
|
||||
|
||||
/// Determines whether the given expr is a `void 0`
|
||||
#[must_use]
|
||||
pub fn is_void_0(&self) -> bool {
|
||||
matches!(self, Self::UnaryExpression(expr) if expr.operator == UnaryOperator::Void)
|
||||
}
|
||||
|
||||
/// Determines whether the given expr evaluate to `undefined`
|
||||
#[must_use]
|
||||
pub fn evaluate_to_undefined(&self) -> bool {
|
||||
self.is_undefined() || self.is_void_0()
|
||||
}
|
||||
|
||||
/// Determines whether the given expr is a `null` or `undefined` or `void 0`
|
||||
#[must_use]
|
||||
pub fn is_null_or_undefined(&self) -> bool {
|
||||
self.is_null() || self.evaluate_to_undefined()
|
||||
}
|
||||
|
||||
/// Remove nested parentheses from this expression.
|
||||
#[must_use]
|
||||
pub fn without_parenthesized(&self) -> &Self {
|
||||
|
|
@ -168,6 +187,21 @@ impl<'a> Expression<'a> {
|
|||
pub fn is_function(&self) -> bool {
|
||||
matches!(self, Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_))
|
||||
}
|
||||
|
||||
/// Returns literal's value converted to the Boolean type
|
||||
/// returns `true` when node is truthy, `false` when node is falsy, `None` when it cannot be determined.
|
||||
#[must_use]
|
||||
pub fn get_boolean_value(&self) -> Option<bool> {
|
||||
match self {
|
||||
Self::BooleanLiteral(lit) => Some(lit.value),
|
||||
Self::NullLiteral(_) => Some(false),
|
||||
Self::NumberLiteral(lit) => Some(lit.value != 0.0),
|
||||
Self::BigintLiteral(lit) => Some(lit.value != BigUint::new(vec![])),
|
||||
Self::RegExpLiteral(_) => Some(true),
|
||||
Self::StringLiteral(lit) => Some(!lit.value.is_empty()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Section 12.6 `IdentifierName`
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use oxc_ast::ast::*;
|
||||
use phf::{phf_set, Set};
|
||||
|
||||
use crate::context::LintContext;
|
||||
|
||||
pub const STRICT_MODE_NAMES: Set<&'static str> = phf_set! {
|
||||
"implements",
|
||||
"interface",
|
||||
|
|
@ -11,3 +15,165 @@ pub const STRICT_MODE_NAMES: Set<&'static str> = phf_set! {
|
|||
"static",
|
||||
"yield",
|
||||
};
|
||||
|
||||
/// Test if an AST node is a boolean value that never changes. Specifically we
|
||||
/// test for:
|
||||
/// 1. Literal booleans (`true` or `false`)
|
||||
/// 2. Unary `!` expressions with a constant value
|
||||
/// 3. Constant booleans created via the `Boolean` global function
|
||||
pub fn is_static_boolean<'a>(expr: &Expression<'a>, ctx: &LintContext<'a>) -> bool {
|
||||
match expr {
|
||||
Expression::BooleanLiteral(_) => true,
|
||||
Expression::CallExpression(call_expr) => call_expr.is_constant(true, ctx),
|
||||
Expression::UnaryExpression(unary_expr) => {
|
||||
unary_expr.operator == UnaryOperator::LogicalNot
|
||||
&& unary_expr.argument.is_constant(true, ctx)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a branch node of `LogicalExpression` short circuits the whole condition
|
||||
fn is_logical_identity(op: LogicalOperator, expr: &Expression) -> bool {
|
||||
match expr {
|
||||
expr if expr.is_literal_expression() => {
|
||||
let boolean_value = expr.get_boolean_value();
|
||||
(op == LogicalOperator::Or && boolean_value == Some(true))
|
||||
|| (op == LogicalOperator::And && boolean_value == Some(false))
|
||||
}
|
||||
Expression::UnaryExpression(unary_expr) => {
|
||||
op == LogicalOperator::And && unary_expr.operator == UnaryOperator::Void
|
||||
}
|
||||
Expression::LogicalExpression(logical_expr) => {
|
||||
op == logical_expr.operator
|
||||
&& (is_logical_identity(logical_expr.operator, &logical_expr.left)
|
||||
|| is_logical_identity(logical_expr.operator, &logical_expr.right))
|
||||
}
|
||||
Expression::AssignmentExpression(assign_expr) => {
|
||||
matches!(
|
||||
assign_expr.operator,
|
||||
AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr
|
||||
) && ((op == LogicalOperator::And
|
||||
&& assign_expr.operator == AssignmentOperator::LogicalAnd)
|
||||
|| (op == LogicalOperator::Or
|
||||
&& assign_expr.operator == AssignmentOperator::LogicalOr))
|
||||
&& is_logical_identity(op, &assign_expr.right)
|
||||
}
|
||||
Expression::ParenthesizedExpression(expr) => is_logical_identity(op, &expr.expression),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a node has a constant truthiness value.
|
||||
/// `inBooleanPosition`:
|
||||
/// `true` if checking the test of a condition.
|
||||
/// `false` in all other cases.
|
||||
/// When `false`, checks if -- for both string and number --
|
||||
/// if coerced to that type, the value will be constant.
|
||||
pub trait IsConstant<'a, 'b> {
|
||||
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool;
|
||||
}
|
||||
|
||||
impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
|
||||
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
|
||||
match self {
|
||||
Self::ArrowFunctionExpression(_)
|
||||
| Self::FunctionExpression(_)
|
||||
| Self::ClassExpression(_)
|
||||
| Self::ObjectExpression(_) => true,
|
||||
Self::TemplateLiteral(template) => {
|
||||
let test_quasis = in_boolean_position
|
||||
&& template.quasis.iter().any(|quasi| {
|
||||
quasi.value.cooked.as_ref().map_or(false, |cooked| !cooked.is_empty())
|
||||
});
|
||||
let test_expressions =
|
||||
template.expressions.iter().all(|expr| expr.is_constant(false, ctx));
|
||||
test_quasis || test_expressions
|
||||
}
|
||||
Self::ArrayExpression(expr) => {
|
||||
if in_boolean_position {
|
||||
return true;
|
||||
}
|
||||
expr.elements
|
||||
.iter()
|
||||
.all(|element| element.as_ref().map_or(true, |e| e.is_constant(false, ctx)))
|
||||
}
|
||||
Self::UnaryExpression(expr) => match expr.operator {
|
||||
UnaryOperator::Void => true,
|
||||
UnaryOperator::Typeof if in_boolean_position => true,
|
||||
UnaryOperator::LogicalNot => expr.argument.is_constant(true, ctx),
|
||||
_ => expr.argument.is_constant(false, ctx),
|
||||
},
|
||||
Self::BinaryExpression(expr) => {
|
||||
expr.operator != BinaryOperator::In
|
||||
&& expr.left.is_constant(false, ctx)
|
||||
&& expr.right.is_constant(false, ctx)
|
||||
}
|
||||
Self::LogicalExpression(expr) => {
|
||||
let is_left_constant = expr.left.is_constant(in_boolean_position, ctx);
|
||||
let is_right_constant = expr.right.is_constant(in_boolean_position, ctx);
|
||||
let is_left_short_circuit =
|
||||
is_left_constant && is_logical_identity(expr.operator, &expr.left);
|
||||
let is_right_short_circuit = in_boolean_position
|
||||
&& is_right_constant
|
||||
&& is_logical_identity(expr.operator, &expr.right);
|
||||
(is_left_constant && is_right_constant)
|
||||
|| is_left_short_circuit
|
||||
|| is_right_short_circuit
|
||||
}
|
||||
Self::NewExpression(_) => in_boolean_position,
|
||||
Self::AssignmentExpression(expr) => match expr.operator {
|
||||
AssignmentOperator::Assign => expr.right.is_constant(in_boolean_position, ctx),
|
||||
AssignmentOperator::LogicalAnd if in_boolean_position => {
|
||||
is_logical_identity(LogicalOperator::And, &expr.right)
|
||||
}
|
||||
AssignmentOperator::LogicalOr if in_boolean_position => {
|
||||
is_logical_identity(LogicalOperator::Or, &expr.right)
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
Self::SequenceExpression(sequence_expr) => sequence_expr
|
||||
.expressions
|
||||
.iter()
|
||||
.last()
|
||||
.map_or(false, |last| last.is_constant(in_boolean_position, ctx)),
|
||||
Self::CallExpression(call_expr) => call_expr.is_constant(in_boolean_position, ctx),
|
||||
Self::ParenthesizedExpression(paren_expr) => {
|
||||
paren_expr.expression.is_constant(in_boolean_position, ctx)
|
||||
}
|
||||
Self::Identifier(ident) => {
|
||||
ident.name == "undefined" && ctx.is_reference_to_global_variable(ident)
|
||||
}
|
||||
_ if self.is_literal_expression() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IsConstant<'a, 'b> for CallExpression<'a> {
|
||||
fn is_constant(&self, _in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
|
||||
if let Expression::Identifier(ident) = &self.callee {
|
||||
if ident.name == "Boolean"
|
||||
&& self.arguments.iter().next().map_or(true, |first| first.is_constant(true, ctx))
|
||||
{
|
||||
return ctx.is_reference_to_global_variable(ident);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IsConstant<'a, 'b> for Argument<'a> {
|
||||
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
|
||||
match self {
|
||||
Self::SpreadElement(element) => element.is_constant(in_boolean_position, ctx),
|
||||
Self::Expression(expr) => expr.is_constant(in_boolean_position, ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> IsConstant<'a, 'b> for SpreadElement<'a> {
|
||||
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
|
||||
self.argument.is_constant(in_boolean_position, ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use indextree::{Ancestors, NodeId};
|
||||
use oxc_ast::{AstKind, SourceType};
|
||||
use oxc_ast::{ast::IdentifierReference, AstKind, SourceType};
|
||||
use oxc_diagnostics::Error;
|
||||
use oxc_semantic::{AstNodes, Scope, ScopeTree, Semantic, SemanticNode};
|
||||
|
||||
|
|
@ -108,4 +108,11 @@ impl<'a> LintContext<'a> {
|
|||
let scope = self.scope(node);
|
||||
node.get().strict_mode(scope)
|
||||
}
|
||||
|
||||
/* Symbols */
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
pub fn is_reference_to_global_variable(&self, _ident: &IdentifierReference) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
75
crates/oxc_linter/src/globals.rs
Normal file
75
crates/oxc_linter/src/globals.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
//! [Globals](https://github.com/sindresorhus/globals/blob/main/globals.json)
|
||||
//! Each global is given a value of true or false.
|
||||
//! A value of true indicates that the variable may be overwritten.
|
||||
//! A value of false indicates that the variable should be considered read-only.
|
||||
|
||||
use phf::{phf_map, Map};
|
||||
|
||||
pub const BUILTINS: Map<&'static str, bool> = phf_map! {
|
||||
"AggregateError" => false,
|
||||
"Array" => false,
|
||||
"ArrayBuffer" => false,
|
||||
"Atomics" => false,
|
||||
"BigInt" => false,
|
||||
"BigInt64Array" => false,
|
||||
"BigUint64Array" => false,
|
||||
"Boolean" => false,
|
||||
"constructor" => false,
|
||||
"DataView" => false,
|
||||
"Date" => false,
|
||||
"decodeURI" => false,
|
||||
"decodeURIComponent" => false,
|
||||
"encodeURI" => false,
|
||||
"encodeURIComponent" => false,
|
||||
"Error" => false,
|
||||
"escape" => false,
|
||||
"eval" => false,
|
||||
"EvalError" => false,
|
||||
"FinalizationRegistry" => false,
|
||||
"Float32Array" => false,
|
||||
"Float64Array" => false,
|
||||
"Function" => false,
|
||||
"globalThis" => false,
|
||||
"hasOwnProperty" => false,
|
||||
"Infinity" => false,
|
||||
"Int16Array" => false,
|
||||
"Int32Array" => false,
|
||||
"Int8Array" => false,
|
||||
"isFinite" => false,
|
||||
"isNaN" => false,
|
||||
"isPrototypeOf" => false,
|
||||
"JSON" => false,
|
||||
"Map" => false,
|
||||
"Math" => false,
|
||||
"NaN" => false,
|
||||
"Number" => false,
|
||||
"Object" => false,
|
||||
"parseFloat" => false,
|
||||
"parseInt" => false,
|
||||
"Promise" => false,
|
||||
"propertyIsEnumerable" => false,
|
||||
"Proxy" => false,
|
||||
"RangeError" => false,
|
||||
"ReferenceError" => false,
|
||||
"Reflect" => false,
|
||||
"RegExp" => false,
|
||||
"Set" => false,
|
||||
"SharedArrayBuffer" => false,
|
||||
"String" => false,
|
||||
"Symbol" => false,
|
||||
"Diagnostic" => false,
|
||||
"toLocaleString" => false,
|
||||
"toString" => false,
|
||||
"TypeError" => false,
|
||||
"Uint16Array" => false,
|
||||
"Uint32Array" => false,
|
||||
"Uint8Array" => false,
|
||||
"Uint8ClampedArray" => false,
|
||||
"undefined" => false,
|
||||
"unescape" => false,
|
||||
"URIError" => false,
|
||||
"valueOf" => false,
|
||||
"WeakMap" => false,
|
||||
"WeakRef" => false,
|
||||
"WeakSet" => false
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ mod tester;
|
|||
mod ast_util;
|
||||
mod context;
|
||||
mod fixer;
|
||||
mod globals;
|
||||
pub mod rule;
|
||||
mod rules;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ oxc_macros::declare_all_lint_rules! {
|
|||
no_empty,
|
||||
no_empty_pattern,
|
||||
no_mixed_operators,
|
||||
no_constant_binary_expression,
|
||||
deepscan::uninvoked_array_callback,
|
||||
}
|
||||
|
|
|
|||
675
crates/oxc_linter/src/rules/no_constant_binary_expression.rs
Normal file
675
crates/oxc_linter/src/rules/no_constant_binary_expression.rs
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use oxc_ast::{ast::*, AstKind, Span};
|
||||
use oxc_diagnostics::{
|
||||
miette::{self, Diagnostic},
|
||||
thiserror::Error,
|
||||
};
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
|
||||
use crate::{
|
||||
ast_util::{self, IsConstant},
|
||||
context::LintContext,
|
||||
globals::BUILTINS,
|
||||
rule::Rule,
|
||||
AstNode,
|
||||
};
|
||||
|
||||
/// `https://eslint.org/docs/latest/rules/no-constant-binary-expression`
|
||||
/// Original Author: Jordan Eldredge <https://jordaneldredge.com>
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct NoConstantBinaryExpression;
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
/// Disallow expressions where the operation doesn't affect the value
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
/// Comparisons which will always evaluate to true or false and logical expressions (||, &&, ??) which either always
|
||||
/// short-circuit or never short-circuit are both likely indications of programmer error.
|
||||
///
|
||||
/// These errors are especially common in complex expressions where operator precedence is easy to misjudge.
|
||||
///
|
||||
/// Additionally, this rule detects comparisons to newly constructed objects/arrays/functions/etc.
|
||||
/// In JavaScript, where objects are compared by reference, a newly constructed object can never === any other value.
|
||||
/// This can be surprising for programmers coming from languages where objects are compared by value.
|
||||
///
|
||||
/// ### Example
|
||||
/// ```javascript
|
||||
/// // One might think this would evaluate as `a + (b ?? c)`:
|
||||
/// const x = a + b ?? c;
|
||||
///
|
||||
/// // But it actually evaluates as `(a + b) ?? c`. Since `a + b` can never be null,
|
||||
/// // the `?? c` has no effect.
|
||||
///
|
||||
/// // Programmers coming from a language where objects are compared by value might expect this to work:
|
||||
/// const isEmpty = x === [];
|
||||
///
|
||||
/// // However, this will always result in `isEmpty` being `false`.
|
||||
/// ```
|
||||
NoConstantBinaryExpression,
|
||||
nursery
|
||||
);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error(
|
||||
"eslint(no-constant-binary-expression): Disallow expressions where the operation doesn't affect the value"
|
||||
)]
|
||||
#[diagnostic()]
|
||||
struct NoConstantBinaryExpressionDiagnostic(#[label] pub Span);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error(
|
||||
"eslint(no-constant-binary-expression): Unexpected constant {0:?} on the left-hand side of a `{1:?}` expression"
|
||||
)]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct ConstantShortCircuit(
|
||||
&'static str, // property
|
||||
String, // operator
|
||||
#[label("This expression always evaluates to the constant on the left-hand side")] Span,
|
||||
);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint(no-constant-binary-expression): Unexpected constant binary expression")]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct ConstantBinaryOperand(
|
||||
&'static str, // otherSide
|
||||
String, // operator
|
||||
#[label("This compares constantly with the {0}-hand side of the `{1}`")] Span,
|
||||
);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("eslint(no-constant-binary-expression): Unexpected comparison to newly constructed object")]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct ConstantAlwaysNew(#[label("These two values can never be equal")] Span);
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error(
|
||||
"eslint(no-constant-binary-expression): Unexpected comparison of two newly constructed objects"
|
||||
)]
|
||||
#[diagnostic(severity(warning))]
|
||||
struct ConstantBothAlwaysNew(#[label("These two values can never be equal")] Span);
|
||||
|
||||
impl Rule for NoConstantBinaryExpression {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
match node.get().kind() {
|
||||
AstKind::LogicalExpression(expr) => match expr.operator {
|
||||
LogicalOperator::Or | LogicalOperator::And if expr.left.is_constant(true, ctx) => {
|
||||
ctx.diagnostic(ConstantShortCircuit(
|
||||
"truthiness",
|
||||
expr.operator.to_string(),
|
||||
expr.span,
|
||||
));
|
||||
}
|
||||
LogicalOperator::Coalesce
|
||||
if Self::has_constant_nullishness(&expr.left, false, ctx) =>
|
||||
{
|
||||
ctx.diagnostic(ConstantShortCircuit(
|
||||
"nullishness",
|
||||
expr.operator.to_string(),
|
||||
expr.span,
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
AstKind::BinaryExpression(expr) => {
|
||||
let left = &expr.left;
|
||||
let right = &expr.right;
|
||||
let operator = expr.operator;
|
||||
|
||||
let right_constant_operand =
|
||||
Self::find_binary_expression_constant_operand(left, right, operator, ctx);
|
||||
|
||||
let left_constant_operand =
|
||||
Self::find_binary_expression_constant_operand(right, left, operator, ctx);
|
||||
|
||||
if right_constant_operand.is_some() {
|
||||
ctx.diagnostic(ConstantBinaryOperand("left", operator.to_string(), expr.span));
|
||||
return;
|
||||
}
|
||||
|
||||
if left_constant_operand.is_some() {
|
||||
ctx.diagnostic(ConstantBinaryOperand("right", operator.to_string(), expr.span));
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
operator,
|
||||
BinaryOperator::StrictEquality | BinaryOperator::StrictInequality
|
||||
) && (Self::is_always_new(left, ctx) || Self::is_always_new(right, ctx))
|
||||
{
|
||||
ctx.diagnostic(ConstantAlwaysNew(expr.span));
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(operator, BinaryOperator::Equality | BinaryOperator::Inequality)
|
||||
&& Self::is_always_new(left, ctx)
|
||||
&& Self::is_always_new(right, ctx)
|
||||
{
|
||||
ctx.diagnostic(ConstantBothAlwaysNew(expr.span));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NoConstantBinaryExpression {
|
||||
/// Test if an AST node has a statically knowable constant nullishness. Meaning,
|
||||
/// it will always resolve to a constant value of either: `null`, `undefined`
|
||||
/// or not `null` _or_ `undefined`. An expression that can vary between those
|
||||
/// three states at runtime would return `false`.
|
||||
fn has_constant_nullishness<'a>(
|
||||
expr: &Expression<'a>,
|
||||
non_nullish: bool,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> bool {
|
||||
if non_nullish && (expr.is_null() || expr.evaluate_to_undefined()) {
|
||||
return false;
|
||||
}
|
||||
match expr.get_inner_expression() {
|
||||
Expression::ObjectExpression(_)
|
||||
| Expression::ArrayExpression(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::FunctionExpression(_)
|
||||
| Expression::ClassExpression(_)
|
||||
| Expression::NewExpression(_)
|
||||
| Expression::TemplateLiteral(_)
|
||||
| Expression::UpdateExpression(_)
|
||||
| Expression::BinaryExpression(_)
|
||||
| Expression::UnaryExpression(_) => true,
|
||||
expr if expr.is_literal_expression() => true,
|
||||
Expression::CallExpression(call_expr) => {
|
||||
if let Expression::Identifier(ident) = &call_expr.callee {
|
||||
return ["Boolean", "String", "Number"].contains(&ident.name.as_str())
|
||||
&& ctx.is_reference_to_global_variable(ident);
|
||||
}
|
||||
false
|
||||
}
|
||||
Expression::LogicalExpression(logical_expr)
|
||||
if logical_expr.operator == LogicalOperator::Coalesce =>
|
||||
{
|
||||
Self::has_constant_nullishness(&logical_expr.right, true, ctx)
|
||||
}
|
||||
Expression::AssignmentExpression(assign_expr) => match assign_expr.operator {
|
||||
AssignmentOperator::Assign => {
|
||||
Self::has_constant_nullishness(&assign_expr.right, non_nullish, ctx)
|
||||
}
|
||||
op if op.is_logical_operator() => false,
|
||||
_ => true,
|
||||
},
|
||||
Expression::SequenceExpression(sequence_expr) => sequence_expr
|
||||
.expressions
|
||||
.iter()
|
||||
.last()
|
||||
.map_or(false, |last| Self::has_constant_nullishness(last, non_nullish, ctx)),
|
||||
Expression::Identifier(_) => expr.evaluate_to_undefined(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if one operand will cause the result to be constant.
|
||||
fn find_binary_expression_constant_operand<'a>(
|
||||
a: &'a Expression<'a>,
|
||||
b: &'a Expression<'a>,
|
||||
operator: BinaryOperator,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> Option<&'a Expression<'a>> {
|
||||
match operator {
|
||||
BinaryOperator::Equality | BinaryOperator::Inequality => {
|
||||
if (a.is_null_or_undefined() && Self::has_constant_nullishness(b, false, ctx))
|
||||
|| (ast_util::is_static_boolean(a, ctx)
|
||||
&& Self::has_constant_loose_boolean_comparison(b, ctx))
|
||||
{
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
BinaryOperator::StrictEquality | BinaryOperator::StrictInequality => {
|
||||
if (a.is_null_or_undefined() && Self::has_constant_nullishness(b, false, ctx))
|
||||
|| (ast_util::is_static_boolean(a, ctx)
|
||||
&& Self::has_constant_strict_boolean_comparison(b, ctx))
|
||||
{
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Test if an AST node will always give the same result when compared to a
|
||||
/// boolean value. Note that comparison to boolean values is different than
|
||||
/// truthiness.
|
||||
/// `https://262.ecma-international.org/5.1/#sec-11.9.3`
|
||||
fn has_constant_loose_boolean_comparison<'a>(
|
||||
expr: &Expression<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> bool {
|
||||
match expr {
|
||||
Expression::ObjectExpression(_)
|
||||
| Expression::ClassExpression(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::FunctionExpression(_) => true,
|
||||
Expression::ArrayExpression(array_expr) => {
|
||||
array_expr.elements.is_empty()
|
||||
|| array_expr
|
||||
.elements
|
||||
.iter()
|
||||
.filter(|e| matches!(e, Some(Argument::Expression(_))))
|
||||
.count()
|
||||
> 1
|
||||
}
|
||||
Expression::UnaryExpression(unary_expr) => match unary_expr.operator {
|
||||
UnaryOperator::Void | UnaryOperator::Typeof => true,
|
||||
UnaryOperator::LogicalNot => unary_expr.argument.is_constant(true, ctx),
|
||||
_ => false,
|
||||
},
|
||||
Expression::CallExpression(call_expr) => call_expr.is_constant(true, ctx),
|
||||
Expression::TemplateLiteral(lit) => lit.expressions.is_empty(),
|
||||
Expression::AssignmentExpression(assignment_expr) => {
|
||||
assignment_expr.operator == AssignmentOperator::Assign
|
||||
&& Self::has_constant_loose_boolean_comparison(&assignment_expr.right, ctx)
|
||||
}
|
||||
Expression::SequenceExpression(sequence_expr) => sequence_expr
|
||||
.expressions
|
||||
.iter()
|
||||
.last()
|
||||
.map_or(false, |last| Self::has_constant_loose_boolean_comparison(last, ctx)),
|
||||
Expression::ParenthesizedExpression(paren_expr) => {
|
||||
Self::has_constant_loose_boolean_comparison(&paren_expr.expression, ctx)
|
||||
}
|
||||
expr if expr.is_literal_expression() => true,
|
||||
expr if expr.evaluate_to_undefined() => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if an AST node will always give the same result when _strictly_ compared
|
||||
/// to a boolean value. This can happen if the expression can never be boolean, or
|
||||
/// if it is always the same boolean value.
|
||||
fn has_constant_strict_boolean_comparison<'a>(
|
||||
expr: &Expression<'a>,
|
||||
ctx: &LintContext<'a>,
|
||||
) -> bool {
|
||||
match expr {
|
||||
Expression::ObjectExpression(_)
|
||||
| Expression::ArrayExpression(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::FunctionExpression(_)
|
||||
| Expression::NewExpression(_)
|
||||
| Expression::TemplateLiteral(_)
|
||||
| Expression::UpdateExpression(_) => true,
|
||||
expr if expr.is_literal_expression() => true,
|
||||
Expression::BinaryExpression(binary_expr) => {
|
||||
binary_expr.operator.is_numeric_or_string_binary_operator()
|
||||
}
|
||||
Expression::UnaryExpression(unary_expr) => match unary_expr.operator {
|
||||
UnaryOperator::Delete => false,
|
||||
UnaryOperator::LogicalNot => unary_expr.argument.is_constant(true, ctx),
|
||||
_ => true,
|
||||
},
|
||||
Expression::CallExpression(call_expr) => {
|
||||
if let Expression::Identifier(ident) = &call_expr.callee {
|
||||
if ident.name == "String"
|
||||
|| ident.name == "Number" && ctx.is_reference_to_global_variable(ident)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if ident.name == "Boolean" && ctx.is_reference_to_global_variable(ident) {
|
||||
return call_expr
|
||||
.arguments
|
||||
.iter()
|
||||
.next()
|
||||
.map_or(true, |first| first.is_constant(true, ctx));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Expression::AssignmentExpression(assign_expr) => match assign_expr.operator {
|
||||
AssignmentOperator::Assign => {
|
||||
Self::has_constant_strict_boolean_comparison(&assign_expr.right, ctx)
|
||||
}
|
||||
op if op.is_logical_operator() => false,
|
||||
_ => true,
|
||||
},
|
||||
Expression::SequenceExpression(sequence_expr) => sequence_expr
|
||||
.expressions
|
||||
.iter()
|
||||
.last()
|
||||
.map_or(false, |last| Self::has_constant_strict_boolean_comparison(last, ctx)),
|
||||
Expression::ParenthesizedExpression(paren_expr) => {
|
||||
Self::has_constant_strict_boolean_comparison(&paren_expr.expression, ctx)
|
||||
}
|
||||
Expression::Identifier(_) => expr.evaluate_to_undefined(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test if an AST node will always result in a newly constructed object
|
||||
fn is_always_new<'a>(expr: &Expression<'a>, ctx: &LintContext<'a>) -> bool {
|
||||
match expr {
|
||||
Expression::ObjectExpression(_)
|
||||
| Expression::ArrayExpression(_)
|
||||
| Expression::ArrowFunctionExpression(_)
|
||||
| Expression::FunctionExpression(_)
|
||||
| Expression::ClassExpression(_)
|
||||
| Expression::RegExpLiteral(_) => true,
|
||||
Expression::NewExpression(call_expr) => {
|
||||
if let Expression::Identifier(ident) = &call_expr.callee {
|
||||
return BUILTINS.contains_key(ident.name.as_str())
|
||||
&& ctx.is_reference_to_global_variable(ident);
|
||||
}
|
||||
false
|
||||
}
|
||||
Expression::SequenceExpression(sequence_expr) => sequence_expr
|
||||
.expressions
|
||||
.iter()
|
||||
.last()
|
||||
.map_or(false, |last| Self::is_always_new(last, ctx)),
|
||||
Expression::AssignmentExpression(assignment_expr)
|
||||
if assignment_expr.operator == AssignmentOperator::Assign =>
|
||||
{
|
||||
Self::is_always_new(&assignment_expr.right, ctx)
|
||||
}
|
||||
Expression::ConditionalExpression(cond_expr) => {
|
||||
Self::is_always_new(&cond_expr.consequent, ctx)
|
||||
&& Self::is_always_new(&cond_expr.alternate, ctx)
|
||||
}
|
||||
Expression::ParenthesizedExpression(paren_expr) => {
|
||||
Self::is_always_new(&paren_expr.expression, ctx)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
// While this _would_ be a constant condition in React, ESLint has a policy of not attributing any specific behavior to JSX.
|
||||
("<p /> && foo", None),
|
||||
("<></> && foo", None),
|
||||
("<p /> ?? foo", None),
|
||||
("<></> ?? foo", None),
|
||||
("arbitraryFunction(n) ?? foo", None),
|
||||
("foo.Boolean(n) ?? foo", None),
|
||||
("(x += 1) && foo", None),
|
||||
("`${bar}` && foo", None),
|
||||
("bar && foo", None),
|
||||
("delete bar.baz && foo", None),
|
||||
("true ? foo : bar", None), // We leave ConditionalExpression for `no-constant-condition`
|
||||
("new Foo() == true", None),
|
||||
("foo == true", None),
|
||||
("`${foo}` == true", None),
|
||||
("`${foo}${bar}` == true", None),
|
||||
("`0${foo}` == true", None),
|
||||
("`00000000${foo}` == true", None),
|
||||
("`0${foo}.000` == true", None),
|
||||
("[n] == true", None),
|
||||
("delete bar.baz === true", None),
|
||||
("foo.Boolean(true) && foo", None),
|
||||
("function Boolean(n) { return n; }; Boolean(x) ?? foo", None),
|
||||
("function String(n) { return n; }; String(x) ?? foo", None),
|
||||
("function Number(n) { return n; }; Number(x) ?? foo", None),
|
||||
("function Boolean(n) { return Math.random(); }; Boolean(x) === 1", None),
|
||||
("function Boolean(n) { return Math.random(); }; Boolean(1) == true", None),
|
||||
("new Foo() === x", None),
|
||||
("x === new someObj.Promise()", None),
|
||||
("Boolean(foo) === true", None),
|
||||
// ("function foo(undefined) { undefined ?? bar;}", None),
|
||||
// ("function foo(undefined) { undefined == true;}", None),
|
||||
// ("function foo(undefined) { undefined === true;}", None),
|
||||
("[...arr, 1] == true", None),
|
||||
("[,,,] == true", None),
|
||||
// { code: "new Foo() === bar;", globals: { Foo: "writable" } },
|
||||
("(foo && true) ?? bar", None),
|
||||
("foo ?? null ?? bar", None),
|
||||
("a ?? (doSomething(), undefined) ?? b", None),
|
||||
("a ?? (something = null) ?? b", None),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
// Error messages
|
||||
("[] && greeting", None),
|
||||
("[] || greeting", None),
|
||||
("[] ?? greeting", None),
|
||||
("[] == true", None),
|
||||
("true == []", None),
|
||||
("[] != true", None),
|
||||
("[] === true", None),
|
||||
("[] !== true", None),
|
||||
// Motivating examples from the original proposal https://github.com/eslint/eslint/issues/13752
|
||||
("!foo == null", None),
|
||||
("!foo ?? bar", None),
|
||||
("(a + b) / 2 ?? bar", None),
|
||||
("String(foo.bar) ?? baz", None),
|
||||
("'hello' + name ?? ''", None),
|
||||
("[foo?.bar ?? ''] ?? []", None),
|
||||
// Logical expression with constant truthiness
|
||||
("true && hello", None),
|
||||
("true || hello", None),
|
||||
("true && foo", None),
|
||||
("'' && foo", None),
|
||||
("100 && foo", None),
|
||||
("+100 && foo", None),
|
||||
("-100 && foo", None),
|
||||
("~100 && foo", None),
|
||||
("/[a-z]/ && foo", None),
|
||||
("Boolean([]) && foo", None),
|
||||
("Boolean() && foo", None),
|
||||
("Boolean([], n) && foo", None),
|
||||
("({}) && foo", None),
|
||||
("[] && foo", None),
|
||||
("(() => {}) && foo", None),
|
||||
("(function() {}) && foo", None),
|
||||
("(class {}) && foo", None),
|
||||
("(class { valueOf() { return x; } }) && foo", None),
|
||||
("(class { [x]() { return x; } }) && foo", None),
|
||||
("new Foo() && foo", None),
|
||||
// (boxed values are always truthy)
|
||||
("new Boolean(unknown) && foo", None),
|
||||
("(bar = false) && foo", None),
|
||||
("(bar.baz = false) && foo", None),
|
||||
("(bar[0] = false) && foo", None),
|
||||
("`hello ${hello}` && foo", None),
|
||||
("void bar && foo", None),
|
||||
("!true && foo", None),
|
||||
("typeof bar && foo", None),
|
||||
("(bar, baz, true) && foo", None),
|
||||
("undefined && foo", None),
|
||||
// Logical expression with constant nullishness
|
||||
("({}) ?? foo", None),
|
||||
("([]) ?? foo", None),
|
||||
("(() => {}) ?? foo", None),
|
||||
("(function() {}) ?? foo", None),
|
||||
("(class {}) ?? foo", None),
|
||||
("new Foo() ?? foo", None),
|
||||
("1 ?? foo", None),
|
||||
("/[a-z]/ ?? foo", None),
|
||||
("`${''}` ?? foo", None),
|
||||
("(a = true) ?? foo", None),
|
||||
("(a += 1) ?? foo", None),
|
||||
("(a -= 1) ?? foo", None),
|
||||
("(a *= 1) ?? foo", None),
|
||||
("(a /= 1) ?? foo", None),
|
||||
("(a %= 1) ?? foo", None),
|
||||
("(a <<= 1) ?? foo", None),
|
||||
("(a >>= 1) ?? foo", None),
|
||||
("(a >>>= 1) ?? foo", None),
|
||||
("(a |= 1) ?? foo", None),
|
||||
("(a ^= 1) ?? foo", None),
|
||||
("(a &= 1) ?? foo", None),
|
||||
("undefined ?? foo", None),
|
||||
("!bar ?? foo", None),
|
||||
("void bar ?? foo", None),
|
||||
("typeof bar ?? foo", None),
|
||||
("+bar ?? foo", None),
|
||||
("-bar ?? foo", None),
|
||||
("~bar ?? foo", None),
|
||||
("++bar ?? foo", None),
|
||||
("bar++ ?? foo", None),
|
||||
("--bar ?? foo", None),
|
||||
("bar-- ?? foo", None),
|
||||
("(x == y) ?? foo", None),
|
||||
("(x + y) ?? foo", None),
|
||||
("(x / y) ?? foo", None),
|
||||
("(x instanceof String) ?? foo", None),
|
||||
("(x in y) ?? foo", None),
|
||||
("Boolean(x) ?? foo", None),
|
||||
("String(x) ?? foo", None),
|
||||
("Number(x) ?? foo", None),
|
||||
// Binary expression with comparison to null
|
||||
("({}) != null", None),
|
||||
("({}) == null", None),
|
||||
("null == ({})", None),
|
||||
("({}) == undefined", None),
|
||||
("undefined == ({})", None),
|
||||
// Binary expression with loose comparison to boolean
|
||||
("({}) != true", None),
|
||||
("({}) == true", None),
|
||||
("([]) == true", None),
|
||||
("([a, b]) == true", None),
|
||||
("(() => {}) == true", None),
|
||||
("(function() {}) == true", None),
|
||||
("void foo == true", None),
|
||||
("typeof foo == true", None),
|
||||
("![] == true", None),
|
||||
("true == class {}", None),
|
||||
("true == 1", None),
|
||||
("undefined == true", None),
|
||||
("true == undefined", None),
|
||||
("`hello` == true", None),
|
||||
("/[a-z]/ == true", None),
|
||||
("({}) == Boolean({})", None),
|
||||
("({}) == Boolean()", None),
|
||||
("({}) == Boolean(() => {}, foo)", None),
|
||||
// Binary expression with strict comparison to boolean
|
||||
("({}) !== true", None),
|
||||
("({}) == !({})", None),
|
||||
("({}) === true", None),
|
||||
("([]) === true", None),
|
||||
("(function() {}) === true", None),
|
||||
("(() => {}) === true", None),
|
||||
("!{} === true", None),
|
||||
("typeof n === true", None),
|
||||
("void n === true", None),
|
||||
("+n === true", None),
|
||||
("-n === true", None),
|
||||
("~n === true", None),
|
||||
("true === true", None),
|
||||
("1 === true", None),
|
||||
("'hello' === true", None),
|
||||
("/[a-z]/ === true", None),
|
||||
("undefined === true", None),
|
||||
("(a = {}) === true", None),
|
||||
("(a += 1) === true", None),
|
||||
("(a -= 1) === true", None),
|
||||
("(a *= 1) === true", None),
|
||||
("(a %= 1) === true", None),
|
||||
("(a ** b) === true", None),
|
||||
("(a << b) === true", None),
|
||||
("(a >> b) === true", None),
|
||||
("(a >>> b) === true", None),
|
||||
("--a === true", None),
|
||||
("a-- === true", None),
|
||||
("++a === true", None),
|
||||
("a++ === true", None),
|
||||
("(a + b) === true", None),
|
||||
("(a - b) === true", None),
|
||||
("(a * b) === true", None),
|
||||
("(a / b) === true", None),
|
||||
("(a % b) === true", None),
|
||||
("(a | b) === true", None),
|
||||
("(a ^ b) === true", None),
|
||||
("(a & b) === true", None),
|
||||
("Boolean(0) === Boolean(1)", None),
|
||||
("true === String(x)", None),
|
||||
("true === Number(x)", None),
|
||||
("Boolean(0) == !({})", None),
|
||||
// Binary expression with strict comparison to null
|
||||
("({}) !== null", None),
|
||||
("({}) === null", None),
|
||||
("([]) === null", None),
|
||||
("(() => {}) === null", None),
|
||||
("(function() {}) === null", None),
|
||||
("(class {}) === null", None),
|
||||
("new Foo() === null", None),
|
||||
("`` === null", None),
|
||||
("1 === null", None),
|
||||
("'hello' === null", None),
|
||||
("/[a-z]/ === null", None),
|
||||
("true === null", None),
|
||||
("null === null", None),
|
||||
("a++ === null", None),
|
||||
("++a === null", None),
|
||||
("--a === null", None),
|
||||
("a-- === null", None),
|
||||
("!a === null", None),
|
||||
("typeof a === null", None),
|
||||
("delete a === null", None),
|
||||
("void a === null", None),
|
||||
("undefined === null", None),
|
||||
("(x = {}) === null", None),
|
||||
("(x += y) === null", None),
|
||||
("(x -= y) === null", None),
|
||||
("(a, b, {}) === null", None),
|
||||
// Binary expression with strict comparison to undefined
|
||||
("({}) !== undefined", None),
|
||||
("({}) === undefined", None),
|
||||
("([]) === undefined", None),
|
||||
("(() => {}) === undefined", None),
|
||||
("(function() {}) === undefined", None),
|
||||
("(class {}) === undefined", None),
|
||||
("new Foo() === undefined", None),
|
||||
("`` === undefined", None),
|
||||
("1 === undefined", None),
|
||||
("'hello' === undefined", None),
|
||||
("/[a-z]/ === undefined", None),
|
||||
("true === undefined", None),
|
||||
("null === undefined", None),
|
||||
("a++ === undefined", None),
|
||||
("++a === undefined", None),
|
||||
("--a === undefined", None),
|
||||
("a-- === undefined", None),
|
||||
("!a === undefined", None),
|
||||
("typeof a === undefined", None),
|
||||
("delete a === undefined", None),
|
||||
("void a === undefined", None),
|
||||
("undefined === undefined", None),
|
||||
("(x = {}) === undefined", None),
|
||||
("(x += y) === undefined", None),
|
||||
("(x -= y) === undefined", None),
|
||||
("(a, b, {}) === undefined", None),
|
||||
/*
|
||||
* If both sides are newly constructed objects, we can tell they will
|
||||
* never be equal, even with == equality.
|
||||
*/
|
||||
("[a] == [a]", None),
|
||||
("[a] != [a]", None),
|
||||
("({}) == []", None),
|
||||
// Comparing to always new objects
|
||||
("x === {}", None),
|
||||
("x !== {}", None),
|
||||
("x === []", None),
|
||||
("x === (() => {})", None),
|
||||
("x === (function() {})", None),
|
||||
("x === (class {})", None),
|
||||
("x === new Boolean()", None),
|
||||
("x === new Promise()", None),
|
||||
("x === new WeakSet()", None),
|
||||
("x === (foo, {})", None),
|
||||
("x === (y = {})", None),
|
||||
("x === (y ? {} : [])", None),
|
||||
("x === /[a-z]/", None),
|
||||
// It's not obvious what this does, but it compares the old value of `x` to the new object.
|
||||
("x === (x = {})", None),
|
||||
("window.abc && false && anything", None),
|
||||
("window.abc || true || anything", None),
|
||||
("window.abc ?? 'non-nullish' ?? anything", None),
|
||||
];
|
||||
|
||||
Tester::new(NoConstantBinaryExpression::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
Loading…
Reference in a new issue