From f793b727ce3648b11519cf61c07d69588814d70b Mon Sep 17 00:00:00 2001 From: Justinas Delinda <8914032+minht11@users.noreply.github.com> Date: Mon, 27 May 2024 23:40:02 +0300 Subject: [PATCH] feat(linter): implement `useDateNow` (#2925) --- CHANGELOG.md | 1 + .../migrate/eslint_any_rule_to_biome.rs | 8 + .../biome_configuration/src/linter/rules.rs | 75 +- .../src/categories.rs | 1 + crates/biome_js_analyze/src/lint/nursery.rs | 2 + .../src/lint/nursery/use_date_now.rs | 276 +++++++ .../lint/nursery/use_explicit_length_check.rs | 45 +- crates/biome_js_analyze/src/options.rs | 1 + .../tests/specs/nursery/useDateNow/invalid.js | 42 + .../specs/nursery/useDateNow/invalid.js.snap | 739 ++++++++++++++++++ .../tests/specs/nursery/useDateNow/valid.js | 62 ++ .../specs/nursery/useDateNow/valid.js.snap | 70 ++ .../@biomejs/backend-jsonrpc/src/workspace.ts | 5 + .../@biomejs/biome/configuration_schema.json | 7 + 14 files changed, 1290 insertions(+), 44 deletions(-) create mode 100644 crates/biome_js_analyze/src/lint/nursery/use_date_now.rs create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js.snap create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js create mode 100644 crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b6f2f2f5ad..bdf64596cc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -229,6 +229,7 @@ our [guidelines for writing a good changelog entry](https://github.com/biomejs/b #### New features +- Add [nursery/useDateNow](https://biomejs.dev/linter/rules/use-date-now/). Contributed by @minht11 - Add [nursery/useErrorMessage](https://biomejs.dev/linter/rules/use_error_message/). Contributed by @minht11 - Add [nursery/useThrowOnlyError](https://biomejs.dev/linter/rules/use_throw_only_error/). Contributed by @minht11 - Add [nursery/useImportExtensions](https://biomejs.dev/linter/rules/use-import-extensions/). Contributed by @minht11 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 e271a4de5ff..7e180b58e1e 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 @@ -1414,6 +1414,14 @@ 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-date-now" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group.use_date_now.get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/prefer-node-protocol" => { let group = rules.style.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b91195962af..b993571558a 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2827,6 +2827,9 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_consistent_builtin_instantiation: Option>, + #[doc = "Use Date.now() to get the number of milliseconds since the Unix Epoch."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_date_now: Option>, #[doc = "Require the default clause in switch statements."] #[serde(skip_serializing_if = "Option::is_none")] pub use_default_switch_clause: Option>, @@ -2915,6 +2918,7 @@ impl Nursery { "useAdjacentOverloadSignatures", "useArrayLiterals", "useConsistentBuiltinInstantiation", + "useDateNow", "useDefaultSwitchClause", "useErrorMessage", "useExplicitLengthCheck", @@ -2965,9 +2969,9 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[21]), 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[33]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3013,6 +3017,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -3179,71 +3184,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.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[37])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_throw_new_error.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[40])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> FxHashSet> { @@ -3398,71 +3408,76 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[29])); } } - if let Some(rule) = self.use_default_switch_clause.as_ref() { + if let Some(rule) = self.use_date_now.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[30])); } } - if let Some(rule) = self.use_error_message.as_ref() { + if let Some(rule) = self.use_default_switch_clause.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[31])); } } - if let Some(rule) = self.use_explicit_length_check.as_ref() { + if let Some(rule) = self.use_error_message.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[32])); } } - if let Some(rule) = self.use_focusable_interactive.as_ref() { + if let Some(rule) = self.use_explicit_length_check.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[33])); } } - if let Some(rule) = self.use_generic_font_names.as_ref() { + if let Some(rule) = self.use_focusable_interactive.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[34])); } } - if let Some(rule) = self.use_import_extensions.as_ref() { + if let Some(rule) = self.use_generic_font_names.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[35])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_import_extensions.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[36])); } } - if let Some(rule) = self.use_number_to_fixed_digits_argument.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[37])); } } - if let Some(rule) = self.use_semantic_elements.as_ref() { + if let Some(rule) = self.use_number_to_fixed_digits_argument.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[38])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_semantic_elements.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[39])); } } - if let Some(rule) = self.use_throw_new_error.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[40])); } } - if let Some(rule) = self.use_throw_only_error.as_ref() { + if let Some(rule) = self.use_throw_new_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[41])); } } - if let Some(rule) = self.use_top_level_regex.as_ref() { + if let Some(rule) = self.use_throw_only_error.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[42])); } } + if let Some(rule) = self.use_top_level_regex.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[43])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3619,6 +3634,10 @@ impl Nursery { .use_consistent_builtin_instantiation .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useDateNow" => self + .use_date_now + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useDefaultSwitchClause" => self .use_default_switch_clause .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index d2b65a69d4a..9aca2a8f794 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -144,6 +144,7 @@ define_categories! { "lint/nursery/useArrayLiterals": "https://biomejs.dev/linter/rules/use-array-literals", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", "lint/nursery/useConsistentBuiltinInstantiation": "https://biomejs.dev/linter/rules/use-consistent-new-builtin", + "lint/nursery/useDateNow": "https://biomejs.dev/linter/rules/use-date-now", "lint/nursery/useDefaultSwitchClause": "https://biomejs.dev/linter/rules/use-default-switch-clause", "lint/nursery/useErrorMessage": "https://biomejs.dev/linter/rules/use-error-message", "lint/nursery/useExplicitLengthCheck": "https://biomejs.dev/linter/rules/use-explicit-length-check", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 1b4d3475a3f..09ed1bb1228 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -19,6 +19,7 @@ pub mod no_yoda_expression; pub mod use_adjacent_overload_signatures; pub mod use_array_literals; pub mod use_consistent_builtin_instantiation; +pub mod use_date_now; pub mod use_default_switch_clause; pub mod use_error_message; pub mod use_explicit_length_check; @@ -53,6 +54,7 @@ declare_group! { self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_array_literals :: UseArrayLiterals , self :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation , + self :: use_date_now :: UseDateNow , self :: use_default_switch_clause :: UseDefaultSwitchClause , self :: use_error_message :: UseErrorMessage , self :: use_explicit_length_check :: UseExplicitLengthCheck , diff --git a/crates/biome_js_analyze/src/lint/nursery/use_date_now.rs b/crates/biome_js_analyze/src/lint/nursery/use_date_now.rs new file mode 100644 index 00000000000..469ec8c417e --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_date_now.rs @@ -0,0 +1,276 @@ +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, Ast, FixKind, Rule, RuleDiagnostic, + RuleSource, +}; +use biome_console::markup; +use biome_js_factory::make; +use biome_js_syntax::{ + AnyJsExpression, JsAssignmentOperator, JsBinaryOperator, JsCallExpression, JsNewExpression, + JsNewOrCallExpression, JsSyntaxKind, JsSyntaxNode, JsUnaryOperator, T, +}; +use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt}; + +use crate::JsRuleAction; + +use super::use_explicit_length_check::does_node_needs_space_before_child; + +declare_rule! { + /// Use `Date.now()` to get the number of milliseconds since the Unix Epoch. + /// + /// `Date.now()` is more readable than `new Date().getTime()` and its variants, + /// it also avoids unnecessary instantiation of `Date` object. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// const foo = new Date().getTime(); + /// ``` + /// ```js,expect_diagnostic + /// const foo = new Date().valueOf(); + /// ``` + /// ```js,expect_diagnostic + /// const foo = +new Date; + /// ``` + /// ```js,expect_diagnostic + /// const foo = Number(new Date()); + /// ``` + /// ```js,expect_diagnostic + /// const foo = new Date() * 2; + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const foo = Date.now(); + /// ``` + /// ```js + /// const foo = Date.now() * 2; + /// ``` + /// + pub UseDateNow { + version: "next", + name: "useDateNow", + language: "js", + sources: &[RuleSource::EslintUnicorn("prefer-date-now")], + recommended: false, + fix_kind: FixKind::Unsafe, + } +} + +impl Rule for UseDateNow { + type Query = Ast; + type State = (AnyJsExpression, UseDateNowIssueKind); + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let expr = ctx.query(); + + match expr { + JsNewOrCallExpression::JsCallExpression(call_expr) => get_date_method_issue(call_expr), + JsNewOrCallExpression::JsNewExpression(expr) => get_new_date_issue(expr), + } + } + + fn diagnostic(_: &RuleContext, (node, kind): &Self::State) -> Option { + let message = match kind { + UseDateNowIssueKind::ReplaceMethod(method) => format!("new Date().{}", method), + UseDateNowIssueKind::ReplaceConstructor => "new Date()".to_string(), + UseDateNowIssueKind::ReplaceNumberConstructor => "Number(new Date())".to_string(), + }; + + Some(RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Use ""Date.now()"" instead of "{message}"." + }, + ).note( + markup! { + "Date.now()"" is more readable and also avoids unnecessary instantiation of ""Date""object." + } + .to_owned(), + )) + } + + fn action(ctx: &RuleContext, (node, _): &Self::State) -> Option { + let mut mutation = ctx.root().begin(); + + let args = make::js_call_arguments( + make::token(T!['(']), + make::js_call_argument_list([], []), + make::token(T![')']), + ); + + let mut date_now_expr = make::js_static_member_expression( + make::js_identifier_expression(make::js_reference_identifier(make::ident("Date"))) + .into(), + make::token(T![.]), + make::js_name(make::ident("now")).into(), + ); + + if does_node_needs_space_before_child(&node.syntax().parent()?) { + // Make fake token to get leading trivia + let leading_trivia = make::token_decorated_with_space(T![=]) + .leading_trivia() + .pieces(); + + // In case `await +a` this produces double space `await +a`. + // TODO. Find a way to avoid this. + date_now_expr = date_now_expr + .trim_leading_trivia()? + .prepend_trivia_pieces(leading_trivia)?; + } + + let new_call_expr = make::js_call_expression(date_now_expr.into(), args).build(); + + mutation.replace_node_discard_trivia::(node.clone(), new_call_expr.into()); + + Some(JsRuleAction::new( + ActionCategory::QuickFix, + ctx.metadata().applicability(), + markup! { + "Replace with ""Date.now()""." + } + .to_owned(), + mutation, + )) + } +} + +pub enum UseDateNowIssueKind { + ReplaceMethod(String), + ReplaceConstructor, + ReplaceNumberConstructor, +} + +fn get_date_method_issue( + call_expr: &JsCallExpression, +) -> Option<(AnyJsExpression, UseDateNowIssueKind)> { + let callee = call_expr.callee().ok()?.omit_parentheses(); + + let member_name = callee.get_callee_member_name()?.token_text_trimmed(); + if member_name != "getTime" && member_name != "valueOf" + || call_expr.is_optional() + || call_expr.arguments().ok()?.args().len() > 0 + { + return None; + } + + let object = callee + .as_js_static_member_expression()? + .object() + .ok()? + .omit_parentheses(); + let object_name = object + .as_js_new_expression()? + .callee() + .ok()? + .get_callee_object_name()? + .token_text_trimmed(); + + if object_name != "Date" { + return None; + } + + Some(( + AnyJsExpression::cast_ref(call_expr.syntax())?, + UseDateNowIssueKind::ReplaceMethod(member_name.to_string()), + )) +} + +fn get_new_date_issue(expr: &JsNewExpression) -> Option<(AnyJsExpression, UseDateNowIssueKind)> { + let callee = expr.callee().ok()?.omit_parentheses(); + + if callee.get_callee_member_name()?.token_text_trimmed() != "Date" + || expr.arguments()?.args().len() > 0 + { + return None; + } + + let parent = &get_parent_without_parenthesis(expr.syntax())?; + + match parent { + AnyJsExpression::JsBinaryExpression(binary_expr) => { + let operator = binary_expr.operator().ok()?; + + if matches!(operator, |JsBinaryOperator::Minus| JsBinaryOperator::Times + | JsBinaryOperator::Divide + | JsBinaryOperator::Remainder + | JsBinaryOperator::Exponent) + { + let any_expr = AnyJsExpression::cast_ref(expr.syntax())?; + + return Some((any_expr, UseDateNowIssueKind::ReplaceConstructor)); + } + + None + } + AnyJsExpression::JsAssignmentExpression(expr) => { + let token = expr.operator().ok()?; + + if matches!( + token, + JsAssignmentOperator::SubtractAssign + | JsAssignmentOperator::TimesAssign + | JsAssignmentOperator::SlashAssign + | JsAssignmentOperator::RemainderAssign + | JsAssignmentOperator::ExponentAssign + ) { + let any_expr = AnyJsExpression::cast_ref(expr.right().ok()?.syntax())?; + + return Some((any_expr, UseDateNowIssueKind::ReplaceConstructor)); + } + + None + } + AnyJsExpression::JsUnaryExpression(unary_expr) => { + let operator = unary_expr.operator().ok()?; + + let syntax = match operator { + JsUnaryOperator::Plus => unary_expr.syntax(), + JsUnaryOperator::Minus => expr.syntax(), + _ => return None, + }; + + Some(( + AnyJsExpression::cast_ref(syntax)?, + UseDateNowIssueKind::ReplaceConstructor, + )) + } + AnyJsExpression::JsCallExpression(call_expr) => { + if call_expr.is_optional() || call_expr.arguments().ok()?.args().len() != 1 { + return None; + } + + let call_name = call_expr + .callee() + .ok()? + .omit_parentheses() + .get_callee_member_name()? + .token_text_trimmed(); + + if call_name == "Number" || call_name == "BigInt" { + return Some(( + AnyJsExpression::cast_ref(call_expr.syntax())?, + UseDateNowIssueKind::ReplaceNumberConstructor, + )); + } + + None + } + _ => None, + } +} + +fn get_parent_without_parenthesis(node: &JsSyntaxNode) -> Option { + node.ancestors() + .skip(1) + .find(|ancestor| { + ancestor.kind() != JsSyntaxKind::JS_PARENTHESIZED_EXPRESSION + && AnyJsExpression::can_cast(ancestor.kind()) + }) + .and_then(AnyJsExpression::cast) +} diff --git a/crates/biome_js_analyze/src/lint/nursery/use_explicit_length_check.rs b/crates/biome_js_analyze/src/lint/nursery/use_explicit_length_check.rs index 7774729ffbd..a2a40e0aac1 100644 --- a/crates/biome_js_analyze/src/lint/nursery/use_explicit_length_check.rs +++ b/crates/biome_js_analyze/src/lint/nursery/use_explicit_length_check.rs @@ -264,22 +264,7 @@ impl Rule for UseExplicitLengthCheck { let parent = state.node.syntax().parent()?; // In cases like `export default!foo.length` -> `export default foo.length === 0` // we need to add a space between keyword and expression - if matches!( - parent.kind(), - JsSyntaxKind::JS_EXPORT_DEFAULT_EXPRESSION_CLAUSE - | JsSyntaxKind::JS_INSTANCEOF_EXPRESSION - | JsSyntaxKind::JS_YIELD_EXPRESSION - | JsSyntaxKind::JS_RETURN_STATEMENT - | JsSyntaxKind::JS_THROW_STATEMENT - | JsSyntaxKind::JS_NEW_EXPRESSION - | JsSyntaxKind::JS_AWAIT_EXPRESSION - | JsSyntaxKind::JS_IN_EXPRESSION - | JsSyntaxKind::JS_FOR_OF_STATEMENT - | JsSyntaxKind::JS_FOR_IN_STATEMENT - | JsSyntaxKind::JS_DO_WHILE_STATEMENT - | JsSyntaxKind::JS_CASE_CLAUSE - ) || does_unary_expr_needs_space(&parent) - { + if does_node_needs_space_before_child(&parent) { // Make fake token to get leading trivia let leading_trivia = make::token_decorated_with_space(T![=]) .leading_trivia() @@ -470,3 +455,31 @@ fn does_unary_expr_needs_space(node: &JsSyntaxNode) -> bool { ) }) } + +/// Checks if node needs space in case inserted child +/// would produce syntax error without it. +/// ## Example +/// ```js +/// export default!foo.length +/// ``` +/// removing slash would produce syntax error without a space +/// ```js +/// export default foo.length +/// ``` +pub(crate) fn does_node_needs_space_before_child(node: &JsSyntaxNode) -> bool { + matches!( + node.kind(), + JsSyntaxKind::JS_EXPORT_DEFAULT_EXPRESSION_CLAUSE + | JsSyntaxKind::JS_INSTANCEOF_EXPRESSION + | JsSyntaxKind::JS_YIELD_EXPRESSION + | JsSyntaxKind::JS_RETURN_STATEMENT + | JsSyntaxKind::JS_THROW_STATEMENT + | JsSyntaxKind::JS_NEW_EXPRESSION + | JsSyntaxKind::JS_AWAIT_EXPRESSION + | JsSyntaxKind::JS_IN_EXPRESSION + | JsSyntaxKind::JS_FOR_OF_STATEMENT + | JsSyntaxKind::JS_FOR_IN_STATEMENT + | JsSyntaxKind::JS_DO_WHILE_STATEMENT + | JsSyntaxKind::JS_CASE_CLAUSE + ) || does_unary_expr_needs_space(node) +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 2d6261f42a6..01b6af2f26a 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -266,6 +266,7 @@ pub type UseCollapsedElseIf = pub type UseConsistentArrayType = < lint :: style :: use_consistent_array_type :: UseConsistentArrayType as biome_analyze :: Rule > :: Options ; pub type UseConsistentBuiltinInstantiation = < lint :: nursery :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation as biome_analyze :: Rule > :: Options ; pub type UseConst = ::Options; +pub type UseDateNow = ::Options; pub type UseDefaultParameterLast = < lint :: style :: use_default_parameter_last :: UseDefaultParameterLast as biome_analyze :: Rule > :: Options ; pub type UseDefaultSwitchClause = < lint :: nursery :: use_default_switch_clause :: UseDefaultSwitchClause as biome_analyze :: Rule > :: Options ; pub type UseDefaultSwitchClauseLast = < lint :: suspicious :: use_default_switch_clause_last :: UseDefaultSwitchClauseLast as biome_analyze :: Rule > :: Options ; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js new file mode 100644 index 00000000000..1c96d8d593a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js @@ -0,0 +1,42 @@ +const ts = new Date().getTime(); +const ts1 = (new Date()).getTime(); +const ts2 = (new Date().getTime()); +const ts3 = new Date().valueOf(); +const ts4 = (new Date()).valueOf(); +const ts5 = (new Date().valueOf()); + +// `Number()` and `BigInt()` +const ts6 = /* 1 */ Number(/* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */); /* 6 */ +const tsBigInt = /* 1 */ BigInt( + /* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */ +); /* 6 */ + +// `BinaryExpression` +const ts10 = new Date() - 0; +const bar = bar - new Date(); +const bar1 = new Date() * bar; +const ts11 = new Date() / 1; +const ts12 = new Date() % Infinity; +const ts13 = new Date() ** 1; +const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + +// `AssignmentExpression` +foo -= new Date(); +foo *= new Date(); +foo /= new Date(); +foo %= new Date(); +foo **= (new Date()); + + +`UnaryExpression` +const ts7 = +(/* 1 */ new Date()); +const ts8 = -(/* 1 */ new Date()); + +function foo() { + return +new Date(); +} +function foo() { + return -new Date(); +} +await +new Date(); +typeof+new Date(); \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js.snap new file mode 100644 index 00000000000..52442938815 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/invalid.js.snap @@ -0,0 +1,739 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +const ts = new Date().getTime(); +const ts1 = (new Date()).getTime(); +const ts2 = (new Date().getTime()); +const ts3 = new Date().valueOf(); +const ts4 = (new Date()).valueOf(); +const ts5 = (new Date().valueOf()); + +// `Number()` and `BigInt()` +const ts6 = /* 1 */ Number(/* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */); /* 6 */ +const tsBigInt = /* 1 */ BigInt( + /* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */ +); /* 6 */ + +// `BinaryExpression` +const ts10 = new Date() - 0; +const bar = bar - new Date(); +const bar1 = new Date() * bar; +const ts11 = new Date() / 1; +const ts12 = new Date() % Infinity; +const ts13 = new Date() ** 1; +const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + +// `AssignmentExpression` +foo -= new Date(); +foo *= new Date(); +foo /= new Date(); +foo %= new Date(); +foo **= (new Date()); + + +`UnaryExpression` +const ts7 = +(/* 1 */ new Date()); +const ts8 = -(/* 1 */ new Date()); + +function foo() { + return +new Date(); +} +function foo() { + return -new Date(); +} +await +new Date(); +typeof+new Date(); +``` + +# Diagnostics +``` +invalid.js:1:12 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().getTime. + + > 1 │ const ts = new Date().getTime(); + │ ^^^^^^^^^^^^^^^^^^^^ + 2 │ const ts1 = (new Date()).getTime(); + 3 │ const ts2 = (new Date().getTime()); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 1 │ - const·ts·=·new·Date().getTime(); + 1 │ + const·ts·=·Date.now(); + 2 2 │ const ts1 = (new Date()).getTime(); + 3 3 │ const ts2 = (new Date().getTime()); + + +``` + +``` +invalid.js:2:13 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().getTime. + + 1 │ const ts = new Date().getTime(); + > 2 │ const ts1 = (new Date()).getTime(); + │ ^^^^^^^^^^^^^^^^^^^^^^ + 3 │ const ts2 = (new Date().getTime()); + 4 │ const ts3 = new Date().valueOf(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 1 1 │ const ts = new Date().getTime(); + 2 │ - const·ts1·=·(new·Date()).getTime(); + 2 │ + const·ts1·=·Date.now(); + 3 3 │ const ts2 = (new Date().getTime()); + 4 4 │ const ts3 = new Date().valueOf(); + + +``` + +``` +invalid.js:3:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().getTime. + + 1 │ const ts = new Date().getTime(); + 2 │ const ts1 = (new Date()).getTime(); + > 3 │ const ts2 = (new Date().getTime()); + │ ^^^^^^^^^^^^^^^^^^^^ + 4 │ const ts3 = new Date().valueOf(); + 5 │ const ts4 = (new Date()).valueOf(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 1 1 │ const ts = new Date().getTime(); + 2 2 │ const ts1 = (new Date()).getTime(); + 3 │ - const·ts2·=·(new·Date().getTime()); + 3 │ + const·ts2·=·(Date.now()); + 4 4 │ const ts3 = new Date().valueOf(); + 5 5 │ const ts4 = (new Date()).valueOf(); + + +``` + +``` +invalid.js:4:13 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().valueOf. + + 2 │ const ts1 = (new Date()).getTime(); + 3 │ const ts2 = (new Date().getTime()); + > 4 │ const ts3 = new Date().valueOf(); + │ ^^^^^^^^^^^^^^^^^^^^ + 5 │ const ts4 = (new Date()).valueOf(); + 6 │ const ts5 = (new Date().valueOf()); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 2 2 │ const ts1 = (new Date()).getTime(); + 3 3 │ const ts2 = (new Date().getTime()); + 4 │ - const·ts3·=·new·Date().valueOf(); + 4 │ + const·ts3·=·Date.now(); + 5 5 │ const ts4 = (new Date()).valueOf(); + 6 6 │ const ts5 = (new Date().valueOf()); + + +``` + +``` +invalid.js:5:13 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().valueOf. + + 3 │ const ts2 = (new Date().getTime()); + 4 │ const ts3 = new Date().valueOf(); + > 5 │ const ts4 = (new Date()).valueOf(); + │ ^^^^^^^^^^^^^^^^^^^^^^ + 6 │ const ts5 = (new Date().valueOf()); + 7 │ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 3 3 │ const ts2 = (new Date().getTime()); + 4 4 │ const ts3 = new Date().valueOf(); + 5 │ - const·ts4·=·(new·Date()).valueOf(); + 5 │ + const·ts4·=·Date.now(); + 6 6 │ const ts5 = (new Date().valueOf()); + 7 7 │ + + +``` + +``` +invalid.js:6:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date().valueOf. + + 4 │ const ts3 = new Date().valueOf(); + 5 │ const ts4 = (new Date()).valueOf(); + > 6 │ const ts5 = (new Date().valueOf()); + │ ^^^^^^^^^^^^^^^^^^^^ + 7 │ + 8 │ // `Number()` and `BigInt()` + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 4 4 │ const ts3 = new Date().valueOf(); + 5 5 │ const ts4 = (new Date()).valueOf(); + 6 │ - const·ts5·=·(new·Date().valueOf()); + 6 │ + const·ts5·=·(Date.now()); + 7 7 │ + 8 8 │ // `Number()` and `BigInt()` + + +``` + +``` +invalid.js:9:21 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of Number(new Date()). + + 8 │ // `Number()` and `BigInt()` + > 9 │ const ts6 = /* 1 */ Number(/* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */); /* 6 */ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ const tsBigInt = /* 1 */ BigInt( + 11 │ /* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 7 7 │ + 8 8 │ // `Number()` and `BigInt()` + 9 │ - const·ts6·=·/*·1·*/·Number(/*·2·*/·new·/*·3·*/·Date(/*·4·*/)·/*·5·*/);·/*·6·*/ + 9 │ + const·ts6·=·/*·1·*/·Date.now();·/*·6·*/ + 10 10 │ const tsBigInt = /* 1 */ BigInt( + 11 11 │ /* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */ + + +``` + +``` +invalid.js:10:26 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of Number(new Date()). + + 8 │ // `Number()` and `BigInt()` + 9 │ const ts6 = /* 1 */ Number(/* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */); /* 6 */ + > 10 │ const tsBigInt = /* 1 */ BigInt( + │ ^^^^^^^ + > 11 │ /* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */ + > 12 │ ); /* 6 */ + │ ^ + 13 │ + 14 │ // `BinaryExpression` + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 8 8 │ // `Number()` and `BigInt()` + 9 9 │ const ts6 = /* 1 */ Number(/* 2 */ new /* 3 */ Date(/* 4 */) /* 5 */); /* 6 */ + 10 │ - const·tsBigInt·=·/*·1·*/·BigInt( + 11 │ - → /*·2·*/·new·/*·3·*/·Date(/*·4·*/)·/*·5·*/ + 12 │ - );·/*·6·*/ + 10 │ + const·tsBigInt·=·/*·1·*/·Date.now();·/*·6·*/ + 13 11 │ + 14 12 │ // `BinaryExpression` + + +``` + +``` +invalid.js:15:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 14 │ // `BinaryExpression` + > 15 │ const ts10 = new Date() - 0; + │ ^^^^^^^^^^ + 16 │ const bar = bar - new Date(); + 17 │ const bar1 = new Date() * bar; + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 13 13 │ + 14 14 │ // `BinaryExpression` + 15 │ - const·ts10·=·new·Date()·-·0; + 15 │ + const·ts10·=·Date.now()-·0; + 16 16 │ const bar = bar - new Date(); + 17 17 │ const bar1 = new Date() * bar; + + +``` + +``` +invalid.js:16:19 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 14 │ // `BinaryExpression` + 15 │ const ts10 = new Date() - 0; + > 16 │ const bar = bar - new Date(); + │ ^^^^^^^^^^ + 17 │ const bar1 = new Date() * bar; + 18 │ const ts11 = new Date() / 1; + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 14 14 │ // `BinaryExpression` + 15 15 │ const ts10 = new Date() - 0; + 16 │ - const·bar·=·bar·-·new·Date(); + 16 │ + const·bar·=·bar·-·Date.now(); + 17 17 │ const bar1 = new Date() * bar; + 18 18 │ const ts11 = new Date() / 1; + + +``` + +``` +invalid.js:17:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 15 │ const ts10 = new Date() - 0; + 16 │ const bar = bar - new Date(); + > 17 │ const bar1 = new Date() * bar; + │ ^^^^^^^^^^ + 18 │ const ts11 = new Date() / 1; + 19 │ const ts12 = new Date() % Infinity; + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 15 15 │ const ts10 = new Date() - 0; + 16 16 │ const bar = bar - new Date(); + 17 │ - const·bar1·=·new·Date()·*·bar; + 17 │ + const·bar1·=·Date.now()*·bar; + 18 18 │ const ts11 = new Date() / 1; + 19 19 │ const ts12 = new Date() % Infinity; + + +``` + +``` +invalid.js:18:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 16 │ const bar = bar - new Date(); + 17 │ const bar1 = new Date() * bar; + > 18 │ const ts11 = new Date() / 1; + │ ^^^^^^^^^^ + 19 │ const ts12 = new Date() % Infinity; + 20 │ const ts13 = new Date() ** 1; + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 16 16 │ const bar = bar - new Date(); + 17 17 │ const bar1 = new Date() * bar; + 18 │ - const·ts11·=·new·Date()·/·1; + 18 │ + const·ts11·=·Date.now()/·1; + 19 19 │ const ts12 = new Date() % Infinity; + 20 20 │ const ts13 = new Date() ** 1; + + +``` + +``` +invalid.js:19:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 17 │ const bar1 = new Date() * bar; + 18 │ const ts11 = new Date() / 1; + > 19 │ const ts12 = new Date() % Infinity; + │ ^^^^^^^^^^ + 20 │ const ts13 = new Date() ** 1; + 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 17 17 │ const bar1 = new Date() * bar; + 18 18 │ const ts11 = new Date() / 1; + 19 │ - const·ts12·=·new·Date()·%·Infinity; + 19 │ + const·ts12·=·Date.now()%·Infinity; + 20 20 │ const ts13 = new Date() ** 1; + 21 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + + +``` + +``` +invalid.js:20:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 18 │ const ts11 = new Date() / 1; + 19 │ const ts12 = new Date() % Infinity; + > 20 │ const ts13 = new Date() ** 1; + │ ^^^^^^^^^^ + 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + 22 │ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 18 18 │ const ts11 = new Date() / 1; + 19 19 │ const ts12 = new Date() % Infinity; + 20 │ - const·ts13·=·new·Date()·**·1; + 20 │ + const·ts13·=·Date.now()**·1; + 21 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + 22 22 │ + + +``` + +``` +invalid.js:21:14 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 19 │ const ts12 = new Date() % Infinity; + 20 │ const ts13 = new Date() ** 1; + > 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + │ ^^^^^^^^^^^^^^^^^ + 22 │ + 23 │ // `AssignmentExpression` + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 19 19 │ const ts12 = new Date() % Infinity; + 20 20 │ const ts13 = new Date() ** 1; + 21 │ - const·zero·=·new·Date(/*·1·*/)·/*·2·*/·/*·3·*/·-·/*·4·*/·new·Date(); + 21 │ + const·zero·=·Date.now()-·/*·4·*/·new·Date(); + 22 22 │ + 23 23 │ // `AssignmentExpression` + + +``` + +``` +invalid.js:21:58 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 19 │ const ts12 = new Date() % Infinity; + 20 │ const ts13 = new Date() ** 1; + > 21 │ const zero = new Date(/* 1 */) /* 2 */ /* 3 */ - /* 4 */ new Date(); + │ ^^^^^^^^^^ + 22 │ + 23 │ // `AssignmentExpression` + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 19 19 │ const ts12 = new Date() % Infinity; + 20 20 │ const ts13 = new Date() ** 1; + 21 │ - const·zero·=·new·Date(/*·1·*/)·/*·2·*/·/*·3·*/·-·/*·4·*/·new·Date(); + 21 │ + const·zero·=·new·Date(/*·1·*/)·/*·2·*/·/*·3·*/·-·/*·4·*/·Date.now(); + 22 22 │ + 23 23 │ // `AssignmentExpression` + + +``` + +``` +invalid.js:24:8 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 23 │ // `AssignmentExpression` + > 24 │ foo -= new Date(); + │ ^^^^^^^^^^ + 25 │ foo *= new Date(); + 26 │ foo /= new Date(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 22 22 │ + 23 23 │ // `AssignmentExpression` + 24 │ - foo·-=·new·Date(); + 24 │ + foo·-=·Date.now(); + 25 25 │ foo *= new Date(); + 26 26 │ foo /= new Date(); + + +``` + +``` +invalid.js:25:8 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 23 │ // `AssignmentExpression` + 24 │ foo -= new Date(); + > 25 │ foo *= new Date(); + │ ^^^^^^^^^^ + 26 │ foo /= new Date(); + 27 │ foo %= new Date(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 23 23 │ // `AssignmentExpression` + 24 24 │ foo -= new Date(); + 25 │ - foo·*=·new·Date(); + 25 │ + foo·*=·Date.now(); + 26 26 │ foo /= new Date(); + 27 27 │ foo %= new Date(); + + +``` + +``` +invalid.js:26:8 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 24 │ foo -= new Date(); + 25 │ foo *= new Date(); + > 26 │ foo /= new Date(); + │ ^^^^^^^^^^ + 27 │ foo %= new Date(); + 28 │ foo **= (new Date()); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 24 24 │ foo -= new Date(); + 25 25 │ foo *= new Date(); + 26 │ - foo·/=·new·Date(); + 26 │ + foo·/=·Date.now(); + 27 27 │ foo %= new Date(); + 28 28 │ foo **= (new Date()); + + +``` + +``` +invalid.js:27:8 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 25 │ foo *= new Date(); + 26 │ foo /= new Date(); + > 27 │ foo %= new Date(); + │ ^^^^^^^^^^ + 28 │ foo **= (new Date()); + 29 │ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 25 25 │ foo *= new Date(); + 26 26 │ foo /= new Date(); + 27 │ - foo·%=·new·Date(); + 27 │ + foo·%=·Date.now(); + 28 28 │ foo **= (new Date()); + 29 29 │ + + +``` + +``` +invalid.js:28:9 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 26 │ foo /= new Date(); + 27 │ foo %= new Date(); + > 28 │ foo **= (new Date()); + │ ^^^^^^^^^^^^ + 29 │ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 26 26 │ foo /= new Date(); + 27 27 │ foo %= new Date(); + 28 │ - foo·**=·(new·Date()); + 28 │ + foo·**=·Date.now(); + 29 29 │ + 30 30 │ + + +``` + +``` +invalid.js:32:13 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 31 │ `UnaryExpression` + > 32 │ const ts7 = +(/* 1 */ new Date()); + │ ^^^^^^^^^^^^^^^^^^^^^ + 33 │ const ts8 = -(/* 1 */ new Date()); + 34 │ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 30 30 │ + 31 31 │ `UnaryExpression` + 32 │ - const·ts7·=·+(/*·1·*/·new·Date()); + 32 │ + const·ts7·=·Date.now(); + 33 33 │ const ts8 = -(/* 1 */ new Date()); + 34 34 │ + + +``` + +``` +invalid.js:33:23 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 31 │ `UnaryExpression` + 32 │ const ts7 = +(/* 1 */ new Date()); + > 33 │ const ts8 = -(/* 1 */ new Date()); + │ ^^^^^^^^^^ + 34 │ + 35 │ function foo() { + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 31 31 │ `UnaryExpression` + 32 32 │ const ts7 = +(/* 1 */ new Date()); + 33 │ - const·ts8·=·-(/*·1·*/·new·Date()); + 33 │ + const·ts8·=·-(/*·1·*/·Date.now()); + 34 34 │ + 35 35 │ function foo() { + + +``` + +``` +invalid.js:36:9 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 35 │ function foo() { + > 36 │ return +new Date(); + │ ^^^^^^^^^^^ + 37 │ } + 38 │ function foo() { + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 34 34 │ + 35 35 │ function foo() { + 36 │ - → return·+new·Date(); + 36 │ + → return··Date.now(); + 37 37 │ } + 38 38 │ function foo() { + + +``` + +``` +invalid.js:39:10 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 37 │ } + 38 │ function foo() { + > 39 │ return -new Date(); + │ ^^^^^^^^^^ + 40 │ } + 41 │ await +new Date(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 37 37 │ } + 38 38 │ function foo() { + 39 │ - → return·-new·Date(); + 39 │ + → return·-Date.now(); + 40 40 │ } + 41 41 │ await +new Date(); + + +``` + +``` +invalid.js:41:7 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 39 │ return -new Date(); + 40 │ } + > 41 │ await +new Date(); + │ ^^^^^^^^^^^ + 42 │ typeof+new Date(); + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 39 39 │ return -new Date(); + 40 40 │ } + 41 │ - await·+new·Date(); + 41 │ + await··Date.now(); + 42 42 │ typeof+new Date(); + + +``` + +``` +invalid.js:42:7 lint/nursery/useDateNow FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Date.now() instead of new Date(). + + 40 │ } + 41 │ await +new Date(); + > 42 │ typeof+new Date(); + │ ^^^^^^^^^^^ + + i Date.now() is more readable and also avoids unnecessary instantiation of Dateobject. + + i Unsafe fix: Replace with Date.now(). + + 40 40 │ } + 41 41 │ await +new Date(); + 42 │ - typeof+new·Date(); + 42 │ + typeof·Date.now(); + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js new file mode 100644 index 00000000000..d3f3af385a7 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js @@ -0,0 +1,62 @@ +const ts = Date.now(); +// Test `new Date()` +// Not `NewExpression` ++Date(); ++Date; +// Not `Date` ++new Moments(); +// More arguments ++new Date(0); ++new Date(...[]); + +// Test `new Date().getTime()` and `new Date().valueOf()` +// Not `CallExpression` +new Date.getTime(); +// Not `MemberExpression` +valueOf(); +// `computed` +new Date()[getTime](); +// Not `Identifier` +new Date()["valueOf"](); +// Not listed names +new Date().notListed(0); +// More arguments +new Date().getTime(0); +new Date().valueOf(...[]); + +// Test `Number(new Date())` and `BigInt(new Date())` +// Not `CallExpression` +new Number(new Date()); +// Not listed names +toNumber(new Date()); +// More/less arguments +BigInt(); +Number(new Date(), extraArgument); +BigInt([...new Date()]); + +// Test `+ new Date()` / `- new Date()` +// Not `UnaryExpression` +throw new Date(); +// Not `+/-` +typeof new Date(); + +// Test `AssignmentExpression` +// Not `AssignmentExpression` +const foo = () => { + return new Date(); +}; +// `operator` not listed +foo += new Date(); + +// Test `BinaryExpression` +// Not `BinaryExpression` +function* foo() { + yield new Date(); +} +// `operator` not listed +new Date() + new Date(); + +// We are not checking these cases +foo = new Date() | 0; +foo &= new Date(); +foo = new Date() >> 0; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js.snap new file mode 100644 index 00000000000..fd6e4a8ae4b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useDateNow/valid.js.snap @@ -0,0 +1,70 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +const ts = Date.now(); +// Test `new Date()` +// Not `NewExpression` ++Date(); ++Date; +// Not `Date` ++new Moments(); +// More arguments ++new Date(0); ++new Date(...[]); + +// Test `new Date().getTime()` and `new Date().valueOf()` +// Not `CallExpression` +new Date.getTime(); +// Not `MemberExpression` +valueOf(); +// `computed` +new Date()[getTime](); +// Not `Identifier` +new Date()["valueOf"](); +// Not listed names +new Date().notListed(0); +// More arguments +new Date().getTime(0); +new Date().valueOf(...[]); + +// Test `Number(new Date())` and `BigInt(new Date())` +// Not `CallExpression` +new Number(new Date()); +// Not listed names +toNumber(new Date()); +// More/less arguments +BigInt(); +Number(new Date(), extraArgument); +BigInt([...new Date()]); + +// Test `+ new Date()` / `- new Date()` +// Not `UnaryExpression` +throw new Date(); +// Not `+/-` +typeof new Date(); + +// Test `AssignmentExpression` +// Not `AssignmentExpression` +const foo = () => { + return new Date(); +}; +// `operator` not listed +foo += new Date(); + +// Test `BinaryExpression` +// Not `BinaryExpression` +function* foo() { + yield new Date(); +} +// `operator` not listed +new Date() + new Date(); + +// We are not checking these cases +foo = new Date() | 0; +foo &= new Date(); +foo = new Date() >> 0; + +``` diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 17e0e0e3641..c9e8f014073 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1081,6 +1081,10 @@ export interface Nursery { * Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt. */ useConsistentBuiltinInstantiation?: RuleFixConfiguration_for_Null; + /** + * Use Date.now() to get the number of milliseconds since the Unix Epoch. + */ + useDateNow?: RuleFixConfiguration_for_Null; /** * Require the default clause in switch statements. */ @@ -2313,6 +2317,7 @@ export type Category = | "lint/nursery/useArrayLiterals" | "lint/nursery/useBiomeSuppressionComment" | "lint/nursery/useConsistentBuiltinInstantiation" + | "lint/nursery/useDateNow" | "lint/nursery/useDefaultSwitchClause" | "lint/nursery/useErrorMessage" | "lint/nursery/useExplicitLengthCheck" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index f4aa712212e..2492574df20 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1825,6 +1825,13 @@ { "type": "null" } ] }, + "useDateNow": { + "description": "Use Date.now() to get the number of milliseconds since the Unix Epoch.", + "anyOf": [ + { "$ref": "#/definitions/RuleFixConfiguration_for_Null" }, + { "type": "null" } + ] + }, "useDefaultSwitchClause": { "description": "Require the default clause in switch statements.", "anyOf": [