Skip to content
Open
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
13 changes: 13 additions & 0 deletions .changeset/tangy-states-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@biomejs/biome": patch
---

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”.

**Invalid:**

```graphql
type Mutation {
SetMessage(message: String): String
}
```
4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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 @@ -13,5 +13,6 @@ pub mod no_empty_source;
pub mod no_root_type;
pub mod use_consistent_graphql_descriptions;
pub mod use_deprecated_date;
pub mod use_input_name;
pub mod use_lone_executable_definition;
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_duplicate_argument_names :: NoDuplicateArgumentNames , self :: no_duplicate_enum_value_names :: NoDuplicateEnumValueNames , self :: no_duplicate_field_definition_names :: NoDuplicateFieldDefinitionNames , self :: no_duplicate_graphql_operation_name :: NoDuplicateGraphqlOperationName , self :: no_duplicate_input_field_names :: NoDuplicateInputFieldNames , self :: no_duplicate_variable_names :: NoDuplicateVariableNames , self :: no_empty_source :: NoEmptySource , self :: no_root_type :: NoRootType , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_lone_executable_definition :: UseLoneExecutableDefinition ,] } }
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_duplicate_argument_names :: NoDuplicateArgumentNames , self :: no_duplicate_enum_value_names :: NoDuplicateEnumValueNames , self :: no_duplicate_field_definition_names :: NoDuplicateFieldDefinitionNames , self :: no_duplicate_graphql_operation_name :: NoDuplicateGraphqlOperationName , self :: no_duplicate_input_field_names :: NoDuplicateInputFieldNames , self :: no_duplicate_variable_names :: NoDuplicateVariableNames , self :: no_empty_source :: NoEmptySource , self :: no_root_type :: NoRootType , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_input_name :: UseInputName , self :: use_lone_executable_definition :: UseLoneExecutableDefinition ,] } }
240 changes: 240 additions & 0 deletions crates/biome_graphql_analyze/src/lint/nursery/use_input_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
use biome_analyze::{
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
};
use biome_console::{MarkupBuf, markup};
use biome_graphql_syntax::{
AnyGraphqlPrimitiveType, AnyGraphqlType, GraphqlFieldDefinition, GraphqlFieldDefinitionList,
GraphqlFieldsDefinition, GraphqlLanguage, GraphqlObjectTypeDefinition,
GraphqlObjectTypeExtension,
};
use biome_rowan::{AstNode, SyntaxToken, TextRange};
use biome_rule_options::use_input_name::UseInputNameOptions;
use biome_string_case::StrOnlyExtension;

declare_lint_rule! {
/// Require mutation argument to be always called "input"
///
/// Using the same name for all input parameters will make your schemas easier to consume and more predictable.
///
/// Optionally, when the option `checkInputType` has been enabled, the input type requires to be called `<mutation name>Input`.
/// 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.
///
/// ## Examples
///
/// ### Invalid
///
/// ```graphql,expect_diagnostic
/// type Mutation {
/// SetMessage(message: InputMessage): String
/// }
/// ```
///
/// ### Valid
///
/// ```graphql
/// type Mutation {
/// SetMessage(input: SetMessageInput): String
/// }
/// ```
///
/// ## Options
///
/// ### `checkInputType`
///
/// Check that the input type name follows the convention <mutationName>Input.
///
/// Default `false`
///
/// ```json,options
/// {
/// "options": {
/// "checkInputType": true
/// }
/// }
/// ```
///
/// ```graphql,expect_diagnostic,use_options
/// type Mutation {
/// SetMessage(input: InputMessage): String
/// }
/// ```
///
/// ### `caseSensitiveInputType`
///
/// Treat input type names as case-sensitive.
///
/// Default `true`
///
/// ```json,options
/// {
/// "options": {
/// "checkInputType": true,
/// "caseSensitiveInputType": true
/// }
/// }
/// ```
///
/// ```graphql,expect_diagnostic,use_options
/// type Mutation {
/// SetMessage(input: setMessageInput): String
/// }
/// ```
///
pub UseInputName {
version: "next",
name: "useInputName",
language: "graphql",
recommended: false,
sources: &[RuleSource::EslintGraphql("input-name").inspired()],
}
}

impl Rule for UseInputName {
type Query = Ast<GraphqlFieldDefinition>;
type State = UseInputNameState;
type Signals = Option<Self::State>;
type Options = UseInputNameOptions;

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

let def_list = node
.syntax()
.parent()
.and_then(GraphqlFieldDefinitionList::cast)?;
let fields_def = def_list
.syntax()
.parent()
.and_then(GraphqlFieldsDefinition::cast)?;

let is_mutation = fields_def.syntax().parent().is_some_and(|parent| {
if let Some(type_def) = GraphqlObjectTypeDefinition::cast(parent.clone()) {
return type_def.is_mutation();
}
if let Some(type_ext) = GraphqlObjectTypeExtension::cast(parent.clone()) {
return type_ext.is_mutation();
}

false
});

if !is_mutation {
return None;
}

let arguments = node.arguments()?;
for argument in arguments.arguments() {
let name = argument.name().ok()?;
let value_token = name.value_token().ok()?;
let current = value_token.text_trimmed();
if current != "input" {
return Some(UseInputNameState::InvalidName(
argument.range(),
current.to_string(),
));
}

let check_input_type = ctx.options().check_input_type();
if check_input_type {
let case_sensitive_input_type = ctx.options().case_sensitive_input_type();

let any_type = argument.ty().ok()?;

let ty = find_input_type(any_type)?;
let ty_string = ty.text_trimmed();

let def_name = node.name().ok()?;
let def_value_token = def_name.value_token().ok()?;

let valid_string = def_value_token.text_trimmed().to_string() + "Input";
if (case_sensitive_input_type && ty_string != valid_string)
|| ty_string.to_lowercase_cow() != valid_string.to_lowercase_cow()
{
return Some(UseInputNameState::InvalidTypeName(
argument.range(),
ty_string.to_string(),
valid_string,
));
}
}
}

None
}

fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> {
Some(
RuleDiagnostic::new(rule_category!(), state.range(), state.message())
.note(state.description()),
)
}
}

/// Representation of the various states
///
/// The `TextRange` of each variant represents the range of where the issue is found.
pub enum UseInputNameState {
/// The input value name does not match "input"
InvalidName(TextRange, String),
/// The input value type name does not equal mutation name + "Input".
InvalidTypeName(TextRange, String, String),
}

impl UseInputNameState {
fn range(&self) -> &TextRange {
match self {
Self::InvalidName(range, _) | Self::InvalidTypeName(range, _, _) => range,
}
}

fn message(&self) -> MarkupBuf {
match self {
Self::InvalidName(_, current) => (markup! {
"Input \""{ current }"\" should be named \"input\"."
})
.to_owned(),
Self::InvalidTypeName(_, current, valid) => (markup! {
"Input type \""{ current }"\" name should be \""{ valid }"\"."
})
.to_owned(),
}
}

fn description(&self) -> MarkupBuf {
match self {
Self::InvalidName(_, _) => (markup! {
"Using the same name for all input parameters will make your schemas easier to consume and more predictable."
})
.to_owned(),
Self::InvalidTypeName(_, _, _) => (markup! {
"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."
})
.to_owned(),
}
}
}

fn find_input_type(any_type: AnyGraphqlType) -> Option<SyntaxToken<GraphqlLanguage>> {
match any_type {
AnyGraphqlType::AnyGraphqlPrimitiveType(primitive_type) => {
find_input_type_primitive_type(primitive_type)
}
AnyGraphqlType::GraphqlNonNullType(non_null_type) => {
let base = non_null_type.base().ok()?;
find_input_type_primitive_type(base)
}
_ => None,
}
}

fn find_input_type_primitive_type(
primitive_type: AnyGraphqlPrimitiveType,
) -> Option<SyntaxToken<GraphqlLanguage>> {
match primitive_type {
AnyGraphqlPrimitiveType::GraphqlNameReference(name_ref) => name_ref.value_token().ok(),
AnyGraphqlPrimitiveType::GraphqlListType(list_type) => {
let any_type = list_type.element().ok()?;
find_input_type(any_type)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# should generate diagnostics

# Input name not "input"
type Mutation { SetMessage(record: String): String }

# Input type not ending with Input
type Mutation { SetMessage(input: String): String }

# Input type not matching mutation name
type Mutation { SetMessage(input: CreateAMessageInput): String }
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
source: crates/biome_graphql_analyze/tests/spec_tests.rs
expression: invalid.graphql
---
# Input
```graphql
# should generate diagnostics

# Input name not "input"
type Mutation { SetMessage(record: String): String }

# Input type not ending with Input
type Mutation { SetMessage(input: String): String }

# Input type not matching mutation name
type Mutation { SetMessage(input: CreateAMessageInput): String }

```

# Diagnostics
```
invalid.graphql:4:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Input "record" should be named "input".

3 │ # Input name not "input"
> 4 │ type Mutation { SetMessage(record: String): String }
│ ^^^^^^^^^^^^^^
5 │
6 │ # Input type not ending with Input

i Using the same name for all input parameters will make your schemas easier to consume and more predictable.

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:7:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Input type "String" name should be "SetMessageInput".

6 │ # Input type not ending with Input
> 7 │ type Mutation { SetMessage(input: String): String }
│ ^^^^^^^^^^^^^
8 │
9 │ # Input type not matching mutation name

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.

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:10:28 lint/nursery/useInputName ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

i Input type "CreateAMessageInput" name should be "SetMessageInput".

9 │ # Input type not matching mutation name
> 10 │ type Mutation { SetMessage(input: CreateAMessageInput): String }
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^
11 │

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.

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.


```
Loading