Skip to content

Commit 1f21fc8

Browse files
Create new lone-executable-definition rule (#1316)
* Create new `lone-executable-definition` rule * Generate docs and configs * Other prettier changes * Allow ignoring first definition * refactor * fix typecheck on GraphQL 15 * add changeset Co-authored-by: Dimitri POSTOLOV <[email protected]>
1 parent fd99852 commit 1f21fc8

18 files changed

+476
-11
lines changed

.changeset/good-baboons-bathe.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
'@graphql-eslint/eslint-plugin': minor
33
---
44

5-
[require-description] add `rootField` option for only field definitions within `Query`, `Mutation`, and `Subscription` root types
5+
[require-description] add `rootField` option for only field definitions within `Query`, `Mutation`,
6+
and `Subscription` root types

.changeset/strange-donuts-warn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
feat: add `lone-executable-definition` to tequire all queries, mutations, subscriptions and fragments to be located in separate files

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,5 @@ dist
103103
# TernJS port file
104104
.tern-port
105105

106-
107106
.bob/
108107
.idea/

.husky/_/husky.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/bin/sh
22
if [ -z "$husky_skip_init" ]; then
3-
debug () {
3+
debug() {
44
[ "$HUSKY_DEBUG" = "1" ] && echo "husky (debug) - $1"
55
}
66

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## Available Rules
1+
# Available Rules
22

33
Each rule has emojis denoting:
44

@@ -26,6 +26,7 @@ Name&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbs
2626
[known-fragment-names](rules/known-fragment-names.md)|A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.|![recommended][]|📦|🔮|
2727
[known-type-names](rules/known-type-names.md)|A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.|![recommended][]|📄 📦|🔮|💡
2828
[lone-anonymous-operation](rules/lone-anonymous-operation.md)|A GraphQL document that contains an anonymous operation (the `query` short-hand) is only valid if it contains only that one operation definition.|![recommended][]|📦|🔮|
29+
[lone-executable-definition](rules/lone-executable-definition.md)|Require all queries, mutations, subscriptions and fragments to be located in separate files.|![all][]|📦|🚀|
2930
[lone-schema-definition](rules/lone-schema-definition.md)|A GraphQL document is only valid if it contains only one schema definition.|![recommended][]|📄|🔮|
3031
[match-document-filename](rules/match-document-filename.md)|This rule allows you to enforce that the file name should match the operation name.|![all][]|📦|🚀|
3132
[naming-convention](rules/naming-convention.md)|Require names to follow specified conventions.|![recommended][]|📄 📦|🚀|💡
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# `lone-executable-definition`
2+
3+
- Category: `Operations`
4+
- Rule name: `@graphql-eslint/lone-executable-definition`
5+
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `false`
7+
[ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
8+
9+
Require all queries, mutations, subscriptions and fragments to be located in separate files.
10+
11+
## Usage Examples
12+
13+
### Incorrect
14+
15+
```graphql
16+
# eslint @graphql-eslint/lone-executable-definition: 'error'
17+
18+
query Foo {
19+
id
20+
}
21+
fragment Bar on Baz {
22+
id
23+
}
24+
```
25+
26+
### Correct
27+
28+
```graphql
29+
# eslint @graphql-eslint/lone-executable-definition: 'error'
30+
31+
query Foo {
32+
id
33+
}
34+
```
35+
36+
## Config Schema
37+
38+
The schema defines the following properties:
39+
40+
### `ignore` (array)
41+
42+
Allow certain definitions to be placed alongside others.
43+
44+
The elements of the array can contain the following enum values:
45+
46+
- `fragment`
47+
- `query`
48+
- `mutation`
49+
- `subscription`
50+
51+
Additional restrictions:
52+
53+
- Minimum items: `1`
54+
- Unique items: `true`
55+
56+
## Resources
57+
58+
- [Rule source](../../packages/plugin/src/rules/lone-executable-definition.ts)
59+
- [Test source](../../packages/plugin/tests/lone-executable-definition.spec.ts)

docs/rules/require-description.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Includes:
8282

8383
### `rootField` (boolean)
8484

85-
Definitions within `Query`, `Mutation`, and `Subscription` root types
85+
Definitions within `Query`, `Mutation`, and `Subscription` root types.
8686

8787
### `DirectiveDefinition` (boolean)
8888

packages/plugin/src/configs/operations-all.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"arguments": ["Field", "Directive"]
1010
}
1111
],
12+
"@graphql-eslint/lone-executable-definition": "error",
1213
"@graphql-eslint/match-document-filename": [
1314
"error",
1415
{

packages/plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';
66
import { rule as alphabetize } from './alphabetize';
77
import { rule as descriptionStyle } from './description-style';
88
import { rule as inputName } from './input-name';
9+
import { rule as loneExecutableDefinition } from './lone-executable-definition';
910
import { rule as matchDocumentFilename } from './match-document-filename';
1011
import { rule as namingConvention } from './naming-convention';
1112
import { rule as noAnonymousOperations } from './no-anonymous-operations';
@@ -37,6 +38,7 @@ export const rules = {
3738
alphabetize,
3839
'description-style': descriptionStyle,
3940
'input-name': inputName,
41+
'lone-executable-definition': loneExecutableDefinition,
4042
'match-document-filename': matchDocumentFilename,
4143
'naming-convention': namingConvention,
4244
'no-anonymous-operations': noAnonymousOperations,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { GraphQLESLintRule } from '../types';
2+
import { ExecutableDefinitionNode } from 'graphql';
3+
import { GraphQLESTreeNode } from '../estree-converter';
4+
import { ARRAY_DEFAULT_OPTIONS, pascalCase, getLocation } from '../utils';
5+
6+
const RULE_ID = 'lone-executable-definition';
7+
8+
type Definition = 'fragment' | 'query' | 'mutation' | 'subscription';
9+
10+
const types: Definition[] = ['fragment', 'query', 'mutation', 'subscription'];
11+
12+
export interface LoneExecutableDefinitionConfig {
13+
ignore?: typeof types;
14+
}
15+
16+
type DefinitionESTreeNode = GraphQLESTreeNode<ExecutableDefinitionNode>;
17+
18+
export const rule: GraphQLESLintRule<[LoneExecutableDefinitionConfig]> = {
19+
meta: {
20+
type: 'suggestion',
21+
docs: {
22+
category: 'Operations',
23+
description:
24+
'Require all queries, mutations, subscriptions and fragments to be located in separate files.',
25+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
26+
examples: [
27+
{
28+
title: 'Incorrect',
29+
code: /* GraphQL */ `
30+
query Foo {
31+
id
32+
}
33+
fragment Bar on Baz {
34+
id
35+
}
36+
`,
37+
},
38+
{
39+
title: 'Correct',
40+
code: /* GraphQL */ `
41+
query Foo {
42+
id
43+
}
44+
`,
45+
},
46+
],
47+
},
48+
messages: {
49+
[RULE_ID]: '{{name}} should be in a separate file.',
50+
},
51+
schema: {
52+
type: 'array',
53+
minItems: 0,
54+
maxItems: 1,
55+
items: {
56+
type: 'object',
57+
minProperties: 1,
58+
additionalProperties: false,
59+
properties: {
60+
ignore: {
61+
...ARRAY_DEFAULT_OPTIONS,
62+
maxItems: 3, // ignore all 4 types is redundant
63+
items: {
64+
enum: types,
65+
},
66+
description: 'Allow certain definitions to be placed alongside others.',
67+
},
68+
},
69+
},
70+
},
71+
},
72+
create(context) {
73+
const ignore = new Set(context.options[0]?.ignore || []);
74+
const definitions: { type: Definition; node: DefinitionESTreeNode }[] = [];
75+
return {
76+
':matches(OperationDefinition, FragmentDefinition)'(node: DefinitionESTreeNode) {
77+
const type = 'operation' in node ? node.operation : 'fragment';
78+
if (!ignore.has(type)) {
79+
definitions.push({ type, node });
80+
}
81+
},
82+
'Program:exit'() {
83+
for (const { node, type } of definitions.slice(1) /* ignore first definition */) {
84+
let name = pascalCase(type);
85+
const definitionName = node.name?.value;
86+
if (definitionName) {
87+
name += ` "${definitionName}"`;
88+
}
89+
context.report({
90+
loc: node.name?.loc || getLocation(node.loc.start, type),
91+
messageId: RULE_ID,
92+
data: { name },
93+
});
94+
}
95+
},
96+
};
97+
},
98+
};

0 commit comments

Comments
 (0)