Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/floppy-phones-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Added the nursery rule [`useUniqueFieldDefinitionNames`](https://biomejs.dev/linter/rules/use-unique-field-definition-names/). Require all fields of a type to be unique.
79 changes: 50 additions & 29 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ define_categories! {
"lint/nursery/useRequiredScripts": "https://biomejs.dev/linter/rules/use-required-scripts",
"lint/nursery/useSortedClasses": "https://biomejs.dev/linter/rules/use-sorted-classes",
"lint/nursery/useSpread": "https://biomejs.dev/linter/rules/no-spread",
"lint/nursery/useUniqueFieldDefinitionNames": "https://biomejs.dev/linter/rules/use-unique-field-definition-names",
"lint/nursery/useUniqueGraphqlOperationName": "https://biomejs.dev/linter/rules/use-unique-graphql-operation-name",
"lint/nursery/useUniqueInputFieldNames": "https://biomejs.dev/linter/rules/use-unique-input-field-names",
"lint/nursery/useUniqueVariableNames": "https://biomejs.dev/linter/rules/use-unique-variable-names",
Expand Down
3 changes: 2 additions & 1 deletion crates/biome_graphql_analyze/src/lint/nursery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ use biome_analyze::declare_lint_group;
pub mod no_empty_source;
pub mod use_consistent_graphql_descriptions;
pub mod use_deprecated_date;
pub mod use_unique_field_definition_names;
pub mod use_unique_graphql_operation_name;
pub mod use_unique_input_field_names;
pub mod use_unique_variable_names;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_empty_source :: NoEmptySource , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationName , self :: use_unique_input_field_names :: UseUniqueInputFieldNames , self :: use_unique_variable_names :: UseUniqueVariableNames ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_empty_source :: NoEmptySource , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_unique_field_definition_names :: UseUniqueFieldDefinitionNames , self :: use_unique_graphql_operation_name :: UseUniqueGraphqlOperationName , self :: use_unique_input_field_names :: UseUniqueInputFieldNames , self :: use_unique_variable_names :: UseUniqueVariableNames ,] } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use std::collections::HashSet;

use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::markup;
use biome_graphql_syntax::{
GraphqlFieldsDefinition, GraphqlInputFieldsDefinition, GraphqlInputObjectTypeDefinition,
GraphqlInterfaceTypeDefinition, GraphqlObjectTypeDefinition,
};
use biome_rowan::{AstNode, TokenText, declare_node_union};
use biome_rule_options::use_unique_field_definition_names::UseUniqueFieldDefinitionNamesOptions;

declare_lint_rule! {
/// Require all fields of a type to be unique.
///
/// A GraphQL complex type is only valid if all its fields are uniquely named.
///
/// ## Examples
///
/// ### Invalid
///
/// ```graphql,expect_diagnostic
/// type SomeObject {
/// foo: String
/// foo: String
/// }
/// ```
///
/// ```graphql,expect_diagnostic
/// interface SomeObject {
/// foo: String
/// foo: String
/// }
/// ```
///
/// ```graphql,expect_diagnostic
/// input SomeObject {
/// foo: String
/// foo: String
/// }
/// ```
///
/// ### Valid
///
/// ```graphql
/// type SomeObject {
/// foo: String
/// bar: String
/// }
/// ```
///
/// ```graphql
/// interface SomeObject {
/// foo: String
/// bar: String
/// }
/// ```
///
/// ```graphql
/// input SomeObject {
/// foo: String
/// bar: String
/// }
/// ```
///
pub UseUniqueFieldDefinitionNames {
version: "next",
name: "useUniqueFieldDefinitionNames",
language: "graphql",
recommended: false,
sources: &[RuleSource::EslintGraphql("unique-field-definition-names").same()],
}
}

impl Rule for UseUniqueFieldDefinitionNames {
type Query = Ast<UseUniqueFieldDefinitionNamesQuery>;
type State = ();
type Signals = Option<Self::State>;
type Options = UseUniqueFieldDefinitionNamesOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let node = ctx.query();

match node {
UseUniqueFieldDefinitionNamesQuery::GraphqlObjectTypeDefinition(object_def) => {
let fields = object_def.fields()?;
check_list(fields)
}
UseUniqueFieldDefinitionNamesQuery::GraphqlInterfaceTypeDefinition(interface_def) => {
let fields = interface_def.fields()?;
check_list(fields)
}
UseUniqueFieldDefinitionNamesQuery::GraphqlInputObjectTypeDefinition(input_def) => {
let fields = input_def.input_fields()?;
check_input_list(fields)
}
}
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let span = ctx.query().range();
Some(
RuleDiagnostic::new(
rule_category!(),
span,
markup! {
"Duplicate field name."
},
)
.note(markup! {
"A GraphQL complex type is only valid if all its fields are uniquely named. Make sure to name every field differently."
}),
)
}
Comment on lines +101 to +115
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Consider emitting diagnostics on the duplicate field itself.

Currently, the diagnostic spans the entire type definition (line 102), which will underline the whole type/interface/input block. For better UX, consider collecting the duplicate field nodes in the helper functions and emitting the diagnostic specifically on the duplicate field's range. This would make it immediately clear which field is problematic.

Suggested approach

Modify the State type to store the duplicate field's range:

-    type State = ();
+    type State = TextRange;

Update helper functions to return the duplicate field's range instead of Some(()), then use that range in the diagnostic:

     fn diagnostic(ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
-        let span = ctx.query().range();
+        let span = *state;
         Some(

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
crates/biome_graphql_analyze/src/lint/nursery/use_unique_field_definition_names.rs
around lines 101–115, the diagnostic currently spans the whole type; change the
State to store the duplicate field's range (e.g., Option<RangeOrSpan>) instead
of just () so helper functions can return the duplicate field's range when
found, update the helper signatures and call sites to propagate and set that
range on the State, and finally use that stored range in diagnostic(ctx, state)
to create the RuleDiagnostic on the exact duplicate field span rather than the
entire type block.

}

fn check_list(fields: GraphqlFieldsDefinition) -> Option<()> {
let mut found: HashSet<TokenText> = HashSet::new();

for element in fields.fields() {
if let Some(name) = element.name().ok()
&& let Some(value_token) = name.value_token().ok()
{
let string = value_token.token_text();
if found.contains(&string) {
return Some(());
} else {
found.insert(string);
}
}
}

None
}

fn check_input_list(fields: GraphqlInputFieldsDefinition) -> Option<()> {
let mut found: HashSet<TokenText> = HashSet::new();

for element in fields.fields() {
if let Some(name) = element.name().ok()
&& let Some(value_token) = name.value_token().ok()
{
let string = value_token.token_text();
if found.contains(&string) {
return Some(());
} else {
found.insert(string);
}
}
}

None
}

declare_node_union! {
pub UseUniqueFieldDefinitionNamesQuery = GraphqlObjectTypeDefinition | GraphqlInterfaceTypeDefinition | GraphqlInputObjectTypeDefinition
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# should generate diagnostics
type SomeObject {
foo: String
bar: String
foo: String
}

interface SomeInterface {
foo: String
bar: String
foo: String
}

input SomeInputObject {
foo: String
bar: String
foo: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
source: crates/biome_graphql_analyze/tests/spec_tests.rs
expression: invalid.graphql
---
# Input
```graphql
# should generate diagnostics
type SomeObject {
foo: String
bar: String
foo: String
}

interface SomeInterface {
foo: String
bar: String
foo: String
}

input SomeInputObject {
foo: String
bar: String
foo: String
}

```

# Diagnostics
```
invalid.graphql:2:1 lint/nursery/useUniqueFieldDefinitionNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Duplicate field name.

1 │ # should generate diagnostics
> 2 │ type SomeObject {
│ ^^^^^^^^^^^^^^^^^
> 3 │ foo: String
> 4 │ bar: String
> 5 │ foo: String
> 6 │ }
│ ^
7 │
8 │ interface SomeInterface {

i A GraphQL complex type is only valid if all its fields are uniquely named. Make sure to name every field differently.

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.graphql:8:1 lint/nursery/useUniqueFieldDefinitionNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Duplicate field name.

6 │ }
7 │
> 8 │ interface SomeInterface {
│ ^^^^^^^^^^^^^^^^^^^^^^^^^
> 9 │ foo: String
> 10 │ bar: String
> 11 │ foo: String
> 12 │ }
│ ^
13 │
14 │ input SomeInputObject {

i A GraphQL complex type is only valid if all its fields are uniquely named. Make sure to name every field differently.

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.graphql:14:1 lint/nursery/useUniqueFieldDefinitionNames ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Duplicate field name.

12 │ }
13 │
> 14 │ input SomeInputObject {
│ ^^^^^^^^^^^^^^^^^^^^^^^
> 15 │ foo: String
> 16 │ bar: String
> 17 │ foo: String
> 18 │ }
│ ^
19 │

i A GraphQL complex type is only valid if all its fields are uniquely named. Make sure to name every field differently.

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.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# should not generate diagnostics
type SomeObjectA
interface SomeInterfaceA
input SomeInputObjectA

# ---

type SomeObjectB {
foo: String
}

interface SomeInterfaceB {
foo: String
}

input SomeInputObjectB {
foo: String
}

# ---

type SomeObjectC {
foo: String
bar: String
}

interface SomeInterfaceC {
foo: String
bar: String
}

input SomeInputObjectC {
foo: String
bar: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
source: crates/biome_graphql_analyze/tests/spec_tests.rs
expression: valid.graphql
---
# Input
```graphql
# should not generate diagnostics
type SomeObjectA
interface SomeInterfaceA
input SomeInputObjectA

# ---

type SomeObjectB {
foo: String
}

interface SomeInterfaceB {
foo: String
}

input SomeInputObjectB {
foo: String
}

# ---

type SomeObjectC {
foo: String
bar: String
}

interface SomeInterfaceC {
foo: String
bar: String
}

input SomeInputObjectC {
foo: String
bar: String
}

```
1 change: 1 addition & 0 deletions crates/biome_rule_options/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ pub mod use_top_level_regex;
pub mod use_trim_start_end;
pub mod use_unified_type_signatures;
pub mod use_unique_element_ids;
pub mod use_unique_field_definition_names;
pub mod use_unique_graphql_operation_name;
pub mod use_unique_input_field_names;
pub mod use_unique_variable_names;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 UseUniqueFieldDefinitionNamesOptions {}
Loading
Loading