-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
require-nullable-result-in-root
lint rule (#1657)
Co-authored-by: Dimitri POSTOLOV <[email protected]>
- Loading branch information
1 parent
496fc36
commit 0a571bb
Showing
10 changed files
with
263 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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 @@ | ||
--- | ||
'@graphql-eslint/eslint-plugin': minor | ||
--- | ||
|
||
Add `require-nullable-result-in-root` rule to report on non-null fields in root types |
This file contains 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 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
90 changes: 90 additions & 0 deletions
90
packages/plugin/src/rules/require-nullable-result-in-root.ts
This file contains 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,90 @@ | ||
import { Kind, ObjectTypeDefinitionNode } from 'graphql'; | ||
import { GraphQLESLintRule } from '../types.js'; | ||
import { getNodeName, requireGraphQLSchemaFromContext, truthy } from '../utils.js'; | ||
import { GraphQLESTreeNode } from '../estree-converter/index.js'; | ||
|
||
const RULE_ID = 'require-nullable-result-in-root'; | ||
|
||
export const rule: GraphQLESLintRule = { | ||
meta: { | ||
type: 'suggestion', | ||
hasSuggestions: true, | ||
docs: { | ||
category: 'Schema', | ||
description: 'Require nullable fields in root types.', | ||
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`, | ||
requiresSchema: true, | ||
examples: [ | ||
{ | ||
title: 'Incorrect', | ||
code: /* GraphQL */ ` | ||
type Query { | ||
user: User! | ||
} | ||
`, | ||
}, | ||
{ | ||
title: 'Correct', | ||
code: /* GraphQL */ ` | ||
type Query { | ||
foo: User | ||
baz: [User]! | ||
bar: [User!]! | ||
} | ||
`, | ||
}, | ||
], | ||
}, | ||
messages: { | ||
[RULE_ID]: 'Unexpected non-null result {{ resultType }} in {{ rootType }}', | ||
}, | ||
schema: [], | ||
}, | ||
create(context) { | ||
const schema = requireGraphQLSchemaFromContext(RULE_ID, context); | ||
const rootTypeNames = new Set( | ||
[schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()] | ||
.filter(truthy) | ||
.map(type => type.name), | ||
); | ||
const sourceCode = context.getSourceCode(); | ||
|
||
return { | ||
'ObjectTypeDefinition,ObjectTypeExtension'( | ||
node: GraphQLESTreeNode<ObjectTypeDefinitionNode>, | ||
) { | ||
if (!rootTypeNames.has(node.name.value)) return; | ||
|
||
for (const field of node.fields || []) { | ||
if ( | ||
field.gqlType.type !== Kind.NON_NULL_TYPE || | ||
field.gqlType.gqlType.type !== Kind.NAMED_TYPE | ||
) | ||
continue; | ||
const name = field.gqlType.gqlType.name.value; | ||
const type = schema.getType(name); | ||
const resultType = type ? getNodeName(type.astNode as any) : ''; | ||
|
||
context.report({ | ||
node: field.gqlType, | ||
messageId: RULE_ID, | ||
data: { | ||
resultType, | ||
rootType: getNodeName(node), | ||
}, | ||
suggest: [ | ||
{ | ||
desc: `Make ${resultType} nullable`, | ||
fix(fixer) { | ||
const text = sourceCode.getText(field.gqlType as any); | ||
|
||
return fixer.replaceText(field.gqlType as any, text.replace('!', '')); | ||
}, | ||
}, | ||
], | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}; |
This file contains 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
63 changes: 63 additions & 0 deletions
63
packages/plugin/tests/__snapshots__/require-nullable-result-in-root.spec.md
This file contains 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,63 @@ | ||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html | ||
|
||
exports[`Invalid #1 1`] = ` | ||
#### ⌨️ Code | ||
|
||
1 | type Query { | ||
2 | user: User! | ||
3 | } | ||
4 | type User { | ||
5 | id: ID! | ||
6 | } | ||
|
||
#### ❌ Error | ||
|
||
1 | type Query { | ||
> 2 | user: User! | ||
| ^^^^ Unexpected non-null result type "User" in type "Query" | ||
3 | } | ||
|
||
#### 💡 Suggestion: Make type "User" nullable | ||
|
||
1 | type Query { | ||
2 | user: User | ||
3 | } | ||
4 | type User { | ||
5 | id: ID! | ||
6 | } | ||
`; | ||
|
||
exports[`should work with extend query 1`] = ` | ||
#### ⌨️ Code | ||
|
||
1 | type MyMutation | ||
2 | extend type MyMutation { | ||
3 | user: User! | ||
4 | } | ||
5 | interface User { | ||
6 | id: ID! | ||
7 | } | ||
8 | schema { | ||
9 | mutation: MyMutation | ||
10 | } | ||
|
||
#### ❌ Error | ||
|
||
2 | extend type MyMutation { | ||
> 3 | user: User! | ||
| ^^^^ Unexpected non-null result interface "User" in type "MyMutation" | ||
4 | } | ||
|
||
#### 💡 Suggestion: Make interface "User" nullable | ||
|
||
1 | type MyMutation | ||
2 | extend type MyMutation { | ||
3 | user: User | ||
4 | } | ||
5 | interface User { | ||
6 | id: ID! | ||
7 | } | ||
8 | schema { | ||
9 | mutation: MyMutation | ||
10 | } | ||
`; |
57 changes: 57 additions & 0 deletions
57
packages/plugin/tests/require-nullable-result-in-root.spec.ts
This file contains 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,57 @@ | ||
import { GraphQLRuleTester, ParserOptions } from '../src'; | ||
import { rule } from '../src/rules/require-nullable-result-in-root'; | ||
|
||
const ruleTester = new GraphQLRuleTester(); | ||
|
||
function useSchema(code: string): { code: string; parserOptions: Omit<ParserOptions, 'filePath'> } { | ||
return { | ||
code, | ||
parserOptions: { schema: code }, | ||
}; | ||
} | ||
|
||
ruleTester.runGraphQLTests('require-nullable-result-in-root', rule, { | ||
valid: [ | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type Query { | ||
foo: User | ||
baz: [User]! | ||
bar: [User!]! | ||
} | ||
type User { | ||
id: ID! | ||
} | ||
`), | ||
}, | ||
], | ||
invalid: [ | ||
{ | ||
...useSchema(/* GraphQL */ ` | ||
type Query { | ||
user: User! | ||
} | ||
type User { | ||
id: ID! | ||
} | ||
`), | ||
errors: 1, | ||
}, | ||
{ | ||
name: 'should work with extend query', | ||
...useSchema(/* GraphQL */ ` | ||
type MyMutation | ||
extend type MyMutation { | ||
user: User! | ||
} | ||
interface User { | ||
id: ID! | ||
} | ||
schema { | ||
mutation: MyMutation | ||
} | ||
`), | ||
errors: 1, | ||
}, | ||
], | ||
}); |
This file contains 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 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
42 changes: 42 additions & 0 deletions
42
website/src/pages/rules/require-nullable-result-in-root.md
This file contains 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,42 @@ | ||
# `require-nullable-result-in-root` | ||
|
||
💡 This rule provides | ||
[suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions) | ||
|
||
- Category: `Schema` | ||
- Rule name: `@graphql-eslint/require-nullable-result-in-root` | ||
- Requires GraphQL Schema: `true` | ||
[ℹ️](/docs/getting-started#extended-linting-rules-with-graphql-schema) | ||
- Requires GraphQL Operations: `false` | ||
[ℹ️](/docs/getting-started#extended-linting-rules-with-siblings-operations) | ||
|
||
Require nullable fields in root types. | ||
|
||
## Usage Examples | ||
|
||
### Incorrect | ||
|
||
```graphql | ||
# eslint @graphql-eslint/require-nullable-result-in-root: 'error' | ||
|
||
type Query { | ||
user: User! | ||
} | ||
``` | ||
|
||
### Correct | ||
|
||
```graphql | ||
# eslint @graphql-eslint/require-nullable-result-in-root: 'error' | ||
|
||
type Query { | ||
foo: User | ||
baz: [User]! | ||
bar: [User!]! | ||
} | ||
``` | ||
|
||
## Resources | ||
|
||
- [Rule source](https://github.com/B2o5T/graphql-eslint/tree/master/packages/plugin/src/rules/require-nullable-result-in-root.ts) | ||
- [Test source](https://github.com/B2o5T/graphql-eslint/tree/master/packages/plugin/tests/require-nullable-result-in-root.spec.ts) |