From 3364640e1fed11496a449fa1a1b43d6ecc8d2eb4 Mon Sep 17 00:00:00 2001 From: GunseiKPaseri Date: Thu, 10 Oct 2024 06:05:03 +0900 Subject: [PATCH] feat(lint): add rule `useAtIndex` (#4120) Co-authored-by: Victorien Elvinger --- .../migrate/eslint_any_rule_to_biome.rs | 12 + .../src/analyzer/linter/rules.rs | 63 +- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../src/lint/nursery/use_at_index.rs | 717 ++++++++ crates/biome_js_analyze/src/options.rs | 1 + .../specs/nursery/useAtIndex/invalid.jsonc | 78 + .../nursery/useAtIndex/invalid.jsonc.snap | 1516 +++++++++++++++++ .../specs/nursery/useAtIndex/valid.jsonc | 91 + .../specs/nursery/useAtIndex/valid.jsonc.snap | 344 ++++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 12 files changed, 2815 insertions(+), 22 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_at_index.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc.snap diff --git a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs index e678a314f819..dc0765b17a81 100644 --- a/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs +++ b/crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs @@ -1541,6 +1541,18 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.use_flat_map.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "unicorn/prefer-at" => { + if !options.include_inspired { + results.has_inspired_rules = true; + return false; + } + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.use_at_index.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/prefer-date-now" => { let group = rules.complexity.get_or_insert_with(Default::default); let rule = group.use_date_now.get_or_insert(Default::default()); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 356c041c7b3f..2968c84822de 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3377,6 +3377,9 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_aria_props_supported_by_role: Option>, + #[doc = "Use at() instead of integer index access."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_at_index: Option>, #[doc = "Enforce declaring components only within modules that export React Components exclusively."] #[serde(skip_serializing_if = "Option::is_none")] pub use_component_export_only_modules: @@ -3463,6 +3466,7 @@ impl Nursery { "noValueAtRule", "useAdjacentOverloadSignatures", "useAriaPropsSupportedByRole", + "useAtIndex", "useComponentExportOnlyModules", "useConsistentCurlyBraces", "useConsistentMemberAccessibility", @@ -3500,9 +3504,9 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[24]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[25]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3544,6 +3548,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3705,56 +3710,61 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_explicit_function_return_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_sorted_classes.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[35])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + 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[36])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3904,56 +3914,61 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[28])); } } - if let Some(rule) = self.use_component_export_only_modules.as_ref() { + if let Some(rule) = self.use_at_index.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_consistent_curly_braces.as_ref() { + if let Some(rule) = self.use_component_export_only_modules.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { + if let Some(rule) = self.use_consistent_curly_braces.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_member_accessibility.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_explicit_function_return_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_explicit_function_return_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_sorted_classes.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[35])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + 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[36])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[37])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4106,6 +4121,10 @@ impl Nursery { .use_aria_props_supported_by_role .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useAtIndex" => self + .use_at_index + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useComponentExportOnlyModules" => self .use_component_export_only_modules .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 9ac86604047c..c748ad922cab 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -182,6 +182,7 @@ define_categories! { "lint/nursery/noValueAtRule": "https://biomejs.dev/linter/rules/no-value-at-rule", "lint/nursery/useAdjacentOverloadSignatures": "https://biomejs.dev/linter/rules/use-adjacent-overload-signatures", "lint/nursery/useAriaPropsSupportedByRole": "https://biomejs.dev/linter/rules/use-aria-props-supported-by-role", + "lint/nursery/useAtIndex": "https://biomejs.dev/linter/rules/use-at-index", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useComponentExportOnlyModules": "https://biomejs.dev/linter/rules/use-components-only-module", "lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index aa6aff21e527..9f15f5405e11 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -23,6 +23,7 @@ pub mod no_template_curly_in_string; pub mod no_useless_escape_in_regex; pub mod use_adjacent_overload_signatures; pub mod use_aria_props_supported_by_role; +pub mod use_at_index; pub mod use_component_export_only_modules; pub mod use_consistent_curly_braces; pub mod use_consistent_member_accessibility; @@ -58,6 +59,7 @@ declare_lint_group! { self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , + self :: use_at_index :: UseAtIndex , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , diff --git a/crates/biome_js_analyze/src/lint/nursery/use_at_index.rs b/crates/biome_js_analyze/src/lint/nursery/use_at_index.rs new file mode 100644 index 000000000000..f2299963c7e9 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_at_index.rs @@ -0,0 +1,717 @@ +use crate::JsRuleAction; +use biome_analyze::{ + context::RuleContext, declare_lint_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, + RuleSource, RuleSourceKind, +}; +use biome_console::{markup, MarkupBuf}; +use biome_js_factory::make::{self}; +use biome_js_syntax::{ + AnyJsCallArgument, AnyJsExpression, AnyJsLiteralExpression, JsBinaryExpression, + JsCallExpression, JsComputedMemberExpression, JsParenthesizedExpression, + JsStaticMemberExpression, JsUnaryExpression, T, +}; +use biome_rowan::{declare_node_union, AstNode, AstSeparatedList, BatchMutationExt}; + +declare_lint_rule! { + /// Use `at()` instead of integer index access. + /// + /// Accessing an element at the end of an array or a string is inconvenient because you have to subtract the length of the array or the string from the backward 1-based index of the element to access. + /// For example, to access the last element of an array or a string, you would have to write `array[array.length - 1]`. + /// A more convenient way to achieve the same thing is to use the `at()` method with a negative index. + /// To access the last element of an array or a string just write `array.at(-1)`. + /// + /// This rule enforces the usage of `at()` over index access, `chatAt()`, and `slice()[0]` when `at()` is more convenient. + /// + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// const foo = array[array.length - 1]; + /// ``` + /// + /// ```js,expect_diagnostic + /// const foo = array[array.length - 5]; + /// ``` + /// + /// ```js,expect_diagnostic + /// const foo = array.slice(-1)[0]; + /// ``` + /// + /// ```js,expect_diagnostic + /// const foo = array.slice(-1).pop(); + /// ``` + /// + /// ```js,expect_diagnostic + /// const foo = array.slice(-5).shift(); + /// ``` + /// + /// ```js,expect_diagnostic + /// const foo = string.charAt(string.length - 5); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const foo = array.at(-1); + /// ``` + /// + /// ```js + /// const foo = array.at(-5); + /// ``` + /// + /// ```js + /// const foo = array[100]; + /// ``` + /// + /// ```js + /// const foo = array.at(array.length - 1); + /// ``` + /// + /// ```js + /// array[array.length - 1] = foo; + /// ``` + pub UseAtIndex { + version: "next", + name: "useAtIndex", + language: "js", + recommended: false, + sources: &[RuleSource::EslintUnicorn("prefer-at")], + source_kind: RuleSourceKind::Inspired, + fix_kind: FixKind::Unsafe, + } +} + +/// The method to retrieve values from `.slice()` +#[derive(Clone)] +pub enum SliceExtractType { + Pop, + Shift, + ZeroMember, +} + +/// The number of arguments for `.slice()` +#[derive(Clone)] +pub enum SliceArgType { + OneArg, + TwoArg, +} + +/// Type of Code to Fix +#[derive(Clone)] +pub enum ErrorType { + Index { + is_negative: bool, + }, + StringCharAt { + is_negative: bool, + }, + Slice { + arg_type: SliceArgType, + extract_type: SliceExtractType, + }, +} + +declare_node_union! { + pub AnyJsArrayAccess = JsComputedMemberExpression | JsCallExpression +} + +pub struct UseAtIndexState { + at_number_exp: AnyJsExpression, + error_type: ErrorType, + object: AnyJsExpression, +} + +impl Rule for UseAtIndex { + type Query = Ast; + type State = UseAtIndexState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let exp = ctx.query(); + + let result: Option = match exp { + // foo[a] + AnyJsArrayAccess::JsComputedMemberExpression(exp) => { + check_computed_member_expression(exp) + } + // foo.bar() + AnyJsArrayAccess::JsCallExpression(call_exp) => check_call_expression(call_exp), + }; + result + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let node = ctx.query(); + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + state.error_type.get_error_message(), + ) + .note(markup! { + "Using "".at()"" is more convenient and is easier to read." + }), + ) + } + + fn action(ctx: &RuleContext, state: &Self::State) -> Option { + let node = ctx.query(); + let mut mutation = ctx.root().begin(); + let prev_node = match node { + AnyJsArrayAccess::JsComputedMemberExpression(node) => { + AnyJsExpression::JsComputedMemberExpression(node.clone()) + } + AnyJsArrayAccess::JsCallExpression(node) => { + AnyJsExpression::JsCallExpression(node.clone()) + } + }; + let UseAtIndexState { + at_number_exp, + error_type: _, + object, + } = state; + let object = overwrap_parentheses_expression(object)?; + + mutation.replace_node( + prev_node, + AnyJsExpression::JsCallExpression(make_at_method( + object, + at_number_exp.clone().trim_trivia()?, + )), + ); + + Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { "Use "".at()""." }.to_owned(), + mutation, + )) + } +} + +impl ErrorType { + /// Return the error message corresponding to the ErrorType. + fn get_error_message(self: &ErrorType) -> MarkupBuf { + match self { + ErrorType::Index { is_negative } | ErrorType::StringCharAt { is_negative } => { + let (method, old_method) = if *is_negative { + ( + "X.at(-Y)", + if matches!(self, ErrorType::StringCharAt { .. }) { + "X.charAt(X.length - Y)" + } else { + "X[X.length - Y]" + }, + ) + } else { + ( + "X.at(Y)", + if matches!(self, ErrorType::StringCharAt { .. }) { + "X.charAt(Y)" + } else { + "X[Y]" + }, + ) + }; + markup! { "Prefer "{method}" over "{old_method}"." }.to_owned() + } + ErrorType::Slice { + arg_type, + extract_type, + } => { + let extract_string = match extract_type { + SliceExtractType::Pop => ".pop()", + SliceExtractType::Shift => ".shift()", + SliceExtractType::ZeroMember => "[0]", + }; + let (method, old_method) = match (arg_type, extract_type) { + (SliceArgType::OneArg, SliceExtractType::Pop) => { + ("X.at(-1)", format!("X.slice(-a){}", extract_string)) + } + (SliceArgType::TwoArg, SliceExtractType::Pop) => { + ("X.at(Y - 1)", format!("X.slice(a, Y){}", extract_string)) + } + _ => ( + "X.at(Y)", + format!( + "X.slice({}){}", + if matches!(arg_type, SliceArgType::OneArg) { + "Y" + } else { + "Y, a" + }, + extract_string + ), + ), + }; + markup! { "Prefer "{method}" over "{old_method}"." }.to_owned() + } + } + } +} + +/// Check if two expressions reference the same value. +/// Only literals are allowed for members. +/// # Examples +/// ```js +/// a == a +/// a.b == a.b +/// a?.b == a.b +/// a[0] == a[0] +/// a['b'] == a['b'] +/// ``` +fn is_same_reference(left: AnyJsExpression, right: AnyJsExpression) -> Option { + // solve JsParenthesizedExpression + let left = left.omit_parentheses(); + let right = right.omit_parentheses(); + match (left, right) { + // x[0] + ( + AnyJsExpression::JsComputedMemberExpression(left), + AnyJsExpression::JsComputedMemberExpression(right), + ) => { + let AnyJsExpression::AnyJsLiteralExpression(left_member) = + left.member().ok()?.omit_parentheses() + else { + return Some(false); + }; + let AnyJsExpression::AnyJsLiteralExpression(right_member) = + right.member().ok()?.omit_parentheses() + else { + return Some(false); + }; + if left_member.text() != right_member.text() { + return Some(false); + } + is_same_reference(left.object().ok()?, right.object().ok()?) + } + // x.y + ( + AnyJsExpression::JsStaticMemberExpression(left), + AnyJsExpression::JsStaticMemberExpression(right), + ) => { + let left_member = left.member().ok()?; + let right_member = right.member().ok()?; + if left_member.text() != right_member.text() { + Some(false) + } else { + is_same_reference(left.object().ok()?, right.object().ok()?) + } + } + // x + ( + AnyJsExpression::JsIdentifierExpression(left), + AnyJsExpression::JsIdentifierExpression(right), + ) => Some(left.name().ok()?.text() == right.name().ok()?.text()), + // this + (AnyJsExpression::JsThisExpression(_), AnyJsExpression::JsThisExpression(_)) => Some(true), + _ => Some(false), + } +} + +/// When using this expression in other operations, enclose it in parentheses as needed. +fn overwrap_parentheses_expression(node: &AnyJsExpression) -> Option { + match node { + AnyJsExpression::JsArrayExpression(exp) => { + Some(AnyJsExpression::JsArrayExpression(exp.clone())) + } + AnyJsExpression::JsCallExpression(exp) => { + Some(AnyJsExpression::JsCallExpression(exp.clone())) + } + AnyJsExpression::JsComputedMemberExpression(exp) => { + Some(AnyJsExpression::JsComputedMemberExpression(exp.clone())) + } + AnyJsExpression::JsIdentifierExpression(exp) => Some( + AnyJsExpression::JsIdentifierExpression(exp.clone().trim_trivia()?), + ), + AnyJsExpression::JsParenthesizedExpression(exp) => { + Some(AnyJsExpression::JsParenthesizedExpression(exp.clone())) + } + AnyJsExpression::JsStaticMemberExpression(exp) => { + Some(AnyJsExpression::JsStaticMemberExpression(exp.clone())) + } + _ => Some(AnyJsExpression::JsParenthesizedExpression( + make::js_parenthesized_expression( + make::token(T!['(']), + node.clone(), + make::token(T![')']), + ), + )), + } +} + +/// If the node is a length method, it returns the object of interest. +fn get_length_node(node: &AnyJsExpression) -> Option { + let AnyJsExpression::JsStaticMemberExpression(node) = node else { + return None; + }; + let member_name = node.member().ok()?; + let member_name = member_name.as_js_name()?.value_token().ok()?; + if member_name.text_trimmed() != "length" { + return None; + } + node.object().ok() +} + +/// AnyJsExpressiion -> Some(i64) if the expression is an integer literal, otherwise None. +fn get_integer_from_literal(node: &AnyJsExpression) -> Option { + if let AnyJsExpression::JsUnaryExpression(unary) = node { + let token = unary.operator_token().ok()?; + if token.kind() != T![-] { + return None; + } + return get_integer_from_literal(&unary.argument().ok()?.omit_parentheses()) + .map(|num| -num); + } + let AnyJsExpression::AnyJsLiteralExpression(AnyJsLiteralExpression::JsNumberLiteralExpression( + number, + )) = node + else { + return None; + }; + let number = number.as_number()?; + if number.fract() == 0.0 { + Some(i64::try_from(number as i128).ok()?) + } else { + None + } +} + +/// Retrieve the value subtracted from the subtraction expression. +/// # Examples +/// ```js +/// a - b // => Some((a, [b])) +/// a - b - c // => Some((a, [b, c])) +/// ``` +fn split_minus_binary_expressions( + mut expression: AnyJsExpression, +) -> Option<(AnyJsExpression, Vec)> { + let mut right_list = vec![]; + + while let AnyJsExpression::JsBinaryExpression(binary) = expression { + let token = binary.operator_token().ok()?; + if token.kind() != T![-] { + return Some((AnyJsExpression::JsBinaryExpression(binary), right_list)); + } + + right_list.push(binary.right().ok()?); + expression = binary.left().ok()?; + } + Some((expression, right_list)) +} + +/// Combine the expressions in the list with the addition operator. +fn make_plus_binary_expression(list: Vec) -> Option { + list.into_iter().rev().reduce(|left, right| { + AnyJsExpression::JsBinaryExpression(make::js_binary_expression( + left, + make::token(T![+]), + right, + )) + }) +} + +/// If the node is a negative index, it returns the negative index. +/// # Examples +/// ```js +/// foo[foo.length - 0] // => None +/// foo[foo.length - 1] // => Some(-1) +/// foo[bar.length - 2] // => None +/// ``` +fn extract_negative_index_expression( + member: AnyJsExpression, + object: AnyJsExpression, +) -> Option { + let (left, right_list) = split_minus_binary_expressions(member)?; + if right_list.is_empty() { + return None; + } + + // left expression should be foo.length + let left = left.omit_parentheses(); + let length_parent = get_length_node(&left)?; + // left expression should be the same as the object + if !is_same_reference(object, length_parent)? { + return None; + } + + if right_list.len() == 1 { + // right expression should be integer + if let Some(number) = get_integer_from_literal(&right_list[0].clone().omit_parentheses()) { + if number > 0 { + Some(AnyJsExpression::JsUnaryExpression( + make::js_unary_expression(make::token(T![-]), right_list[0].clone()), + )) + } else { + None + } + } else { + Some(AnyJsExpression::JsUnaryExpression( + make::js_unary_expression( + make::token(T![-]), + overwrap_parentheses_expression(&right_list[0])?, + ), + )) + } + } else { + make_plus_binary_expression(right_list) + } +} + +/// Is the node a child node of `delete`? +fn is_within_delete_expression(node: &AnyJsExpression) -> Option { + node.syntax().parent()?.ancestors().find_map(|ancestor| { + if let Some(unary) = JsUnaryExpression::cast(ancestor.clone()) { + unary + .operator_token() + .ok() + .map(|token| token.kind() == T![delete]) + .or(Some(false)) + } else { + (!JsParenthesizedExpression::can_cast(ancestor.kind())).then_some(false) + } + }) +} + +fn make_number_literal(value: i64) -> AnyJsExpression { + AnyJsExpression::AnyJsLiteralExpression(AnyJsLiteralExpression::JsNumberLiteralExpression( + make::js_number_literal_expression(make::js_number_literal(value)), + )) +} + +/// check if the node is a slice +/// # Examples +/// ```js +/// .slice(0)[0] +/// .slice(0, 1).pop() +/// ``` +fn analyze_slice_element_access(node: &AnyJsExpression) -> Option { + if is_within_delete_expression(node).unwrap_or(false) { + return None; + } + // selector + let (selected_exp, extract_type): (AnyJsExpression, SliceExtractType) = match node { + // .pop() or .shift() + AnyJsExpression::JsCallExpression(call_exp) => { + let has_args = !call_exp.arguments().ok()?.args().is_empty(); + if has_args { + return None; + } + let member = call_exp.callee().ok()?.omit_parentheses(); + let AnyJsExpression::JsStaticMemberExpression(member) = member else { + return None; + }; + if call_exp.is_optional_chain() || member.is_optional_chain() { + return None; + } + let member_name = member.member().ok()?.as_js_name()?.value_token().ok()?; + let object = member.object().ok()?.omit_parentheses(); + match member_name.text_trimmed() { + "pop" => (object, SliceExtractType::Pop), + "shift" => (object, SliceExtractType::Shift), + _ => { + return None; + } + } + } + AnyJsExpression::JsComputedMemberExpression(member) => { + let object = member.object().ok()?.omit_parentheses(); + if member.is_optional_chain() { + return None; + } + let value = get_integer_from_literal(&member.member().ok()?.omit_parentheses())?; + // enable only x[0] + if value != 0 { + return None; + } + (object, SliceExtractType::ZeroMember) + } + _ => return None, + }; + // .slice(0,1) + let AnyJsExpression::JsCallExpression(call_exp) = selected_exp else { + return None; + }; + let AnyJsExpression::JsStaticMemberExpression(member) = call_exp.callee().ok()? else { + return None; + }; + let member_name = member.member().ok()?.as_js_name()?.value_token().ok()?; + if member_name.text_trimmed() != "slice" { + return None; + } + // arg length should be 1 or 2 + let [Some(arg0), optional_arg1, None] = + call_exp.arguments().ok()?.get_arguments_by_index([0, 1, 2]) + else { + return None; + }; + let AnyJsCallArgument::AnyJsExpression(arg0) = arg0.clone() else { + return None; + }; + let start_exp = arg0.omit_parentheses(); + let sliced_exp = member.object().ok()?; + + match (extract_type.clone(), optional_arg1) { + (SliceExtractType::ZeroMember | SliceExtractType::Shift, None) => Some(UseAtIndexState { + at_number_exp: start_exp, + error_type: ErrorType::Slice { + arg_type: SliceArgType::OneArg, + extract_type, + }, + object: sliced_exp, + }), + (SliceExtractType::Pop, None) if get_integer_from_literal(&start_exp)? < 0 => { + Some(UseAtIndexState { + at_number_exp: make_number_literal(-1), + error_type: ErrorType::Slice { + arg_type: SliceArgType::OneArg, + extract_type: SliceExtractType::Pop, + }, + object: sliced_exp, + }) + } + (SliceExtractType::ZeroMember | SliceExtractType::Shift, Some(arg1)) => { + let start_index = get_integer_from_literal(&start_exp)?; + let end_index = + get_integer_from_literal(&arg1.as_any_js_expression()?.clone().omit_parentheses())?; + (start_index * end_index >= 0 && start_index < end_index).then_some(UseAtIndexState { + at_number_exp: start_exp, + error_type: ErrorType::Slice { + arg_type: SliceArgType::TwoArg, + extract_type, + }, + object: sliced_exp, + }) + } + (SliceExtractType::Pop, Some(arg1)) => { + let start_index = get_integer_from_literal(&start_exp)?; + let end_index = + get_integer_from_literal(&arg1.as_any_js_expression()?.clone().omit_parentheses())?; + (start_index * end_index >= 0 && start_index < end_index).then_some(UseAtIndexState { + at_number_exp: make_number_literal(end_index - 1), + error_type: ErrorType::Slice { + arg_type: SliceArgType::TwoArg, + extract_type: SliceExtractType::Pop, + }, + object: sliced_exp, + }) + } + _ => None, + } +} + +fn check_binary_expression_member( + member: JsBinaryExpression, + object: AnyJsExpression, +) -> Option { + let member = AnyJsExpression::JsBinaryExpression(member); + let negative_index_exp = + extract_negative_index_expression(member, object.clone().omit_parentheses()); + let negative_index = negative_index_exp?; + + Some(UseAtIndexState { + at_number_exp: negative_index, + error_type: ErrorType::Index { is_negative: true }, + object, + }) +} + +/// check foo[foo.length - 1] +fn check_computed_member_expression(exp: &JsComputedMemberExpression) -> Option { + // check slice + if let Some(slice_err) = + analyze_slice_element_access(&AnyJsExpression::JsComputedMemberExpression(exp.clone())) + { + return Some(slice_err); + } + // invalid optional chain, mutable case + if exp.is_optional_chain() + || is_within_delete_expression(&AnyJsExpression::JsComputedMemberExpression(exp.clone())) + .unwrap_or(false) + { + return None; + } + // check member + let member = exp.member().ok()?.omit_parentheses(); + let object = exp.object().ok()?; + match member.clone() { + // foo[foo.length - 1] + AnyJsExpression::JsBinaryExpression(binary) => { + check_binary_expression_member(binary, object) + } + _ => None, + } +} + +/// check foo.charAt(foo.length - 1) +fn check_call_expression_char_at( + call_exp: &JsCallExpression, + member: &JsStaticMemberExpression, +) -> Option { + let [Some(arg0), None] = call_exp.arguments().ok()?.get_arguments_by_index([0, 1]) else { + return None; + }; + let AnyJsCallArgument::AnyJsExpression(arg0) = arg0.clone() else { + return None; + }; + let arg0 = arg0.omit_parentheses(); + let char_at_parent = member.object().ok()?.omit_parentheses(); + match arg0.clone() { + // foo.charAt(foo.length - 1) + AnyJsExpression::JsBinaryExpression(_) => { + let at_number_exp = extract_negative_index_expression(arg0, char_at_parent.clone()); + at_number_exp.map(|at_number_exp| UseAtIndexState { + at_number_exp, + error_type: ErrorType::StringCharAt { is_negative: true }, + object: char_at_parent, + }) + } + _ => None, + } +} + +/// check foo.bar() +fn check_call_expression(call_exp: &JsCallExpression) -> Option { + // check slice + if let Some(slice_err) = + analyze_slice_element_access(&AnyJsExpression::JsCallExpression(call_exp.clone())) + { + return Some(slice_err); + } + + if call_exp.is_optional_chain() { + return None; + } + + match call_exp.callee().ok()?.omit_parentheses() { + AnyJsExpression::JsStaticMemberExpression(member) => { + if member.is_optional_chain() { + return None; + } + let member_name = member.member().ok()?.as_js_name()?.value_token().ok()?; + match member_name.text_trimmed() { + "charAt" => check_call_expression_char_at(call_exp, &member), + _ => None, + } + } + _ => None, + } +} + +/// make `object.at(arg)` +fn make_at_method(object: AnyJsExpression, arg: AnyJsExpression) -> JsCallExpression { + let at_member = make::js_static_member_expression( + object, + make::token(T![.]), + make::js_name(make::ident("at")).into(), + ); + let args = make::js_call_arguments( + make::token(T!['(']), + make::js_call_argument_list([AnyJsCallArgument::AnyJsExpression(arg)], []), + make::token(T![')']), + ); + make::js_call_expression(at_member.into(), args).build() +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index ff12191a85ef..3a32afd8290d 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -289,6 +289,7 @@ pub type UseArrowFunction = ::Options; pub type UseAsConstAssertion = ::Options; +pub type UseAtIndex = ::Options; pub type UseAwait = ::Options; pub type UseBlockStatements = ::Options; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc new file mode 100644 index 000000000000..dec9b46d727d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc @@ -0,0 +1,78 @@ +[ + // ======================================================================== + // Index access + // ======================================================================== + "array[array.length - 1];", + "array[array.length -1];", + "array[array.length - /* comment */ 1];", + "array[array.length - 1.];", + "array[array.length - 0b1];", + "array[array.length - 9];", + "array[0][array[0].length - 1];", + "array[(( array.length )) - 1];", + "array[array.length - (( 1 ))];", + "array[(( array.length - 1 ))];", + "(( array ))[array.length - 1];", + "(( array[array.length - 1] ));", + "array[array.length - 1].pop().shift()[0];", + "a = array[array.length - 1]", + "const a = array[array.length - 1]", + "const {a = array[array.length - 1]} = {}", + "typeof array[array.length - 1]", + "function foo() {return arguments[arguments.length - 1]}", + "class Foo {bar; baz() {return this.bar[this.bar.length - 1]}}", + "class Foo {#bar; baz() {return this.#bar[this.#bar.length - 1]}}", + // Support some polynomials as well. + "array[array.length - unknown - 1]", + "array[array.length - (unknown + 1)]", + // ======================================================================== + // `String#charAt` + // ======================================================================== + "string.charAt(string.length - 1);", + "string.charAt(string.length - 0o11);", + "some.string.charAt(some.string.length - 1);", + "string.charAt((( string.length )) - 0xFF);", + "string.charAt(string.length - (( 1 )));", + "string.charAt((( string.length - 1 )));", + "(( string )).charAt(string.length - 1);", + "(( string.charAt ))(string.length - 1);", + "(( string.charAt(string.length - 1) ));", + "string.charAt(string.length - unknown - 1 );", + "string.charAt(string.length - (unknown + 1));", + // ======================================================================== + // `.slice(x)` + // ======================================================================== + "array.slice(0)[0]", + "array.slice(-0)[0]", + "array.slice(-1)[0]", + "array.slice(-1).pop()", + "array.slice(-1.0).shift()", + "array.slice(-9)[0]", + "array.slice(-9).pop()", + "array.slice(-1.1)[0]", + "array.slice(-0xA)[0b000]", + "array.slice(-9).shift()", + "array.slice(-1)[(( 0 ))];", + "array.slice(-(( 1 )))[0];", + "array.slice((( -1 )))[0];", + "(( array.slice(-1) ))[0];", + "(( array )).slice(-1)[0];", + "(( array.slice(-1)[0] ));", + "(( array.slice(-1) )).pop();", + "(( array.slice(-1).pop ))();", + "(( array.slice(-1).pop() ));", + "array.slice(-1)[0].pop().shift().slice(-1)", + // ======================================================================== + // `.slice(x, y)` + // ======================================================================== + "array.slice(-9, -8)[0]", + "array.slice(-9, -0o10)[0]", + "array.slice(-9, -8).pop()", + "array.slice(-9, -8).shift()", + "array.slice((( -9 )), (( -8 )), ).shift()", + "(( array.slice(-9, -8).shift ))()", + "array.slice(-0o11, -7)[0]", + "array.slice(-9, 0)[0]", + "array.slice(hoge)[0]", + "array.slice(hoge).shift()" +] diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc.snap b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc.snap new file mode 100644 index 000000000000..cd3a4ea89fd8 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/invalid.jsonc.snap @@ -0,0 +1,1516 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: invalid.jsonc +--- +# Input +```cjs +array[array.length - 1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - 1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·1]; + + array.at(-1); + + +``` + +# Input +```cjs +array[array.length -1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length -1]; + │ ^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-1]; + + array.at(-1); + + +``` + +# Input +```cjs +array[array.length - /* comment */ 1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - /* comment */ 1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·/*·comment·*/·1]; + + array.at(-1); + + +``` + +# Input +```cjs +array[array.length - 1.]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - 1.]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·1.]; + + array.at(-1.); + + +``` + +# Input +```cjs +array[array.length - 0b1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - 0b1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·0b1]; + + array.at(-0b1); + + +``` + +# Input +```cjs +array[array.length - 9]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - 9]; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·9]; + + array.at(-9); + + +``` + +# Input +```cjs +array[0][array[0].length - 1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[0][array[0].length - 1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[0][array[0].length·-·1]; + + array[0].at(-1); + + +``` + +# Input +```cjs +array[(( array.length )) - 1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[(( array.length )) - 1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[((·array.length·))·-·1]; + + array.at(-1); + + +``` + +# Input +```cjs +array[array.length - (( 1 ))]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - (( 1 ))]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·((·1·))]; + + array.at(-((·1·))); + + +``` + +# Input +```cjs +array[(( array.length - 1 ))]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[(( array.length - 1 ))]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[((·array.length·-·1·))]; + + array.at(-1); + + +``` + +# Input +```cjs +(( array ))[array.length - 1]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ (( array ))[array.length - 1]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array·))[array.length·-·1]; + + ((·array·)).at(-1); + + +``` + +# Input +```cjs +(( array[array.length - 1] )); +``` + +# Diagnostics +``` +invalid.jsonc:1:4 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ (( array[array.length - 1] )); + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array[array.length·-·1]·)); + + ((·array.at(-1)·)); + + +``` + +# Input +```cjs +array[array.length - 1].pop().shift()[0]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - 1].pop().shift()[0]; + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·1].pop().shift()[0]; + + array.at(-1).pop().shift()[0]; + + +``` + +# Input +```cjs +a = array[array.length - 1] +``` + +# Diagnostics +``` +invalid.jsonc:1:5 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ a = array[array.length - 1] + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - a·=·array[array.length·-·1] + + a·=·array.at(-1) + + +``` + +# Input +```cjs +const a = array[array.length - 1] +``` + +# Diagnostics +``` +invalid.jsonc:1:11 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ const a = array[array.length - 1] + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - const·a·=·array[array.length·-·1] + + const·a·=·array.at(-1) + + +``` + +# Input +```cjs +const {a = array[array.length - 1]} = {} +``` + +# Diagnostics +``` +invalid.jsonc:1:12 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ const {a = array[array.length - 1]} = {} + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - const·{a·=·array[array.length·-·1]}·=·{} + + const·{a·=·array.at(-1)}·=·{} + + +``` + +# Input +```cjs +typeof array[array.length - 1] +``` + +# Diagnostics +``` +invalid.jsonc:1:8 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ typeof array[array.length - 1] + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - typeof·array[array.length·-·1] + + typeof·array.at(-1) + + +``` + +# Input +```cjs +function foo() {return arguments[arguments.length - 1]} +``` + +# Diagnostics +``` +invalid.jsonc:1:24 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ function foo() {return arguments[arguments.length - 1]} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - function·foo()·{return·arguments[arguments.length·-·1]} + + function·foo()·{return·arguments.at(-1)} + + +``` + +# Input +```cjs +class Foo {bar; baz() {return this.bar[this.bar.length - 1]}} +``` + +# Diagnostics +``` +invalid.jsonc:1:31 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ class Foo {bar; baz() {return this.bar[this.bar.length - 1]}} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - class·Foo·{bar;·baz()·{return·this.bar[this.bar.length·-·1]}} + + class·Foo·{bar;·baz()·{return·this.bar.at(-1)}} + + +``` + +# Input +```cjs +class Foo {#bar; baz() {return this.#bar[this.#bar.length - 1]}} +``` + +# Diagnostics +``` +invalid.jsonc:1:32 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ class Foo {#bar; baz() {return this.#bar[this.#bar.length - 1]}} + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - class·Foo·{#bar;·baz()·{return·this.#bar[this.#bar.length·-·1]}} + + class·Foo·{#bar;·baz()·{return·this.#bar.at(-1)}} + + +``` + +# Input +```cjs +array[array.length - unknown - 1] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - unknown - 1] + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·unknown·-·1] + + array.at(unknown·+1) + + +``` + +# Input +```cjs +array[array.length - (unknown + 1)] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X[X.length - Y]. + + > 1 │ array[array.length - (unknown + 1)] + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array[array.length·-·(unknown·+·1)] + + array.at(-(unknown·+·1)) + + +``` + +# Input +```cjs +string.charAt(string.length - 1); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt(string.length - 1); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(string.length·-·1); + + string.at(-1); + + +``` + +# Input +```cjs +string.charAt(string.length - 0o11); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt(string.length - 0o11); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(string.length·-·0o11); + + string.at(-0o11); + + +``` + +# Input +```cjs +some.string.charAt(some.string.length - 1); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ some.string.charAt(some.string.length - 1); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - some.string.charAt(some.string.length·-·1); + + some.string.at(-1); + + +``` + +# Input +```cjs +string.charAt((( string.length )) - 0xFF); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt((( string.length )) - 0xFF); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(((·string.length·))·-·0xFF); + + string.at(-0xFF); + + +``` + +# Input +```cjs +string.charAt(string.length - (( 1 ))); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt(string.length - (( 1 ))); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(string.length·-·((·1·))); + + string.at(-((·1·))); + + +``` + +# Input +```cjs +string.charAt((( string.length - 1 ))); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt((( string.length - 1 ))); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(((·string.length·-·1·))); + + string.at(-1); + + +``` + +# Input +```cjs +(( string )).charAt(string.length - 1); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ (( string )).charAt(string.length - 1); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·string·)).charAt(string.length·-·1); + + string.at(-1); + + +``` + +# Input +```cjs +(( string.charAt ))(string.length - 1); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ (( string.charAt ))(string.length - 1); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·string.charAt·))(string.length·-·1); + + string.at(-1); + + +``` + +# Input +```cjs +(( string.charAt(string.length - 1) )); +``` + +# Diagnostics +``` +invalid.jsonc:1:4 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ (( string.charAt(string.length - 1) )); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·string.charAt(string.length·-·1)·)); + + ((·string.at(-1)·)); + + +``` + +# Input +```cjs +string.charAt(string.length - unknown - 1 ); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt(string.length - unknown - 1 ); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(string.length·-·unknown·-·1·); + + string.at(unknown·+1); + + +``` + +# Input +```cjs +string.charAt(string.length - (unknown + 1)); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-Y) over X.charAt(X.length - Y). + + > 1 │ string.charAt(string.length - (unknown + 1)); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - string.charAt(string.length·-·(unknown·+·1)); + + string.at(-(unknown·+·1)); + + +``` + +# Input +```cjs +array.slice(0)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(0)[0] + │ ^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(0)[0] + + array.at(0) + + +``` + +# Input +```cjs +array.slice(-0)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-0)[0] + │ ^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-0)[0] + + array.at(-0) + + +``` + +# Input +```cjs +array.slice(-1)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-1)[0] + │ ^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1)[0] + + array.at(-1) + + +``` + +# Input +```cjs +array.slice(-1).pop() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-1) over X.slice(-a).pop(). + + > 1 │ array.slice(-1).pop() + │ ^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1).pop() + + array.at(-1) + + +``` + +# Input +```cjs +array.slice(-1.0).shift() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y).shift(). + + > 1 │ array.slice(-1.0).shift() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1.0).shift() + + array.at(-1.0) + + +``` + +# Input +```cjs +array.slice(-9)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-9)[0] + │ ^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9)[0] + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-9).pop() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-1) over X.slice(-a).pop(). + + > 1 │ array.slice(-9).pop() + │ ^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9).pop() + + array.at(-1) + + +``` + +# Input +```cjs +array.slice(-1.1)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-1.1)[0] + │ ^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1.1)[0] + + array.at(-1.1) + + +``` + +# Input +```cjs +array.slice(-0xA)[0b000] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-0xA)[0b000] + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-0xA)[0b000] + + array.at(-0xA) + + +``` + +# Input +```cjs +array.slice(-9).shift() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y).shift(). + + > 1 │ array.slice(-9).shift() + │ ^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9).shift() + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-1)[(( 0 ))]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-1)[(( 0 ))]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1)[((·0·))]; + + array.at(-1); + + +``` + +# Input +```cjs +array.slice(-(( 1 )))[0]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-(( 1 )))[0]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-((·1·)))[0]; + + array.at(-((·1·))); + + +``` + +# Input +```cjs +array.slice((( -1 )))[0]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice((( -1 )))[0]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(((·-1·)))[0]; + + array.at(-1); + + +``` + +# Input +```cjs +(( array.slice(-1) ))[0]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ (( array.slice(-1) ))[0]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-1)·))[0]; + + array.at(-1); + + +``` + +# Input +```cjs +(( array )).slice(-1)[0]; +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ (( array )).slice(-1)[0]; + │ ^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array·)).slice(-1)[0]; + + ((·array·)).at(-1); + + +``` + +# Input +```cjs +(( array.slice(-1)[0] )); +``` + +# Diagnostics +``` +invalid.jsonc:1:4 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ (( array.slice(-1)[0] )); + │ ^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-1)[0]·)); + + ((·array.at(-1)·)); + + +``` + +# Input +```cjs +(( array.slice(-1) )).pop(); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-1) over X.slice(-a).pop(). + + > 1 │ (( array.slice(-1) )).pop(); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-1)·)).pop(); + + array.at(-1); + + +``` + +# Input +```cjs +(( array.slice(-1).pop ))(); +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-1) over X.slice(-a).pop(). + + > 1 │ (( array.slice(-1).pop ))(); + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-1).pop·))(); + + array.at(-1); + + +``` + +# Input +```cjs +(( array.slice(-1).pop() )); +``` + +# Diagnostics +``` +invalid.jsonc:1:4 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(-1) over X.slice(-a).pop(). + + > 1 │ (( array.slice(-1).pop() )); + │ ^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-1).pop()·)); + + ((·array.at(-1)·)); + + +``` + +# Input +```cjs +array.slice(-1)[0].pop().shift().slice(-1) +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(-1)[0].pop().shift().slice(-1) + │ ^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-1)[0].pop().shift().slice(-1) + + array.at(-1).pop().shift().slice(-1) + + +``` + +# Input +```cjs +array.slice(-9, -8)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a)[0]. + + > 1 │ array.slice(-9, -8)[0] + │ ^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9,·-8)[0] + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-9, -0o10)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a)[0]. + + > 1 │ array.slice(-9, -0o10)[0] + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9,·-0o10)[0] + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-9, -8).pop() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y - 1) over X.slice(a, Y).pop(). + + > 1 │ array.slice(-9, -8).pop() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9,·-8).pop() + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-9, -8).shift() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a).shift(). + + > 1 │ array.slice(-9, -8).shift() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9,·-8).shift() + + array.at(-9) + + +``` + +# Input +```cjs +array.slice((( -9 )), (( -8 )), ).shift() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a).shift(). + + > 1 │ array.slice((( -9 )), (( -8 )), ).shift() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(((·-9·)),·((·-8·)),·).shift() + + array.at(-9) + + +``` + +# Input +```cjs +(( array.slice(-9, -8).shift ))() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a).shift(). + + > 1 │ (( array.slice(-9, -8).shift ))() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - ((·array.slice(-9,·-8).shift·))() + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(-0o11, -7)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a)[0]. + + > 1 │ array.slice(-0o11, -7)[0] + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-0o11,·-7)[0] + + array.at(-0o11) + + +``` + +# Input +```cjs +array.slice(-9, 0)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y, a)[0]. + + > 1 │ array.slice(-9, 0)[0] + │ ^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(-9,·0)[0] + + array.at(-9) + + +``` + +# Input +```cjs +array.slice(hoge)[0] +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y)[0]. + + > 1 │ array.slice(hoge)[0] + │ ^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(hoge)[0] + + array.at(hoge) + + +``` + +# Input +```cjs +array.slice(hoge).shift() +``` + +# Diagnostics +``` +invalid.jsonc:1:1 lint/nursery/useAtIndex FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Prefer X.at(Y) over X.slice(Y).shift(). + + > 1 │ array.slice(hoge).shift() + │ ^^^^^^^^^^^^^^^^^^^^^^^^^ + + i Using .at() is more convenient and is easier to read. + + i Unsafe fix: Use .at(). + + - array.slice(hoge).shift() + + array.at(hoge) + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc new file mode 100644 index 000000000000..f1d4cc4ef24c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc @@ -0,0 +1,91 @@ +[ + // ======================================================================== + // Index access + // ======================================================================== + "array.at(-1)", + "array[array.length - 0];", + "array[array.length + 1]", + "array[array.length + -1]", + "foo[bar.length - 1]", + "array?.[array.length - 1];", + // LHS + "array[array.length - 1] = 1", + "array[array.length - 1] %= 1", + "++ array[array.length - 1]", + "array[array.length - 1] --", + "delete array[array.length - 1]", + "class Foo {bar; #bar; baz() {return this.#bar[this.bar.length - 1]}}", + "([array[array.length - 1]] = [])", + "({foo: array[array.length - 1] = 9} = {})", + // ======================================================================== + // `String#charAt` + // ======================================================================== + "string.charAt(string.length - 0);", + "string.charAt(string.length + 1)", + "string.charAt(string.length + -1)", + "foo.charAt(bar.length - 1)", + "string?.charAt?.(string.length - 1);", + "string?.charAt(string.length - 1);", + "string.charAt(9);", + "string1.charAt(string2.length - 1);", + "string.charAt(hoge.string.length - 1)", + "string.charAt(string.length - 1 + 1)", + "string.charAt(string.length + 1 - 1)", + // ======================================================================== + // `.slice(x)` + // ======================================================================== + "array.slice(-1)", + "new array.slice(-1)", + "array.slice(-1)?.[0]", + "array.slice?.(-1)[0]", + "array?.slice(-1)[0]", + "array.notSlice(-1)[0]", + "array.slice()[0]", + "array.slice(...[-1])[0]", + "array.slice(-1).shift?.()", + "array.slice(-1)?.shift()", + "array.slice(-1).shift(...[])", + "new array.slice(-1).shift()", + // LHS + "array.slice(-1)[0] += 1", + "++ array.slice(-1)[0]", + "array.slice(-1)[0] --", + "delete array.slice(-1)[0]", + // ======================================================================== + // `.slice(x, y)` + // ======================================================================== + "array.slice(-9.1, -8.1)[0]", + "array.slice(-unknown, -unknown2)[0]", + "array.slice(-9.1, unknown)[0]", + "array.slice(-9, unknown).pop()", + "array.slice(-9, ...unknown)[0]", + "array.slice(...[-9], unknown)[0]", + // Since the second argument is explicitly specified and its value can change the output if it is not a number, ignore all cases. + "array.slice(-9, unknown)[0]", + "array.slice(-9, unknown).shift()", + "const KNOWN = -8; array.slice(-9, KNOWN).shift()", + "(( (( array.slice( ((-9)), ((unknown)), ).shift ))() ));", + "array.slice(-9, (a, really, _really, complicated, second) => argument)[0]", + // ======================================================================== + // `lodash.last(array)` + // ======================================================================== + // Under the original rules, it was considered an error, but due to concerns about false positives, it is currently allowed. + "_.last(array)", + "lodash.last(array)", + "underscore.last(array)", + "_.last(new Array)", + "const foo = []; _.last([bar])", + "const foo = []; _.last(new Array)", + "const foo = []; _.last(((new Array)))", + "if (foo) _.last([bar])", + "function foo() {return _.last(arguments)}", + // valid + "new _.last(array)", + "_.last(array, 2)", + "_.last(...array)", + // not lodash + "_.last()", + "other._.last(array)", + "other.underscore.last(array)", + "other.lodash.last(array)" +] diff --git a/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc.snap b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc.snap new file mode 100644 index 000000000000..29185ee65b7d --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useAtIndex/valid.jsonc.snap @@ -0,0 +1,344 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +assertion_line: 86 +expression: valid.jsonc +--- +# Input +```cjs +array.at(-1) +``` + +# Input +```cjs +array[array.length - 0]; +``` + +# Input +```cjs +array[array.length + 1] +``` + +# Input +```cjs +array[array.length + -1] +``` + +# Input +```cjs +foo[bar.length - 1] +``` + +# Input +```cjs +array?.[array.length - 1]; +``` + +# Input +```cjs +array[array.length - 1] = 1 +``` + +# Input +```cjs +array[array.length - 1] %= 1 +``` + +# Input +```cjs +++ array[array.length - 1] +``` + +# Input +```cjs +array[array.length - 1] -- +``` + +# Input +```cjs +delete array[array.length - 1] +``` + +# Input +```cjs +class Foo {bar; #bar; baz() {return this.#bar[this.bar.length - 1]}} +``` + +# Input +```cjs +([array[array.length - 1]] = []) +``` + +# Input +```cjs +({foo: array[array.length - 1] = 9} = {}) +``` + +# Input +```cjs +string.charAt(string.length - 0); +``` + +# Input +```cjs +string.charAt(string.length + 1) +``` + +# Input +```cjs +string.charAt(string.length + -1) +``` + +# Input +```cjs +foo.charAt(bar.length - 1) +``` + +# Input +```cjs +string?.charAt?.(string.length - 1); +``` + +# Input +```cjs +string?.charAt(string.length - 1); +``` + +# Input +```cjs +string.charAt(9); +``` + +# Input +```cjs +string1.charAt(string2.length - 1); +``` + +# Input +```cjs +string.charAt(hoge.string.length - 1) +``` + +# Input +```cjs +string.charAt(string.length - 1 + 1) +``` + +# Input +```cjs +string.charAt(string.length + 1 - 1) +``` + +# Input +```cjs +array.slice(-1) +``` + +# Input +```cjs +new array.slice(-1) +``` + +# Input +```cjs +array.slice(-1)?.[0] +``` + +# Input +```cjs +array.slice?.(-1)[0] +``` + +# Input +```cjs +array?.slice(-1)[0] +``` + +# Input +```cjs +array.notSlice(-1)[0] +``` + +# Input +```cjs +array.slice()[0] +``` + +# Input +```cjs +array.slice(...[-1])[0] +``` + +# Input +```cjs +array.slice(-1).shift?.() +``` + +# Input +```cjs +array.slice(-1)?.shift() +``` + +# Input +```cjs +array.slice(-1).shift(...[]) +``` + +# Input +```cjs +new array.slice(-1).shift() +``` + +# Input +```cjs +array.slice(-1)[0] += 1 +``` + +# Input +```cjs +++ array.slice(-1)[0] +``` + +# Input +```cjs +array.slice(-1)[0] -- +``` + +# Input +```cjs +delete array.slice(-1)[0] +``` + +# Input +```cjs +array.slice(-9.1, -8.1)[0] +``` + +# Input +```cjs +array.slice(-unknown, -unknown2)[0] +``` + +# Input +```cjs +array.slice(-9.1, unknown)[0] +``` + +# Input +```cjs +array.slice(-9, unknown).pop() +``` + +# Input +```cjs +array.slice(-9, ...unknown)[0] +``` + +# Input +```cjs +array.slice(...[-9], unknown)[0] +``` + +# Input +```cjs +array.slice(-9, unknown)[0] +``` + +# Input +```cjs +array.slice(-9, unknown).shift() +``` + +# Input +```cjs +const KNOWN = -8; array.slice(-9, KNOWN).shift() +``` + +# Input +```cjs +(( (( array.slice( ((-9)), ((unknown)), ).shift ))() )); +``` + +# Input +```cjs +array.slice(-9, (a, really, _really, complicated, second) => argument)[0] +``` + +# Input +```cjs +_.last(array) +``` + +# Input +```cjs +lodash.last(array) +``` + +# Input +```cjs +underscore.last(array) +``` + +# Input +```cjs +_.last(new Array) +``` + +# Input +```cjs +const foo = []; _.last([bar]) +``` + +# Input +```cjs +const foo = []; _.last(new Array) +``` + +# Input +```cjs +const foo = []; _.last(((new Array))) +``` + +# Input +```cjs +if (foo) _.last([bar]) +``` + +# Input +```cjs +function foo() {return _.last(arguments)} +``` + +# Input +```cjs +new _.last(array) +``` + +# Input +```cjs +_.last(array, 2) +``` + +# Input +```cjs +_.last(...array) +``` + +# Input +```cjs +_.last() +``` + +# Input +```cjs +other._.last(array) +``` + +# Input +```cjs +other.underscore.last(array) +``` + +# Input +```cjs +other.lodash.last(array) +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index f9d86a4b9081..563e6984626a 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1338,6 +1338,10 @@ export interface Nursery { * Enforce that ARIA properties are valid for the roles that are supported by the element. */ useAriaPropsSupportedByRole?: RuleConfiguration_for_Null; + /** + * Use at() instead of integer index access. + */ + useAtIndex?: RuleFixConfiguration_for_Null; /** * Enforce declaring components only within modules that export React Components exclusively. */ @@ -2926,6 +2930,7 @@ export type Category = | "lint/nursery/noValueAtRule" | "lint/nursery/useAdjacentOverloadSignatures" | "lint/nursery/useAriaPropsSupportedByRole" + | "lint/nursery/useAtIndex" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useComponentExportOnlyModules" | "lint/nursery/useConsistentCurlyBraces" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 5c47f3ae950b..91f74cb8dd00 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2292,6 +2292,13 @@ { "type": "null" } ] }, + "useAtIndex": { + "description": "Use at() instead of integer index access.", + "anyOf": [ + { "$ref": "#/definitions/RuleFixConfiguration" }, + { "type": "null" } + ] + }, "useComponentExportOnlyModules": { "description": "Enforce declaring components only within modules that export React Components exclusively.", "anyOf": [