diff --git a/.changeset/famous-ravens-cross.md b/.changeset/famous-ravens-cross.md new file mode 100644 index 000000000000..25c2f090f795 --- /dev/null +++ b/.changeset/famous-ravens-cross.md @@ -0,0 +1,33 @@ +--- +"@biomejs/biome": minor +--- + +Add the new JavaScript rule `useConsistentObjectDefinition` rule. The rule enforces a consistent style for the definition of objects: + +By the default, the rule enforces a shorthand style: + +```js +const validShorthand = { + // Property shorthand + foo, + + // Method shorthand + method() { + return "method"; + }, +} +``` + +Alternatively, the rule can be configured to enforce an explicit style: + +```js +const invalidExplicit = { + // Basic property shorthand violations + foo: foo, + + // Method shorthand violations + method: function () { + return "method"; + }, +} +``` 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 ecfe268cd9ce..4c511541b679 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 @@ -1809,6 +1809,21 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "object-shorthand" => { + if !options.include_inspired { + results.has_inspired_rules = true; + return false; + } + if !options.include_nursery { + return false; + } + let group = rules.nursery.get_or_insert_with(Default::default); + let rule = group + .unwrap_group_as_mut() + .use_consistent_object_definition + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "one-var" => { let group = rules.style.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 5b245e3024f1..ea86a9c8b2a8 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -3207,6 +3207,10 @@ pub struct Nursery { #[serde(skip_serializing_if = "Option::is_none")] pub use_consistent_member_accessibility: Option>, + #[doc = "Require the consistent declaration of object literals. Defaults to explicit definitions."] + #[serde(skip_serializing_if = "Option::is_none")] + pub use_consistent_object_definition: + Option>, #[doc = "Require specifying the reason argument when using @deprecated directive"] #[serde(skip_serializing_if = "Option::is_none")] pub use_deprecated_reason: @@ -3310,6 +3314,7 @@ impl Nursery { "useComponentExportOnlyModules", "useConsistentCurlyBraces", "useConsistentMemberAccessibility", + "useConsistentObjectDefinition", "useDeprecatedReason", "useExplicitType", "useExportsLast", @@ -3339,10 +3344,10 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[40]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[45]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59]), - RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62]), ]; const ALL_RULES_AS_FILTERS: &'static [RuleFilter<'static>] = &[ RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[0]), @@ -3409,6 +3414,7 @@ impl Nursery { RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62]), RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[63]), + RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[64]), ]; } impl RuleGroupExt for Nursery { @@ -3675,71 +3681,76 @@ impl RuleGroupExt for Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_object_definition.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_explicit_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_exports_last.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_google_font_display.as_ref() { + if let Some(rule) = self.use_exports_last.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_google_font_preconnect.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_preconnect.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } - if let Some(rule) = self.use_named_operation.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_named_operation.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } } - if let Some(rule) = self.use_parse_int_radix.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_parse_int_radix.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_enabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[63])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_enabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[64])); + } + } index_set } fn get_disabled_rules(&self) -> FxHashSet> { @@ -3999,71 +4010,76 @@ impl RuleGroupExt for Nursery { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[50])); } } - if let Some(rule) = self.use_deprecated_reason.as_ref() { + if let Some(rule) = self.use_consistent_object_definition.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[51])); } } - if let Some(rule) = self.use_explicit_type.as_ref() { + if let Some(rule) = self.use_deprecated_reason.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[52])); } } - if let Some(rule) = self.use_exports_last.as_ref() { + if let Some(rule) = self.use_explicit_type.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[53])); } } - if let Some(rule) = self.use_google_font_display.as_ref() { + if let Some(rule) = self.use_exports_last.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[54])); } } - if let Some(rule) = self.use_google_font_preconnect.as_ref() { + if let Some(rule) = self.use_google_font_display.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[55])); } } - if let Some(rule) = self.use_guard_for_in.as_ref() { + if let Some(rule) = self.use_google_font_preconnect.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[56])); } } - if let Some(rule) = self.use_named_operation.as_ref() { + if let Some(rule) = self.use_guard_for_in.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[57])); } } - if let Some(rule) = self.use_naming_convention.as_ref() { + if let Some(rule) = self.use_named_operation.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[58])); } } - if let Some(rule) = self.use_parse_int_radix.as_ref() { + if let Some(rule) = self.use_naming_convention.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[59])); } } - if let Some(rule) = self.use_sorted_classes.as_ref() { + if let Some(rule) = self.use_parse_int_radix.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[60])); } } - if let Some(rule) = self.use_strict_mode.as_ref() { + if let Some(rule) = self.use_sorted_classes.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[61])); } } - if let Some(rule) = self.use_trim_start_end.as_ref() { + if let Some(rule) = self.use_strict_mode.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[62])); } } - if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if let Some(rule) = self.use_trim_start_end.as_ref() { if rule.is_disabled() { index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[63])); } } + if let Some(rule) = self.use_valid_autocomplete.as_ref() { + if rule.is_disabled() { + index_set.insert(RuleFilter::Rule(Self::GROUP_NAME, Self::GROUP_RULES[64])); + } + } index_set } #[doc = r" Checks if, given a rule name, matches one of the rules contained in this category"] @@ -4298,6 +4314,10 @@ impl RuleGroupExt for Nursery { .use_consistent_member_accessibility .as_ref() .map(|conf| (conf.level(), conf.get_options())), + "useConsistentObjectDefinition" => self + .use_consistent_object_definition + .as_ref() + .map(|conf| (conf.level(), conf.get_options())), "useDeprecatedReason" => self .use_deprecated_reason .as_ref() diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index e30445d25d5d..5eda78606a2d 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -206,6 +206,7 @@ define_categories! { "lint/nursery/useComponentExportOnlyModules": "https://biomejs.dev/linter/rules/use-components-only-module", "lint/nursery/useConsistentCurlyBraces": "https://biomejs.dev/linter/rules/use-consistent-curly-braces", "lint/nursery/useConsistentMemberAccessibility": "https://biomejs.dev/linter/rules/use-consistent-member-accessibility", + "lint/nursery/useConsistentObjectDefinition": "https://biomejs.dev/linter/rules/use-consistent-object-definition", "lint/nursery/useDeprecatedReason": "https://biomejs.dev/linter/rules/use-deprecated-reason", "lint/nursery/useExplicitFunctionReturnType": "https://biomejs.dev/linter/rules/use-explicit-type", "lint/nursery/useExplicitType": "https://biomejs.dev/linter/rules/use-explicit-type", diff --git a/crates/biome_js_analyze/src/lint/nursery.rs b/crates/biome_js_analyze/src/lint/nursery.rs index dad10bd25812..1bf09ac09222 100644 --- a/crates/biome_js_analyze/src/lint/nursery.rs +++ b/crates/biome_js_analyze/src/lint/nursery.rs @@ -44,6 +44,7 @@ pub mod use_collapsed_if; pub mod use_component_export_only_modules; pub mod use_consistent_curly_braces; pub mod use_consistent_member_accessibility; +pub mod use_consistent_object_definition; pub mod use_explicit_type; pub mod use_exports_last; pub mod use_google_font_display; @@ -54,4 +55,4 @@ pub mod use_sorted_classes; pub mod use_strict_mode; pub mod use_trim_start_end; pub mod use_valid_autocomplete; -declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } } +declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_await_in_loop :: NoAwaitInLoop , self :: no_common_js :: NoCommonJs , self :: no_constant_binary_expression :: NoConstantBinaryExpression , self :: no_destructured_props :: NoDestructuredProps , self :: no_document_cookie :: NoDocumentCookie , self :: no_document_import_in_page :: NoDocumentImportInPage , self :: no_duplicate_else_if :: NoDuplicateElseIf , self :: no_dynamic_namespace_import_access :: NoDynamicNamespaceImportAccess , self :: no_enum :: NoEnum , self :: no_exported_imports :: NoExportedImports , self :: no_floating_promises :: NoFloatingPromises , self :: no_global_dirname_filename :: NoGlobalDirnameFilename , self :: no_head_element :: NoHeadElement , self :: no_head_import_in_document :: NoHeadImportInDocument , self :: no_img_element :: NoImgElement , self :: no_import_cycles :: NoImportCycles , self :: no_irregular_whitespace :: NoIrregularWhitespace , self :: no_nested_ternary :: NoNestedTernary , self :: no_noninteractive_element_interactions :: NoNoninteractiveElementInteractions , self :: no_octal_escape :: NoOctalEscape , self :: no_package_private_imports :: NoPackagePrivateImports , self :: no_process_env :: NoProcessEnv , self :: no_process_global :: NoProcessGlobal , self :: no_restricted_imports :: NoRestrictedImports , self :: no_restricted_types :: NoRestrictedTypes , self :: no_secrets :: NoSecrets , self :: no_static_element_interactions :: NoStaticElementInteractions , self :: no_substr :: NoSubstr , self :: no_template_curly_in_string :: NoTemplateCurlyInString , self :: no_ts_ignore :: NoTsIgnore , self :: no_unwanted_polyfillio :: NoUnwantedPolyfillio , self :: no_useless_escape_in_regex :: NoUselessEscapeInRegex , self :: no_useless_string_raw :: NoUselessStringRaw , self :: no_useless_undefined :: NoUselessUndefined , self :: use_adjacent_overload_signatures :: UseAdjacentOverloadSignatures , self :: use_aria_props_supported_by_role :: UseAriaPropsSupportedByRole , self :: use_at_index :: UseAtIndex , self :: use_collapsed_if :: UseCollapsedIf , self :: use_component_export_only_modules :: UseComponentExportOnlyModules , self :: use_consistent_curly_braces :: UseConsistentCurlyBraces , self :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility , self :: use_consistent_object_definition :: UseConsistentObjectDefinition , self :: use_explicit_type :: UseExplicitType , self :: use_exports_last :: UseExportsLast , self :: use_google_font_display :: UseGoogleFontDisplay , self :: use_google_font_preconnect :: UseGoogleFontPreconnect , self :: use_guard_for_in :: UseGuardForIn , self :: use_parse_int_radix :: UseParseIntRadix , self :: use_sorted_classes :: UseSortedClasses , self :: use_strict_mode :: UseStrictMode , self :: use_trim_start_end :: UseTrimStartEnd , self :: use_valid_autocomplete :: UseValidAutocomplete ,] } } diff --git a/crates/biome_js_analyze/src/lint/nursery/use_consistent_object_definition.rs b/crates/biome_js_analyze/src/lint/nursery/use_consistent_object_definition.rs new file mode 100644 index 000000000000..a22740d3c69a --- /dev/null +++ b/crates/biome_js_analyze/src/lint/nursery/use_consistent_object_definition.rs @@ -0,0 +1,237 @@ +use biome_analyze::{ + context::RuleContext, declare_lint_rule, Ast, Rule, RuleDiagnostic, RuleSource, RuleSourceKind, +}; +use biome_console::markup; +use biome_deserialize_macros::Deserializable; +use biome_diagnostics::Severity; +use biome_js_syntax::{ + inner_string_text, AnyJsExpression, AnyJsObjectMember, AnyJsObjectMemberName, +}; +use biome_rowan::AstNode; +use serde::{Deserialize, Serialize}; + +declare_lint_rule! { + /// Require the consistent declaration of object literals. Defaults to explicit definitions. + /// + /// ECMAScript 6 provides two ways to define an object literal: `{foo: foo}` and `{foo}`. + /// The two styles are functionally equivalent. + /// Using the same style consistently across your codebase makes it easier to quickly read and understand object definitions. + /// + /// ## Example + /// + /// ### Invalid + /// + /// ```json,options + /// { + /// "options": { + /// "syntax": "explicit" + /// } + /// } + /// ``` + /// + /// ```js,expect_diagnostic,use_options + /// let foo = 1; + /// let invalid = { + /// foo + /// }; + /// ``` + /// + /// ```js,expect_diagnostic,use_options + /// let invalid = { + /// bar() { return "bar"; }, + /// }; + /// ``` + /// + /// ### Valid + /// + /// ```js,use_options + /// let foo = 1; + /// let valid = { + /// foo: foo, + /// bar: function() { return "bar"; }, + /// }; + /// ``` + /// + /// ### Invalid + /// + /// ```json,options + /// { + /// "options": { + /// "syntax": "shorthand" + /// } + /// } + /// ``` + /// + /// ```js,expect_diagnostic,use_options + /// let foo = 1; + /// let invalid = { + /// foo: foo + /// }; + /// ``` + /// + /// ```js,expect_diagnostic,use_options + /// let invalid = { + /// bar: function() { return "bar"; }, + /// }; + /// ``` + /// + /// ### Valid + /// + /// ```js,use_options + /// let foo = 1; + /// let valid = { + /// foo, + /// bar() { return "bar"; }, + /// }; + /// ``` + /// + /// ## Options + /// + /// Use the options to specify the syntax of object literals to enforce. + /// + /// ```json,options + /// { + /// "options": { + /// "syntax": "explicit" + /// } + /// } + /// ``` + /// + /// ### syntax + /// + /// The syntax to use: + /// - `explicit`: enforces the use of explicit object property syntax in every case. + /// - `shorthand`: enforces the use of shorthand object property syntax when possible. + /// + /// **Default:** `explicit` + /// + pub UseConsistentObjectDefinition { + version: "next", + name: "useConsistentObjectDefinition", + language: "js", + recommended: false, + severity: Severity::Error, + sources: &[RuleSource::Eslint("object-shorthand")], + source_kind: RuleSourceKind::Inspired, + } +} + +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, default)] +pub struct UseConsistentObjectDefinitionOptions { + /// The preferred syntax to enforce. + syntax: ObjectPropertySyntax, +} + +#[derive(Clone, Debug, Default, Deserialize, Deserializable, Eq, PartialEq, Serialize)] +#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub enum ObjectPropertySyntax { + /// `{foo: foo}` + #[default] + Explicit, + /// `{foo}` + Shorthand, +} + +impl Rule for UseConsistentObjectDefinition { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = UseConsistentObjectDefinitionOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let binding = ctx.query(); + let options = ctx.options(); + match binding { + AnyJsObjectMember::JsShorthandPropertyObjectMember(_) => match options.syntax { + // Shorthand properties should error when explicit is expected + ObjectPropertySyntax::Shorthand => None, + ObjectPropertySyntax::Explicit => Some(()), + }, + AnyJsObjectMember::JsMethodObjectMember(_) => match options.syntax { + // Shorthand methods should error when explicit is expected + ObjectPropertySyntax::Shorthand => None, + ObjectPropertySyntax::Explicit => Some(()), + }, + AnyJsObjectMember::JsPropertyObjectMember(source) => { + let value_token = source.value().ok()?; + let value_id = match value_token { + AnyJsExpression::JsIdentifierExpression(identifier_token) => { + // If expression is an identifier, get ID to compare it against the property name later + let variable_token = identifier_token.name().ok()?.value_token().ok()?; + inner_string_text(&variable_token) + } + AnyJsExpression::JsFunctionExpression(_function_token) => { + // Functions are always shorthandable + match options.syntax { + ObjectPropertySyntax::Shorthand => return Some(()), + ObjectPropertySyntax::Explicit => return None, + } + } + _ => return None, + }; + let name_token = source.name().ok()?; + match name_token { + AnyJsObjectMemberName::JsLiteralMemberName(literal_token) => { + match options.syntax { + ObjectPropertySyntax::Shorthand => { + // Throw shorthand error if the value is the same as the property name + // We use `text_trimmed` to preserve quotes when comparing, we need this + // because {foo: foo} can be shorthanded, but {"foo": foo} cannot + if literal_token.value().ok()?.text_trimmed() == value_id.trim() { + Some(()) + } else { + None + } + } + ObjectPropertySyntax::Explicit => None, + } + } + AnyJsObjectMemberName::JsComputedMemberName(_computed_token) => { + let reference_token = source.value().ok()?; + // Computed is always shorthandable if the value is a function, else never + match reference_token { + AnyJsExpression::JsFunctionExpression(_function_token) => { + match options.syntax { + ObjectPropertySyntax::Shorthand => Some(()), + ObjectPropertySyntax::Explicit => None, + } + } + _ => None, + } + } + _ => None, + } + } + _ => None, + } + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let node = ctx.query(); + let options = ctx.options(); + + let title = match options.syntax { + ObjectPropertySyntax::Shorthand => { + "Do not use explicit object property syntax when shorthand syntax is possible." + } + ObjectPropertySyntax::Explicit => "Do not use shorthand object property syntax.", + }; + + let note = match options.syntax { + ObjectPropertySyntax::Shorthand => { + "Using shorthand object property syntax makes object definitions more concise." + } + ObjectPropertySyntax::Explicit => { + "Using explicit object property syntax makes object definitions more readable and consistent." + } + }; + + Some( + RuleDiagnostic::new(rule_category!(), node.range(), markup! {{title}}) + .note(markup! {{note}}), + ) + } +} diff --git a/crates/biome_js_analyze/src/options.rs b/crates/biome_js_analyze/src/options.rs index 23b9bad77ca3..ba59a90fe681 100644 --- a/crates/biome_js_analyze/src/options.rs +++ b/crates/biome_js_analyze/src/options.rs @@ -325,6 +325,7 @@ pub type UseConsistentArrayType = < lint :: style :: use_consistent_array_type : pub type UseConsistentBuiltinInstantiation = < lint :: style :: use_consistent_builtin_instantiation :: UseConsistentBuiltinInstantiation as biome_analyze :: Rule > :: Options ; pub type UseConsistentCurlyBraces = < lint :: nursery :: use_consistent_curly_braces :: UseConsistentCurlyBraces as biome_analyze :: Rule > :: Options ; pub type UseConsistentMemberAccessibility = < lint :: nursery :: use_consistent_member_accessibility :: UseConsistentMemberAccessibility as biome_analyze :: Rule > :: Options ; +pub type UseConsistentObjectDefinition = < lint :: nursery :: use_consistent_object_definition :: UseConsistentObjectDefinition 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 ; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js new file mode 100644 index 000000000000..ea192cc46897 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js @@ -0,0 +1,20 @@ +const invalidExplicit = { + // Basic property shorthand violations + foo: foo, + bar: bar, + baz: baz, + + // Method shorthand violations + method: function () { return "method"; }, + async: async function () { return "async"; }, + generator: function* () { yield "gen"; }, + asyncGenerator: async function* () { yield "async gen"; }, + + // Computed methods shorthand violations + [computed]: function () { return "computed"; }, + [computed]: async function () { return "async computed"; }, + [computed]: function* () { yield "computed gen"; }, + ["computed-string"]: function () { return "computed string"; }, + ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + [computed()]: function () { return "computed dynamic"; }, +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js.snap new file mode 100644 index 000000000000..89729a97884a --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.js.snap @@ -0,0 +1,248 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalidExplicit.js +--- +# Input +```js +const invalidExplicit = { + // Basic property shorthand violations + foo: foo, + bar: bar, + baz: baz, + + // Method shorthand violations + method: function () { return "method"; }, + async: async function () { return "async"; }, + generator: function* () { yield "gen"; }, + asyncGenerator: async function* () { yield "async gen"; }, + + // Computed methods shorthand violations + [computed]: function () { return "computed"; }, + [computed]: async function () { return "async computed"; }, + [computed]: function* () { yield "computed gen"; }, + ["computed-string"]: function () { return "computed string"; }, + ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + [computed()]: function () { return "computed dynamic"; }, +}; + +``` + +# Diagnostics +``` +invalidExplicit.js:3:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 1 │ const invalidExplicit = { + 2 │ // Basic property shorthand violations + > 3 │ foo: foo, + │ ^^^^^^^^ + 4 │ bar: bar, + 5 │ baz: baz, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:4:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 2 │ // Basic property shorthand violations + 3 │ foo: foo, + > 4 │ bar: bar, + │ ^^^^^^^^ + 5 │ baz: baz, + 6 │ + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:5:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 3 │ foo: foo, + 4 │ bar: bar, + > 5 │ baz: baz, + │ ^^^^^^^^ + 6 │ + 7 │ // Method shorthand violations + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:8:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 7 │ // Method shorthand violations + > 8 │ method: function () { return "method"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 9 │ async: async function () { return "async"; }, + 10 │ generator: function* () { yield "gen"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:9:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 7 │ // Method shorthand violations + 8 │ method: function () { return "method"; }, + > 9 │ async: async function () { return "async"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ generator: function* () { yield "gen"; }, + 11 │ asyncGenerator: async function* () { yield "async gen"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:10:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 8 │ method: function () { return "method"; }, + 9 │ async: async function () { return "async"; }, + > 10 │ generator: function* () { yield "gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ asyncGenerator: async function* () { yield "async gen"; }, + 12 │ + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:11:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 9 │ async: async function () { return "async"; }, + 10 │ generator: function* () { yield "gen"; }, + > 11 │ asyncGenerator: async function* () { yield "async gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 12 │ + 13 │ // Computed methods shorthand violations + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:14:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 13 │ // Computed methods shorthand violations + > 14 │ [computed]: function () { return "computed"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 15 │ [computed]: async function () { return "async computed"; }, + 16 │ [computed]: function* () { yield "computed gen"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:15:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 13 │ // Computed methods shorthand violations + 14 │ [computed]: function () { return "computed"; }, + > 15 │ [computed]: async function () { return "async computed"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ [computed]: function* () { yield "computed gen"; }, + 17 │ ["computed-string"]: function () { return "computed string"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:16:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 14 │ [computed]: function () { return "computed"; }, + 15 │ [computed]: async function () { return "async computed"; }, + > 16 │ [computed]: function* () { yield "computed gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 17 │ ["computed-string"]: function () { return "computed string"; }, + 18 │ ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:17:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 15 │ [computed]: async function () { return "async computed"; }, + 16 │ [computed]: function* () { yield "computed gen"; }, + > 17 │ ["computed-string"]: function () { return "computed string"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 │ ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + 19 │ [computed()]: function () { return "computed dynamic"; }, + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:18:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 16 │ [computed]: function* () { yield "computed gen"; }, + 17 │ ["computed-string"]: function () { return "computed string"; }, + > 18 │ ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 19 │ [computed()]: function () { return "computed dynamic"; }, + 20 │ }; + + i Using shorthand object property syntax makes object definitions more concise. + + +``` + +``` +invalidExplicit.js:19:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use explicit object property syntax when shorthand syntax is possible. + + 17 │ ["computed-string"]: function () { return "computed string"; }, + 18 │ ["comp" + "uted" + "-con" + "cat"]: function () { return "computed concat"; }, + > 19 │ [computed()]: function () { return "computed dynamic"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 20 │ }; + 21 │ + + i Using shorthand object property syntax makes object definitions more concise. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.options.json b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.options.json new file mode 100644 index 000000000000..746d3b469c7f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidExplicit.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "nursery": { + "useConsistentObjectDefinition": { + "level": "error", + "options": { + "syntax": "shorthand" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js new file mode 100644 index 000000000000..6862fecc0545 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js @@ -0,0 +1,24 @@ +const invalidShorthand = { + // Basic property explicit violations + prop, + shortProp, + + // Method explicit violations + method() { return "method"; }, + async async() { return "async"; }, + *generator() { yield "gen"; }, + async *asyncGenerator() { yield "async gen"; }, + + // Computed methods + [computed]() { return "computed"; }, + async [computed]() { return "async computed"; }, + *[computed]() { yield "computed gen"; }, + ["computed-string"]() { return "computed string"; }, + ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + [computed()]() { return "computed dynamic"; }, + + // String literal methods + 'quotedMethod'() { return "quoted"; }, + "doubleQuoted"() { return "double quoted"; }, + async 'asyncQuoted'() { return "async quoted"; }, +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js.snap new file mode 100644 index 000000000000..357a1da6fc25 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.js.snap @@ -0,0 +1,285 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: invalidShorthand.js +--- +# Input +```js +const invalidShorthand = { + // Basic property explicit violations + prop, + shortProp, + + // Method explicit violations + method() { return "method"; }, + async async() { return "async"; }, + *generator() { yield "gen"; }, + async *asyncGenerator() { yield "async gen"; }, + + // Computed methods + [computed]() { return "computed"; }, + async [computed]() { return "async computed"; }, + *[computed]() { yield "computed gen"; }, + ["computed-string"]() { return "computed string"; }, + ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + [computed()]() { return "computed dynamic"; }, + + // String literal methods + 'quotedMethod'() { return "quoted"; }, + "doubleQuoted"() { return "double quoted"; }, + async 'asyncQuoted'() { return "async quoted"; }, +}; + +``` + +# Diagnostics +``` +invalidShorthand.js:3:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 1 │ const invalidShorthand = { + 2 │ // Basic property explicit violations + > 3 │ prop, + │ ^^^^ + 4 │ shortProp, + 5 │ + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:4:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 2 │ // Basic property explicit violations + 3 │ prop, + > 4 │ shortProp, + │ ^^^^^^^^^ + 5 │ + 6 │ // Method explicit violations + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:7:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 6 │ // Method explicit violations + > 7 │ method() { return "method"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 8 │ async async() { return "async"; }, + 9 │ *generator() { yield "gen"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:8:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 6 │ // Method explicit violations + 7 │ method() { return "method"; }, + > 8 │ async async() { return "async"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 9 │ *generator() { yield "gen"; }, + 10 │ async *asyncGenerator() { yield "async gen"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:9:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 7 │ method() { return "method"; }, + 8 │ async async() { return "async"; }, + > 9 │ *generator() { yield "gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 10 │ async *asyncGenerator() { yield "async gen"; }, + 11 │ + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:10:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 8 │ async async() { return "async"; }, + 9 │ *generator() { yield "gen"; }, + > 10 │ async *asyncGenerator() { yield "async gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 11 │ + 12 │ // Computed methods + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:13:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 12 │ // Computed methods + > 13 │ [computed]() { return "computed"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 14 │ async [computed]() { return "async computed"; }, + 15 │ *[computed]() { yield "computed gen"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:14:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 12 │ // Computed methods + 13 │ [computed]() { return "computed"; }, + > 14 │ async [computed]() { return "async computed"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 15 │ *[computed]() { yield "computed gen"; }, + 16 │ ["computed-string"]() { return "computed string"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:15:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 13 │ [computed]() { return "computed"; }, + 14 │ async [computed]() { return "async computed"; }, + > 15 │ *[computed]() { yield "computed gen"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 16 │ ["computed-string"]() { return "computed string"; }, + 17 │ ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:16:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 14 │ async [computed]() { return "async computed"; }, + 15 │ *[computed]() { yield "computed gen"; }, + > 16 │ ["computed-string"]() { return "computed string"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 17 │ ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + 18 │ [computed()]() { return "computed dynamic"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:17:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 15 │ *[computed]() { yield "computed gen"; }, + 16 │ ["computed-string"]() { return "computed string"; }, + > 17 │ ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 18 │ [computed()]() { return "computed dynamic"; }, + 19 │ + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:18:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 16 │ ["computed-string"]() { return "computed string"; }, + 17 │ ["comp" + "uted" + "-con" + "cat"]() { return "computed concat"; }, + > 18 │ [computed()]() { return "computed dynamic"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 19 │ + 20 │ // String literal methods + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:21:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 20 │ // String literal methods + > 21 │ 'quotedMethod'() { return "quoted"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 22 │ "doubleQuoted"() { return "double quoted"; }, + 23 │ async 'asyncQuoted'() { return "async quoted"; }, + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:22:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 20 │ // String literal methods + 21 │ 'quotedMethod'() { return "quoted"; }, + > 22 │ "doubleQuoted"() { return "double quoted"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 23 │ async 'asyncQuoted'() { return "async quoted"; }, + 24 │ }; + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` + +``` +invalidShorthand.js:23:5 lint/nursery/useConsistentObjectDefinition ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + ! Do not use shorthand object property syntax. + + 21 │ 'quotedMethod'() { return "quoted"; }, + 22 │ "doubleQuoted"() { return "double quoted"; }, + > 23 │ async 'asyncQuoted'() { return "async quoted"; }, + │ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 24 │ }; + 25 │ + + i Using explicit object property syntax makes object definitions more readable and consistent. + + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.options.json b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.options.json new file mode 100644 index 000000000000..c4c21e3b4e8f --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/invalidShorthand.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "nursery": { + "useConsistentObjectDefinition": { + "level": "error", + "options": { + "syntax": "explicit" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js new file mode 100644 index 000000000000..839fc0ffa4d5 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js @@ -0,0 +1,44 @@ +const validExplicit = { + // Properties with explicit values + foo: foo, + bar: bar, + baz: baz, + + // Methods with function expressions + method: function () { return "method"; }, + async: async function () { return "async"; }, + generator: function* () { yield "gen"; }, + asyncGenerator: async function* () { yield "async gen"; }, + + // Computed methods + [computed]: function () { return "computed"; }, + [computed]: async function () { return "async computed"; }, + + // Under this sections should go properties that can't be shorthanded + // Meaning they are valid with either explicit or shorthand property option + + // String literals + "stringLiteral": "stringLiteral", + "quotedProperty": quotedProperty, + 'singleQuoted': singleQuoted, + + // Call expressions + call: example(), + callLiteral: "example"(), + + // Computed properties + [dynamic()]: dynamicValue, + [computed]: computed, + ["computed-string"]: computedString, + + // Arrow functions + arrow: () => "arrow", + arrowWithBlock: () => { return "arrow block"; }, + asyncArrow: async () => "async arrow", + + // Accessors + get getter() { return "getter"; }, + set setter(value) { this._setter = value; }, + + ...spread, +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js.snap new file mode 100644 index 000000000000..128e444a2200 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.js.snap @@ -0,0 +1,52 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: validExplicit.js +--- +# Input +```js +const validExplicit = { + // Properties with explicit values + foo: foo, + bar: bar, + baz: baz, + + // Methods with function expressions + method: function () { return "method"; }, + async: async function () { return "async"; }, + generator: function* () { yield "gen"; }, + asyncGenerator: async function* () { yield "async gen"; }, + + // Computed methods + [computed]: function () { return "computed"; }, + [computed]: async function () { return "async computed"; }, + + // Under this sections should go properties that can't be shorthanded + // Meaning they are valid with either explicit or shorthand property option + + // String literals + "stringLiteral": "stringLiteral", + "quotedProperty": quotedProperty, + 'singleQuoted': singleQuoted, + + // Call expressions + call: example(), + callLiteral: "example"(), + + // Computed properties + [dynamic()]: dynamicValue, + [computed]: computed, + ["computed-string"]: computedString, + + // Arrow functions + arrow: () => "arrow", + arrowWithBlock: () => { return "arrow block"; }, + asyncArrow: async () => "async arrow", + + // Accessors + get getter() { return "getter"; }, + set setter(value) { this._setter = value; }, + + ...spread, +}; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.options.json b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.options.json new file mode 100644 index 000000000000..0e77667554a3 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validExplicit.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "nursery": { + "useConsistentObjectDefinition": { + "level": "error", + "options": { + "syntax": "explicit" + } + } + } + } + } +} diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js new file mode 100644 index 000000000000..6d97be010593 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js @@ -0,0 +1,50 @@ +const validShorthand = { + // Property shorthand + foo, + bar, + baz, + + // Method shorthand + method() { return "method"; }, + async async() { return "async"; }, + *generator() { yield "gen"; }, + async *asyncGenerator() { yield "async gen"; }, + + // String literal methods + 'quotedMethod'() { return "quoted"; }, + "doubleQuoted"() { return "double quoted"; }, + async 'asyncQuoted'() { return "async quoted"; }, + + // Computed methods + [computed]() { return "computed"; }, + async [computed]() { return "async computed"; }, + *[computed]() { yield "computed gen"; }, + + // Under this sections should go properties that can't be shorthanded + // Meaning they are valid with either explicit or shorthand property option + + // String literals + "stringLiteral": "stringLiteral", + "quotedProperty": quotedProperty, + 'singleQuoted': singleQuoted, + + // Call expressions + call: example(), + callLiteral: "example"(), + + // Computed properties + [dynamic()]: dynamicValue, + [computed]: computed, + ["computed-string"]: computedString, + + // Arrow functions + arrow: () => "arrow", + arrowWithBlock: () => { return "arrow block"; }, + asyncArrow: async () => "async arrow", + + // Accessors + get getter() { return "getter"; }, + set setter(value) { this._setter = value; }, + + ...spread, +}; diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js.snap b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js.snap new file mode 100644 index 000000000000..e30d0ca9b27c --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.js.snap @@ -0,0 +1,58 @@ +--- +source: crates/biome_js_analyze/tests/spec_tests.rs +expression: validShorthand.js +--- +# Input +```js +const validShorthand = { + // Property shorthand + foo, + bar, + baz, + + // Method shorthand + method() { return "method"; }, + async async() { return "async"; }, + *generator() { yield "gen"; }, + async *asyncGenerator() { yield "async gen"; }, + + // String literal methods + 'quotedMethod'() { return "quoted"; }, + "doubleQuoted"() { return "double quoted"; }, + async 'asyncQuoted'() { return "async quoted"; }, + + // Computed methods + [computed]() { return "computed"; }, + async [computed]() { return "async computed"; }, + *[computed]() { yield "computed gen"; }, + + // Under this sections should go properties that can't be shorthanded + // Meaning they are valid with either explicit or shorthand property option + + // String literals + "stringLiteral": "stringLiteral", + "quotedProperty": quotedProperty, + 'singleQuoted': singleQuoted, + + // Call expressions + call: example(), + callLiteral: "example"(), + + // Computed properties + [dynamic()]: dynamicValue, + [computed]: computed, + ["computed-string"]: computedString, + + // Arrow functions + arrow: () => "arrow", + arrowWithBlock: () => { return "arrow block"; }, + asyncArrow: async () => "async arrow", + + // Accessors + get getter() { return "getter"; }, + set setter(value) { this._setter = value; }, + + ...spread, +}; + +``` diff --git a/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.options.json b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.options.json new file mode 100644 index 000000000000..5ca7d68645b1 --- /dev/null +++ b/crates/biome_js_analyze/tests/specs/nursery/useConsistentObjectDefinition/validShorthand.options.json @@ -0,0 +1,16 @@ +{ + "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", + "linter": { + "enabled": true, + "rules": { + "nursery": { + "useConsistentObjectDefinition": { + "level": "error", + "options": { + "syntax": "shorthand" + } + } + } + } + } +} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 806b7573dc09..039a935cb580 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -1653,6 +1653,10 @@ export interface Nursery { * Require consistent accessibility modifiers on class properties and methods. */ useConsistentMemberAccessibility?: RuleConfiguration_for_ConsistentMemberAccessibilityOptions; + /** + * Require the consistent declaration of object literals. Defaults to explicit definitions. + */ + useConsistentObjectDefinition?: RuleConfiguration_for_UseConsistentObjectDefinitionOptions; /** * Require specifying the reason argument when using @deprecated directive */ @@ -2306,6 +2310,9 @@ export type RuleConfiguration_for_UseComponentExportOnlyModulesOptions = export type RuleConfiguration_for_ConsistentMemberAccessibilityOptions = | RulePlainConfiguration | RuleWithOptions_for_ConsistentMemberAccessibilityOptions; +export type RuleConfiguration_for_UseConsistentObjectDefinitionOptions = + | RulePlainConfiguration + | RuleWithOptions_for_UseConsistentObjectDefinitionOptions; export type RuleFixConfiguration_for_UtilityClassSortingOptions = | RulePlainConfiguration | RuleWithFixOptions_for_UtilityClassSortingOptions; @@ -2528,6 +2535,16 @@ export interface RuleWithOptions_for_ConsistentMemberAccessibilityOptions { */ options: ConsistentMemberAccessibilityOptions; } +export interface RuleWithOptions_for_UseConsistentObjectDefinitionOptions { + /** + * The severity of the emitted diagnostics by the rule + */ + level: RulePlainConfiguration; + /** + * Rule's options + */ + options: UseConsistentObjectDefinitionOptions; +} export interface RuleWithFixOptions_for_UtilityClassSortingOptions { /** * The kind of the code actions emitted by the rule @@ -2767,6 +2784,12 @@ export interface UseComponentExportOnlyModulesOptions { export interface ConsistentMemberAccessibilityOptions { accessibility?: Accessibility; } +export interface UseConsistentObjectDefinitionOptions { + /** + * The preferred syntax to enforce. + */ + syntax?: ObjectPropertySyntax; +} export interface UtilityClassSortingOptions { /** * Additional attributes that will be sorted. @@ -2889,6 +2912,7 @@ For example, for React's `useRef()` hook the value would be `true`, while for `u stableResult?: StableHookResult; } export type Accessibility = "noPublic" | "explicit" | "none"; +export type ObjectPropertySyntax = "explicit" | "shorthand"; export type ConsistentArrayType = "shorthand" | "generic"; export type FilenameCases = FilenameCase[]; export type Regex = string; @@ -3203,6 +3227,7 @@ export type Category = | "lint/nursery/useComponentExportOnlyModules" | "lint/nursery/useConsistentCurlyBraces" | "lint/nursery/useConsistentMemberAccessibility" + | "lint/nursery/useConsistentObjectDefinition" | "lint/nursery/useDeprecatedReason" | "lint/nursery/useExplicitFunctionReturnType" | "lint/nursery/useExplicitType" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index 55d84270eecd..c2c99ea70096 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -2817,6 +2817,15 @@ { "type": "null" } ] }, + "useConsistentObjectDefinition": { + "description": "Require the consistent declaration of object literals. Defaults to explicit definitions.", + "anyOf": [ + { + "$ref": "#/definitions/UseConsistentObjectDefinitionConfiguration" + }, + { "type": "null" } + ] + }, "useDeprecatedReason": { "description": "Require specifying the reason argument when using @deprecated directive", "anyOf": [ @@ -2911,6 +2920,16 @@ }, "additionalProperties": false }, + "ObjectPropertySyntax": { + "oneOf": [ + { + "description": "`{foo: foo}`", + "type": "string", + "enum": ["explicit"] + }, + { "description": "`{foo}`", "type": "string", "enum": ["shorthand"] } + ] + }, "ObjectWrap": { "type": "string", "enum": ["preserve", "collapse"] }, "Options": { "type": "object", @@ -3619,6 +3638,23 @@ }, "additionalProperties": false }, + "RuleWithUseConsistentObjectDefinitionOptions": { + "type": "object", + "required": ["level"], + "properties": { + "level": { + "description": "The severity of the emitted diagnostics by the rule", + "allOf": [{ "$ref": "#/definitions/RulePlainConfiguration" }] + }, + "options": { + "description": "Rule's options", + "allOf": [ + { "$ref": "#/definitions/UseConsistentObjectDefinitionOptions" } + ] + } + }, + "additionalProperties": false + }, "RuleWithUseExhaustiveDependenciesOptions": { "type": "object", "required": ["level"], @@ -4858,6 +4894,23 @@ }, "additionalProperties": false }, + "UseConsistentObjectDefinitionConfiguration": { + "anyOf": [ + { "$ref": "#/definitions/RulePlainConfiguration" }, + { "$ref": "#/definitions/RuleWithUseConsistentObjectDefinitionOptions" } + ] + }, + "UseConsistentObjectDefinitionOptions": { + "type": "object", + "properties": { + "syntax": { + "description": "The preferred syntax to enforce.", + "default": "explicit", + "allOf": [{ "$ref": "#/definitions/ObjectPropertySyntax" }] + } + }, + "additionalProperties": false + }, "UseExhaustiveDependenciesConfiguration": { "anyOf": [ { "$ref": "#/definitions/RulePlainConfiguration" },