diff --git a/.changeset/wild-times-design.md b/.changeset/wild-times-design.md new file mode 100644 index 000000000000..6eb3264d4fe5 --- /dev/null +++ b/.changeset/wild-times-design.md @@ -0,0 +1,11 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`noFloatingClasses`](https://biomejs.dev/linter/rules/no-floating-classes). Disallow `new` operators outside of assignments or comparisons. + +**Invalid:** + +```js +new Date(); +``` 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 5a64085ba377..89f059a772bd 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 @@ -1937,6 +1937,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "no-new" => { + if !options.include_nursery { + results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .no_floating_classes + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "no-new-native-nonconstructor" => { let group = rules.correctness.get_or_insert_with(Default::default); let rule = group diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 8835d0092846..84ce54c08a91 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -185,6 +185,7 @@ pub enum RuleName { NoExtraNonNullAssertion, NoFallthroughSwitchClause, NoFlatMapIdentity, + NoFloatingClasses, NoFloatingPromises, NoFocusedTests, NoForEach, @@ -614,6 +615,7 @@ impl RuleName { Self::NoExtraNonNullAssertion => "noExtraNonNullAssertion", Self::NoFallthroughSwitchClause => "noFallthroughSwitchClause", Self::NoFlatMapIdentity => "noFlatMapIdentity", + Self::NoFloatingClasses => "noFloatingClasses", Self::NoFloatingPromises => "noFloatingPromises", Self::NoFocusedTests => "noFocusedTests", Self::NoForEach => "noForEach", @@ -1047,6 +1049,7 @@ impl RuleName { Self::NoExtraNonNullAssertion => RuleGroup::Suspicious, Self::NoFallthroughSwitchClause => RuleGroup::Suspicious, Self::NoFlatMapIdentity => RuleGroup::Complexity, + Self::NoFloatingClasses => RuleGroup::Nursery, Self::NoFloatingPromises => RuleGroup::Nursery, Self::NoFocusedTests => RuleGroup::Suspicious, Self::NoForEach => RuleGroup::Complexity, @@ -1481,6 +1484,7 @@ impl std::str::FromStr for RuleName { "noExtraNonNullAssertion" => Ok(Self::NoExtraNonNullAssertion), "noFallthroughSwitchClause" => Ok(Self::NoFallthroughSwitchClause), "noFlatMapIdentity" => Ok(Self::NoFlatMapIdentity), + "noFloatingClasses" => Ok(Self::NoFloatingClasses), "noFloatingPromises" => Ok(Self::NoFloatingPromises), "noFocusedTests" => Ok(Self::NoFocusedTests), "noForEach" => Ok(Self::NoForEach), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index efe2d480b015..91850751cd99 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -181,6 +181,7 @@ define_categories! { "lint/nursery/noEmptySource": "https://biomejs.dev/linter/rules/no-empty-source", "lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null", "lint/nursery/noExcessiveLinesPerFile": "https://biomejs.dev/linter/rules/no-excessive-lines-per-file", + "lint/nursery/noFloatingClasses": "https://biomejs.dev/linter/rules/no-floating-classes", "lint/nursery/noFloatingPromises": "https://biomejs.dev/linter/rules/no-floating-promises", "lint/nursery/noForIn": "https://biomejs.dev/linter/rules/no-for-in", "lint/nursery/noImplicitCoercion": "https://biomejs.dev/linter/rules/no-implicit-coercion", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index 827d093375c9..0845d38741fc 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_duplicated_spread_props; pub mod no_empty_source; pub mod no_equals_to_null; pub mod no_excessive_lines_per_file; +pub mod no_floating_classes; pub mod no_floating_promises; pub mod no_for_in; pub mod no_import_cycles; @@ -63,4 +64,4 @@ pub mod use_spread; pub mod use_vue_consistent_define_props_declaration; pub mod use_vue_define_macros_order; pub mod use_vue_multi_word_component_names; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_ambiguous_anchor_text :: NoAmbiguousAnchorText , self :: no_before_interactive_script_outside_document :: NoBeforeInteractiveScriptOutsideDocument , self :: no_continue :: NoContinue , self :: no_deprecated_imports :: NoDeprecatedImports , self :: no_div_regex :: NoDivRegex , self :: no_duplicate_enum_values :: NoDuplicateEnumValues , self :: no_duplicated_spread_props :: NoDuplicatedSpreadProps , self :: no_empty_source :: NoEmptySource , self :: no_equals_to_null :: NoEqualsToNull , self :: no_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_floating_classes :: NoFloatingClasses , self :: no_floating_promises :: NoFloatingPromises , self :: no_for_in :: NoForIn , self :: no_import_cycles :: NoImportCycles , self :: no_increment_decrement :: NoIncrementDecrement , self :: no_jsx_literals :: NoJsxLiterals , self :: no_jsx_props_bind :: NoJsxPropsBind , self :: no_leaked_render :: NoLeakedRender , self :: no_misused_promises :: NoMisusedPromises , self :: no_multi_assign :: NoMultiAssign , self :: no_multi_str :: NoMultiStr , self :: no_next_async_client_component :: NoNextAsyncClientComponent , self :: no_parameters_only_used_in_recursion :: NoParametersOnlyUsedInRecursion , self :: no_proto :: NoProto , self :: no_react_forward_ref :: NoReactForwardRef , self :: no_return_assign :: NoReturnAssign , self :: no_script_url :: NoScriptUrl , self :: no_shadow :: NoShadow , self :: no_sync_scripts :: NoSyncScripts , self :: no_ternary :: NoTernary , self :: no_undeclared_env_vars :: NoUndeclaredEnvVars , self :: no_unknown_attribute :: NoUnknownAttribute , self :: no_unnecessary_conditions :: NoUnnecessaryConditions , self :: no_unresolved_imports :: NoUnresolvedImports , self :: no_unused_expressions :: NoUnusedExpressions , self :: no_useless_catch_binding :: NoUselessCatchBinding , self :: no_useless_undefined :: NoUselessUndefined , self :: no_vue_data_object_declaration :: NoVueDataObjectDeclaration , self :: no_vue_duplicate_keys :: NoVueDuplicateKeys , self :: no_vue_options_api :: NoVueOptionsApi , self :: no_vue_reserved_keys :: NoVueReservedKeys , self :: no_vue_reserved_props :: NoVueReservedProps , self :: no_vue_setup_props_reactivity_loss :: NoVueSetupPropsReactivityLoss , self :: use_array_sort_compare :: UseArraySortCompare , self :: use_await_thenable :: UseAwaitThenable , self :: use_consistent_arrow_return :: UseConsistentArrowReturn , self :: use_destructuring :: UseDestructuring , self :: use_error_cause :: UseErrorCause , self :: use_exhaustive_switch_cases :: UseExhaustiveSwitchCases , self :: use_explicit_type :: UseExplicitType , self :: use_find :: UseFind , self :: use_inline_script_id :: UseInlineScriptId , self :: use_max_params :: UseMaxParams , self :: use_qwik_method_usage :: UseQwikMethodUsage , self :: use_qwik_valid_lexical_scope :: UseQwikValidLexicalScope , self :: use_regexp_exec :: UseRegexpExec , self :: use_sorted_classes :: UseSortedClasses , self :: use_spread :: UseSpread , self :: use_vue_consistent_define_props_declaration :: UseVueConsistentDefinePropsDeclaration , self :: use_vue_define_macros_order :: UseVueDefineMacrosOrder , self :: use_vue_multi_word_component_names :: UseVueMultiWordComponentNames ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/no_floating_classes.rs b/crates/biome_js_analyze/src/lint/nursery/no_floating_classes.rs new file mode 100644 index 000000000000..5f68f45ac291 --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/no_floating_classes.rs @@ -0,0 +1,80 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_js_syntax::{JsNewExpression, JsSyntaxKind}; +use biome_rowan::{AstNode, SyntaxNodeOptionExt}; +use biome_rule_options::no_floating_classes::NoFloatingClassesOptions; + +declare_lint_rule! { + /// Disallow `new` operators outside of assignments or comparisons. + /// + /// The goal of using `new` with a constructor is typically to create an object of a particular type and store that object in a variable, such as: + /// + /// ```js + /// const person = new Person(); + /// ``` + /// + /// It's less common to use `new` and not store the result, such as: + /// + /// ```js,ignore + /// new Person(); + /// ``` + /// + /// In this case, the created object is thrown away because its reference isn't stored anywhere, and in many cases, this means that the constructor should be replaced with a function that doesn't require `new` to be used. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```js,expect_diagnostic + /// new Thing(); + /// ``` + /// + /// ### Valid + /// + /// ```js + /// const thing = new Thing(); + /// ``` + /// + pub NoFloatingClasses { + version: "next", + name: "noFloatingClasses", + language: "js", + recommended: false, + sources: &[RuleSource::Eslint("no-new").same()], + } +} + +impl Rule for NoFloatingClasses { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = NoFloatingClassesOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + + node.syntax() + .parent() + .kind() + .is_some_and(|kind| kind == JsSyntaxKind::JS_EXPRESSION_STATEMENT) + .then_some(()) + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + Some( + RuleDiagnostic::new( + rule_category!(), + node.range(), + markup! { + "Using the `new` operator outside of assignments or comparisons is not allowed." + }, + ) + .note(markup! { + "The created object is thrown away because its reference isn't stored anywhere. Assign the object to a variable or replace with a function that doesn't require `new` to be used." + }), + ) + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js new file mode 100644 index 000000000000..52f9be0cc96a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js @@ -0,0 +1,2 @@ +/* should generate diagnostics */ +new Date() diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js.snap new file mode 100644 index 000000000000..fadf6de5d1b0 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/invalid.js.snap @@ -0,0 +1,28 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalid.js +--- +# Input +```js +/* should generate diagnostics */ +new Date() + +``` + +# Diagnostics +``` +invalid.js:2:1 lint/nursery/noFloatingClasses ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Using the `new` operator outside of assignments or comparisons is not allowed. + + 1 │ /* should generate diagnostics */ + > 2 │ new Date() + │ ^^^^^^^^^^ + 3 │ + + i The created object is thrown away because its reference isn't stored anywhere. Assign the object to a variable or replace with a function that doesn't require `new` to be used. + + i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js new file mode 100644 index 000000000000..427d15aed108 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js @@ -0,0 +1,7 @@ +/* should not generate diagnostics */ +var a = new Date() + +var a; +if (a === new Date()) { + a = false; +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js.snap b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js.snap new file mode 100644 index 000000000000..daed780c87a2 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/noFloatingClasses/valid.js.snap @@ -0,0 +1,15 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: valid.js +--- +# Input +```js +/* should not generate diagnostics */ +var a = new Date() + +var a; +if (a === new Date()) { + a = false; +} + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 6cb2ed45336e..5af6936b702c 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -98,6 +98,7 @@ pub mod no_extra_boolean_cast; pub mod no_extra_non_null_assertion; pub mod no_fallthrough_switch_clause; pub mod no_flat_map_identity; +pub mod no_floating_classes; pub mod no_floating_promises; pub mod no_focused_tests; pub mod no_for_each; diff --git a/crates/biome_rule_options/src/no_floating_classes.rs b/crates/biome_rule_options/src/no_floating_classes.rs new file mode 100644 index 000000000000..5c66050b3b5b --- /dev/null +++ b/crates/biome_rule_options/src/no_floating_classes.rs @@ -0,0 +1,6 @@ +use biome_deserialize_macros::{Deserializable, Merge}; +use serde::{Deserialize, Serialize}; +#[derive(Default, Clone, Debug, Deserialize, Deserializable, Merge, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct NoFloatingClassesOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index a8bcc0674f4b..1893bb605819 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1949,6 +1949,11 @@ See https://biomejs.dev/linter/rules/no-excessive-lines-per-file */ noExcessiveLinesPerFile?: NoExcessiveLinesPerFileConfiguration; /** + * Disallow new operators outside of assignments or comparisons. +See https://biomejs.dev/linter/rules/no-floating-classes + */ + noFloatingClasses?: NoFloatingClassesConfiguration; + /** * Require Promise-like statements to be handled appropriately. See https://biomejs.dev/linter/rules/no-floating-promises */ @@ -3790,6 +3795,9 @@ export type NoEqualsToNullConfiguration = export type NoExcessiveLinesPerFileConfiguration = | RulePlainConfiguration | RuleWithNoExcessiveLinesPerFileOptions; +export type NoFloatingClassesConfiguration = + | RulePlainConfiguration + | RuleWithNoFloatingClassesOptions; export type NoFloatingPromisesConfiguration = | RulePlainConfiguration | RuleWithNoFloatingPromisesOptions; @@ -5307,6 +5315,10 @@ export interface RuleWithNoExcessiveLinesPerFileOptions { level: RulePlainConfiguration; options?: NoExcessiveLinesPerFileOptions; } +export interface RuleWithNoFloatingClassesOptions { + level: RulePlainConfiguration; + options?: NoFloatingClassesOptions; +} export interface RuleWithNoFloatingPromisesOptions { fix?: FixKind; level: RulePlainConfiguration; @@ -6747,6 +6759,7 @@ export interface NoExcessiveLinesPerFileOptions { */ skipBlankLines?: boolean; } +export type NoFloatingClassesOptions = {}; export type NoFloatingPromisesOptions = {}; export type NoForInOptions = {}; export interface NoImportCyclesOptions { @@ -7684,6 +7697,7 @@ export type Category = | "lint/nursery/noEmptySource" | "lint/nursery/noEqualsToNull" | "lint/nursery/noExcessiveLinesPerFile" + | "lint/nursery/noFloatingClasses" | "lint/nursery/noFloatingPromises" | "lint/nursery/noForIn" | "lint/nursery/noImplicitCoercion" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index f7cd3aaef6a7..02590adc746a 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3452,6 +3452,16 @@ "type": "object", "additionalProperties": false }, + "NoFloatingClassesConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoFloatingClassesOptions" } + ] + }, + "NoFloatingClassesOptions": { + "type": "object", + "additionalProperties": false + }, "NoFloatingPromisesConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5459,6 +5469,13 @@ { "type": "null" } ] }, + "noFloatingClasses": { + "description": "Disallow new operators outside of assignments or comparisons.\nSee https://biomejs.dev/linter/rules/no-floating-classes", + "anyOf": [ + { "$ref": "#/$defs/NoFloatingClassesConfiguration" }, + { "type": "null" } + ] + }, "noFloatingPromises": { "description": "Require Promise-like statements to be handled appropriately.\nSee https://biomejs.dev/linter/rules/no-floating-promises", "anyOf": [ @@ -7381,6 +7398,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoFloatingClassesOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoFloatingClassesOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoFloatingPromisesOptions": { "type": "object", "properties": {