-
-
Notifications
You must be signed in to change notification settings - Fork 964
feat(graphql_analyze): implement useUniqueFieldDefinitionNames #8598
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Netail
merged 3 commits into
biomejs:main
from
Netail:feat/use-unique-field-definition-names
Dec 28, 2025
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
158 changes: 158 additions & 0 deletions
158
crates/biome_graphql_analyze/src/lint/nursery/use_unique_field_definition_names.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." | ||
| }), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
18 changes: 18 additions & 0 deletions
18
...s/biome_graphql_analyze/tests/specs/nursery/useUniqueFieldDefinitionNames/invalid.graphql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
97 changes: 97 additions & 0 deletions
97
...me_graphql_analyze/tests/specs/nursery/useUniqueFieldDefinitionNames/invalid.graphql.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
|
|
||
| ``` |
35 changes: 35 additions & 0 deletions
35
crates/biome_graphql_analyze/tests/specs/nursery/useUniqueFieldDefinitionNames/valid.graphql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
43 changes: 43 additions & 0 deletions
43
...iome_graphql_analyze/tests/specs/nursery/useUniqueFieldDefinitionNames/valid.graphql.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } | ||
|
|
||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
crates/biome_rule_options/src/use_unique_field_definition_names.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 {} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
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(🤖 Prompt for AI Agents