Skip to content

Commit 6af6c62

Browse files
committed
feat(graphql_analyze): implement useInputName
1 parent 499594b commit 6af6c62

File tree

27 files changed

+877
-1
lines changed

27 files changed

+877
-1
lines changed

.changeset/tangy-states-obey.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useInputName`](https://biomejs.dev/linter/rules/use-input-name/). Require mutation argument to be always called “input” and (optionally) input type to be called Mutation name + “Input”.
6+
7+
**Invalid:**
8+
9+
```graphql
10+
type Mutation {
11+
SetMessage(message: String): String
12+
}
13+
```

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_graphql_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ use biome_analyze::declare_lint_group;
66
pub mod no_empty_source;
77
pub mod use_consistent_graphql_descriptions;
88
pub mod use_deprecated_date;
9+
pub mod use_input_name;
910
pub mod use_unique_argument_names;
1011
pub mod use_unique_enum_value_names;
1112
pub mod use_unique_field_definition_names;
1213
pub mod use_unique_graphql_operation_name;
1314
pub mod use_unique_input_field_names;
1415
pub mod use_unique_variable_names;
15-
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_argument_names :: UseUniqueArgumentNames , self :: use_unique_enum_value_names :: UseUniqueEnumValueNames , 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 ,] } }
16+
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_input_name :: UseInputName , self :: use_unique_argument_names :: UseUniqueArgumentNames , self :: use_unique_enum_value_names :: UseUniqueEnumValueNames , 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 ,] } }
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::{MarkupBuf, markup};
5+
use biome_graphql_syntax::{
6+
AnyGraphqlPrimitiveType, AnyGraphqlType, GraphqlFieldDefinition, GraphqlFieldDefinitionList,
7+
GraphqlFieldsDefinition, GraphqlLanguage, GraphqlObjectTypeDefinition,
8+
GraphqlObjectTypeExtension,
9+
};
10+
use biome_rowan::{AstNode, SyntaxToken, TextRange};
11+
use biome_rule_options::use_input_name::UseInputNameOptions;
12+
use biome_string_case::StrOnlyExtension;
13+
14+
declare_lint_rule! {
15+
/// Require mutation argument to be always called "input"
16+
///
17+
/// Using the same name for all input parameters will make your schemas easier to consume and more predictable.
18+
///
19+
/// Optionally, when the option `checkInputType` has been enabled, the input type requires to be called `<mutation name>Input`.
20+
/// Using the name of the mutation in the input type name will make it easier to find the mutation that the input type belongs to.
21+
///
22+
/// ## Examples
23+
///
24+
/// ### Invalid
25+
///
26+
/// ```graphql,expect_diagnostic
27+
/// type Mutation {
28+
/// SetMessage(message: InputMessage): String
29+
/// }
30+
/// ```
31+
///
32+
/// ### Valid
33+
///
34+
/// ```graphql
35+
/// type Mutation {
36+
/// SetMessage(input: SetMessageInput): String
37+
/// }
38+
/// ```
39+
///
40+
/// ## Options
41+
///
42+
/// ### `checkInputType`
43+
///
44+
/// Check that the input type name follows the convention <mutationName>Input.
45+
///
46+
/// Default `false`
47+
///
48+
/// ```json,options
49+
/// {
50+
/// "options": {
51+
/// "checkInputType": true
52+
/// }
53+
/// }
54+
/// ```
55+
///
56+
/// ```graphql,expect_diagnostic,use_options
57+
/// type Mutation {
58+
/// SetMessage(input: InputMessage): String
59+
/// }
60+
/// ```
61+
///
62+
/// ### `caseSensitiveInputType`
63+
///
64+
/// Treat input type names as case-sensitive.
65+
///
66+
/// Default `true`
67+
///
68+
/// ```json,options
69+
/// {
70+
/// "options": {
71+
/// "checkInputType": true,
72+
/// "caseSensitiveInputType": true,
73+
/// }
74+
/// }
75+
/// ```
76+
///
77+
/// ```graphql,expect_diagnostic,use_options
78+
/// type Mutation {
79+
/// SetMessage(input: setMessageInput): String
80+
/// }
81+
/// ```
82+
///
83+
pub UseInputName {
84+
version: "next",
85+
name: "useInputName",
86+
language: "graphql",
87+
recommended: false,
88+
sources: &[RuleSource::EslintGraphql("input-name").inspired()],
89+
}
90+
}
91+
92+
impl Rule for UseInputName {
93+
type Query = Ast<GraphqlFieldDefinition>;
94+
type State = UseInputNameState;
95+
type Signals = Option<Self::State>;
96+
type Options = UseInputNameOptions;
97+
98+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
99+
let node = ctx.query();
100+
101+
let def_list = node
102+
.syntax()
103+
.parent()
104+
.and_then(GraphqlFieldDefinitionList::cast)?;
105+
let fields_def = def_list
106+
.syntax()
107+
.parent()
108+
.and_then(GraphqlFieldsDefinition::cast)?;
109+
110+
let is_mutation = fields_def.syntax().parent().is_some_and(|parent| {
111+
if let Some(type_def) = GraphqlObjectTypeDefinition::cast(parent.clone()) {
112+
return type_def.is_mutation();
113+
}
114+
if let Some(type_ext) = GraphqlObjectTypeExtension::cast(parent.clone()) {
115+
return type_ext.is_mutation();
116+
}
117+
118+
false
119+
});
120+
121+
if !is_mutation {
122+
return None;
123+
}
124+
125+
let arguments = node.arguments()?;
126+
for argument in arguments.arguments() {
127+
let name = argument.name().ok()?;
128+
let value_token = name.value_token().ok()?;
129+
let current = value_token.text_trimmed();
130+
if current != "input" {
131+
return Some(UseInputNameState::InvalidName(
132+
argument.range(),
133+
current.to_string(),
134+
));
135+
}
136+
137+
let check_input_type = ctx.options().check_input_type();
138+
if check_input_type {
139+
let case_sensitive_input_type = ctx.options().case_sensitive_input_type();
140+
141+
let any_type = argument.ty().ok()?;
142+
143+
let ty = find_input_type(any_type)?;
144+
let ty_string = ty.text_trimmed();
145+
146+
let def_name = node.name().ok()?;
147+
let def_value_token = def_name.value_token().ok()?;
148+
149+
let valid_string = def_value_token.text_trimmed().to_string() + "Input";
150+
if (case_sensitive_input_type && ty_string != valid_string)
151+
|| ty_string.to_lowercase_cow() != valid_string.to_lowercase_cow()
152+
{
153+
return Some(UseInputNameState::InvalidTypeName(
154+
argument.range(),
155+
ty_string.to_string(),
156+
valid_string,
157+
));
158+
}
159+
}
160+
}
161+
162+
None
163+
}
164+
165+
fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
166+
Some(
167+
RuleDiagnostic::new(rule_category!(), state.range(), state.message())
168+
.note(state.description()),
169+
)
170+
}
171+
}
172+
173+
/// Representation of the various states
174+
///
175+
/// The `TextRange` of each variant represents the range of where the issue is found.
176+
pub enum UseInputNameState {
177+
/// The input value name does not match "input"
178+
InvalidName(TextRange, String),
179+
/// The input value type name does not equal mutation name + "Input".
180+
InvalidTypeName(TextRange, String, String),
181+
}
182+
183+
impl UseInputNameState {
184+
fn range(&self) -> &TextRange {
185+
match self {
186+
Self::InvalidName(range, _) | Self::InvalidTypeName(range, _, _) => range,
187+
}
188+
}
189+
190+
fn message(&self) -> MarkupBuf {
191+
match self {
192+
Self::InvalidName(_, current) => (markup! {
193+
"Input \""{ current }"\" should be named \"input\"."
194+
})
195+
.to_owned(),
196+
Self::InvalidTypeName(_, current, valid) => (markup! {
197+
"Input type \""{ current }"\" name should be \""{ valid }"\"."
198+
})
199+
.to_owned(),
200+
}
201+
}
202+
203+
fn description(&self) -> MarkupBuf {
204+
match self {
205+
Self::InvalidName(_, _) => (markup! {
206+
"Using the same name for all input parameters will make your schemas easier to consume and more predictable."
207+
})
208+
.to_owned(),
209+
Self::InvalidTypeName(_, _, _) => (markup! {
210+
"Using the name of the operation in the input type name will make it easier to find the operation that the input type belongs to."
211+
})
212+
.to_owned(),
213+
}
214+
}
215+
}
216+
217+
fn find_input_type(any_type: AnyGraphqlType) -> Option<SyntaxToken<GraphqlLanguage>> {
218+
match any_type {
219+
AnyGraphqlType::AnyGraphqlPrimitiveType(primitive_type) => {
220+
find_input_type_primitive_type(primitive_type)
221+
}
222+
AnyGraphqlType::GraphqlNonNullType(non_null_type) => {
223+
let base = non_null_type.base().ok()?;
224+
find_input_type_primitive_type(base)
225+
}
226+
_ => None,
227+
}
228+
}
229+
230+
fn find_input_type_primitive_type(
231+
primitive_type: AnyGraphqlPrimitiveType,
232+
) -> Option<SyntaxToken<GraphqlLanguage>> {
233+
match primitive_type {
234+
AnyGraphqlPrimitiveType::GraphqlNameReference(name_ref) => name_ref.value_token().ok(),
235+
AnyGraphqlPrimitiveType::GraphqlListType(list_type) => {
236+
let any_type = list_type.element().ok()?;
237+
find_input_type(any_type)
238+
}
239+
}
240+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# should generate diagnostics
2+
3+
# Input name not "input"
4+
type Mutation { SetMessage(record: String): String }
5+
6+
# Input type not ending with Input
7+
type Mutation { SetMessage(input: String): String }
8+
9+
# Input type not matching mutation name
10+
type Mutation { SetMessage(input: CreateAMessageInput): String }
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
source: crates/biome_graphql_analyze/tests/spec_tests.rs
3+
expression: invalid.graphql
4+
---
5+
# Input
6+
```graphql
7+
# should generate diagnostics
8+
9+
# Input name not "input"
10+
type Mutation { SetMessage(record: String): String }
11+
12+
# Input type not ending with Input
13+
type Mutation { SetMessage(input: String): String }
14+
15+
# Input type not matching mutation name
16+
type Mutation { SetMessage(input: CreateAMessageInput): String }
17+
18+
```
19+
20+
# Diagnostics
21+
```
22+
invalid.graphql:4:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23+
24+
i Input "record" should be named "input".
25+
26+
3 │ # Input name not "input"
27+
> 4 │ type Mutation { SetMessage(record: String): String }
28+
│ ^^^^^^^^^^^^^^
29+
5 │
30+
6 │ # Input type not ending with Input
31+
32+
i Using the same name for all input parameters will make your schemas easier to consume and more predictable.
33+
34+
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+
36+
37+
```
38+
39+
```
40+
invalid.graphql:7:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41+
42+
i Input type "String" name should be "SetMessageInput".
43+
44+
6 │ # Input type not ending with Input
45+
> 7 │ type Mutation { SetMessage(input: String): String }
46+
│ ^^^^^^^^^^^^^
47+
8 │
48+
9 │ # Input type not matching mutation name
49+
50+
i Using the name of the operation in the input type name will make it easier to find the operation that the input type belongs to.
51+
52+
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.
53+
54+
55+
```
56+
57+
```
58+
invalid.graphql:10:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
59+
60+
i Input type "CreateAMessageInput" name should be "SetMessageInput".
61+
62+
9 │ # Input type not matching mutation name
63+
> 10 │ type Mutation { SetMessage(input: CreateAMessageInput): String }
64+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
65+
11 │
66+
67+
i Using the name of the operation in the input type name will make it easier to find the operation that the input type belongs to.
68+
69+
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.
70+
71+
72+
```

0 commit comments

Comments
 (0)