Skip to content

Commit

Permalink
refactor(linter): all ast_util functions take Semantic (#6753)
Browse files Browse the repository at this point in the history
Needed in `oxc/no-map-spread` rule.
  • Loading branch information
DonIsaac committed Oct 21, 2024
1 parent 744aa74 commit b884577
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 181 deletions.
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

0 comments on commit b884577

Please sign in to comment.