@graphql-eslint/eslint-plugin 3.0.0-alpha-5388f29.0 → 3.0.0-alpha-3168a9b.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -60
- package/configs/all.d.ts +3 -0
- package/configs/index.d.ts +3 -0
- package/docs/README.md +1 -0
- package/docs/custom-rules.md +36 -36
- package/docs/rules/no-root-type.md +56 -0
- package/estree-parser/converter.d.ts +2 -3
- package/index.js +127 -29
- package/index.mjs +128 -30
- package/package.json +9 -8
- package/rules/alphabetize.d.ts +6 -5
- package/rules/index.d.ts +9 -6
- package/rules/no-root-type.d.ts +7 -0
- package/rules/require-description.d.ts +2 -1
- package/utils.d.ts +3 -3
package/README.md
CHANGED
@@ -6,18 +6,18 @@ This project integrates GraphQL and ESLint, for a better developer experience.
|
|
6
6
|
|
7
7
|
[](https://badge.fury.io/js/%40graphql-eslint%2Feslint-plugin)
|
8
8
|
|
9
|
-
> Created and maintained by [The Guild](http://the-guild.dev
|
9
|
+
> Created and maintained by [The Guild](http://the-guild.dev)
|
10
10
|
|
11
11
|
## Key Features
|
12
12
|
|
13
|
-
- 🚀 Integrates with ESLint core (as a ESTree parser)
|
14
|
-
- 🚀 Works on `.graphql` files, `gql` usages and `/* GraphQL */` magic comments
|
15
|
-
- 🚀 Lints both GraphQL schema and GraphQL operations
|
13
|
+
- 🚀 Integrates with ESLint core (as a ESTree parser)
|
14
|
+
- 🚀 Works on `.graphql` files, `gql` usages and `/* GraphQL */` magic comments
|
15
|
+
- 🚀 Lints both GraphQL schema and GraphQL operations
|
16
16
|
- 🚀 Extended type info for more advanced usages
|
17
|
-
- 🚀 Supports ESLint directives (for example: `disable-next-line`)
|
18
|
-
- 🚀 Easily extendable - supports custom rules based on GraphQL's AST and ESLint API
|
19
|
-
- 🚀 Validates, lints, prettifies and checks for best practices across GraphQL schema and GraphQL operations
|
20
|
-
- 🚀 Integrates with [`graphql-config`](https://graphql-config.com
|
17
|
+
- 🚀 Supports ESLint directives (for example: `eslint-disable-next-line`)
|
18
|
+
- 🚀 Easily extendable - supports custom rules based on GraphQL's AST and ESLint API
|
19
|
+
- 🚀 Validates, lints, prettifies and checks for best practices across GraphQL schema and GraphQL operations
|
20
|
+
- 🚀 Integrates with [`graphql-config`](https://graphql-config.com)
|
21
21
|
- 🚀 Integrates and visualizes lint issues in popular IDEs (VSCode / WebStorm)
|
22
22
|
|
23
23
|
> Special thanks to [ilyavolodin](https://github.com/ilyavolodin) for his work on a similar project!
|
@@ -42,11 +42,13 @@ Or, with NPM:
|
|
42
42
|
npm install --save-dev @graphql-eslint/eslint-plugin
|
43
43
|
```
|
44
44
|
|
45
|
-
>
|
45
|
+
> Make sure you have `graphql` dependency in your project.
|
46
46
|
|
47
47
|
### Configuration
|
48
48
|
|
49
|
-
To get started,
|
49
|
+
To get started, define an override in your ESLint config to apply this plugin to `.graphql` files. Add the [rules](docs/README.md) you want applied.
|
50
|
+
|
51
|
+
> 🚨 Important! This step is necessary even if you are declaring operations and/or schema in code files.
|
50
52
|
|
51
53
|
```json
|
52
54
|
{
|
@@ -56,74 +58,90 @@ To get started, create an override configuration for your ESLint, while applying
|
|
56
58
|
"parser": "@graphql-eslint/eslint-plugin",
|
57
59
|
"plugins": ["@graphql-eslint"],
|
58
60
|
"rules": {
|
59
|
-
|
61
|
+
"@graphql-eslint/known-type-names": "error"
|
60
62
|
}
|
61
63
|
}
|
62
64
|
]
|
63
65
|
}
|
64
66
|
```
|
65
67
|
|
66
|
-
If
|
68
|
+
If your GraphQL definitions are defined only in `.graphql` files, and you're only using rules that apply to individual files, you should be good to go 👍. If you would like use a remote schema or use rules that apply across the entire collection of definitions at once, see [here](#using-a-remote-schema-or-rules-with-constraints-that-span-the-entire-schema).
|
67
69
|
|
68
|
-
|
70
|
+
#### Tell ESLint to apply this plugin to GraphQL definitions defined in code files
|
71
|
+
|
72
|
+
If you are defining GraphQL schema or GraphQL operations in code files, you'll want to define an additional override to extend the functionality of this plugin to the schema and operations in those files.
|
73
|
+
|
74
|
+
```diff
|
69
75
|
{
|
70
76
|
"overrides": [
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
77
|
+
+ {
|
78
|
+
+ "files": ["*.js"],
|
79
|
+
+ "processor": "@graphql-eslint/graphql"
|
80
|
+
+ },
|
75
81
|
{
|
76
82
|
"files": ["*.graphql"],
|
77
83
|
"parser": "@graphql-eslint/eslint-plugin",
|
78
84
|
"plugins": ["@graphql-eslint"],
|
79
85
|
"rules": {
|
80
|
-
|
86
|
+
"@graphql-eslint/known-type-names": "error"
|
81
87
|
}
|
82
88
|
}
|
83
89
|
]
|
84
90
|
}
|
85
91
|
```
|
86
92
|
|
87
|
-
|
93
|
+
Under the hood, specifying the `@graphql-eslint/graphql` processor for code files will cause `graphql-eslint/graphql` to extract the schema and operation definitions from these files into virtual GraphQL documents with `.graphql` extensions. This will allow the overrides you've defined for `.graphql` files, via `"files": ["*.graphql"]`, to get applied to the definitions defined in your code files.
|
88
94
|
|
89
|
-
|
95
|
+
#### Using a remote schema or rules with constraints that span the entire schema
|
90
96
|
|
91
|
-
|
97
|
+
Some rules require an understanding of the entire schema at once. For example, [no-unreachable-types](https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-unreachable-types.md#no-unreachable-types) checks that all types are reachable by root-level fields.
|
92
98
|
|
93
|
-
|
99
|
+
To use these rules, you'll need to tell ESLint how to identify the entire set of schema definitions.
|
94
100
|
|
95
|
-
|
101
|
+
If you are using [`graphql-config`](https://graphql-config.com), you are good to go. `graphql-eslint` integrates with it automatically and will use it to load your schema!
|
102
|
+
|
103
|
+
Alternatively, you can define `parserOptions.schema` in the `*.graphql` override in your ESLint config.
|
104
|
+
|
105
|
+
The parser allows you to specify a json file / graphql files(s) / url / raw string to locate your schema (We are using `graphql-tools` to do that). Just add `parserOptions.schema` to your configuration file:
|
106
|
+
|
107
|
+
```diff
|
96
108
|
{
|
97
109
|
"files": ["*.graphql"],
|
98
110
|
"parser": "@graphql-eslint/eslint-plugin",
|
99
111
|
"plugins": ["@graphql-eslint"],
|
100
|
-
"
|
101
|
-
"
|
102
|
-
}
|
112
|
+
"rules": {
|
113
|
+
"@graphql-eslint/no-unreachable-types": "error"
|
114
|
+
},
|
115
|
+
+ "parserOptions": {
|
116
|
+
+ "schema": "./schema.graphql"
|
117
|
+
+ }
|
103
118
|
}
|
104
119
|
```
|
105
120
|
|
106
|
-
> You can find a complete [documentation of the `parserOptions` here](
|
121
|
+
> You can find a complete [documentation of the `parserOptions` here](docs/parser-options.md).
|
107
122
|
|
108
|
-
> Some rules
|
123
|
+
> Some rules require type information to operate, it's marked in the docs for each rule!
|
109
124
|
|
110
125
|
#### Extended linting rules with siblings operations
|
111
126
|
|
112
127
|
While implementing this tool, we had to find solutions for a better integration of the GraphQL ecosystem and ESLint core.
|
113
128
|
|
114
|
-
GraphQL operations can be distributed across many files, while ESLint operates on one file at a time. If you are using GraphQL fragments in separate files, some rules might yield incorrect results, due the
|
129
|
+
GraphQL operations can be distributed across many files, while ESLint operates on one file at a time. If you are using GraphQL fragments in separate files, some rules might yield incorrect results, due the missing information.
|
115
130
|
|
116
131
|
To workaround that, we allow you to provide additional information on your GraphQL operations, making it available for rules while doing the actual linting.
|
117
132
|
|
118
|
-
To provide that, we are using
|
133
|
+
To provide that, we are using `graphql-tools` loaders to load your sibling operations and fragments, just specify a glob expression(s) that points to your code/`.graphql` files:
|
119
134
|
|
120
|
-
```
|
135
|
+
```diff
|
121
136
|
{
|
122
137
|
"files": ["*.graphql"],
|
123
138
|
"parser": "@graphql-eslint/eslint-plugin",
|
124
139
|
"plugins": ["@graphql-eslint"],
|
140
|
+
"rules": {
|
141
|
+
"@graphql-eslint/unique-operation-name": "error"
|
142
|
+
},
|
125
143
|
"parserOptions": {
|
126
|
-
|
144
|
+
+ "operations": "./src/**/*.graphql",
|
127
145
|
"schema": "./schema.graphql"
|
128
146
|
}
|
129
147
|
}
|
@@ -131,13 +149,13 @@ To provide that, we are using `@graphql-tools` loaders to load your sibling oper
|
|
131
149
|
|
132
150
|
### VSCode Integration
|
133
151
|
|
134
|
-
By default, [ESLint VSCode plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) will not lint files with extensions other
|
152
|
+
By default, [ESLint VSCode plugin](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) will not lint files with extensions other than `js`, `jsx`, `ts` and `tsx`.
|
135
153
|
|
136
154
|
In order to enable it processing other extensions, add the following section in `settings.json` or workspace configuration.
|
137
155
|
|
138
156
|
```json
|
139
157
|
{
|
140
|
-
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "graphql"]
|
158
|
+
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact", "graphql"]
|
141
159
|
}
|
142
160
|
```
|
143
161
|
|
@@ -156,13 +174,13 @@ type Query {
|
|
156
174
|
}
|
157
175
|
```
|
158
176
|
|
159
|
-
You can also specify specific rules to disable, apply it over the entire file, `next-line` or
|
177
|
+
You can also specify specific rules to disable, apply it over the entire file, `eslint-disable-next-line` or current `eslint-disable-line`.
|
160
178
|
|
161
179
|
You can find a list of [ESLint directives here](https://eslint.org/docs/2.13.1/user-guide/configuring#disabling-rules-with-inline-comments).
|
162
180
|
|
163
181
|
## Available Rules
|
164
182
|
|
165
|
-
You can find a complete list of [all available rules here](
|
183
|
+
You can find a complete list of [all available rules here](docs/README.md).
|
166
184
|
|
167
185
|
## Available Configs
|
168
186
|
|
@@ -177,19 +195,13 @@ Enable it in your `.eslintrc` file with the `extends` option.
|
|
177
195
|
"overrides": [
|
178
196
|
{
|
179
197
|
"files": ["*.js"],
|
180
|
-
"processor": "@graphql-eslint/graphql"
|
181
|
-
"rules": {
|
182
|
-
// your rules for JavaScript files
|
183
|
-
}
|
198
|
+
"processor": "@graphql-eslint/graphql"
|
184
199
|
},
|
185
200
|
{
|
186
201
|
"files": ["*.graphql"],
|
187
202
|
- "parser": "@graphql-eslint/eslint-plugin",
|
188
203
|
- "plugins": ["@graphql-eslint"],
|
189
|
-
+ "extends": "plugin:@graphql-eslint/recommended"
|
190
|
-
"rules": {
|
191
|
-
// your rules for GraphQL files
|
192
|
-
}
|
204
|
+
+ "extends": "plugin:@graphql-eslint/recommended" // or plugin:@graphql-eslint/all
|
193
205
|
}
|
194
206
|
]
|
195
207
|
}
|
@@ -197,26 +209,23 @@ Enable it in your `.eslintrc` file with the `extends` option.
|
|
197
209
|
|
198
210
|
### `prettier` rule
|
199
211
|
|
200
|
-
|
201
|
-
|
202
|
-
All you need to do is like the following for now:
|
212
|
+
`eslint-plugin-prettier` supports `.graphql` files. You need to do the following:
|
203
213
|
|
204
214
|
```js
|
205
|
-
// .eslintrc.js
|
206
215
|
module.exports = {
|
207
216
|
overrides: [
|
208
217
|
{
|
209
218
|
files: ['*.js'],
|
210
219
|
processor: '@graphql-eslint/graphql',
|
211
|
-
extends: ['plugin:prettier/recommended']
|
220
|
+
extends: ['plugin:prettier/recommended']
|
212
221
|
},
|
213
222
|
{
|
214
223
|
files: ['*.graphql'],
|
215
224
|
parser: '@graphql-eslint/eslint-plugin',
|
216
225
|
plugins: ['@graphql-eslint'],
|
217
226
|
rules: {
|
218
|
-
'prettier/prettier': 'error'
|
219
|
-
}
|
227
|
+
'prettier/prettier': 'error'
|
228
|
+
}
|
220
229
|
},
|
221
230
|
// the following is required for `eslint-plugin-prettier@<=3.4.0` temporarily
|
222
231
|
// after https://github.com/prettier/eslint-plugin-prettier/pull/415
|
@@ -224,22 +233,22 @@ module.exports = {
|
|
224
233
|
{
|
225
234
|
files: ['*.js/*.graphql'],
|
226
235
|
rules: {
|
227
|
-
'prettier/prettier': 'off'
|
228
|
-
}
|
229
|
-
}
|
230
|
-
]
|
231
|
-
}
|
236
|
+
'prettier/prettier': 'off'
|
237
|
+
}
|
238
|
+
}
|
239
|
+
]
|
240
|
+
}
|
232
241
|
```
|
233
242
|
|
234
243
|
You can take [`examples/prettier`](examples/prettier/.eslintrc.js) as example.
|
235
244
|
|
236
|
-
It could be better to remove the unnecessary `*.js/*.graphql`
|
245
|
+
It could be better to remove the unnecessary `*.js/*.graphql` override setting if <https://github.com/prettier/eslint-plugin-prettier/pull/415> will be merged and released.
|
237
246
|
|
238
247
|
Please help to vote up if you want to speed up the progress.
|
239
248
|
|
240
249
|
## Further Reading
|
241
250
|
|
242
|
-
If you wish to learn more about this project, how the parser works, how to add custom rules and more, [please refer to the docs directory](
|
251
|
+
If you wish to learn more about this project, how the parser works, how to add custom rules and more, [please refer to the docs directory](docs/README.md).
|
243
252
|
|
244
253
|
## Contributions
|
245
254
|
|
@@ -249,8 +258,8 @@ And if this is your first time contributing to this project, please do read our
|
|
249
258
|
|
250
259
|
### Code of Conduct
|
251
260
|
|
252
|
-
Help us keep GraphQL ESLint open and inclusive. Please read and follow our [Code of Conduct](https://github.com/the-guild-org/Stack/blob/master/CODE_OF_CONDUCT.md) as adopted from [Contributor Covenant](https://
|
261
|
+
Help us keep GraphQL ESLint open and inclusive. Please read and follow our [Code of Conduct](https://github.com/the-guild-org/Stack/blob/master/CODE_OF_CONDUCT.md) as adopted from [Contributor Covenant](https://contributor-covenant.org).
|
253
262
|
|
254
263
|
## License
|
255
264
|
|
256
|
-
Released under the [MIT license](
|
265
|
+
Released under the [MIT license](LICENSE).
|
package/configs/all.d.ts
CHANGED
@@ -15,6 +15,9 @@ export declare const allConfig: {
|
|
15
15
|
'@graphql-eslint/match-document-filename': string;
|
16
16
|
'@graphql-eslint/no-deprecated': string;
|
17
17
|
'@graphql-eslint/no-hashtag-description': string;
|
18
|
+
'@graphql-eslint/no-root-type': (string | {
|
19
|
+
disallow: string[];
|
20
|
+
})[];
|
18
21
|
'@graphql-eslint/no-unreachable-types': string;
|
19
22
|
'@graphql-eslint/no-unused-fields': string;
|
20
23
|
'@graphql-eslint/require-deprecation-date': string;
|
package/configs/index.d.ts
CHANGED
@@ -16,6 +16,9 @@ export declare const configs: {
|
|
16
16
|
'@graphql-eslint/match-document-filename': string;
|
17
17
|
'@graphql-eslint/no-deprecated': string;
|
18
18
|
'@graphql-eslint/no-hashtag-description': string;
|
19
|
+
'@graphql-eslint/no-root-type': (string | {
|
20
|
+
disallow: string[];
|
21
|
+
})[];
|
19
22
|
'@graphql-eslint/no-unreachable-types': string;
|
20
23
|
'@graphql-eslint/no-unused-fields': string;
|
21
24
|
'@graphql-eslint/require-deprecation-date': string;
|
package/docs/README.md
CHANGED
@@ -35,6 +35,7 @@ Name &nbs
|
|
35
35
|
[no-fragment-cycles](rules/no-fragment-cycles.md)|A GraphQL fragment is only valid when it does not have cycles in fragments usage.|🔮||✅
|
36
36
|
[no-hashtag-description](rules/no-hashtag-description.md)|Requires to use `"""` or `"` for adding a GraphQL description instead of `#`.|🚀||
|
37
37
|
[no-operation-name-suffix](rules/no-operation-name-suffix.md)|Makes sure you are not adding the operation type to the name of the operation.|🚀|🔧|✅
|
38
|
+
[no-root-type](rules/no-root-type.md)|Disallow using root types for `read-only` or `write-only` schemas.|🚀||
|
38
39
|
[no-undefined-variables](rules/no-undefined-variables.md)|A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.|🔮||✅
|
39
40
|
[no-unreachable-types](rules/no-unreachable-types.md)|Requires all types to be reachable at some level by root level fields.|🚀|🔧|
|
40
41
|
[no-unused-fields](rules/no-unused-fields.md)|Requires all fields to be used at some level by siblings operations.|🚀|🔧|
|
package/docs/custom-rules.md
CHANGED
@@ -17,22 +17,22 @@ The `graphql-eslint` comes with a TypeScript wrapper for ESLint rules, and provi
|
|
17
17
|
Here's an example for a simple rule that reports on anonymous GraphQL operations:
|
18
18
|
|
19
19
|
```ts
|
20
|
-
import { GraphQLESLintRule } from '@graphql-eslint/eslint-plugin'
|
20
|
+
import { GraphQLESLintRule } from '@graphql-eslint/eslint-plugin'
|
21
21
|
|
22
22
|
const rule: GraphQLESLintRule = {
|
23
23
|
create(context) {
|
24
24
|
return {
|
25
25
|
OperationDefinition(node) {
|
26
|
-
if (
|
26
|
+
if (!node.name || !node.name.value) {
|
27
27
|
context.report({
|
28
|
-
node
|
29
|
-
message:
|
30
|
-
})
|
28
|
+
node,
|
29
|
+
message: 'Oops, name is required!'
|
30
|
+
})
|
31
31
|
}
|
32
|
-
}
|
33
|
-
}
|
34
|
-
}
|
35
|
-
}
|
32
|
+
}
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
36
|
```
|
37
37
|
|
38
38
|
So what happens here?
|
@@ -58,23 +58,23 @@ This is useful if you wish to use other GraphQL tools that works with the origin
|
|
58
58
|
Here's an example for using original `graphql-js` validate method to validate `OperationDefinition`:
|
59
59
|
|
60
60
|
```ts
|
61
|
-
import { validate } from 'graphql'
|
62
|
-
import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'
|
61
|
+
import { validate } from 'graphql'
|
62
|
+
import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'
|
63
63
|
|
64
64
|
export const rule = {
|
65
65
|
create(context) {
|
66
66
|
return {
|
67
67
|
OperationDefinition(node) {
|
68
|
-
const schema = requireGraphQLSchemaFromContext(context)
|
68
|
+
const schema = requireGraphQLSchemaFromContext(context)
|
69
69
|
|
70
70
|
validate(context, schema, {
|
71
71
|
kind: Kind.DOCUMENT,
|
72
|
-
definitions: [node.rawNode()]
|
73
|
-
})
|
74
|
-
}
|
75
|
-
}
|
76
|
-
}
|
77
|
-
}
|
72
|
+
definitions: [node.rawNode()]
|
73
|
+
})
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
78
|
```
|
79
79
|
|
80
80
|
## `TypeInfo` / `GraphQLSchema`
|
@@ -89,7 +89,7 @@ If you provide GraphQL schema in your ESLint configuration, it will get loaded a
|
|
89
89
|
To mark your ESLint rules as a rule that needs access to GraphQL schema, start by running `requireGraphQLSchemaFromContext` from the plugin package, it will make sure to return a schema, or throw an error for the user about the missing schema.
|
90
90
|
|
91
91
|
```ts
|
92
|
-
const schema = requireGraphQLSchemaFromContext(context)
|
92
|
+
const schema = requireGraphQLSchemaFromContext(context)
|
93
93
|
```
|
94
94
|
|
95
95
|
#### Accessing TypeInfo
|
@@ -100,23 +100,23 @@ If your plugin requires `typeInfo` in order to operate and run, make sure to cal
|
|
100
100
|
`typeInfo` is provided on every node, based on the type of that node, for example, to access the `GraphQLOutputType` while you are visiting a `SelectionSet` node, you can do:
|
101
101
|
|
102
102
|
```ts
|
103
|
-
import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'
|
103
|
+
import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'
|
104
104
|
|
105
105
|
export const rule = {
|
106
106
|
create(context) {
|
107
|
-
requireGraphQLSchemaFromContext(context)
|
107
|
+
requireGraphQLSchemaFromContext(context)
|
108
108
|
|
109
109
|
return {
|
110
110
|
SelectionSet(node) {
|
111
|
-
const typeInfo = node.typeInfo()
|
111
|
+
const typeInfo = node.typeInfo()
|
112
112
|
|
113
113
|
if (typeInfo && typeInfo.gqlType) {
|
114
|
-
console.log(`The GraphQLOutputType is: ${typeInfo.gqlType}`)
|
114
|
+
console.log(`The GraphQLOutputType is: ${typeInfo.gqlType}`)
|
115
115
|
}
|
116
|
-
}
|
117
|
-
}
|
118
|
-
}
|
119
|
-
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
120
120
|
```
|
121
121
|
|
122
122
|
The structure of the return value of `.typeInfo()` is [defined here](https://github.com/dotansimha/graphql-eslint/blob/master/packages/plugin/src/estree-parser/converter.ts#L38-L46). So based on the `node` you are using, you'll get a different values on `.typeInfo()` result.
|
@@ -128,21 +128,21 @@ To test your rules, you can either use the wrapped `GraphQLRuleTester` from this
|
|
128
128
|
The wrapped `GraphQLRuleTester` provides built-in configured parser, and a schema loader, if you need to test your rule with a loaded schema.
|
129
129
|
|
130
130
|
```ts
|
131
|
-
import { GraphQLRuleTester } from '@graphql-eslint/eslint-plugin'
|
132
|
-
import { rule } from './my-rule'
|
131
|
+
import { GraphQLRuleTester } from '@graphql-eslint/eslint-plugin'
|
132
|
+
import { rule } from './my-rule'
|
133
133
|
|
134
|
-
const ruleTester = new GraphQLRuleTester()
|
134
|
+
const ruleTester = new GraphQLRuleTester()
|
135
135
|
|
136
136
|
ruleTester.runGraphQLTests('my-rule', rule, {
|
137
137
|
valid: [
|
138
138
|
{
|
139
|
-
code:
|
140
|
-
}
|
139
|
+
code: 'query something { foo }'
|
140
|
+
}
|
141
141
|
],
|
142
142
|
invalid: [
|
143
143
|
{
|
144
|
-
code:
|
145
|
-
}
|
146
|
-
]
|
147
|
-
})
|
144
|
+
code: 'query invalid { foo }'
|
145
|
+
}
|
146
|
+
]
|
147
|
+
})
|
148
148
|
```
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# `no-root-type`
|
2
|
+
|
3
|
+
- Category: `Validation`
|
4
|
+
- Rule name: `@graphql-eslint/no-root-type`
|
5
|
+
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
|
6
|
+
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
|
7
|
+
|
8
|
+
Disallow using root types for `read-only` or `write-only` schemas.
|
9
|
+
|
10
|
+
## Usage Examples
|
11
|
+
|
12
|
+
### Incorrect (`read-only` schema)
|
13
|
+
|
14
|
+
```graphql
|
15
|
+
# eslint @graphql-eslint/no-root-type: ['error', { disallow: ['mutation', 'subscription'] }]
|
16
|
+
|
17
|
+
type Mutation {
|
18
|
+
createUser(input: CreateUserInput!): User!
|
19
|
+
}
|
20
|
+
```
|
21
|
+
|
22
|
+
### Incorrect (`write-only` schema)
|
23
|
+
|
24
|
+
```graphql
|
25
|
+
# eslint @graphql-eslint/no-root-type: ['error', { disallow: ['query'] }]
|
26
|
+
|
27
|
+
type Query {
|
28
|
+
users: [User!]!
|
29
|
+
}
|
30
|
+
```
|
31
|
+
|
32
|
+
### Correct (`read-only` schema)
|
33
|
+
|
34
|
+
```graphql
|
35
|
+
# eslint @graphql-eslint/no-root-type: ['error', { disallow: ['mutation', 'subscription'] }]
|
36
|
+
|
37
|
+
type Query {
|
38
|
+
users: [User!]!
|
39
|
+
}
|
40
|
+
```
|
41
|
+
|
42
|
+
## Config Schema
|
43
|
+
|
44
|
+
The schema defines the following properties:
|
45
|
+
|
46
|
+
### `disallow` (array, required)
|
47
|
+
|
48
|
+
Additional restrictions:
|
49
|
+
|
50
|
+
* Minimum items: `1`
|
51
|
+
* Unique items: `true`
|
52
|
+
|
53
|
+
## Resources
|
54
|
+
|
55
|
+
- [Rule source](../../packages/plugin/src/rules/no-root-type.ts)
|
56
|
+
- [Test source](../../packages/plugin/tests/no-root-type.spec.ts)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import { GraphQLESTreeNode } from './estree-ast';
|
2
2
|
import { ASTNode, TypeInfo } from 'graphql';
|
3
|
-
import { Comment } from 'estree';
|
4
3
|
export declare function convertToESTree<T extends ASTNode>(node: T, typeInfo?: TypeInfo): {
|
5
|
-
rootTree: GraphQLESTreeNode<T>;
|
6
|
-
comments: Comment[];
|
4
|
+
rootTree: GraphQLESTreeNode<T, false>;
|
5
|
+
comments: import("estree").Comment[];
|
7
6
|
};
|
package/index.js
CHANGED
@@ -118,6 +118,7 @@ const allConfig = {
|
|
118
118
|
'@graphql-eslint/match-document-filename': 'error',
|
119
119
|
'@graphql-eslint/no-deprecated': 'error',
|
120
120
|
'@graphql-eslint/no-hashtag-description': 'error',
|
121
|
+
'@graphql-eslint/no-root-type': ['error', { disallow: ['subscription'] }],
|
121
122
|
'@graphql-eslint/no-unreachable-types': 'error',
|
122
123
|
'@graphql-eslint/no-unused-fields': 'error',
|
123
124
|
'@graphql-eslint/require-deprecation-date': 'error',
|
@@ -176,7 +177,7 @@ function getLexer(source) {
|
|
176
177
|
throw new Error(`Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!`);
|
177
178
|
}
|
178
179
|
function extractTokens(source) {
|
179
|
-
const lexer = getLexer(
|
180
|
+
const lexer = getLexer(source);
|
180
181
|
const tokens = [];
|
181
182
|
let token = lexer.advance();
|
182
183
|
while (token && token.kind !== '<EOF>') {
|
@@ -1885,7 +1886,7 @@ const rule$b = {
|
|
1885
1886
|
requireGraphQLSchemaFromContext('no-deprecated', context);
|
1886
1887
|
const typeInfo = node.typeInfo();
|
1887
1888
|
if (typeInfo && typeInfo.enumValue) {
|
1888
|
-
if (typeInfo.enumValue.
|
1889
|
+
if (typeInfo.enumValue.deprecationReason) {
|
1889
1890
|
const enumValueName = node.value;
|
1890
1891
|
context.report({
|
1891
1892
|
loc: getLocation(node.loc, enumValueName),
|
@@ -1902,7 +1903,7 @@ const rule$b = {
|
|
1902
1903
|
requireGraphQLSchemaFromContext('no-deprecated', context);
|
1903
1904
|
const typeInfo = node.typeInfo();
|
1904
1905
|
if (typeInfo && typeInfo.fieldDef) {
|
1905
|
-
if (typeInfo.fieldDef.
|
1906
|
+
if (typeInfo.fieldDef.deprecationReason) {
|
1906
1907
|
const fieldName = node.name.value;
|
1907
1908
|
context.report({
|
1908
1909
|
loc: getLocation(node.loc, fieldName),
|
@@ -2059,9 +2060,100 @@ const rule$d = {
|
|
2059
2060
|
},
|
2060
2061
|
};
|
2061
2062
|
|
2063
|
+
const ROOT_TYPES = ['query', 'mutation', 'subscription'];
|
2064
|
+
const rule$e = {
|
2065
|
+
meta: {
|
2066
|
+
type: 'suggestion',
|
2067
|
+
docs: {
|
2068
|
+
category: 'Validation',
|
2069
|
+
description: 'Disallow using root types for `read-only` or `write-only` schemas.',
|
2070
|
+
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md',
|
2071
|
+
requiresSchema: true,
|
2072
|
+
examples: [
|
2073
|
+
{
|
2074
|
+
title: 'Incorrect (`read-only` schema)',
|
2075
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2076
|
+
code: /* GraphQL */ `
|
2077
|
+
type Mutation {
|
2078
|
+
createUser(input: CreateUserInput!): User!
|
2079
|
+
}
|
2080
|
+
`,
|
2081
|
+
},
|
2082
|
+
{
|
2083
|
+
title: 'Incorrect (`write-only` schema)',
|
2084
|
+
usage: [{ disallow: ['query'] }],
|
2085
|
+
code: /* GraphQL */ `
|
2086
|
+
type Query {
|
2087
|
+
users: [User!]!
|
2088
|
+
}
|
2089
|
+
`,
|
2090
|
+
},
|
2091
|
+
{
|
2092
|
+
title: 'Correct (`read-only` schema)',
|
2093
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2094
|
+
code: /* GraphQL */ `
|
2095
|
+
type Query {
|
2096
|
+
users: [User!]!
|
2097
|
+
}
|
2098
|
+
`,
|
2099
|
+
},
|
2100
|
+
],
|
2101
|
+
optionsForConfig: [{ disallow: ['subscription'] }],
|
2102
|
+
},
|
2103
|
+
schema: {
|
2104
|
+
type: 'array',
|
2105
|
+
minItems: 1,
|
2106
|
+
maxItems: 1,
|
2107
|
+
items: {
|
2108
|
+
type: 'object',
|
2109
|
+
additionalProperties: false,
|
2110
|
+
required: ['disallow'],
|
2111
|
+
properties: {
|
2112
|
+
disallow: {
|
2113
|
+
type: 'array',
|
2114
|
+
uniqueItems: true,
|
2115
|
+
minItems: 1,
|
2116
|
+
items: {
|
2117
|
+
enum: ROOT_TYPES,
|
2118
|
+
},
|
2119
|
+
},
|
2120
|
+
},
|
2121
|
+
},
|
2122
|
+
},
|
2123
|
+
},
|
2124
|
+
create(context) {
|
2125
|
+
const schema = requireGraphQLSchemaFromContext('no-root-type', context);
|
2126
|
+
const disallow = new Set(context.options[0].disallow);
|
2127
|
+
const rootTypeNames = [
|
2128
|
+
disallow.has('query') && schema.getQueryType(),
|
2129
|
+
disallow.has('mutation') && schema.getMutationType(),
|
2130
|
+
disallow.has('subscription') && schema.getSubscriptionType(),
|
2131
|
+
]
|
2132
|
+
.filter(Boolean)
|
2133
|
+
.map(type => type.name);
|
2134
|
+
if (rootTypeNames.length === 0) {
|
2135
|
+
return {};
|
2136
|
+
}
|
2137
|
+
const selector = [
|
2138
|
+
`:matches(${graphql.Kind.OBJECT_TYPE_DEFINITION}, ${graphql.Kind.OBJECT_TYPE_EXTENSION})`,
|
2139
|
+
'>',
|
2140
|
+
`${graphql.Kind.NAME}[value=/^(${rootTypeNames.join('|')})$/]`,
|
2141
|
+
].join(' ');
|
2142
|
+
return {
|
2143
|
+
[selector](node) {
|
2144
|
+
const typeName = node.value;
|
2145
|
+
context.report({
|
2146
|
+
loc: getLocation(node.loc, typeName),
|
2147
|
+
message: `Root type "${typeName}" is forbidden`,
|
2148
|
+
});
|
2149
|
+
},
|
2150
|
+
};
|
2151
|
+
},
|
2152
|
+
};
|
2153
|
+
|
2062
2154
|
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
|
2063
2155
|
const RULE_NAME = 'no-unreachable-types';
|
2064
|
-
const rule$
|
2156
|
+
const rule$f = {
|
2065
2157
|
meta: {
|
2066
2158
|
messages: {
|
2067
2159
|
[UNREACHABLE_TYPE]: `Type "{{ typeName }}" is unreachable`,
|
@@ -2137,7 +2229,7 @@ const rule$e = {
|
|
2137
2229
|
|
2138
2230
|
const UNUSED_FIELD = 'UNUSED_FIELD';
|
2139
2231
|
const RULE_NAME$1 = 'no-unused-fields';
|
2140
|
-
const rule$
|
2232
|
+
const rule$g = {
|
2141
2233
|
meta: {
|
2142
2234
|
messages: {
|
2143
2235
|
[UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
|
@@ -2326,7 +2418,7 @@ const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
|
|
2326
2418
|
const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
|
2327
2419
|
const MESSAGE_INVALID_DATE = 'MESSAGE_INVALID_DATE';
|
2328
2420
|
const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
|
2329
|
-
const rule$
|
2421
|
+
const rule$h = {
|
2330
2422
|
meta: {
|
2331
2423
|
type: 'suggestion',
|
2332
2424
|
docs: {
|
@@ -2429,7 +2521,7 @@ const rule$g = {
|
|
2429
2521
|
},
|
2430
2522
|
};
|
2431
2523
|
|
2432
|
-
const rule$
|
2524
|
+
const rule$i = {
|
2433
2525
|
meta: {
|
2434
2526
|
docs: {
|
2435
2527
|
description: `Require all deprecation directives to specify a reason.`,
|
@@ -2509,7 +2601,7 @@ function verifyRule(context, node) {
|
|
2509
2601
|
}
|
2510
2602
|
}
|
2511
2603
|
}
|
2512
|
-
const rule$
|
2604
|
+
const rule$j = {
|
2513
2605
|
meta: {
|
2514
2606
|
docs: {
|
2515
2607
|
category: 'Best Practices',
|
@@ -2518,7 +2610,7 @@ const rule$i = {
|
|
2518
2610
|
examples: [
|
2519
2611
|
{
|
2520
2612
|
title: 'Incorrect',
|
2521
|
-
usage: [{ on: [
|
2613
|
+
usage: [{ on: [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.FIELD_DEFINITION] }],
|
2522
2614
|
code: /* GraphQL */ `
|
2523
2615
|
type someTypeName {
|
2524
2616
|
name: String
|
@@ -2527,7 +2619,7 @@ const rule$i = {
|
|
2527
2619
|
},
|
2528
2620
|
{
|
2529
2621
|
title: 'Correct',
|
2530
|
-
usage: [{ on: [
|
2622
|
+
usage: [{ on: [graphql.Kind.OBJECT_TYPE_DEFINITION, graphql.Kind.FIELD_DEFINITION] }],
|
2531
2623
|
code: /* GraphQL */ `
|
2532
2624
|
"""
|
2533
2625
|
Some type description
|
@@ -2574,7 +2666,7 @@ const rule$i = {
|
|
2574
2666
|
};
|
2575
2667
|
|
2576
2668
|
const RULE_NAME$2 = 'require-field-of-type-query-in-mutation-result';
|
2577
|
-
const rule$
|
2669
|
+
const rule$k = {
|
2578
2670
|
meta: {
|
2579
2671
|
type: 'suggestion',
|
2580
2672
|
docs: {
|
@@ -2742,7 +2834,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2742
2834
|
|
2743
2835
|
const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2744
2836
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2745
|
-
const rule$
|
2837
|
+
const rule$l = {
|
2746
2838
|
meta: {
|
2747
2839
|
type: 'suggestion',
|
2748
2840
|
docs: {
|
@@ -2878,7 +2970,7 @@ const rule$k = {
|
|
2878
2970
|
},
|
2879
2971
|
};
|
2880
2972
|
|
2881
|
-
const rule$
|
2973
|
+
const rule$m = {
|
2882
2974
|
meta: {
|
2883
2975
|
docs: {
|
2884
2976
|
category: 'Best Practices',
|
@@ -3000,7 +3092,7 @@ const shouldIgnoreNode = ({ node, exceptions }) => {
|
|
3000
3092
|
}
|
3001
3093
|
return false;
|
3002
3094
|
};
|
3003
|
-
const rule$
|
3095
|
+
const rule$n = {
|
3004
3096
|
meta: {
|
3005
3097
|
type: 'suggestion',
|
3006
3098
|
docs: {
|
@@ -3177,7 +3269,7 @@ const checkNode = (context, node, ruleName, messageId) => {
|
|
3177
3269
|
});
|
3178
3270
|
}
|
3179
3271
|
};
|
3180
|
-
const rule$
|
3272
|
+
const rule$o = {
|
3181
3273
|
meta: {
|
3182
3274
|
type: 'suggestion',
|
3183
3275
|
docs: {
|
@@ -3236,7 +3328,7 @@ const rule$n = {
|
|
3236
3328
|
|
3237
3329
|
const RULE_NAME$4 = 'unique-operation-name';
|
3238
3330
|
const UNIQUE_OPERATION_NAME = 'UNIQUE_OPERATION_NAME';
|
3239
|
-
const rule$
|
3331
|
+
const rule$p = {
|
3240
3332
|
meta: {
|
3241
3333
|
type: 'suggestion',
|
3242
3334
|
docs: {
|
@@ -3316,17 +3408,18 @@ const rules = {
|
|
3316
3408
|
'no-deprecated': rule$b,
|
3317
3409
|
'no-hashtag-description': rule$c,
|
3318
3410
|
'no-operation-name-suffix': rule$d,
|
3319
|
-
'no-
|
3320
|
-
'no-
|
3321
|
-
'
|
3322
|
-
'require-deprecation-
|
3323
|
-
'require-
|
3324
|
-
'require-
|
3325
|
-
'require-
|
3326
|
-
'
|
3327
|
-
'
|
3328
|
-
'
|
3329
|
-
'unique-
|
3411
|
+
'no-root-type': rule$e,
|
3412
|
+
'no-unreachable-types': rule$f,
|
3413
|
+
'no-unused-fields': rule$g,
|
3414
|
+
'require-deprecation-date': rule$h,
|
3415
|
+
'require-deprecation-reason': rule$i,
|
3416
|
+
'require-description': rule$j,
|
3417
|
+
'require-field-of-type-query-in-mutation-result': rule$k,
|
3418
|
+
'require-id-when-available': rule$l,
|
3419
|
+
'selection-set-depth': rule$m,
|
3420
|
+
'strict-id-in-types': rule$n,
|
3421
|
+
'unique-fragment-name': rule$o,
|
3422
|
+
'unique-operation-name': rule$p,
|
3330
3423
|
};
|
3331
3424
|
|
3332
3425
|
const RELEVANT_KEYWORDS = ['gql', 'graphql', '/* GraphQL */'];
|
@@ -3629,7 +3722,12 @@ function getReachableTypes(schema) {
|
|
3629
3722
|
Directive: collect,
|
3630
3723
|
NamedType: collect,
|
3631
3724
|
};
|
3632
|
-
for (const type of [
|
3725
|
+
for (const type of [
|
3726
|
+
schema,
|
3727
|
+
schema.getQueryType(),
|
3728
|
+
schema.getMutationType(),
|
3729
|
+
schema.getSubscriptionType(),
|
3730
|
+
]) {
|
3633
3731
|
if (type) {
|
3634
3732
|
graphql.visit(type.astNode, visitor);
|
3635
3733
|
}
|
@@ -3688,7 +3786,7 @@ function parseForESLint(code, options = {}) {
|
|
3688
3786
|
noLocation: false,
|
3689
3787
|
});
|
3690
3788
|
const { rootTree, comments } = convertToESTree(graphqlAst.document, schema ? new graphql.TypeInfo(schema) : null);
|
3691
|
-
const tokens = extractTokens(code);
|
3789
|
+
const tokens = extractTokens(new graphql.Source(code, filePath));
|
3692
3790
|
return {
|
3693
3791
|
services: parserServices,
|
3694
3792
|
parserServices,
|
package/index.mjs
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Kind,
|
1
|
+
import { Kind, validate, isScalarType, TokenKind, isNonNullType, isListType, isObjectType as isObjectType$1, visit, visitWithTypeInfo, GraphQLObjectType, GraphQLInterfaceType, TypeInfo, isInterfaceType, Source, GraphQLError } from 'graphql';
|
2
2
|
import { validateSDL } from 'graphql/validation/validate';
|
3
3
|
import { processImport, parseImportLine } from '@graphql-tools/import';
|
4
4
|
import { statSync, existsSync, readFileSync } from 'fs';
|
@@ -112,6 +112,7 @@ const allConfig = {
|
|
112
112
|
'@graphql-eslint/match-document-filename': 'error',
|
113
113
|
'@graphql-eslint/no-deprecated': 'error',
|
114
114
|
'@graphql-eslint/no-hashtag-description': 'error',
|
115
|
+
'@graphql-eslint/no-root-type': ['error', { disallow: ['subscription'] }],
|
115
116
|
'@graphql-eslint/no-unreachable-types': 'error',
|
116
117
|
'@graphql-eslint/no-unused-fields': 'error',
|
117
118
|
'@graphql-eslint/require-deprecation-date': 'error',
|
@@ -170,7 +171,7 @@ function getLexer(source) {
|
|
170
171
|
throw new Error(`Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!`);
|
171
172
|
}
|
172
173
|
function extractTokens(source) {
|
173
|
-
const lexer = getLexer(
|
174
|
+
const lexer = getLexer(source);
|
174
175
|
const tokens = [];
|
175
176
|
let token = lexer.advance();
|
176
177
|
while (token && token.kind !== '<EOF>') {
|
@@ -1879,7 +1880,7 @@ const rule$b = {
|
|
1879
1880
|
requireGraphQLSchemaFromContext('no-deprecated', context);
|
1880
1881
|
const typeInfo = node.typeInfo();
|
1881
1882
|
if (typeInfo && typeInfo.enumValue) {
|
1882
|
-
if (typeInfo.enumValue.
|
1883
|
+
if (typeInfo.enumValue.deprecationReason) {
|
1883
1884
|
const enumValueName = node.value;
|
1884
1885
|
context.report({
|
1885
1886
|
loc: getLocation(node.loc, enumValueName),
|
@@ -1896,7 +1897,7 @@ const rule$b = {
|
|
1896
1897
|
requireGraphQLSchemaFromContext('no-deprecated', context);
|
1897
1898
|
const typeInfo = node.typeInfo();
|
1898
1899
|
if (typeInfo && typeInfo.fieldDef) {
|
1899
|
-
if (typeInfo.fieldDef.
|
1900
|
+
if (typeInfo.fieldDef.deprecationReason) {
|
1900
1901
|
const fieldName = node.name.value;
|
1901
1902
|
context.report({
|
1902
1903
|
loc: getLocation(node.loc, fieldName),
|
@@ -2053,9 +2054,100 @@ const rule$d = {
|
|
2053
2054
|
},
|
2054
2055
|
};
|
2055
2056
|
|
2057
|
+
const ROOT_TYPES = ['query', 'mutation', 'subscription'];
|
2058
|
+
const rule$e = {
|
2059
|
+
meta: {
|
2060
|
+
type: 'suggestion',
|
2061
|
+
docs: {
|
2062
|
+
category: 'Validation',
|
2063
|
+
description: 'Disallow using root types for `read-only` or `write-only` schemas.',
|
2064
|
+
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md',
|
2065
|
+
requiresSchema: true,
|
2066
|
+
examples: [
|
2067
|
+
{
|
2068
|
+
title: 'Incorrect (`read-only` schema)',
|
2069
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2070
|
+
code: /* GraphQL */ `
|
2071
|
+
type Mutation {
|
2072
|
+
createUser(input: CreateUserInput!): User!
|
2073
|
+
}
|
2074
|
+
`,
|
2075
|
+
},
|
2076
|
+
{
|
2077
|
+
title: 'Incorrect (`write-only` schema)',
|
2078
|
+
usage: [{ disallow: ['query'] }],
|
2079
|
+
code: /* GraphQL */ `
|
2080
|
+
type Query {
|
2081
|
+
users: [User!]!
|
2082
|
+
}
|
2083
|
+
`,
|
2084
|
+
},
|
2085
|
+
{
|
2086
|
+
title: 'Correct (`read-only` schema)',
|
2087
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2088
|
+
code: /* GraphQL */ `
|
2089
|
+
type Query {
|
2090
|
+
users: [User!]!
|
2091
|
+
}
|
2092
|
+
`,
|
2093
|
+
},
|
2094
|
+
],
|
2095
|
+
optionsForConfig: [{ disallow: ['subscription'] }],
|
2096
|
+
},
|
2097
|
+
schema: {
|
2098
|
+
type: 'array',
|
2099
|
+
minItems: 1,
|
2100
|
+
maxItems: 1,
|
2101
|
+
items: {
|
2102
|
+
type: 'object',
|
2103
|
+
additionalProperties: false,
|
2104
|
+
required: ['disallow'],
|
2105
|
+
properties: {
|
2106
|
+
disallow: {
|
2107
|
+
type: 'array',
|
2108
|
+
uniqueItems: true,
|
2109
|
+
minItems: 1,
|
2110
|
+
items: {
|
2111
|
+
enum: ROOT_TYPES,
|
2112
|
+
},
|
2113
|
+
},
|
2114
|
+
},
|
2115
|
+
},
|
2116
|
+
},
|
2117
|
+
},
|
2118
|
+
create(context) {
|
2119
|
+
const schema = requireGraphQLSchemaFromContext('no-root-type', context);
|
2120
|
+
const disallow = new Set(context.options[0].disallow);
|
2121
|
+
const rootTypeNames = [
|
2122
|
+
disallow.has('query') && schema.getQueryType(),
|
2123
|
+
disallow.has('mutation') && schema.getMutationType(),
|
2124
|
+
disallow.has('subscription') && schema.getSubscriptionType(),
|
2125
|
+
]
|
2126
|
+
.filter(Boolean)
|
2127
|
+
.map(type => type.name);
|
2128
|
+
if (rootTypeNames.length === 0) {
|
2129
|
+
return {};
|
2130
|
+
}
|
2131
|
+
const selector = [
|
2132
|
+
`:matches(${Kind.OBJECT_TYPE_DEFINITION}, ${Kind.OBJECT_TYPE_EXTENSION})`,
|
2133
|
+
'>',
|
2134
|
+
`${Kind.NAME}[value=/^(${rootTypeNames.join('|')})$/]`,
|
2135
|
+
].join(' ');
|
2136
|
+
return {
|
2137
|
+
[selector](node) {
|
2138
|
+
const typeName = node.value;
|
2139
|
+
context.report({
|
2140
|
+
loc: getLocation(node.loc, typeName),
|
2141
|
+
message: `Root type "${typeName}" is forbidden`,
|
2142
|
+
});
|
2143
|
+
},
|
2144
|
+
};
|
2145
|
+
},
|
2146
|
+
};
|
2147
|
+
|
2056
2148
|
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
|
2057
2149
|
const RULE_NAME = 'no-unreachable-types';
|
2058
|
-
const rule$
|
2150
|
+
const rule$f = {
|
2059
2151
|
meta: {
|
2060
2152
|
messages: {
|
2061
2153
|
[UNREACHABLE_TYPE]: `Type "{{ typeName }}" is unreachable`,
|
@@ -2131,7 +2223,7 @@ const rule$e = {
|
|
2131
2223
|
|
2132
2224
|
const UNUSED_FIELD = 'UNUSED_FIELD';
|
2133
2225
|
const RULE_NAME$1 = 'no-unused-fields';
|
2134
|
-
const rule$
|
2226
|
+
const rule$g = {
|
2135
2227
|
meta: {
|
2136
2228
|
messages: {
|
2137
2229
|
[UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
|
@@ -2320,7 +2412,7 @@ const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
|
|
2320
2412
|
const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
|
2321
2413
|
const MESSAGE_INVALID_DATE = 'MESSAGE_INVALID_DATE';
|
2322
2414
|
const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
|
2323
|
-
const rule$
|
2415
|
+
const rule$h = {
|
2324
2416
|
meta: {
|
2325
2417
|
type: 'suggestion',
|
2326
2418
|
docs: {
|
@@ -2423,7 +2515,7 @@ const rule$g = {
|
|
2423
2515
|
},
|
2424
2516
|
};
|
2425
2517
|
|
2426
|
-
const rule$
|
2518
|
+
const rule$i = {
|
2427
2519
|
meta: {
|
2428
2520
|
docs: {
|
2429
2521
|
description: `Require all deprecation directives to specify a reason.`,
|
@@ -2503,7 +2595,7 @@ function verifyRule(context, node) {
|
|
2503
2595
|
}
|
2504
2596
|
}
|
2505
2597
|
}
|
2506
|
-
const rule$
|
2598
|
+
const rule$j = {
|
2507
2599
|
meta: {
|
2508
2600
|
docs: {
|
2509
2601
|
category: 'Best Practices',
|
@@ -2512,7 +2604,7 @@ const rule$i = {
|
|
2512
2604
|
examples: [
|
2513
2605
|
{
|
2514
2606
|
title: 'Incorrect',
|
2515
|
-
usage: [{ on: [
|
2607
|
+
usage: [{ on: [Kind.OBJECT_TYPE_DEFINITION, Kind.FIELD_DEFINITION] }],
|
2516
2608
|
code: /* GraphQL */ `
|
2517
2609
|
type someTypeName {
|
2518
2610
|
name: String
|
@@ -2521,7 +2613,7 @@ const rule$i = {
|
|
2521
2613
|
},
|
2522
2614
|
{
|
2523
2615
|
title: 'Correct',
|
2524
|
-
usage: [{ on: [
|
2616
|
+
usage: [{ on: [Kind.OBJECT_TYPE_DEFINITION, Kind.FIELD_DEFINITION] }],
|
2525
2617
|
code: /* GraphQL */ `
|
2526
2618
|
"""
|
2527
2619
|
Some type description
|
@@ -2568,7 +2660,7 @@ const rule$i = {
|
|
2568
2660
|
};
|
2569
2661
|
|
2570
2662
|
const RULE_NAME$2 = 'require-field-of-type-query-in-mutation-result';
|
2571
|
-
const rule$
|
2663
|
+
const rule$k = {
|
2572
2664
|
meta: {
|
2573
2665
|
type: 'suggestion',
|
2574
2666
|
docs: {
|
@@ -2736,7 +2828,7 @@ const convertNode = (typeInfo) => (node, key, parent) => {
|
|
2736
2828
|
|
2737
2829
|
const REQUIRE_ID_WHEN_AVAILABLE = 'REQUIRE_ID_WHEN_AVAILABLE';
|
2738
2830
|
const DEFAULT_ID_FIELD_NAME = 'id';
|
2739
|
-
const rule$
|
2831
|
+
const rule$l = {
|
2740
2832
|
meta: {
|
2741
2833
|
type: 'suggestion',
|
2742
2834
|
docs: {
|
@@ -2872,7 +2964,7 @@ const rule$k = {
|
|
2872
2964
|
},
|
2873
2965
|
};
|
2874
2966
|
|
2875
|
-
const rule$
|
2967
|
+
const rule$m = {
|
2876
2968
|
meta: {
|
2877
2969
|
docs: {
|
2878
2970
|
category: 'Best Practices',
|
@@ -2994,7 +3086,7 @@ const shouldIgnoreNode = ({ node, exceptions }) => {
|
|
2994
3086
|
}
|
2995
3087
|
return false;
|
2996
3088
|
};
|
2997
|
-
const rule$
|
3089
|
+
const rule$n = {
|
2998
3090
|
meta: {
|
2999
3091
|
type: 'suggestion',
|
3000
3092
|
docs: {
|
@@ -3171,7 +3263,7 @@ const checkNode = (context, node, ruleName, messageId) => {
|
|
3171
3263
|
});
|
3172
3264
|
}
|
3173
3265
|
};
|
3174
|
-
const rule$
|
3266
|
+
const rule$o = {
|
3175
3267
|
meta: {
|
3176
3268
|
type: 'suggestion',
|
3177
3269
|
docs: {
|
@@ -3230,7 +3322,7 @@ const rule$n = {
|
|
3230
3322
|
|
3231
3323
|
const RULE_NAME$4 = 'unique-operation-name';
|
3232
3324
|
const UNIQUE_OPERATION_NAME = 'UNIQUE_OPERATION_NAME';
|
3233
|
-
const rule$
|
3325
|
+
const rule$p = {
|
3234
3326
|
meta: {
|
3235
3327
|
type: 'suggestion',
|
3236
3328
|
docs: {
|
@@ -3310,17 +3402,18 @@ const rules = {
|
|
3310
3402
|
'no-deprecated': rule$b,
|
3311
3403
|
'no-hashtag-description': rule$c,
|
3312
3404
|
'no-operation-name-suffix': rule$d,
|
3313
|
-
'no-
|
3314
|
-
'no-
|
3315
|
-
'
|
3316
|
-
'require-deprecation-
|
3317
|
-
'require-
|
3318
|
-
'require-
|
3319
|
-
'require-
|
3320
|
-
'
|
3321
|
-
'
|
3322
|
-
'
|
3323
|
-
'unique-
|
3405
|
+
'no-root-type': rule$e,
|
3406
|
+
'no-unreachable-types': rule$f,
|
3407
|
+
'no-unused-fields': rule$g,
|
3408
|
+
'require-deprecation-date': rule$h,
|
3409
|
+
'require-deprecation-reason': rule$i,
|
3410
|
+
'require-description': rule$j,
|
3411
|
+
'require-field-of-type-query-in-mutation-result': rule$k,
|
3412
|
+
'require-id-when-available': rule$l,
|
3413
|
+
'selection-set-depth': rule$m,
|
3414
|
+
'strict-id-in-types': rule$n,
|
3415
|
+
'unique-fragment-name': rule$o,
|
3416
|
+
'unique-operation-name': rule$p,
|
3324
3417
|
};
|
3325
3418
|
|
3326
3419
|
const RELEVANT_KEYWORDS = ['gql', 'graphql', '/* GraphQL */'];
|
@@ -3623,7 +3716,12 @@ function getReachableTypes(schema) {
|
|
3623
3716
|
Directive: collect,
|
3624
3717
|
NamedType: collect,
|
3625
3718
|
};
|
3626
|
-
for (const type of [
|
3719
|
+
for (const type of [
|
3720
|
+
schema,
|
3721
|
+
schema.getQueryType(),
|
3722
|
+
schema.getMutationType(),
|
3723
|
+
schema.getSubscriptionType(),
|
3724
|
+
]) {
|
3627
3725
|
if (type) {
|
3628
3726
|
visit(type.astNode, visitor);
|
3629
3727
|
}
|
@@ -3682,7 +3780,7 @@ function parseForESLint(code, options = {}) {
|
|
3682
3780
|
noLocation: false,
|
3683
3781
|
});
|
3684
3782
|
const { rootTree, comments } = convertToESTree(graphqlAst.document, schema ? new TypeInfo(schema) : null);
|
3685
|
-
const tokens = extractTokens(code);
|
3783
|
+
const tokens = extractTokens(new Source(code, filePath));
|
3686
3784
|
return {
|
3687
3785
|
services: parserServices,
|
3688
3786
|
parserServices,
|
package/package.json
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
{
|
2
2
|
"name": "@graphql-eslint/eslint-plugin",
|
3
|
-
"version": "3.0.0-alpha-
|
3
|
+
"version": "3.0.0-alpha-3168a9b.0",
|
4
4
|
"sideEffects": false,
|
5
5
|
"peerDependencies": {
|
6
|
-
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0"
|
6
|
+
"graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0"
|
7
7
|
},
|
8
8
|
"dependencies": {
|
9
|
-
"@
|
10
|
-
"@graphql-tools/
|
11
|
-
"@graphql-tools/
|
12
|
-
"@graphql-tools/
|
13
|
-
"graphql-
|
9
|
+
"@babel/code-frame": "7.16.0",
|
10
|
+
"@graphql-tools/code-file-loader": "7.2.2",
|
11
|
+
"@graphql-tools/graphql-tag-pluck": "7.1.3",
|
12
|
+
"@graphql-tools/import": "6.6.1",
|
13
|
+
"@graphql-tools/utils": "8.5.3",
|
14
|
+
"graphql-config": "4.1.0",
|
14
15
|
"graphql-depth-limit": "1.1.0",
|
15
|
-
"lodash.lowercase": "
|
16
|
+
"lodash.lowercase": "4.3.0"
|
16
17
|
},
|
17
18
|
"repository": "https://github.com/dotansimha/graphql-eslint",
|
18
19
|
"author": "Dotan Simha <dotansimha@gmail.com>",
|
package/rules/alphabetize.d.ts
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
import { Kind } from 'graphql';
|
1
2
|
import { GraphQLESLintRule } from '../types';
|
2
|
-
declare const fieldsEnum:
|
3
|
-
declare const valuesEnum:
|
4
|
-
declare const selectionsEnum:
|
5
|
-
declare const variablesEnum:
|
6
|
-
declare const argumentsEnum:
|
3
|
+
declare const fieldsEnum: Kind[];
|
4
|
+
declare const valuesEnum: Kind[];
|
5
|
+
declare const selectionsEnum: Kind[];
|
6
|
+
declare const variablesEnum: Kind[];
|
7
|
+
declare const argumentsEnum: Kind[];
|
7
8
|
declare type AlphabetizeConfig = [
|
8
9
|
{
|
9
10
|
fields?: typeof fieldsEnum;
|
package/rules/index.d.ts
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
export declare const rules: {
|
2
2
|
alphabetize: import("..").GraphQLESLintRule<[{
|
3
|
-
fields?: ("
|
4
|
-
values?: "
|
5
|
-
selections?: ("
|
6
|
-
variables?: "
|
7
|
-
arguments?: ("
|
3
|
+
fields?: import("graphql").Kind[];
|
4
|
+
values?: import("graphql").Kind[];
|
5
|
+
selections?: import("graphql").Kind[];
|
6
|
+
variables?: import("graphql").Kind[];
|
7
|
+
arguments?: import("graphql").Kind[];
|
8
8
|
}], false>;
|
9
9
|
'avoid-duplicate-fields': import("..").GraphQLESLintRule<any[], false>;
|
10
10
|
'avoid-operation-name-prefix': import("..").GraphQLESLintRule<import("./avoid-operation-name-prefix").AvoidOperationNamePrefixConfig, false>;
|
@@ -161,6 +161,9 @@ export declare const rules: {
|
|
161
161
|
'no-deprecated': import("..").GraphQLESLintRule<[], true>;
|
162
162
|
'no-hashtag-description': import("..").GraphQLESLintRule<any[], false>;
|
163
163
|
'no-operation-name-suffix': import("..").GraphQLESLintRule<any[], false>;
|
164
|
+
'no-root-type': import("..").GraphQLESLintRule<[{
|
165
|
+
disallow: ("query" | "mutation" | "subscription")[];
|
166
|
+
}], false>;
|
164
167
|
'no-unreachable-types': import("..").GraphQLESLintRule<any[], false>;
|
165
168
|
'no-unused-fields': import("..").GraphQLESLintRule<any[], false>;
|
166
169
|
'require-deprecation-date': import("..").GraphQLESLintRule<[{
|
@@ -168,7 +171,7 @@ export declare const rules: {
|
|
168
171
|
}], false>;
|
169
172
|
'require-deprecation-reason': import("..").GraphQLESLintRule<any[], false>;
|
170
173
|
'require-description': import("..").GraphQLESLintRule<[{
|
171
|
-
on: ("
|
174
|
+
on: import("graphql").Kind[];
|
172
175
|
}], false>;
|
173
176
|
'require-field-of-type-query-in-mutation-result': import("..").GraphQLESLintRule<any[], false>;
|
174
177
|
'require-id-when-available': import("..").GraphQLESLintRule<[{
|
@@ -0,0 +1,7 @@
|
|
1
|
+
import { GraphQLESLintRule } from '../types';
|
2
|
+
declare const ROOT_TYPES: ('query' | 'mutation' | 'subscription')[];
|
3
|
+
declare type NoRootTypeConfig = {
|
4
|
+
disallow: typeof ROOT_TYPES;
|
5
|
+
};
|
6
|
+
declare const rule: GraphQLESLintRule<[NoRootTypeConfig]>;
|
7
|
+
export default rule;
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { GraphQLESLintRule } from '../types';
|
2
|
-
|
2
|
+
import { Kind } from 'graphql';
|
3
|
+
declare const DESCRIBABLE_NODES: Kind[];
|
3
4
|
declare type RequireDescriptionRuleConfig = [{
|
4
5
|
on: typeof DESCRIBABLE_NODES;
|
5
6
|
}];
|
package/utils.d.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { GraphQLSchema } from 'graphql';
|
1
|
+
import { GraphQLSchema, Source, Kind } from 'graphql';
|
2
2
|
import { AST } from 'eslint';
|
3
3
|
import { Source as LoaderSource } from '@graphql-tools/utils';
|
4
4
|
import { GraphQLESLintRuleContext } from './types';
|
@@ -8,7 +8,7 @@ export declare function requireSiblingsOperations(ruleName: string, context: Gra
|
|
8
8
|
export declare function requireGraphQLSchemaFromContext(ruleName: string, context: GraphQLESLintRuleContext): GraphQLSchema | never;
|
9
9
|
export declare function requireReachableTypesFromContext(ruleName: string, context: GraphQLESLintRuleContext): ReachableTypes | never;
|
10
10
|
export declare function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext): UsedFields | never;
|
11
|
-
export declare function extractTokens(source:
|
11
|
+
export declare function extractTokens(source: Source): AST.Token[];
|
12
12
|
export declare const normalizePath: (path: string) => string;
|
13
13
|
/**
|
14
14
|
* https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
|
@@ -19,7 +19,7 @@ export declare const normalizePath: (path: string) => string;
|
|
19
19
|
export declare const getOnDiskFilepath: (filepath: string) => string;
|
20
20
|
export declare const getTypeName: (node: any) => any;
|
21
21
|
export declare const loaderCache: Record<string, LoaderSource[]>;
|
22
|
-
export declare const TYPES_KINDS:
|
22
|
+
export declare const TYPES_KINDS: Kind[];
|
23
23
|
export declare enum CaseStyle {
|
24
24
|
camelCase = "camelCase",
|
25
25
|
pascalCase = "PascalCase",
|