Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 64 additions & 53 deletions crates/oxc_linter/src/ast_util.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
use oxc_ast::{ast::BindingIdentifier, AstKind};
use oxc_ecmascript::ToBoolean;
use oxc_semantic::{AstNode, IsGlobalReference, NodeId, SymbolId};
use oxc_semantic::{AstNode, IsGlobalReference, NodeId, Semantic, SymbolId};
use oxc_span::{GetSpan, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator};

#[allow(clippy::wildcard_imports)]
use oxc_ast::ast::*;

use crate::context::LintContext;

/// 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 {
pub fn is_static_boolean<'a>(expr: &Expression<'a>, semantic: &Semantic<'a>) -> bool {
match expr {
Expression::BooleanLiteral(_) => true,
Expression::CallExpression(call_expr) => call_expr.is_constant(true, ctx),
Expression::CallExpression(call_expr) => call_expr.is_constant(true, semantic),
Expression::UnaryExpression(unary_expr) => {
unary_expr.operator == UnaryOperator::LogicalNot
&& unary_expr.argument.is_constant(true, ctx)
&& unary_expr.argument.is_constant(true, semantic)
}
_ => false,
}
Expand Down Expand Up @@ -64,11 +62,11 @@ fn is_logical_identity(op: LogicalOperator, expr: &Expression) -> bool {
/// 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;
fn is_constant(&self, in_boolean_position: bool, semantic: &Semantic<'a>) -> bool;
}

impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
fn is_constant(&self, in_boolean_position: bool, semantic: &Semantic<'a>) -> bool {
match self {
Self::ArrowFunctionExpression(_)
| Self::FunctionExpression(_)
Expand All @@ -80,29 +78,29 @@ impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
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));
template.expressions.iter().all(|expr| expr.is_constant(false, semantic));
test_quasis || test_expressions
}
Self::ArrayExpression(expr) => {
if in_boolean_position {
return true;
}
expr.elements.iter().all(|element| element.is_constant(false, ctx))
expr.elements.iter().all(|element| element.is_constant(false, semantic))
}
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),
UnaryOperator::LogicalNot => expr.argument.is_constant(true, semantic),
_ => expr.argument.is_constant(false, semantic),
},
Self::BinaryExpression(expr) => {
expr.operator != BinaryOperator::In
&& expr.left.is_constant(false, ctx)
&& expr.right.is_constant(false, ctx)
&& expr.left.is_constant(false, semantic)
&& expr.right.is_constant(false, semantic)
}
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_constant = expr.left.is_constant(in_boolean_position, semantic);
let is_right_constant = expr.right.is_constant(in_boolean_position, semantic);
let is_left_short_circuit =
is_left_constant && is_logical_identity(expr.operator, &expr.left);
let is_right_short_circuit = in_boolean_position
Expand All @@ -114,7 +112,7 @@ impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
}
Self::NewExpression(_) => in_boolean_position,
Self::AssignmentExpression(expr) => match expr.operator {
AssignmentOperator::Assign => expr.right.is_constant(in_boolean_position, ctx),
AssignmentOperator::Assign => expr.right.is_constant(in_boolean_position, semantic),
AssignmentOperator::LogicalAnd if in_boolean_position => {
is_logical_identity(LogicalOperator::And, &expr.right)
}
Expand All @@ -127,13 +125,13 @@ impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
.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),
.map_or(false, |last| last.is_constant(in_boolean_position, semantic)),
Self::CallExpression(call_expr) => call_expr.is_constant(in_boolean_position, semantic),
Self::ParenthesizedExpression(paren_expr) => {
paren_expr.expression.is_constant(in_boolean_position, ctx)
paren_expr.expression.is_constant(in_boolean_position, semantic)
}
Self::Identifier(ident) => {
ident.name == "undefined" && ctx.semantic().is_reference_to_global_variable(ident)
ident.name == "undefined" && semantic.is_reference_to_global_variable(ident)
}
_ if self.is_literal() => true,
_ => false,
Expand All @@ -142,48 +140,56 @@ impl<'a, 'b> IsConstant<'a, 'b> for Expression<'a> {
}

impl<'a, 'b> IsConstant<'a, 'b> for CallExpression<'a> {
fn is_constant(&self, _in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
fn is_constant(&self, _in_boolean_position: bool, semantic: &Semantic<'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))
&& self
.arguments
.iter()
.next()
.map_or(true, |first| first.is_constant(true, semantic))
{
return ctx.semantic().is_reference_to_global_variable(ident);
return semantic.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 {
fn is_constant(&self, in_boolean_position: bool, semantic: &Semantic<'a>) -> bool {
match self {
Self::SpreadElement(element) => element.is_constant(in_boolean_position, ctx),
match_expression!(Self) => self.to_expression().is_constant(in_boolean_position, ctx),
Self::SpreadElement(element) => element.is_constant(in_boolean_position, semantic),
match_expression!(Self) => {
self.to_expression().is_constant(in_boolean_position, semantic)
}
}
}
}

impl<'a, 'b> IsConstant<'a, 'b> for ArrayExpressionElement<'a> {
fn is_constant(&self, in_boolean_position: bool, ctx: &LintContext<'a>) -> bool {
fn is_constant(&self, in_boolean_position: bool, semantic: &Semantic<'a>) -> bool {
match self {
Self::SpreadElement(element) => element.is_constant(in_boolean_position, ctx),
match_expression!(Self) => self.to_expression().is_constant(in_boolean_position, ctx),
Self::SpreadElement(element) => element.is_constant(in_boolean_position, semantic),
match_expression!(Self) => {
self.to_expression().is_constant(in_boolean_position, semantic)
}
Self::Elision(_) => true,
}
}
}

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)
fn is_constant(&self, in_boolean_position: bool, semantic: &Semantic<'a>) -> bool {
self.argument.is_constant(in_boolean_position, semantic)
}
}

/// Return the innermost `Function` or `ArrowFunctionExpression` Node
/// enclosing the specified node
pub fn get_enclosing_function<'a, 'b>(
node: &'b AstNode<'a>,
ctx: &'b LintContext<'a>,
semantic: &'b Semantic<'a>,
) -> Option<&'b AstNode<'a>> {
let mut current_node = node;
loop {
Expand All @@ -194,7 +200,7 @@ pub fn get_enclosing_function<'a, 'b>(
{
return Some(current_node);
}
current_node = ctx.nodes().parent_node(current_node.id())?;
current_node = semantic.nodes().parent_node(current_node.id())?;
}
}

Expand All @@ -205,11 +211,14 @@ pub fn is_nth_argument<'a>(call: &CallExpression<'a>, arg: &Argument<'a>, n: usi
}

/// Jump to the outer most of chained parentheses if any
pub fn outermost_paren<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'a>) -> &'b AstNode<'a> {
pub fn outermost_paren<'a, 'b>(
node: &'b AstNode<'a>,
semantic: &'b Semantic<'a>,
) -> &'b AstNode<'a> {
let mut node = node;

loop {
if let Some(parent) = ctx.nodes().parent_node(node.id()) {
if let Some(parent) = semantic.nodes().parent_node(node.id()) {
if let AstKind::ParenthesizedExpression(_) = parent.kind() {
node = parent;
continue;
Expand All @@ -224,32 +233,34 @@ pub fn outermost_paren<'a, 'b>(node: &'b AstNode<'a>, ctx: &'b LintContext<'a>)

pub fn outermost_paren_parent<'a, 'b>(
node: &'b AstNode<'a>,
ctx: &'b LintContext<'a>,
semantic: &'b Semantic<'a>,
) -> Option<&'b AstNode<'a>> {
ctx.nodes()
semantic
.nodes()
.iter_parents(node.id())
.skip(1)
.find(|parent| !matches!(parent.kind(), AstKind::ParenthesizedExpression(_)))
}

pub fn nth_outermost_paren_parent<'a, 'b>(
node: &'b AstNode<'a>,
ctx: &'b LintContext<'a>,
semantic: &'b Semantic<'a>,
n: usize,
) -> Option<&'b AstNode<'a>> {
ctx.nodes()
semantic
.nodes()
.iter_parents(node.id())
.skip(1)
.filter(|parent| !matches!(parent.kind(), AstKind::ParenthesizedExpression(_)))
.nth(n)
}
/// Iterate over parents of `node`, skipping nodes that are also ignored by
/// [`Expression::get_inner_expression`].
pub fn iter_outer_expressions<'a, 'ctx>(
ctx: &'ctx LintContext<'a>,
pub fn iter_outer_expressions<'a, 's>(
semantic: &'s Semantic<'a>,
node_id: NodeId,
) -> impl Iterator<Item = &'ctx AstNode<'a>> + 'ctx {
ctx.nodes().iter_parents(node_id).skip(1).filter(|parent| {
) -> impl Iterator<Item = &'s AstNode<'a>> + 's {
semantic.nodes().iter_parents(node_id).skip(1).filter(|parent| {
!matches!(
parent.kind(),
AstKind::ParenthesizedExpression(_)
Expand All @@ -263,19 +274,19 @@ pub fn iter_outer_expressions<'a, 'ctx>(
}

pub fn get_declaration_of_variable<'a, 'b>(
ident: &IdentifierReference,
ctx: &'b LintContext<'a>,
ident: &IdentifierReference<'a>,
semantic: &'b Semantic<'a>,
) -> Option<&'b AstNode<'a>> {
let symbol_id = get_symbol_id_of_variable(ident, ctx)?;
let symbol_table = ctx.semantic().symbols();
Some(ctx.nodes().get_node(symbol_table.get_declaration(symbol_id)))
let symbol_id = get_symbol_id_of_variable(ident, semantic)?;
let symbol_table = semantic.symbols();
Some(semantic.nodes().get_node(symbol_table.get_declaration(symbol_id)))
}

pub fn get_symbol_id_of_variable(
ident: &IdentifierReference,
ctx: &LintContext<'_>,
semantic: &Semantic<'_>,
) -> Option<SymbolId> {
let symbol_table = ctx.semantic().symbols();
let symbol_table = semantic.symbols();
let reference_id = ident.reference_id.get()?;
let reference = symbol_table.get_reference(reference_id);
reference.symbol_id()
Expand Down Expand Up @@ -389,7 +400,7 @@ pub fn get_new_expr_ident_name<'a>(new_expr: &'a NewExpression<'a>) -> Option<&'
Some(ident.name.as_str())
}

pub fn is_global_require_call(call_expr: &CallExpression, ctx: &LintContext) -> bool {
pub fn is_global_require_call(call_expr: &CallExpression, ctx: &Semantic) -> bool {
if call_expr.arguments.len() != 1 {
return false;
}
Expand All @@ -407,7 +418,7 @@ pub fn is_function_node(node: &AstNode) -> bool {

pub fn get_function_like_declaration<'b>(
node: &AstNode<'b>,
ctx: &LintContext<'b>,
ctx: &Semantic<'b>,
) -> Option<&'b BindingIdentifier<'b>> {
let parent = outermost_paren_parent(node, ctx)?;
let decl = parent.kind().as_variable_declarator()?;
Expand Down
Loading