Skip to content

Commit

Permalink
feat(lint/useIsNan): add code fix action (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
victor-teles committed Sep 9, 2023
1 parent b5f54c5 commit 424f68d
Show file tree
Hide file tree
Showing 3 changed files with 535 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
use biome_analyze::context::RuleContext;
use biome_analyze::{declare_rule, Rule, RuleDiagnostic};
use biome_analyze::{declare_rule, ActionCategory, Rule, RuleDiagnostic};
use biome_console::markup;
use biome_diagnostics::Applicability;
use rome_js_factory::make;
use rome_js_semantic::SemanticModel;
use rome_js_syntax::{
global_identifier, AnyJsExpression, AnyJsMemberExpression, JsBinaryExpression, JsCaseClause,
JsSwitchStatement, TextRange,
global_identifier, AnyJsCallArgument, AnyJsExpression, AnyJsMemberExpression,
JsBinaryExpression, JsBinaryOperator, JsCaseClause, JsSwitchStatement, TextRange, T,
};
use rome_rowan::{declare_node_union, AstNode};
use rome_rowan::{declare_node_union, AstNode, BatchMutationExt};

use crate::semantic_services::Semantic;
use crate::{semantic_services::Semantic, JsRuleAction};

declare_rule! {
/// Require calls to `isNaN()` when checking for `NaN`.
Expand Down Expand Up @@ -99,6 +102,7 @@ impl Rule for UseIsNan {
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let model = ctx.model();

match node {
UseIsNanQuery::JsBinaryExpression(bin_expr) => {
if bin_expr.is_comparison_operator()
Expand Down Expand Up @@ -142,6 +146,144 @@ impl Rule for UseIsNan {
state.message_id.as_str(),
))
}

fn action(ctx: &RuleContext<Self>, _: &Self::State) -> Option<JsRuleAction> {
let query = ctx.query();
let model = ctx.model();
let mut mutation = ctx.root().begin();

match query {
UseIsNanQuery::JsBinaryExpression(binary_expression) => {
let has_nan = binary_expression.is_comparison_operator()
&& (has_nan(binary_expression.left().ok()?, model)
|| has_nan(binary_expression.right().ok()?, model));

if !has_nan {
return None;
}

let (literal, nan) = get_literal(binary_expression, model)?;
let with_inequality = contains_inequality(binary_expression).unwrap_or(false);
let is_nan_expression: AnyJsExpression = create_is_nan_expression(nan)
.and_then(|result| create_unary_expression(with_inequality, result))?;

let arg = AnyJsCallArgument::AnyJsExpression(literal);
let args = make::js_call_arguments(
make::token(T!['(']),
make::js_call_argument_list([arg], []),
make::token(T![')']),
);

let call = make::js_call_expression(is_nan_expression, args).build();

mutation.replace_node(
AnyJsExpression::JsBinaryExpression(binary_expression.clone()),
call.into(),
);

return Some(JsRuleAction {
category: ActionCategory::QuickFix,
applicability: Applicability::MaybeIncorrect,
message: markup! {
"Use "<Emphasis>"Number.isNaN()"</Emphasis>" instead."
}
.to_owned(),
mutation,
});
}
UseIsNanQuery::JsCaseClause(_) => None,
UseIsNanQuery::JsSwitchStatement(_) => None,
}
}
}

fn create_unary_expression(
with_inequality: bool,
nan_expression: AnyJsExpression,
) -> Option<AnyJsExpression> {
if with_inequality {
let unary = make::js_unary_expression(make::token(T![!]), nan_expression);
return Some(unary.into());
}

Some(nan_expression)
}

fn create_is_nan_expression(nan: AnyJsExpression) -> Option<AnyJsExpression> {
match nan {
AnyJsExpression::JsIdentifierExpression(_)
| AnyJsExpression::JsComputedMemberExpression(_) => {
let is_nan_expression = make::js_static_member_expression(
make::js_identifier_expression(make::js_reference_identifier(make::ident(
"Number",
)))
.into(),
make::token(T![.]),
make::js_name(make::ident("isNaN")).into(),
);

Some(is_nan_expression.into())
}
AnyJsExpression::JsStaticMemberExpression(member_expression) => {
let is_nan_expression =
member_expression.with_member(make::js_name(make::ident("isNaN")).into());
let member_object = is_nan_expression.object().ok()?.omit_parentheses();
let (reference, _) = global_identifier(&member_object)?;
let number_identifier_exists = is_nan_expression
.object()
.ok()?
.as_js_static_member_expression()
.is_some_and(|y| y.member().is_ok_and(|z| z.text() == "Number"));

if !reference.is_global_this() && !reference.has_name("window")
|| number_identifier_exists
{
return Some(is_nan_expression.into());
}

let member_expression = make::js_static_member_expression(
is_nan_expression.object().ok()?,
make::token(T![.]),
make::js_name(make::ident("Number")).into(),
);

Some(
is_nan_expression
.with_object(member_expression.into())
.into(),
)
}
_ => None,
}
}

fn contains_inequality(bin_expr: &JsBinaryExpression) -> Option<bool> {
Some(matches!(
bin_expr.operator().ok()?,
JsBinaryOperator::Inequality | JsBinaryOperator::StrictInequality
))
}

fn get_literal(
bin_expr: &JsBinaryExpression,
model: &SemanticModel,
) -> Option<(AnyJsExpression, AnyJsExpression)> {
let left_expression = bin_expr.left().ok()?;
let right_expression = bin_expr.right().ok()?;
let is_nan_on_left = has_nan(left_expression.clone(), model);

let left = left_expression
.with_leading_trivia_pieces([])?
.with_trailing_trivia_pieces([])?;
let right = right_expression
.with_leading_trivia_pieces([])?
.with_trailing_trivia_pieces([])?;

if !is_nan_on_left {
Some((left, right))
} else {
Some((right, left))
}
}

/// Checks whether an expression has `NaN`, `Number.NaN`, or `Number['NaN']`.
Expand Down
Loading

0 comments on commit 424f68d

Please sign in to comment.