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
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 arguments to be called “input”, and the input type to be called Mutation name + “Input”.

**Invalid:**

```graphql
type Mutation {
SetMessage(message: String): String
}
```
12 changes: 12 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

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

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 @@ -14,6 +14,7 @@ pub mod no_excessive_lines_per_file;
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_anonymous_operation;
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_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_root_type :: NoRootType , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_lone_anonymous_operation :: UseLoneAnonymousOperation , 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_excessive_lines_per_file :: NoExcessiveLinesPerFile , self :: no_root_type :: NoRootType , self :: use_consistent_graphql_descriptions :: UseConsistentGraphqlDescriptions , self :: use_deprecated_date :: UseDeprecatedDate , self :: use_input_name :: UseInputName , self :: use_lone_anonymous_operation :: UseLoneAnonymousOperation , self :: use_lone_executable_definition :: UseLoneExecutableDefinition ,] } }
254 changes: 254 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,254 @@
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, GraphqlObjectTypeDefinition, GraphqlObjectTypeExtension,
GraphqlSyntaxToken,
};
use biome_rowan::{AstNode, TextRange};
use biome_rule_options::use_input_name::{CheckInputType, UseInputNameOptions};

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.
///
/// ## Examples
///
/// ### Invalid
///
/// ```graphql,expect_diagnostic
/// type Mutation {
/// SetMessage(message: InputMessage): String
/// }
/// ```
///
/// ### Valid
///
/// ```graphql
/// type Mutation {
/// SetMessage(input: SetMessageInput): String
/// }
/// ```
///
/// ## Options
///
/// ### `checkInputType`
///
/// With the option `checkInputType` on, the input type name requires to be called `<mutation name>Input`.
/// This can either be "loose" (case-insensitive) or "strict" (case-sensitive).
/// 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.
///
/// Default `"off"`
///
/// ```json,options
/// {
/// "options": {
/// "checkInputType": "loose"
/// }
/// }
/// ```
///
/// ```graphql,expect_diagnostic,use_options
/// type Mutation {
/// SetMessage(input: InputMessage): String
/// }
/// ```
///
/// ```graphql,use_options
/// type Mutation {
/// SetMessage(input: setMessageInput): String
/// }
/// ```
///
/// ```graphql,use_options
/// type Mutation {
/// SetMessage(input: SetMessageInput): String
/// }
/// ```
///
///
/// ```json,options
/// {
/// "options": {
/// "checkInputType": "strict"
/// }
/// }
/// ```
///
/// ```graphql,expect_diagnostic,use_options
/// type Mutation {
/// SetMessage(input: InputMessage): String
/// }
/// ```
///
/// ```graphql,expect_diagnostic,use_options
/// type Mutation {
/// SetMessage(input: setMessageInput): String
/// }
/// ```
///
/// ```graphql,use_options
/// type Mutation {
/// SetMessage(input: SetMessageInput): String
/// }
/// ```
///
pub UseInputName {
version: "next",
name: "useInputName",
language: "graphql",
recommended: false,
sources: &[RuleSource::EslintGraphql("input-name").same()],
}
}

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_ref(&parent) {
return type_def.is_mutation();
}
if let Some(type_ext) = GraphqlObjectTypeExtension::cast_ref(&parent) {
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 let Some(check_input_type) = check_input_type
&& check_input_type != CheckInputType::Off
{
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_str = format!("{}Input", def_value_token.text_trimmed());
if (check_input_type == CheckInputType::Strict && ty_string != valid_str)
|| (check_input_type == CheckInputType::Loose
&& !ty_string.eq_ignore_ascii_case(&valid_str))
{
return Some(UseInputNameState::InvalidTypeName(
argument.range(),
ty_string.to_string(),
valid_str,
));
}
}
}

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! {
"Unexpected input name, expected the input name \""{ current }"\" to be named \"input\"."
})
.to_owned(),
Self::InvalidTypeName(_, current, valid) => (markup! {
"Unexpected input type name, expected the input type name \""{ current }"\" to be named \""{ 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<GraphqlSyntaxToken> {
let mut current_type = any_type;

loop {
match current_type {
AnyGraphqlType::AnyGraphqlPrimitiveType(primitive_type) => match primitive_type {
AnyGraphqlPrimitiveType::GraphqlNameReference(name_ref) => {
return name_ref.value_token().ok();
}
AnyGraphqlPrimitiveType::GraphqlListType(list_type) => {
current_type = list_type.element().ok()?;
}
},
AnyGraphqlType::GraphqlNonNullType(non_null_type) => {
let base = non_null_type.base().ok()?;
current_type = AnyGraphqlType::AnyGraphqlPrimitiveType(base);
}
_ => return None,
}
}
}
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 }
Loading
Loading