From 75af8016b50476ae32193ec87c475e182925e9e0 Mon Sep 17 00:00:00 2001 From: Michael Gomulak Date: Sun, 14 Apr 2024 15:34:44 +0200 Subject: [PATCH] feat(biome_js_analyze): noConstantMathMinMaxClamp (#2404) --- CHANGELOG.md | 2 + .../biome_configuration/src/linter/rules.rs | 119 +++--- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../nursery/no_constant_math_min_max_clamp.rs | 206 +++++++++++ crates/biome_js_analyze/src/options.rs | 1 + .../noConstantMathMinMaxClamp/invalid.js | 19 + .../noConstantMathMinMaxClamp/invalid.js.snap | 349 ++++++++++++++++++ .../invalid.jsonc.snap | 57 +++ .../valid-shadowing.js | 3 + .../valid-shadowing.js.snap | 11 + .../noConstantMathMinMaxClamp/valid.js | 11 + .../noConstantMathMinMaxClamp/valid.js.snap | 19 + .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + .../components/generated/NumberOfRules.astro | 2 +- .../src/content/docs/internals/changelog.md | 2 + .../src/content/docs/linter/rules/index.mdx | 1 + .../rules/no-constant-math-min-max-clamp.md | 84 +++++ 19 files changed, 850 insertions(+), 51 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/no_constant_math_min_max_clamp.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.jsonc.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js.snap create mode 100644 website/src/content/docs/linter/rules/no-constant-math-min-max-clamp.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 90769fef7c8f..c1b8becb350f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index a8eb8c513f8f..9713dd05b31b 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2593,6 +2593,9 @@ pub struct Nursery { #[doc = "Disallow the use of console."] #[serde(skip_serializing_if = "Option::is_none")] pub no_console: Option>, + #[doc = "Disallow the use of Math.min and Math.max to clamp a value where the result itself is constant."] + #[serde(skip_serializing_if = "Option::is_none")] + pub no_constant_math_min_max_clamp: Option>, #[doc = "Disallow using a callback in asynchronous tests and hooks."] #[serde(skip_serializing_if = "Option::is_none")] pub no_done_callback: Option>, @@ -2676,10 +2679,11 @@ impl DeserializableValidator for Nursery { } impl Nursery { const GROUP_NAME: &'static str = "nursery"; - pub(crate) const GROUP_RULES: [&'static str; 25] = [ + pub(crate) const GROUP_RULES: [&'static str; 26] = [ "noBarrelFile", "noColorInvalidHex", "noConsole", + "noConstantMathMinMaxClamp", "noDoneCallback", "noDuplicateElseIf", "noDuplicateFontNames", @@ -2717,7 +2721,6 @@ impl Nursery { "noUselessTernary", ]; const RECOMMENDED_RULES_AS_FILTERS: [RuleFilter<'static>; 11] = [ - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6]), @@ -2726,10 +2729,11 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), ]; - const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 25] = [ + const ALL_RULES_AS_FILTERS: [RuleFilter<'static>; 26] = [ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[1]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2]), @@ -2755,6 +2759,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2786,116 +2791,121 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_done_callback.as_ref() { + if let Some(rule) = self.no_constant_math_min_max_clamp.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_done_callback.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicate_font_names.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_duplicate_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_test_hooks.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_evolving_any.as_ref() { + if let Some(rule) = self.no_duplicate_test_hooks.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_excessive_nested_test_suites.as_ref() { + if let Some(rule) = self.no_evolving_any.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_exports_in_test.as_ref() { + if let Some(rule) = self.no_excessive_nested_test_suites.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_focused_tests.as_ref() { + if let Some(rule) = self.no_exports_in_test.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_namespace_import.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_namespace_import.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_re_export_all.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_re_export_all.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_skipped_tests.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_node_assert_strict.as_ref() { + if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_node_assert_strict.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } + if let Some(rule) = self.use_sorted_classes.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -2915,116 +2925,121 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[2])); } } - if let Some(rule) = self.no_done_callback.as_ref() { + if let Some(rule) = self.no_constant_math_min_max_clamp.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[3])); } } - if let Some(rule) = self.no_duplicate_else_if.as_ref() { + if let Some(rule) = self.no_done_callback.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[4])); } } - if let Some(rule) = self.no_duplicate_font_names.as_ref() { + if let Some(rule) = self.no_duplicate_else_if.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[5])); } } - if let Some(rule) = self.no_duplicate_json_keys.as_ref() { + if let Some(rule) = self.no_duplicate_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[6])); } } - if let Some(rule) = self.no_duplicate_test_hooks.as_ref() { + if let Some(rule) = self.no_duplicate_json_keys.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[7])); } } - if let Some(rule) = self.no_evolving_any.as_ref() { + if let Some(rule) = self.no_duplicate_test_hooks.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[8])); } } - if let Some(rule) = self.no_excessive_nested_test_suites.as_ref() { + if let Some(rule) = self.no_evolving_any.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[9])); } } - if let Some(rule) = self.no_exports_in_test.as_ref() { + if let Some(rule) = self.no_excessive_nested_test_suites.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[10])); } } - if let Some(rule) = self.no_focused_tests.as_ref() { + if let Some(rule) = self.no_exports_in_test.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[11])); } } - if let Some(rule) = self.no_misplaced_assertion.as_ref() { + if let Some(rule) = self.no_focused_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[12])); } } - if let Some(rule) = self.no_namespace_import.as_ref() { + if let Some(rule) = self.no_misplaced_assertion.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[13])); } } - if let Some(rule) = self.no_nodejs_modules.as_ref() { + if let Some(rule) = self.no_namespace_import.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[14])); } } - if let Some(rule) = self.no_re_export_all.as_ref() { + if let Some(rule) = self.no_nodejs_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[15])); } } - if let Some(rule) = self.no_restricted_imports.as_ref() { + if let Some(rule) = self.no_re_export_all.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[16])); } } - if let Some(rule) = self.no_skipped_tests.as_ref() { + if let Some(rule) = self.no_restricted_imports.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { + if let Some(rule) = self.no_skipped_tests.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - if let Some(rule) = self.no_undeclared_dependencies.as_ref() { + if let Some(rule) = self.no_suspicious_semicolon_in_jsx.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19])); } } - if let Some(rule) = self.no_useless_ternary.as_ref() { + if let Some(rule) = self.no_undeclared_dependencies.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.no_useless_ternary.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21])); } } - if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { + if let Some(rule) = self.use_import_restrictions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[22])); } } - if let Some(rule) = self.use_node_assert_strict.as_ref() { + if let Some(rule) = self.use_jsx_key_in_iterable.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[23])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_node_assert_strict.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24])); } } + if let Some(rule) = self.use_sorted_classes.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3038,7 +3053,7 @@ impl Nursery { pub(crate) fn recommended_rules_as_filters() -> [RuleFilter<'static>; 11] { Self::RECOMMENDED_RULES_AS_FILTERS } - pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 25] { + pub(crate) fn all_rules_as_filters() -> [RuleFilter<'static>; 26] { Self::ALL_RULES_AS_FILTERS } #[doc = r" Select preset rules"] @@ -3073,6 +3088,10 @@ impl Nursery { .no_console .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "noConstantMathMinMaxClamp" => self + .no_constant_math_min_max_clamp + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "noDoneCallback" => self .no_done_callback .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 8021c4aab59b..0bcf3812e308 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -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", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index f1c673533ec1..c27632815635 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -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; @@ -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 , diff --git a/crates/biome_js_analyze/src/lint/nursery/no_constant_math_min_max_clamp.rs b/crates/biome_js_analyze/src/lint/nursery/no_constant_math_min_max_clamp.rs new file mode 100644 index 000000000000..cc2c6f1eb022 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_constant_math_min_max_clamp.rs @@ -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; + type State = (JsNumberLiteralExpression, JsNumberLiteralExpression); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> 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, state: &Self::State) -> Option { + let node = ctx.query(); + + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "This ""Math.min/Math.max"" combination leads to a constant result." + } + ).detail( + state.0.range(), + markup! { + "It always evaluates to "{state.0.text()}"." + } + ) + ) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + 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 "{state.0.text()}" with "{state.1.text()}"."} + .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 { + 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 { + 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, + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 4e4479ccc48e..bd00edea37ed 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -44,6 +44,7 @@ pub type NoConstEnum = ::Options; pub type NoConstantCondition = ::Options; +pub type NoConstantMathMinMaxClamp = < lint :: nursery :: no_constant_math_min_max_clamp :: NoConstantMathMinMaxClamp as biome_analyze :: Rule > :: Options ; pub type NoConstructorReturn = ::Options; pub type NoControlCharactersInRegex = < lint :: suspicious :: no_control_characters_in_regex :: NoControlCharactersInRegex as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js new file mode 100644 index 000000000000..da39f3945387 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js @@ -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))); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js.snap new file mode 100644 index 000000000000..21d605e306f8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.js.snap @@ -0,0 +1,349 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +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))); + +``` + +# Diagnostics +``` +invalid.js:1:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + > 1 │ Math.min(0, Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 │ + 3 │ Math.max(100, Math.min(0, x)); + + i It always evaluates to 0. + + > 1 │ Math.min(0, Math.max(100, x)); + │ ^ + 2 │ + 3 │ Math.max(100, Math.min(0, x)); + + i Unsafe fix: Swap 0 with 100. + + 1 │ - Math.min(0,·Math.max(100,·x)); + 1 │ + Math.min(100,·Math.max(0,·x)); + 2 2 │ + 3 3 │ Math.max(100, Math.min(0, x)); + + +``` + +``` +invalid.js:3:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 1 │ Math.min(0, Math.max(100, x)); + 2 │ + > 3 │ Math.max(100, Math.min(0, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 4 │ + 5 │ Math.max(100, Math.min(x, 0)); + + i It always evaluates to 100. + + 1 │ Math.min(0, Math.max(100, x)); + 2 │ + > 3 │ Math.max(100, Math.min(0, x)); + │ ^^^ + 4 │ + 5 │ Math.max(100, Math.min(x, 0)); + + i Unsafe fix: Swap 100 with 0. + + 1 1 │ Math.min(0, Math.max(100, x)); + 2 2 │ + 3 │ - Math.max(100,·Math.min(0,·x)); + 3 │ + Math.max(0,·Math.min(100,·x)); + 4 4 │ + 5 5 │ Math.max(100, Math.min(x, 0)); + + +``` + +``` +invalid.js:5:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 3 │ Math.max(100, Math.min(0, x)); + 4 │ + > 5 │ Math.max(100, Math.min(x, 0)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 6 │ + 7 │ window.Math.min(0, window.Math.max(100, x)); + + i It always evaluates to 100. + + 3 │ Math.max(100, Math.min(0, x)); + 4 │ + > 5 │ Math.max(100, Math.min(x, 0)); + │ ^^^ + 6 │ + 7 │ window.Math.min(0, window.Math.max(100, x)); + + i Unsafe fix: Swap 100 with 0. + + 3 3 │ Math.max(100, Math.min(0, x)); + 4 4 │ + 5 │ - Math.max(100,·Math.min(x,·0)); + 5 │ + Math.max(0,·Math.min(x,·100)); + 6 6 │ + 7 7 │ window.Math.min(0, window.Math.max(100, x)); + + +``` + +``` +invalid.js:7:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 5 │ Math.max(100, Math.min(x, 0)); + 6 │ + > 7 │ window.Math.min(0, window.Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ + 9 │ window.Math.min(0, Math.max(100, x)); + + i It always evaluates to 0. + + 5 │ Math.max(100, Math.min(x, 0)); + 6 │ + > 7 │ window.Math.min(0, window.Math.max(100, x)); + │ ^ + 8 │ + 9 │ window.Math.min(0, Math.max(100, x)); + + i Unsafe fix: Swap 0 with 100. + + 5 5 │ Math.max(100, Math.min(x, 0)); + 6 6 │ + 7 │ - window.Math.min(0,·window.Math.max(100,·x)); + 7 │ + window.Math.min(100,·window.Math.max(0,·x)); + 8 8 │ + 9 9 │ window.Math.min(0, Math.max(100, x)); + + +``` + +``` +invalid.js:9:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 7 │ window.Math.min(0, window.Math.max(100, x)); + 8 │ + > 9 │ window.Math.min(0, Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ + 11 │ Math.min(0, window.Math.max(100, x)); + + i It always evaluates to 0. + + 7 │ window.Math.min(0, window.Math.max(100, x)); + 8 │ + > 9 │ window.Math.min(0, Math.max(100, x)); + │ ^ + 10 │ + 11 │ Math.min(0, window.Math.max(100, x)); + + i Unsafe fix: Swap 0 with 100. + + 7 7 │ window.Math.min(0, window.Math.max(100, x)); + 8 8 │ + 9 │ - window.Math.min(0,·Math.max(100,·x)); + 9 │ + window.Math.min(100,·Math.max(0,·x)); + 10 10 │ + 11 11 │ Math.min(0, window.Math.max(100, x)); + + +``` + +``` +invalid.js:11:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 9 │ window.Math.min(0, Math.max(100, x)); + 10 │ + > 11 │ Math.min(0, window.Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 12 │ + 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + + i It always evaluates to 0. + + 9 │ window.Math.min(0, Math.max(100, x)); + 10 │ + > 11 │ Math.min(0, window.Math.max(100, x)); + │ ^ + 12 │ + 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + + i Unsafe fix: Swap 0 with 100. + + 9 9 │ window.Math.min(0, Math.max(100, x)); + 10 10 │ + 11 │ - Math.min(0,·window.Math.max(100,·x)); + 11 │ + Math.min(100,·window.Math.max(0,·x)); + 12 12 │ + 13 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + + +``` + +``` +invalid.js:13:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 11 │ Math.min(0, window.Math.max(100, x)); + 12 │ + > 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ + 15 │ globalThis.Math.min(0, Math.max(100, x)); + + i It always evaluates to 0. + + 11 │ Math.min(0, window.Math.max(100, x)); + 12 │ + > 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + │ ^ + 14 │ + 15 │ globalThis.Math.min(0, Math.max(100, x)); + + i Unsafe fix: Swap 0 with 100. + + 11 11 │ Math.min(0, window.Math.max(100, x)); + 12 12 │ + 13 │ - globalThis.Math.min(0,·globalThis.Math.max(100,·x)); + 13 │ + globalThis.Math.min(100,·globalThis.Math.max(0,·x)); + 14 14 │ + 15 15 │ globalThis.Math.min(0, Math.max(100, x)); + + +``` + +``` +invalid.js:15:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + 14 │ + > 15 │ globalThis.Math.min(0, Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ + 17 │ Math.min(0, globalThis.Math.max(100, x)); + + i It always evaluates to 0. + + 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + 14 │ + > 15 │ globalThis.Math.min(0, Math.max(100, x)); + │ ^ + 16 │ + 17 │ Math.min(0, globalThis.Math.max(100, x)); + + i Unsafe fix: Swap 0 with 100. + + 13 13 │ globalThis.Math.min(0, globalThis.Math.max(100, x)); + 14 14 │ + 15 │ - globalThis.Math.min(0,·Math.max(100,·x)); + 15 │ + globalThis.Math.min(100,·Math.max(0,·x)); + 16 16 │ + 17 17 │ Math.min(0, globalThis.Math.max(100, x)); + + +``` + +``` +invalid.js:17:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 15 │ globalThis.Math.min(0, Math.max(100, x)); + 16 │ + > 17 │ Math.min(0, globalThis.Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 │ + 19 │ foo(Math.min(0, Math.max(100, x))); + + i It always evaluates to 0. + + 15 │ globalThis.Math.min(0, Math.max(100, x)); + 16 │ + > 17 │ Math.min(0, globalThis.Math.max(100, x)); + │ ^ + 18 │ + 19 │ foo(Math.min(0, Math.max(100, x))); + + i Unsafe fix: Swap 0 with 100. + + 15 15 │ globalThis.Math.min(0, Math.max(100, x)); + 16 16 │ + 17 │ - Math.min(0,·globalThis.Math.max(100,·x)); + 17 │ + Math.min(100,·globalThis.Math.max(0,·x)); + 18 18 │ + 19 19 │ foo(Math.min(0, Math.max(100, x))); + + +``` + +``` +invalid.js:19:5 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + 17 │ Math.min(0, globalThis.Math.max(100, x)); + 18 │ + > 19 │ foo(Math.min(0, Math.max(100, x))); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 20 │ + + i It always evaluates to 0. + + 17 │ Math.min(0, globalThis.Math.max(100, x)); + 18 │ + > 19 │ foo(Math.min(0, Math.max(100, x))); + │ ^ + 20 │ + + i Unsafe fix: Swap 0 with 100. + + 17 17 │ Math.min(0, globalThis.Math.max(100, x)); + 18 18 │ + 19 │ - foo(Math.min(0,·Math.max(100,·x))); + 19 │ + foo(Math.min(100,·Math.max(0,·x))); + 20 20 │ + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.jsonc.snap b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.jsonc.snap new file mode 100644 index 000000000000..ab4f617e6bc3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/invalid.jsonc.snap @@ -0,0 +1,57 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.jsonc +--- +# Input +```cjs +Math.min(0, Math.max(100, x)); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + > 1 │ Math.min(0, Math.max(100, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i It always evaluates to 0. + + > 1 │ Math.min(0, Math.max(100, x)); + │ ^ + + i Unsafe fix: Swap 0 with 100. + + - Math.min(0,·Math.max(100,·x)); + + Math.min(100,·Math.max(0,·x)); + + +``` + +# Input +```cjs +Math.max(100, Math.min(0, x)); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/noConstantMathMinMaxClamp FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! This Math.min/Math.max combination leads to a constant result. + + > 1 │ Math.max(100, Math.min(0, x)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i It always evaluates to 100. + + > 1 │ Math.max(100, Math.min(0, x)); + │ ^^^ + + i Unsafe fix: Swap 100 with 0. + + - Math.max(100,·Math.min(0,·x)); + + Math.max(0,·Math.min(100,·x)); + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js new file mode 100644 index 000000000000..362f6d4ec155 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js @@ -0,0 +1,3 @@ +const Math = { min: () => {}, max: () => {} }; + +Math.min(0, Math.max(100, x)); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js.snap new file mode 100644 index 000000000000..8a94d08c01be --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid-shadowing.js.snap @@ -0,0 +1,11 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid-shadowing.js +--- +# Input +```jsx +const Math = { min: () => {}, max: () => {} }; + +Math.min(0, Math.max(100, x)); + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js new file mode 100644 index 000000000000..a00ad2ab06b6 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js @@ -0,0 +1,11 @@ +Math.min(100, Math.max(0, x)); + +Math.max(0, Math.min(100, x)); + +function foo(Math) { + Math.min(0, Math.max(100, x)); +} + +Math.min(0, 1, Math.max(0, x)); + +Math.min(0, Math.max(100, 110)); diff --git a/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js.snap new file mode 100644 index 000000000000..d2d69e1aaf7d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noConstantMathMinMaxClamp/valid.js.snap @@ -0,0 +1,19 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +Math.min(100, Math.max(0, x)); + +Math.max(0, Math.min(100, x)); + +function foo(Math) { + Math.min(0, Math.max(100, x)); +} + +Math.min(0, 1, Math.max(0, x)); + +Math.min(0, Math.max(100, 110)); + +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index b26b8aceae46..b62ed514e6c9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -908,6 +908,10 @@ export interface Nursery { * Disallow the use of console. */ noConsole?: RuleConfiguration_for_Null; + /** + * Disallow the use of Math.min and Math.max to clamp a value where the result itself is constant. + */ + noConstantMathMinMaxClamp?: RuleConfiguration_for_Null; /** * Disallow using a callback in asynchronous tests and hooks. */ @@ -1915,6 +1919,7 @@ export type Category = | "lint/nursery/noBarrelFile" | "lint/nursery/noColorInvalidHex" | "lint/nursery/noConsole" + | "lint/nursery/noConstantMathMinMaxClamp" | "lint/nursery/noDoneCallback" | "lint/nursery/noDuplicateElseIf" | "lint/nursery/noDuplicateJsonKeys" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index df8040691f96..668e3f630d48 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1421,6 +1421,13 @@ { "type": "null" } ] }, + "noConstantMathMinMaxClamp": { + "description": "Disallow the use of Math.min and Math.max to clamp a value where the result itself is constant.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "noDoneCallback": { "description": "Disallow using a callback in asynchronous tests and hooks.", "anyOf": [ diff --git a/website/src/components/generated/NumberOfRules.astro b/website/src/components/generated/NumberOfRules.astro index 417b1a3a416a..ca8f1fff2aa8 100644 --- a/website/src/components/generated/NumberOfRules.astro +++ b/website/src/components/generated/NumberOfRules.astro @@ -1,2 +1,2 @@ -

Biome's linter has a total of 214 rules

\ No newline at end of file +

Biome's linter has a total of 215 rules

\ No newline at end of file diff --git a/website/src/content/docs/internals/changelog.md b/website/src/content/docs/internals/changelog.md index bf1f7e29c8d9..27684c6072bd 100644 --- a/website/src/content/docs/internals/changelog.md +++ b/website/src/content/docs/internals/changelog.md @@ -279,6 +279,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)). diff --git a/website/src/content/docs/linter/rules/index.mdx b/website/src/content/docs/linter/rules/index.mdx index b74d43bd8711..b454ca80fd72 100644 --- a/website/src/content/docs/linter/rules/index.mdx +++ b/website/src/content/docs/linter/rules/index.mdx @@ -254,6 +254,7 @@ Rules that belong to this group are not subject to semantic versionWIP: This rule hasn't been implemented yet. | | | [noConsole](/linter/rules/no-console) | Disallow the use of console. | ⚠️ | +| [noConstantMathMinMaxClamp](/linter/rules/no-constant-math-min-max-clamp) | Disallow the use of Math.min and Math.max to clamp a value where the result itself is constant. | ⚠️ | | [noDoneCallback](/linter/rules/no-done-callback) | Disallow using a callback in asynchronous tests and hooks. | | | [noDuplicateElseIf](/linter/rules/no-duplicate-else-if) | Disallow duplicate conditions in if-else-if chains | | | [noDuplicateFontNames](/linter/rules/no-duplicate-font-names) | Disallow duplicate names within font families. | | diff --git a/website/src/content/docs/linter/rules/no-constant-math-min-max-clamp.md b/website/src/content/docs/linter/rules/no-constant-math-min-max-clamp.md new file mode 100644 index 000000000000..40b10c7f16ba --- /dev/null +++ b/website/src/content/docs/linter/rules/no-constant-math-min-max-clamp.md @@ -0,0 +1,84 @@ +--- +title: noConstantMathMinMaxClamp (not released) +--- + +**Diagnostic Category: `lint/nursery/noConstantMathMinMaxClamp`** + +:::danger +This rule hasn't been released yet. +::: + +:::caution +This rule is part of the [nursery](/linter/rules/#nursery) group. +::: + +Source: min_max + +Disallow the use of `Math.min` and `Math.max` to clamp a value where the result itself is constant. + +## Examples + +### Invalid + +```jsx +Math.min(0, Math.max(100, x)); +``` + +

nursery/noConstantMathMinMaxClamp.js:1:1 lint/nursery/noConstantMathMinMaxClamp  FIXABLE  ━━━━━━━━━━
+
+   This Math.min/Math.max combination leads to a constant result.
+  
+  > 1 │ Math.min(0, Math.max(100, x));
+   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   It always evaluates to 0.
+  
+  > 1 │ Math.min(0, Math.max(100, x));
+            ^
+    2 │ 
+  
+   Unsafe fix: Swap 0 with 100.
+  
+    1  - Math.min(0,·Math.max(100,·x));
+      1+ Math.min(100,·Math.max(0,·x));
+    2 2  
+  
+
+ +```jsx +Math.max(100, Math.min(0, x)); +``` + +
nursery/noConstantMathMinMaxClamp.js:1:1 lint/nursery/noConstantMathMinMaxClamp  FIXABLE  ━━━━━━━━━━
+
+   This Math.min/Math.max combination leads to a constant result.
+  
+  > 1 │ Math.max(100, Math.min(0, x));
+   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+    2 │ 
+  
+   It always evaluates to 100.
+  
+  > 1 │ Math.max(100, Math.min(0, x));
+            ^^^
+    2 │ 
+  
+   Unsafe fix: Swap 100 with 0.
+  
+    1  - Math.max(100,·Math.min(0,·x));
+      1+ Math.max(0,·Math.min(100,·x));
+    2 2  
+  
+
+ +### Valid + +```jsx +Math.min(100, Math.max(0, x)); +``` + +## Related links + +- [Disable a rule](/linter/#disable-a-lint-rule) +- [Rule options](/linter/#rule-options)