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 e33f3f9a9124..bb08d8fe0cf4 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 @@ -856,6 +856,16 @@ pub(crate) fn migrate_eslint_any_rule( let rule = group.no_new_symbol.get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "no-new-wrappers" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .use_consistent_new_builtin + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "no-nonoctal-decimal-escape" => { let group = rules.correctness.get_or_insert_with(Default::default); let rule = group @@ -1218,6 +1228,16 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule_severity.into()); } + "unicorn/new-for-builtins" => { + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .use_consistent_new_builtin + .get_or_insert(Default::default()); + rule.set_level(rule_severity.into()); + } "unicorn/no-array-for-each" => { let group = rules.complexity.get_or_insert_with(Default::default); let rule = group.no_for_each.get_or_insert(Default::default()); diff --git a/crates/biome_configuration/src/linter/rules.rs b/crates/biome_configuration/src/linter/rules.rs index b6463594cb8c..52b54d422436 100644 --- a/crates/biome_configuration/src/linter/rules.rs +++ b/crates/biome_configuration/src/linter/rules.rs @@ -2702,6 +2702,9 @@ pub struct Nursery { #[doc = "Disallow unknown CSS units."] #[serde(skip_serializing_if = "Option::is_none")] pub no_unknown_unit: Option>, + #[doc = "Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_consistent_new_builtin: Option>, #[doc = "Disallows package private imports."] #[serde(skip_serializing_if = "Option::is_none")] pub use_import_restrictions: Option>, @@ -2744,6 +2747,7 @@ impl Nursery { "noRestrictedImports", "noUndeclaredDependencies", "noUnknownUnit", + "useConsistentNewBuiltin", "useImportRestrictions", "useSortedClasses", ]; @@ -2792,6 +2796,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[19]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[20]), ]; #[doc = r" Retrieves the recommended rules"] pub(crate) fn is_recommended_true(&self) -> bool { @@ -2898,16 +2903,21 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_consistent_new_builtin.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - 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[19])); } } + 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[20])); + } + } index_set } pub(crate) fn get_disabled_rules(&self) -> IndexSet { @@ -3002,16 +3012,21 @@ impl Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[17])); } } - if let Some(rule) = self.use_import_restrictions.as_ref() { + if let Some(rule) = self.use_consistent_new_builtin.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[18])); } } - 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[19])); } } + 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[20])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -3120,6 +3135,10 @@ impl Nursery { .no_unknown_unit .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useConsistentNewBuiltin" => self + .use_consistent_new_builtin + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useImportRestrictions" => self .use_import_restrictions .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index 187dd23936e7..8633f8b7fc12 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -130,6 +130,7 @@ define_categories! { "lint/nursery/noUndeclaredDependencies": "https://biomejs.dev/linter/rules/no-undeclared-dependencies", "lint/nursery/noUnknownUnit": "https://biomejs.dev/linter/rules/no-unknown-unit", "lint/nursery/useBiomeSuppressionComment": "https://biomejs.dev/linter/rules/use-biome-suppression-comment", + "lint/nursery/useConsistentNewBuiltin": "https://biomejs.dev/linter/rules/use-consistent-new-builtin", "lint/nursery/useImportRestrictions": "https://biomejs.dev/linter/rules/use-import-restrictions", "lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes", "lint/performance/noAccumulatingSpread": "https://biomejs.dev/linter/rules/no-accumulating-spread", diff --git a/crates/biome_js_analyze/src/lint/complexity/use_regex_literals.rs b/crates/biome_js_analyze/src/lint/complexity/use_regex_literals.rs index f3e4434f804e..1bffc5c8bf8f 100644 --- a/crates/biome_js_analyze/src/lint/complexity/use_regex_literals.rs +++ b/crates/biome_js_analyze/src/lint/complexity/use_regex_literals.rs @@ -7,12 +7,10 @@ use biome_js_factory::make::js_regex_literal_expression; use biome_js_semantic::SemanticModel; use biome_js_syntax::{ global_identifier, static_value::StaticValue, AnyJsCallArgument, AnyJsExpression, - AnyJsLiteralExpression, JsCallArguments, JsCallExpression, JsComputedMemberExpression, - JsNewExpression, JsSyntaxKind, JsSyntaxToken, -}; -use biome_rowan::{ - declare_node_union, AstNode, AstSeparatedList, BatchMutationExt, SyntaxError, TokenText, + AnyJsLiteralExpression, JsCallArguments, JsComputedMemberExpression, JsNewOrCallExpression, + JsSyntaxKind, JsSyntaxToken, }; +use biome_rowan::{AstNode, AstSeparatedList, BatchMutationExt, SyntaxError, TokenText}; use crate::{services::semantic::Semantic, JsRuleAction}; @@ -54,10 +52,6 @@ declare_rule! { } } -declare_node_union! { - pub JsNewOrCallExpression = JsNewExpression | JsCallExpression -} - pub struct UseRegexLiteralsState { pattern: String, flags: Option, diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index b850281e2548..c7c98b6932e6 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -13,6 +13,7 @@ pub mod no_nodejs_modules; pub mod no_react_specific_props; pub mod no_restricted_imports; pub mod no_undeclared_dependencies; +pub mod use_consistent_new_builtin; pub mod use_import_restrictions; pub mod use_sorted_classes; @@ -31,6 +32,7 @@ declare_group! { self :: no_react_specific_props :: NoReactSpecificProps , self :: no_restricted_imports :: NoRestrictedImports , self :: no_undeclared_dependencies :: NoUndeclaredDependencies , + self :: use_consistent_new_builtin :: UseConsistentNewBuiltin , self :: use_import_restrictions :: UseImportRestrictions , self :: use_sorted_classes :: UseSortedClasses , ] diff --git a/crates/biome_js_analyze/src/lint/nursery/use_consistent_new_builtin.rs b/crates/biome_js_analyze/src/lint/nursery/use_consistent_new_builtin.rs new file mode 100644 index 000000000000..cc9579ebca3c --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_consistent_new_builtin.rs @@ -0,0 +1,294 @@ +use crate::{services::semantic::Semantic, JsRuleAction}; +use biome_analyze::{ + context::RuleContext, declare_rule, ActionCategory, FixKind, Rule, RuleDiagnostic, RuleSource, +}; +use biome_console::markup; +use biome_diagnostics::Applicability; +use biome_js_factory::make; +use biome_js_syntax::{ + global_identifier, AnyJsExpression, JsCallExpression, JsNewExpression, JsNewOrCallExpression, T, +}; +use biome_rowan::{chain_trivia_pieces, AstNode, BatchMutationExt, TriviaPieceKind}; + +declare_rule! { + /// Enforce the use of new for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. + /// + /// `new Builtin()` and `Builtin()` work the same, but new should be preferred for consistency with other constructors. + /// Enforces the use of new for following builtins: + /// + /// - Object + /// - Array + /// - ArrayBuffer + /// - BigInt64Array + /// - BigUint64Array + /// - DataView + /// - Date + /// - Error + /// - Float32Array + /// - Float64Array + /// - Function + /// - Int8Array + /// - Int16Array + /// - Int32Array + /// - Map + /// - WeakMap + /// - Set + /// - WeakSet + /// - Promise + /// - RegExp + /// - Uint8Array + /// - Uint16Array + /// - Uint32Array + /// - Uint8ClampedArray + /// - SharedArrayBuffer + /// - Proxy + /// - WeakRef + /// - FinalizationRegistry + /// + /// Disallows the use of new for following builtins: + /// + /// - String + /// - Number + /// - Boolean + /// + /// These builtins are handled by [noInvalidBuiltin](https://biomejs.dev/linter/rules/no-invalid-new-builtin/) rule: + /// + /// - Symbol + /// - BigInt + /// + /// > These should not use new as that would create object wrappers for the primitive values, which is not what you want. However, without new they can be useful for coercing a value to that type. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// const text = new String(10); + /// ``` + /// + /// ```js,expect_diagnostic + /// const now = Date(); + /// ``` + /// + /// ```js,expect_diagnostic + /// const map = Map([ + /// ['foo', 'bar'] + /// ]); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const text = String(10); + /// ``` + /// + /// ```js + /// const now = new Date(); + /// ``` + /// + /// ```js + /// const map = new Map([ + /// ['foo', 'bar'] + /// ]); + /// ``` + /// + pub UseConsistentNewBuiltin { + version: "next", + name: "useConsistentNewBuiltin", + sources: &[RuleSource::EslintUnicorn("new-for-builtins"), RuleSource::Eslint("no-new-wrappers")], + recommended: false, + fix_kind: FixKind::Unsafe, + } +} + +impl Rule for UseConsistentNewBuiltin { + type Query = Semantic; + type State = UseConsistentNewBuiltinState; + type Signals = Option; + type Options = (); + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let (callee, creation_rule) = extract_callee_and_rule(node)?; + let (reference, name) = global_identifier(&callee.omit_parentheses())?; + + let name_text = name.text(); + + if creation_rule + .forbidden_builtins_list() + .binary_search(&name_text) + .is_ok() + { + return ctx.model().binding(&reference).is_none().then_some( + UseConsistentNewBuiltinState { + name: name_text.to_string(), + creation_rule, + }, + ); + } + + None + } + + fn diagnostic(ctx: &RuleContext, state: &Self::State) -> Option { + let node = ctx.query(); + + let name = &state.name; + + let (use_this, instead_of) = match state.creation_rule { + BuiltinCreationRule::MustUseNew => ("new ", ""), + BuiltinCreationRule::MustNotUseNew => ("", "new "), + }; + + Some(RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Use "{use_this}{name}"()"" instead of "{instead_of}{name}"()""." + }, + )) + } + + fn action(ctx: &RuleContext, _: &Self::State) -> Option { + let node = ctx.query(); + + let mut mutation = ctx.root().begin(); + + match node { + JsNewOrCallExpression::JsNewExpression(node) => { + let call_expression = convert_new_expression_to_call_expression(node)?; + + mutation + .replace_node::(node.clone().into(), call_expression.into()); + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::MaybeIncorrect, + message: markup! { "Remove ""new"" keyword." }.to_owned(), + mutation, + }) + } + JsNewOrCallExpression::JsCallExpression(node) => { + let new_expression = convert_call_expression_to_new_expression(node)?; + + mutation + .replace_node::(node.clone().into(), new_expression.into()); + Some(JsRuleAction { + category: ActionCategory::QuickFix, + applicability: Applicability::MaybeIncorrect, + message: markup! { "Add ""new"" keyword." }.to_owned(), + mutation, + }) + } + } + } +} + +/// Sorted array of builtins that require new keyword. +const BUILTINS_REQUIRING_NEW: &[&str] = &[ + "Array", + "ArrayBuffer", + "BigInt64Array", + "BigUint64Array", + "DataView", + "Date", + "Error", + "FinalizationRegistry", + "Float32Array", + "Float64Array", + "Function", + "Int16Array", + "Int32Array", + "Int8Array", + "Map", + "Object", + "Promise", + "Proxy", + "RegExp", + "Set", + "SharedArrayBuffer", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "WeakMap", + "WeakRef", + "WeakSet", +]; + +/// Sorted array of builtins that should not use new keyword. +const BUILTINS_NOT_REQUIRING_NEW: &[&str] = &["Boolean", "Number", "String"]; + +enum BuiltinCreationRule { + MustUseNew, + MustNotUseNew, +} + +impl BuiltinCreationRule { + fn forbidden_builtins_list(&self) -> &[&str] { + match self { + BuiltinCreationRule::MustUseNew => BUILTINS_REQUIRING_NEW, + BuiltinCreationRule::MustNotUseNew => BUILTINS_NOT_REQUIRING_NEW, + } + } +} + +pub struct UseConsistentNewBuiltinState { + name: String, + creation_rule: BuiltinCreationRule, +} + +fn extract_callee_and_rule( + node: &JsNewOrCallExpression, +) -> Option<(AnyJsExpression, BuiltinCreationRule)> { + match node { + JsNewOrCallExpression::JsNewExpression(node) => { + let callee = node.callee().ok()?; + + Some((callee, BuiltinCreationRule::MustNotUseNew)) + } + JsNewOrCallExpression::JsCallExpression(node) => { + let callee: AnyJsExpression = node.callee().ok()?; + + Some((callee, BuiltinCreationRule::MustUseNew)) + } + } +} + +fn convert_new_expression_to_call_expression(expr: &JsNewExpression) -> Option { + let new_token = expr.new_token().ok()?; + let mut callee = expr.callee().ok()?; + if new_token.has_leading_comments() || new_token.has_trailing_comments() { + callee = callee.prepend_trivia_pieces(chain_trivia_pieces( + new_token.leading_trivia().pieces(), + new_token.trailing_trivia().pieces(), + ))?; + } + Some(make::js_call_expression(callee, expr.arguments()?).build()) +} + +fn convert_call_expression_to_new_expression(expr: &JsCallExpression) -> Option { + let mut callee = expr.callee().ok()?; + let leading_trivia_pieces = callee.syntax().first_leading_trivia()?.pieces(); + + let new_token = make::token(T![new]) + .with_leading_trivia_pieces(leading_trivia_pieces) + .with_trailing_trivia([(TriviaPieceKind::Whitespace, " ")]); + + callee = callee.with_leading_trivia_pieces([])?; + + Some( + make::js_new_expression(new_token, callee) + .with_arguments(expr.arguments().ok()?) + .build(), + ) +} + +#[test] +fn test_order() { + for items in BUILTINS_REQUIRING_NEW.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } + for items in BUILTINS_NOT_REQUIRING_NEW.windows(2) { + assert!(items[0] < items[1], "{} < {}", items[0], items[1]); + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index eabce4134d18..53de58d055f9 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -257,6 +257,7 @@ pub type UseButtonType = pub type UseCollapsedElseIf = ::Options; pub type UseConsistentArrayType = < lint :: style :: use_consistent_array_type :: UseConsistentArrayType as biome_analyze :: Rule > :: Options ; +pub type UseConsistentNewBuiltin = < lint :: nursery :: use_consistent_new_builtin :: UseConsistentNewBuiltin as biome_analyze :: Rule > :: Options ; pub type UseConst = ::Options; pub type UseDefaultParameterLast = < lint :: style :: use_default_parameter_last :: UseDefaultParameterLast 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/useConsistentNewBuiltin/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/invalid.js new file mode 100644 index 000000000000..4e2ba875040a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/invalid.js @@ -0,0 +1,42 @@ +Object() +Array() +ArrayBuffer() +BigInt64Array() +BigUint64Array() +DataView() +Date() +Error() +Float32Array() +Float64Array() +Function() +Int8Array() +Int16Array() +Int32Array() +Map() +WeakMap() +Set() +WeakSet() +Promise() +RegExp() +Uint8Array() +Uint16Array() +Uint32Array() +Uint8ClampedArray() +SharedArrayBuffer() +Proxy() +WeakRef() +FinalizationRegistry() +window.Object({}) +globalThis.Object() +function foo() { + return /** Start */ globalThis.Object({ foo: 'bar' }) /** End */ +} + +new String() +new Number() +new Boolean() +new window.String(123) +new globalThis.String() +function foo() { + return /** Start */ new globalThis.String("foo") /** End */ +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/invalid.js.snap new file mode 100644 index 000000000000..f4dffe3ced4b --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/invalid.js.snap @@ -0,0 +1,748 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```jsx +Object() +Array() +ArrayBuffer() +BigInt64Array() +BigUint64Array() +DataView() +Date() +Error() +Float32Array() +Float64Array() +Function() +Int8Array() +Int16Array() +Int32Array() +Map() +WeakMap() +Set() +WeakSet() +Promise() +RegExp() +Uint8Array() +Uint16Array() +Uint32Array() +Uint8ClampedArray() +SharedArrayBuffer() +Proxy() +WeakRef() +FinalizationRegistry() +window.Object({}) +globalThis.Object() +function foo() { + return /** Start */ globalThis.Object({ foo: 'bar' }) /** End */ +} + +new String() +new Number() +new Boolean() +new window.String(123) +new globalThis.String() +function foo() { + return /** Start */ new globalThis.String("foo") /** End */ +} +``` + +# Diagnostics +``` +invalid.js:1:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Object() instead of Object(). + + > 1 │ Object() + │ ^^^^^^^^ + 2 │ Array() + 3 │ ArrayBuffer() + + i Unsafe fix: Add new keyword. + + 1 │ new·Object() + │ ++++ + +``` + +``` +invalid.js:2:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Array() instead of Array(). + + 1 │ Object() + > 2 │ Array() + │ ^^^^^^^ + 3 │ ArrayBuffer() + 4 │ BigInt64Array() + + i Unsafe fix: Add new keyword. + + 2 │ new·Array() + │ ++++ + +``` + +``` +invalid.js:3:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new ArrayBuffer() instead of ArrayBuffer(). + + 1 │ Object() + 2 │ Array() + > 3 │ ArrayBuffer() + │ ^^^^^^^^^^^^^ + 4 │ BigInt64Array() + 5 │ BigUint64Array() + + i Unsafe fix: Add new keyword. + + 3 │ new·ArrayBuffer() + │ ++++ + +``` + +``` +invalid.js:4:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new BigInt64Array() instead of BigInt64Array(). + + 2 │ Array() + 3 │ ArrayBuffer() + > 4 │ BigInt64Array() + │ ^^^^^^^^^^^^^^^ + 5 │ BigUint64Array() + 6 │ DataView() + + i Unsafe fix: Add new keyword. + + 4 │ new·BigInt64Array() + │ ++++ + +``` + +``` +invalid.js:5:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new BigUint64Array() instead of BigUint64Array(). + + 3 │ ArrayBuffer() + 4 │ BigInt64Array() + > 5 │ BigUint64Array() + │ ^^^^^^^^^^^^^^^^ + 6 │ DataView() + 7 │ Date() + + i Unsafe fix: Add new keyword. + + 5 │ new·BigUint64Array() + │ ++++ + +``` + +``` +invalid.js:6:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new DataView() instead of DataView(). + + 4 │ BigInt64Array() + 5 │ BigUint64Array() + > 6 │ DataView() + │ ^^^^^^^^^^ + 7 │ Date() + 8 │ Error() + + i Unsafe fix: Add new keyword. + + 6 │ new·DataView() + │ ++++ + +``` + +``` +invalid.js:7:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Date() instead of Date(). + + 5 │ BigUint64Array() + 6 │ DataView() + > 7 │ Date() + │ ^^^^^^ + 8 │ Error() + 9 │ Float32Array() + + i Unsafe fix: Add new keyword. + + 7 │ new·Date() + │ ++++ + +``` + +``` +invalid.js:8:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Error() instead of Error(). + + 6 │ DataView() + 7 │ Date() + > 8 │ Error() + │ ^^^^^^^ + 9 │ Float32Array() + 10 │ Float64Array() + + i Unsafe fix: Add new keyword. + + 8 │ new·Error() + │ ++++ + +``` + +``` +invalid.js:9:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Float32Array() instead of Float32Array(). + + 7 │ Date() + 8 │ Error() + > 9 │ Float32Array() + │ ^^^^^^^^^^^^^^ + 10 │ Float64Array() + 11 │ Function() + + i Unsafe fix: Add new keyword. + + 9 │ new·Float32Array() + │ ++++ + +``` + +``` +invalid.js:10:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Float64Array() instead of Float64Array(). + + 8 │ Error() + 9 │ Float32Array() + > 10 │ Float64Array() + │ ^^^^^^^^^^^^^^ + 11 │ Function() + 12 │ Int8Array() + + i Unsafe fix: Add new keyword. + + 10 │ new·Float64Array() + │ ++++ + +``` + +``` +invalid.js:11:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Function() instead of Function(). + + 9 │ Float32Array() + 10 │ Float64Array() + > 11 │ Function() + │ ^^^^^^^^^^ + 12 │ Int8Array() + 13 │ Int16Array() + + i Unsafe fix: Add new keyword. + + 11 │ new·Function() + │ ++++ + +``` + +``` +invalid.js:12:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Int8Array() instead of Int8Array(). + + 10 │ Float64Array() + 11 │ Function() + > 12 │ Int8Array() + │ ^^^^^^^^^^^ + 13 │ Int16Array() + 14 │ Int32Array() + + i Unsafe fix: Add new keyword. + + 12 │ new·Int8Array() + │ ++++ + +``` + +``` +invalid.js:13:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Int16Array() instead of Int16Array(). + + 11 │ Function() + 12 │ Int8Array() + > 13 │ Int16Array() + │ ^^^^^^^^^^^^ + 14 │ Int32Array() + 15 │ Map() + + i Unsafe fix: Add new keyword. + + 13 │ new·Int16Array() + │ ++++ + +``` + +``` +invalid.js:14:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Int32Array() instead of Int32Array(). + + 12 │ Int8Array() + 13 │ Int16Array() + > 14 │ Int32Array() + │ ^^^^^^^^^^^^ + 15 │ Map() + 16 │ WeakMap() + + i Unsafe fix: Add new keyword. + + 14 │ new·Int32Array() + │ ++++ + +``` + +``` +invalid.js:15:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Map() instead of Map(). + + 13 │ Int16Array() + 14 │ Int32Array() + > 15 │ Map() + │ ^^^^^ + 16 │ WeakMap() + 17 │ Set() + + i Unsafe fix: Add new keyword. + + 15 │ new·Map() + │ ++++ + +``` + +``` +invalid.js:16:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new WeakMap() instead of WeakMap(). + + 14 │ Int32Array() + 15 │ Map() + > 16 │ WeakMap() + │ ^^^^^^^^^ + 17 │ Set() + 18 │ WeakSet() + + i Unsafe fix: Add new keyword. + + 16 │ new·WeakMap() + │ ++++ + +``` + +``` +invalid.js:17:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Set() instead of Set(). + + 15 │ Map() + 16 │ WeakMap() + > 17 │ Set() + │ ^^^^^ + 18 │ WeakSet() + 19 │ Promise() + + i Unsafe fix: Add new keyword. + + 17 │ new·Set() + │ ++++ + +``` + +``` +invalid.js:18:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new WeakSet() instead of WeakSet(). + + 16 │ WeakMap() + 17 │ Set() + > 18 │ WeakSet() + │ ^^^^^^^^^ + 19 │ Promise() + 20 │ RegExp() + + i Unsafe fix: Add new keyword. + + 18 │ new·WeakSet() + │ ++++ + +``` + +``` +invalid.js:19:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Promise() instead of Promise(). + + 17 │ Set() + 18 │ WeakSet() + > 19 │ Promise() + │ ^^^^^^^^^ + 20 │ RegExp() + 21 │ Uint8Array() + + i Unsafe fix: Add new keyword. + + 19 │ new·Promise() + │ ++++ + +``` + +``` +invalid.js:20:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new RegExp() instead of RegExp(). + + 18 │ WeakSet() + 19 │ Promise() + > 20 │ RegExp() + │ ^^^^^^^^ + 21 │ Uint8Array() + 22 │ Uint16Array() + + i Unsafe fix: Add new keyword. + + 20 │ new·RegExp() + │ ++++ + +``` + +``` +invalid.js:21:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Uint8Array() instead of Uint8Array(). + + 19 │ Promise() + 20 │ RegExp() + > 21 │ Uint8Array() + │ ^^^^^^^^^^^^ + 22 │ Uint16Array() + 23 │ Uint32Array() + + i Unsafe fix: Add new keyword. + + 21 │ new·Uint8Array() + │ ++++ + +``` + +``` +invalid.js:22:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Uint16Array() instead of Uint16Array(). + + 20 │ RegExp() + 21 │ Uint8Array() + > 22 │ Uint16Array() + │ ^^^^^^^^^^^^^ + 23 │ Uint32Array() + 24 │ Uint8ClampedArray() + + i Unsafe fix: Add new keyword. + + 22 │ new·Uint16Array() + │ ++++ + +``` + +``` +invalid.js:23:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Uint32Array() instead of Uint32Array(). + + 21 │ Uint8Array() + 22 │ Uint16Array() + > 23 │ Uint32Array() + │ ^^^^^^^^^^^^^ + 24 │ Uint8ClampedArray() + 25 │ SharedArrayBuffer() + + i Unsafe fix: Add new keyword. + + 23 │ new·Uint32Array() + │ ++++ + +``` + +``` +invalid.js:24:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Uint8ClampedArray() instead of Uint8ClampedArray(). + + 22 │ Uint16Array() + 23 │ Uint32Array() + > 24 │ Uint8ClampedArray() + │ ^^^^^^^^^^^^^^^^^^^ + 25 │ SharedArrayBuffer() + 26 │ Proxy() + + i Unsafe fix: Add new keyword. + + 24 │ new·Uint8ClampedArray() + │ ++++ + +``` + +``` +invalid.js:25:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new SharedArrayBuffer() instead of SharedArrayBuffer(). + + 23 │ Uint32Array() + 24 │ Uint8ClampedArray() + > 25 │ SharedArrayBuffer() + │ ^^^^^^^^^^^^^^^^^^^ + 26 │ Proxy() + 27 │ WeakRef() + + i Unsafe fix: Add new keyword. + + 25 │ new·SharedArrayBuffer() + │ ++++ + +``` + +``` +invalid.js:26:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Proxy() instead of Proxy(). + + 24 │ Uint8ClampedArray() + 25 │ SharedArrayBuffer() + > 26 │ Proxy() + │ ^^^^^^^ + 27 │ WeakRef() + 28 │ FinalizationRegistry() + + i Unsafe fix: Add new keyword. + + 26 │ new·Proxy() + │ ++++ + +``` + +``` +invalid.js:27:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new WeakRef() instead of WeakRef(). + + 25 │ SharedArrayBuffer() + 26 │ Proxy() + > 27 │ WeakRef() + │ ^^^^^^^^^ + 28 │ FinalizationRegistry() + 29 │ window.Object({}) + + i Unsafe fix: Add new keyword. + + 27 │ new·WeakRef() + │ ++++ + +``` + +``` +invalid.js:28:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new FinalizationRegistry() instead of FinalizationRegistry(). + + 26 │ Proxy() + 27 │ WeakRef() + > 28 │ FinalizationRegistry() + │ ^^^^^^^^^^^^^^^^^^^^^^ + 29 │ window.Object({}) + 30 │ globalThis.Object() + + i Unsafe fix: Add new keyword. + + 28 │ new·FinalizationRegistry() + │ ++++ + +``` + +``` +invalid.js:29:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Object() instead of Object(). + + 27 │ WeakRef() + 28 │ FinalizationRegistry() + > 29 │ window.Object({}) + │ ^^^^^^^^^^^^^^^^^ + 30 │ globalThis.Object() + 31 │ function foo() { + + i Unsafe fix: Add new keyword. + + 29 │ new·window.Object({}) + │ ++++ + +``` + +``` +invalid.js:30:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Object() instead of Object(). + + 28 │ FinalizationRegistry() + 29 │ window.Object({}) + > 30 │ globalThis.Object() + │ ^^^^^^^^^^^^^^^^^^^ + 31 │ function foo() { + 32 │ return /** Start */ globalThis.Object({ foo: 'bar' }) /** End */ + + i Unsafe fix: Add new keyword. + + 30 │ new·globalThis.Object() + │ ++++ + +``` + +``` +invalid.js:32:25 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use new Object() instead of Object(). + + 30 │ globalThis.Object() + 31 │ function foo() { + > 32 │ return /** Start */ globalThis.Object({ foo: 'bar' }) /** End */ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 33 │ } + 34 │ + + i Unsafe fix: Add new keyword. + + 32 │ ····return·/**·Start·*/·new·globalThis.Object({·foo:·'bar'·})·/**·End·*/ + │ ++++ + +``` + +``` +invalid.js:35:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use String() instead of new String(). + + 33 │ } + 34 │ + > 35 │ new String() + │ ^^^^^^^^^^^^ + 36 │ new Number() + 37 │ new Boolean() + + i Unsafe fix: Remove new keyword. + + 35 │ new·String() + │ ---- + +``` + +``` +invalid.js:36:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Number() instead of new Number(). + + 35 │ new String() + > 36 │ new Number() + │ ^^^^^^^^^^^^ + 37 │ new Boolean() + 38 │ new window.String(123) + + i Unsafe fix: Remove new keyword. + + 36 │ new·Number() + │ ---- + +``` + +``` +invalid.js:37:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use Boolean() instead of new Boolean(). + + 35 │ new String() + 36 │ new Number() + > 37 │ new Boolean() + │ ^^^^^^^^^^^^^ + 38 │ new window.String(123) + 39 │ new globalThis.String() + + i Unsafe fix: Remove new keyword. + + 37 │ new·Boolean() + │ ---- + +``` + +``` +invalid.js:38:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use String() instead of new String(). + + 36 │ new Number() + 37 │ new Boolean() + > 38 │ new window.String(123) + │ ^^^^^^^^^^^^^^^^^^^^^^ + 39 │ new globalThis.String() + 40 │ function foo() { + + i Unsafe fix: Remove new keyword. + + 38 │ new·window.String(123) + │ ---- + +``` + +``` +invalid.js:39:1 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use String() instead of new String(). + + 37 │ new Boolean() + 38 │ new window.String(123) + > 39 │ new globalThis.String() + │ ^^^^^^^^^^^^^^^^^^^^^^^ + 40 │ function foo() { + 41 │ return /** Start */ new globalThis.String("foo") /** End */ + + i Unsafe fix: Remove new keyword. + + 39 │ new·globalThis.String() + │ ---- + +``` + +``` +invalid.js:41:25 lint/nursery/useConsistentNewBuiltin FIXABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Use String() instead of new String(). + + 39 │ new globalThis.String() + 40 │ function foo() { + > 41 │ return /** Start */ new globalThis.String("foo") /** End */ + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 42 │ } + + i Unsafe fix: Remove new keyword. + + 41 │ ····return·/**·Start·*/·new·globalThis.String("foo")·/**·End·*/ + │ ---- + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js new file mode 100644 index 000000000000..ee0db4caa817 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js @@ -0,0 +1,50 @@ +new Object() +new Array() +new ArrayBuffer() +new BigInt64Array() +new BigUint64Array() +new DataView() +new Date() +new Error() +new Float32Array() +new Float64Array() +new Function() +new Int8Array() +new Int16Array() +new Int32Array() +new Map() +new WeakMap() +new Set() +new WeakSet() +new Promise() +new RegExp() +new Uint8Array() +new Uint16Array() +new Uint32Array() +new Uint8ClampedArray() +new SharedArrayBuffer() +new Proxy() +new WeakRef() +new FinalizationRegistry() +new window.Object({}) +new globalThis.Object() +function foo() { + return new globalThis.Object() +} + +String() +Number() +Boolean() +window.String() +globalThis.String(123) +function foo() { + return globalThis.String() +} + +function varCheck() { + { + var String = class {} + } + // This should not be reported + return new String() +} \ No newline at end of file diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js.snap new file mode 100644 index 000000000000..c705032a3fc5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentNewBuiltin/valid.js.snap @@ -0,0 +1,57 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```jsx +new Object() +new Array() +new ArrayBuffer() +new BigInt64Array() +new BigUint64Array() +new DataView() +new Date() +new Error() +new Float32Array() +new Float64Array() +new Function() +new Int8Array() +new Int16Array() +new Int32Array() +new Map() +new WeakMap() +new Set() +new WeakSet() +new Promise() +new RegExp() +new Uint8Array() +new Uint16Array() +new Uint32Array() +new Uint8ClampedArray() +new SharedArrayBuffer() +new Proxy() +new WeakRef() +new FinalizationRegistry() +new window.Object({}) +new globalThis.Object() +function foo() { + return new globalThis.Object() +} + +String() +Number() +Boolean() +window.String() +globalThis.String(123) +function foo() { + return globalThis.String() +} + +function varCheck() { + { + var String = class {} + } + // This should not be reported + return new String() +} +``` diff --git a/crates/biome_js_syntax/src/expr_ext.rs b/crates/biome_js_syntax/src/expr_ext.rs index c10d60fdb1dd..7715e4461486 100644 --- a/crates/biome_js_syntax/src/expr_ext.rs +++ b/crates/biome_js_syntax/src/expr_ext.rs @@ -23,6 +23,10 @@ const GLOBAL_THIS: &str = "globalThis"; const UNDEFINED: &str = "undefined"; const WINDOW: &str = "window"; +declare_node_union! { + pub JsNewOrCallExpression = JsNewExpression | JsCallExpression +} + impl JsReferenceIdentifier { /// Returns `true` if this identifier refers to the `undefined` symbol. /// diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 4757de7c4be0..76d4b2e91b98 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -984,6 +984,10 @@ export interface Nursery { * It enables the recommended rules for this group */ recommended?: boolean; + /** + * Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt. + */ + useConsistentNewBuiltin?: RuleConfiguration_for_Null; /** * Disallows package private imports. */ @@ -1972,6 +1976,7 @@ export type Category = | "lint/nursery/noUndeclaredDependencies" | "lint/nursery/noUnknownUnit" | "lint/nursery/useBiomeSuppressionComment" + | "lint/nursery/useConsistentNewBuiltin" | "lint/nursery/useImportRestrictions" | "lint/nursery/useSortedClasses" | "lint/performance/noAccumulatingSpread" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 2f62583a753c..9b603719da03 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -1563,6 +1563,13 @@ "description": "It enables the recommended rules for this group", "type": ["boolean", "null"] }, + "useConsistentNewBuiltin": { + "description": "Enforce the use of new for all builtins, except String, Number, Boolean, Symbol and BigInt.", + "anyOf": [ + { "$ref": "#/definitions/RuleConfiguration" }, + { "type": "null" } + ] + }, "useImportRestrictions": { "description": "Disallows package private imports.", "anyOf": [