Skip to content

Commit

Permalink
feat(biome_js_analyze): noConstantMathMinMaxClamp (#2404)
Browse files Browse the repository at this point in the history
  • Loading branch information
mgomulak authored Apr 14, 2024
1 parent 421e953 commit 75af801
Show file tree
Hide file tree
Showing 19 changed files with 850 additions and 51 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,8 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b

- Implement [#2043](https://github.com/biomejs/biome/issues/2043): The React rule [`useExhaustiveDependencies`](https://biomejs.dev/linter/rules/use-exhaustive-dependencies/) is now also compatible with Preact hooks imported from `preact/hooks` or `preact/compat`. Contributed by @arendjr

- Add rule [noConstantMathMinMaxClamp](https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp), which disallows using `Math.min` and `Math.max` to clamp a value where the result itself is constant. Contributed by @mgomulak

#### Enhancements

- [style/useFilenamingConvention](https://biomejs.dev/linter/rules/use-filenaming-convention/) now allows prefixing a filename with `+` ([#2341](https://github.com/biomejs/biome/issues/2341)).
Expand Down
119 changes: 69 additions & 50 deletions crates/biome_configuration/src/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ define_categories! {
"lint/nursery/noBarrelFile": "https://biomejs.dev/linter/rules/no-barrel-file",
"lint/nursery/noColorInvalidHex": "https://biomejs.dev/linter/rules/no-color-invalid-hex",
"lint/nursery/noConsole": "https://biomejs.dev/linter/rules/no-console",
"lint/nursery/noConstantMathMinMaxClamp": "https://biomejs.dev/linter/rules/no-constant-math-min-max-clamp",
"lint/nursery/noDoneCallback": "https://biomejs.dev/linter/rules/no-done-callback",
"lint/nursery/noDuplicateElseIf": "https://biomejs.dev/linter/rules/no-duplicate-else-if",
"lint/nursery/noDuplicateJsonKeys": "https://biomejs.dev/linter/rules/no-duplicate-json-keys",
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_js_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use biome_analyze::declare_group;

pub mod no_barrel_file;
pub mod no_console;
pub mod no_constant_math_min_max_clamp;
pub mod no_done_callback;
pub mod no_duplicate_else_if;
pub mod no_duplicate_test_hooks;
Expand Down Expand Up @@ -31,6 +32,7 @@ declare_group! {
rules : [
self :: no_barrel_file :: NoBarrelFile ,
self :: no_console :: NoConsole ,
self :: no_constant_math_min_max_clamp :: NoConstantMathMinMaxClamp ,
self :: no_done_callback :: NoDoneCallback ,
self :: no_duplicate_else_if :: NoDuplicateElseIf ,
self :: no_duplicate_test_hooks :: NoDuplicateTestHooks ,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
use std::{cmp::Ordering, str::FromStr};

use biome_analyze::{
context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic, RuleSource,
};
use biome_console::markup;
use biome_diagnostics::Applicability;
use biome_js_semantic::SemanticModel;
use biome_js_syntax::{
global_identifier, AnyJsExpression, AnyJsLiteralExpression, AnyJsMemberExpression,
JsCallExpression, JsNumberLiteralExpression,
};
use biome_rowan::{AstNode, BatchMutationExt};

use crate::{services::semantic::Semantic, JsRuleAction};

declare_rule! {
/// Disallow the use of `Math.min` and `Math.max` to clamp a value where the result itself is constant.
///
/// ## Examples
///
/// ### Invalid
///
/// ```js,expect_diagnostic
/// Math.min(0, Math.max(100, x));
/// ```
///
/// ```js,expect_diagnostic
/// Math.max(100, Math.min(0, x));
/// ```
/// ### Valid
///
/// ```js
/// Math.min(100, Math.max(0, x));
/// ```
///
pub NoConstantMathMinMaxClamp {
version: "next",
name: "noConstantMathMinMaxClamp",
sources: &[RuleSource::Clippy("min_max")],
recommended: false,
fix_kind: FixKind::Unsafe,
}
}

impl Rule for NoConstantMathMinMaxClamp {
type Query = Semantic<JsCallExpression>;
type State = (JsNumberLiteralExpression, JsNumberLiteralExpression);
type Signals = Option<Self::State>;
type Options = ();

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();
let model = ctx.model();

let outer_call = get_math_min_or_max_call(node, model)?;

let inner_call = get_math_min_or_max_call(
outer_call
.other_expression_argument
.as_js_call_expression()?,
model,
)?;

if outer_call.kind == inner_call.kind {
return None;
}

match (
outer_call.kind,
outer_call
.constant_argument
.as_number()?
.partial_cmp(&inner_call.constant_argument.as_number()?),
) {
(MinMaxKind::Min, Some(Ordering::Less))
| (MinMaxKind::Max, Some(Ordering::Greater)) => {
Some((outer_call.constant_argument, inner_call.constant_argument))
}
_ => None,
}
}

fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();

Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"This "<Emphasis>"Math.min/Math.max"</Emphasis>" combination leads to a constant result."
}
).detail(
state.0.range(),
markup! {
"It always evaluates to "<Emphasis>{state.0.text()}</Emphasis>"."
}
)
)
}

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

mutation.replace_node(state.0.clone(), state.1.clone());
mutation.replace_node(state.1.clone(), state.0.clone());

Some(JsRuleAction {
mutation,
message: markup! {"Swap "<Emphasis>{state.0.text()}</Emphasis>" with "<Emphasis>{state.1.text()}</Emphasis>"."}
.to_owned(),
category: ActionCategory::QuickFix,
applicability: Applicability::MaybeIncorrect,
})
}
}

#[derive(PartialEq, Eq, Debug, Clone, Copy)]
enum MinMaxKind {
Min,
Max,
}

impl FromStr for MinMaxKind {
type Err = &'static str;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"min" => Ok(MinMaxKind::Min),
"max" => Ok(MinMaxKind::Max),
_ => Err("Value not supported for math min max kind"),
}
}
}

#[derive(Debug, Clone)]
struct MathMinOrMaxCall {
kind: MinMaxKind,
constant_argument: JsNumberLiteralExpression,
other_expression_argument: AnyJsExpression,
}

fn get_math_min_or_max_call(
call_expression: &JsCallExpression,
model: &SemanticModel,
) -> Option<MathMinOrMaxCall> {
let callee = call_expression.callee().ok()?.omit_parentheses();
let member_expr = AnyJsMemberExpression::cast_ref(callee.syntax())?;

let member_name = member_expr.member_name()?;
let member_name = member_name.text();

let min_or_max = MinMaxKind::from_str(member_name).ok()?;

let object = member_expr.object().ok()?.omit_parentheses();
let (reference, name) = global_identifier(&object)?;

if name.text() != "Math" || model.binding(&reference).is_some() {
return None;
}

let arguments = call_expression.arguments().ok()?.args();
let mut iter = arguments.into_iter();

let first_argument = iter.next()?.ok()?;
let first_argument = first_argument.as_any_js_expression()?;

let second_argument = iter.next()?.ok()?;
let second_argument = second_argument.as_any_js_expression()?;

// `Math.min` and `Math.max` are variadic functions.
// We give up if they have more than 2 arguments.
if iter.next().is_some() {
return None;
}

match (first_argument, second_argument) {
(
any_expression,
AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsNumberLiteralExpression(constant_value),
),
)
| (
AnyJsExpression::AnyJsLiteralExpression(
AnyJsLiteralExpression::JsNumberLiteralExpression(constant_value),
),
any_expression,
) => {
// The non-number literal argument must either be a call expression or an identifier expression.
if any_expression.as_js_call_expression().is_none()
&& any_expression.as_js_identifier_expression().is_none()
{
return None;
}

Some(MathMinOrMaxCall {
kind: min_or_max,
constant_argument: constant_value.clone(),
other_expression_argument: any_expression.clone(),
})
}
_ => None,
}
}
1 change: 1 addition & 0 deletions crates/biome_js_analyze/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub type NoConstEnum =
<lint::suspicious::no_const_enum::NoConstEnum as biome_analyze::Rule>::Options;
pub type NoConstantCondition =
<lint::correctness::no_constant_condition::NoConstantCondition as biome_analyze::Rule>::Options;
pub type NoConstantMathMinMaxClamp = < lint :: nursery :: no_constant_math_min_max_clamp :: NoConstantMathMinMaxClamp as biome_analyze :: Rule > :: Options ;
pub type NoConstructorReturn =
<lint::correctness::no_constructor_return::NoConstructorReturn as biome_analyze::Rule>::Options;
pub type NoControlCharactersInRegex = < lint :: suspicious :: no_control_characters_in_regex :: NoControlCharactersInRegex as biome_analyze :: Rule > :: Options ;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Math.min(0, Math.max(100, x));

Math.max(100, Math.min(0, x));

Math.max(100, Math.min(x, 0));

window.Math.min(0, window.Math.max(100, x));

window.Math.min(0, Math.max(100, x));

Math.min(0, window.Math.max(100, x));

globalThis.Math.min(0, globalThis.Math.max(100, x));

globalThis.Math.min(0, Math.max(100, x));

Math.min(0, globalThis.Math.max(100, x));

foo(Math.min(0, Math.max(100, x)));
Loading

0 comments on commit 75af801

Please sign in to comment.