diff --git a/.changeset/wild-dolls-talk.md b/.changeset/wild-dolls-talk.md new file mode 100644 index 000000000000..2217a2640482 --- /dev/null +++ b/.changeset/wild-dolls-talk.md @@ -0,0 +1,13 @@ +--- +"@biomejs/biome": patch +--- + +Added the nursery rule [`noEmptyObjectKeys`](https://biomejs.dev/linter/rules/no-empty-object-keys/), which disallows the use of empty keys in JSON objects. + +**Invalid:** + +```json +{ + "": "value" +} +``` diff --git a/Cargo.lock b/Cargo.lock index c77f87ffcaf6..78bb0d2a5896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1144,6 +1144,7 @@ dependencies = [ "biome_analyze_macros", "biome_configuration", "biome_console", + "biome_deserialize", "biome_diagnostics", "biome_json_factory", "biome_json_parser", 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 84edb1c32fed..faf8a615d3d7 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 @@ -1473,6 +1473,18 @@ pub(crate) fn migrate_eslint_any_rule( .get_or_insert(Default::default()); rule.set_level(rule.level().max(rule_severity.into())); } + "json/no-empty-keys" => { + 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_empty_object_keys + .get_or_insert(Default::default()); + rule.set_level(rule.level().max(rule_severity.into())); + } "json/top-level-interop" => { if !options.include_nursery { results.add(eslint_name, eslint_to_biome::RuleMigrationResult::Nursery); diff --git a/crates/biome_configuration/src/analyzer/linter/rules.rs b/crates/biome_configuration/src/analyzer/linter/rules.rs index 7e8bbd6c6d0f..1692da63e25e 100644 --- a/crates/biome_configuration/src/analyzer/linter/rules.rs +++ b/crates/biome_configuration/src/analyzer/linter/rules.rs @@ -170,6 +170,7 @@ pub enum RuleName { NoEmptyBlockStatements, NoEmptyCharacterClassInRegex, NoEmptyInterface, + NoEmptyObjectKeys, NoEmptyPattern, NoEmptySource, NoEmptyTypeParameters, @@ -632,6 +633,7 @@ impl RuleName { Self::NoEmptyBlockStatements => "noEmptyBlockStatements", Self::NoEmptyCharacterClassInRegex => "noEmptyCharacterClassInRegex", Self::NoEmptyInterface => "noEmptyInterface", + Self::NoEmptyObjectKeys => "noEmptyObjectKeys", Self::NoEmptyPattern => "noEmptyPattern", Self::NoEmptySource => "noEmptySource", Self::NoEmptyTypeParameters => "noEmptyTypeParameters", @@ -1098,6 +1100,7 @@ impl RuleName { Self::NoEmptyBlockStatements => RuleGroup::Suspicious, Self::NoEmptyCharacterClassInRegex => RuleGroup::Correctness, Self::NoEmptyInterface => RuleGroup::Suspicious, + Self::NoEmptyObjectKeys => RuleGroup::Nursery, Self::NoEmptyPattern => RuleGroup::Correctness, Self::NoEmptySource => RuleGroup::Suspicious, Self::NoEmptyTypeParameters => RuleGroup::Complexity, @@ -1565,6 +1568,7 @@ impl std::str::FromStr for RuleName { "noEmptyBlockStatements" => Ok(Self::NoEmptyBlockStatements), "noEmptyCharacterClassInRegex" => Ok(Self::NoEmptyCharacterClassInRegex), "noEmptyInterface" => Ok(Self::NoEmptyInterface), + "noEmptyObjectKeys" => Ok(Self::NoEmptyObjectKeys), "noEmptyPattern" => Ok(Self::NoEmptyPattern), "noEmptySource" => Ok(Self::NoEmptySource), "noEmptyTypeParameters" => Ok(Self::NoEmptyTypeParameters), diff --git a/crates/biome_diagnostics_categories/src/categories.rs b/crates/biome_diagnostics_categories/src/categories.rs index fe889cccb9d4..6eeb63c77ee0 100644 --- a/crates/biome_diagnostics_categories/src/categories.rs +++ b/crates/biome_diagnostics_categories/src/categories.rs @@ -189,6 +189,7 @@ define_categories! { "lint/nursery/noDuplicateInputFieldNames": "https://biomejs.dev/linter/rules/no-duplicate-input-field-names", "lint/nursery/noDuplicateVariableNames": "https://biomejs.dev/linter/rules/no-duplicate-variable-names", "lint/nursery/noDuplicatedSpreadProps": "https://biomejs.dev/linter/rules/no-duplicated-spread-props", + "lint/nursery/noEmptyObjectKeys": "https://biomejs.dev/linter/rules/no-empty-object-keys", "lint/nursery/noEqualsToNull": "https://biomejs.dev/linter/rules/no-equals-to-null", "lint/nursery/noExcessiveClassesPerFile": "https://biomejs.dev/linter/rules/no-excessive-classes-per-file", "lint/nursery/noExcessiveLinesPerFile": "https://biomejs.dev/linter/rules/no-excessive-lines-per-file", diff --git a/crates/biome_json_analyze/Cargo.toml b/crates/biome_json_analyze/Cargo.toml index b226cf23727f..286653753ca7 100644 --- a/crates/biome_json_analyze/Cargo.toml +++ b/crates/biome_json_analyze/Cargo.toml @@ -21,6 +21,7 @@ name = "json_analyzer" biome_analyze = { workspace = true } biome_analyze_macros = { workspace = true } biome_console = { workspace = true } +biome_deserialize = { workspace = true } biome_diagnostics = { workspace = true } biome_json_factory = { workspace = true } biome_json_syntax = { workspace = true } diff --git a/crates/biome_json_analyze/src/lint/nursery/no_empty_object_keys.rs b/crates/biome_json_analyze/src/lint/nursery/no_empty_object_keys.rs new file mode 100644 index 000000000000..7b1a8adddf7d --- /dev/null +++ b/crates/biome_json_analyze/src/lint/nursery/no_empty_object_keys.rs @@ -0,0 +1,101 @@ +use biome_analyze::{ + Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, +}; +use biome_console::markup; +use biome_deserialize::json::unescape_json_string; +use biome_json_syntax::JsonMemberName; +use biome_rowan::AstNode; +use biome_rule_options::no_empty_object_keys::NoEmptyObjectKeysOptions; + +declare_lint_rule! { + /// Disallow empty keys in JSON objects. + /// + /// In JSON, using empty keys (keys that are empty strings or contain only whitespace) can lead to accessibility and maintenance issues. + /// While technically valid in JSON, empty keys make objects harder to read, can cause confusion when debugging, and may create problems with some JSON parsers or processors. + /// Additionally, empty keys often indicate mistakes or oversights in the processes. + /// + /// ## Examples + /// + /// ### Invalid + /// + /// ```json,expect_diagnostic + /// { + /// "": "value" + /// } + /// ``` + /// + /// ```json,expect_diagnostic + /// { + /// "validKey": "value", + /// "": "another value" + /// } + /// ``` + /// + /// ```json,expect_diagnostic + /// { + /// " ": "space as key" + /// } + /// ``` + /// + /// ```json,expect_diagnostic + /// { + /// "\t": "tab as key" + /// } + /// ``` + /// + /// ```json,expect_diagnostic + /// { + /// "\n": "newline as key" + /// } + /// ``` + /// + /// ### Valid + /// + /// ```json + /// { + /// "key": "value" + /// } + /// ``` + /// + pub NoEmptyObjectKeys { + version: "next", + name: "noEmptyObjectKeys", + language: "json", + recommended: false, + sources: &[RuleSource::EslintJson("no-empty-keys").same()], + } +} + +impl Rule for NoEmptyObjectKeys { + type Query = Ast; + type State = (); + type Signals = Option; + type Options = NoEmptyObjectKeysOptions; + + fn run(ctx: &RuleContext) -> Self::Signals { + let node = ctx.query(); + let value = node.inner_string_text().ok()?; + let binding = unescape_json_string(value); + if binding.trim().is_empty() { + return Some(()); + } + + None + } + + fn diagnostic(ctx: &RuleContext, _state: &Self::State) -> Option { + let span = ctx.query().range(); + Some( + RuleDiagnostic::new( + rule_category!(), + span, + markup! { + "Unexpected empty object key." + }, + ) + .note(markup! { + "Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name." + }), + ) + } +} diff --git a/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json new file mode 100644 index 000000000000..5ec2830d1ff4 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json @@ -0,0 +1,7 @@ +{ + "": "another value", + " ": "space as key", + "\t": "tab as key", + "\n": "newline as key", + "\n\n\n": "multi newline as key" +} diff --git a/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json.snap b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json.snap new file mode 100644 index 000000000000..82f80a3ff3e1 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/invalid.json.snap @@ -0,0 +1,110 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +expression: invalid.json +--- +# Input +```json +{ + "": "another value", + " ": "space as key", + "\t": "tab as key", + "\n": "newline as key", + "\n\n\n": "multi newline as key" +} + +``` + +# Diagnostics +``` +invalid.json:2:3 lint/nursery/noEmptyObjectKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected empty object key. + + 1 │ { + > 2 │ "": "another value", + │ ^^ + 3 │ " ": "space as key", + 4 │ "\t": "tab as key", + + i Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name. + + 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. + + +``` + +``` +invalid.json:3:3 lint/nursery/noEmptyObjectKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected empty object key. + + 1 │ { + 2 │ "": "another value", + > 3 │ " ": "space as key", + │ ^^^ + 4 │ "\t": "tab as key", + 5 │ "\n": "newline as key", + + i Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name. + + 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. + + +``` + +``` +invalid.json:4:3 lint/nursery/noEmptyObjectKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected empty object key. + + 2 │ "": "another value", + 3 │ " ": "space as key", + > 4 │ "\t": "tab as key", + │ ^^^^ + 5 │ "\n": "newline as key", + 6 │ "\n\n\n": "multi newline as key" + + i Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name. + + 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. + + +``` + +``` +invalid.json:5:3 lint/nursery/noEmptyObjectKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected empty object key. + + 3 │ " ": "space as key", + 4 │ "\t": "tab as key", + > 5 │ "\n": "newline as key", + │ ^^^^ + 6 │ "\n\n\n": "multi newline as key" + 7 │ } + + i Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name. + + 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. + + +``` + +``` +invalid.json:6:3 lint/nursery/noEmptyObjectKeys ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + i Unexpected empty object key. + + 4 │ "\t": "tab as key", + 5 │ "\n": "newline as key", + > 6 │ "\n\n\n": "multi newline as key" + │ ^^^^^^^^ + 7 │ } + 8 │ + + i Empty keys often cause confusion and may cause issues with parsers or processors. Either remove this property or provide a meaningful key name. + + 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_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json new file mode 100644 index 000000000000..c481f133dada --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json @@ -0,0 +1,7 @@ +{ + "key1": "value1", + "key2": { + "nestedKey": "nested value", + " nestedKey \n\t ": "nested value" + } +} diff --git a/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json.snap b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json.snap new file mode 100644 index 000000000000..3e75955766c4 --- /dev/null +++ b/crates/biome_json_analyze/tests/specs/nursery/noEmptyObjectKeys/valid.json.snap @@ -0,0 +1,15 @@ +--- +source: crates/biome_json_analyze/tests/spec_tests.rs +expression: valid.json +--- +# Input +```json +{ + "key1": "value1", + "key2": { + "nestedKey": "nested value", + " nestedKey \n\t ": "nested value" + } +} + +``` diff --git a/crates/biome_rule_options/src/lib.rs b/crates/biome_rule_options/src/lib.rs index 3e86f88791c4..a5f010a68e4a 100644 --- a/crates/biome_rule_options/src/lib.rs +++ b/crates/biome_rule_options/src/lib.rs @@ -84,6 +84,7 @@ pub mod no_empty_block; pub mod no_empty_block_statements; pub mod no_empty_character_class_in_regex; pub mod no_empty_interface; +pub mod no_empty_object_keys; pub mod no_empty_pattern; pub mod no_empty_source; pub mod no_empty_type_parameters; diff --git a/crates/biome_rule_options/src/no_empty_object_keys.rs b/crates/biome_rule_options/src/no_empty_object_keys.rs new file mode 100644 index 000000000000..3198c2085e57 --- /dev/null +++ b/crates/biome_rule_options/src/no_empty_object_keys.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 NoEmptyObjectKeysOptions {} diff --git a/packages/@biomejs/backend-jsonrpc/src/workspace.ts b/packages/@biomejs/backend-jsonrpc/src/workspace.ts index 8b41272c70c8..eed46b4a2bc9 100644 --- a/packages/@biomejs/backend-jsonrpc/src/workspace.ts +++ b/packages/@biomejs/backend-jsonrpc/src/workspace.ts @@ -2111,6 +2111,11 @@ See https://biomejs.dev/linter/rules/no-duplicated-spread-props */ noDuplicatedSpreadProps?: NoDuplicatedSpreadPropsConfiguration; /** + * Disallow empty keys in JSON objects. +See https://biomejs.dev/linter/rules/no-empty-object-keys + */ + noEmptyObjectKeys?: NoEmptyObjectKeysConfiguration; + /** * Require the use of === or !== for comparison with null. See https://biomejs.dev/linter/rules/no-equals-to-null */ @@ -4107,6 +4112,9 @@ export type NoDuplicateVariableNamesConfiguration = export type NoDuplicatedSpreadPropsConfiguration = | RulePlainConfiguration | RuleWithNoDuplicatedSpreadPropsOptions; +export type NoEmptyObjectKeysConfiguration = + | RulePlainConfiguration + | RuleWithNoEmptyObjectKeysOptions; export type NoEqualsToNullConfiguration = | RulePlainConfiguration | RuleWithNoEqualsToNullOptions; @@ -5751,6 +5759,10 @@ export interface RuleWithNoDuplicatedSpreadPropsOptions { level: RulePlainConfiguration; options?: NoDuplicatedSpreadPropsOptions; } +export interface RuleWithNoEmptyObjectKeysOptions { + level: RulePlainConfiguration; + options?: NoEmptyObjectKeysOptions; +} export interface RuleWithNoEqualsToNullOptions { fix?: FixKind; level: RulePlainConfiguration; @@ -7335,6 +7347,7 @@ export type NoDuplicateGraphqlOperationNameOptions = {}; export type NoDuplicateInputFieldNamesOptions = {}; export type NoDuplicateVariableNamesOptions = {}; export type NoDuplicatedSpreadPropsOptions = {}; +export type NoEmptyObjectKeysOptions = {}; export type NoEqualsToNullOptions = {}; export interface NoExcessiveClassesPerFileOptions { /** @@ -8376,6 +8389,7 @@ export type Category = | "lint/nursery/noDuplicateInputFieldNames" | "lint/nursery/noDuplicateVariableNames" | "lint/nursery/noDuplicatedSpreadProps" + | "lint/nursery/noEmptyObjectKeys" | "lint/nursery/noEqualsToNull" | "lint/nursery/noExcessiveClassesPerFile" | "lint/nursery/noExcessiveLinesPerFile" diff --git a/packages/@biomejs/biome/configuration_schema.json b/packages/@biomejs/biome/configuration_schema.json index aad037dd5f11..4ddaaf592894 100644 --- a/packages/@biomejs/biome/configuration_schema.json +++ b/packages/@biomejs/biome/configuration_schema.json @@ -3441,6 +3441,16 @@ "type": "object", "additionalProperties": false }, + "NoEmptyObjectKeysConfiguration": { + "oneOf": [ + { "$ref": "#/$defs/RulePlainConfiguration" }, + { "$ref": "#/$defs/RuleWithNoEmptyObjectKeysOptions" } + ] + }, + "NoEmptyObjectKeysOptions": { + "type": "object", + "additionalProperties": false + }, "NoEmptyPatternConfiguration": { "oneOf": [ { "$ref": "#/$defs/RulePlainConfiguration" }, @@ -5860,6 +5870,13 @@ { "type": "null" } ] }, + "noEmptyObjectKeys": { + "description": "Disallow empty keys in JSON objects.\nSee https://biomejs.dev/linter/rules/no-empty-object-keys", + "anyOf": [ + { "$ref": "#/$defs/NoEmptyObjectKeysConfiguration" }, + { "type": "null" } + ] + }, "noEqualsToNull": { "description": "Require the use of === or !== for comparison with null.\nSee https://biomejs.dev/linter/rules/no-equals-to-null", "anyOf": [ @@ -7780,6 +7797,15 @@ "additionalProperties": false, "required": ["level"] }, + "RuleWithNoEmptyObjectKeysOptions": { + "type": "object", + "properties": { + "level": { "$ref": "#/$defs/RulePlainConfiguration" }, + "options": { "$ref": "#/$defs/NoEmptyObjectKeysOptions" } + }, + "additionalProperties": false, + "required": ["level"] + }, "RuleWithNoEmptyPatternOptions": { "type": "object", "properties": {