Skip to content

Commit c6886ba

Browse files
authored
🎉 NEW RULE: Compare operation/fragment name to the file name (#458)
1 parent 7b12bbf commit c6886ba

18 files changed

+717
-204
lines changed

.changeset/nasty-pumpkins-taste.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+
[New rule] Compare operation/fragment name to the file name

.changeset/swift-cobras-crash.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': patch
3+
---
4+
5+
NEW PLUGIN: Compare operation/fragment name to the file name

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Each rule has emojis denoting:
2424
| [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. |     🔮 | |
2525
| [lone-anonymous-operation](rules/lone-anonymous-operation.md) | A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition. |     🔮 | |
2626
| [lone-schema-definition](rules/lone-schema-definition.md) | A GraphQL document is only valid if it contains only one schema definition. |     🔮 | |
27+
| [match-document-filename](rules/match-document-filename.md) | This rule allows you to enforce that the file name should match the operation name |     🚀 | |
2728
| [naming-convention](rules/naming-convention.md) | Require names to follow specified conventions. |     🚀 | |
2829
| [no-anonymous-operations](rules/no-anonymous-operations.md) | Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes. |     🚀 | |
2930
| [no-case-insensitive-enum-values-duplicates](rules/no-case-insensitive-enum-values-duplicates.md) | |     🚀 | 🔧 |

docs/rules/match-document-filename.md

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# `match-document-filename`
2+
3+
- Category: `Best Practices`
4+
- Rule name: `@graphql-eslint/match-document-filename`
5+
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7+
8+
This rule allows you to enforce that the file name should match the operation name
9+
10+
## Usage Examples
11+
12+
### Correct
13+
14+
```graphql
15+
# eslint @graphql-eslint/match-document-filename: ['error', { fileExtension: '.gql' }]
16+
17+
# user.gql
18+
type User {
19+
id: ID!
20+
}
21+
```
22+
23+
### Correct
24+
25+
```graphql
26+
# eslint @graphql-eslint/match-document-filename: ['error', { query: 'snake_case' }]
27+
28+
# user_by_id.gql
29+
query UserById {
30+
userById(id: 5) {
31+
id
32+
name
33+
fullName
34+
}
35+
}
36+
```
37+
38+
### Correct
39+
40+
```graphql
41+
# eslint @graphql-eslint/match-document-filename: ['error', { fragment: { style: 'kebab-case', suffix: '.fragment' } }]
42+
43+
# user-fields.fragment.gql
44+
fragment user_fields on User {
45+
id
46+
email
47+
}
48+
```
49+
50+
### Correct
51+
52+
```graphql
53+
# eslint @graphql-eslint/match-document-filename: ['error', { mutation: { style: 'PascalCase', suffix: 'Mutation' } }]
54+
55+
# DeleteUserMutation.gql
56+
mutation DELETE_USER {
57+
deleteUser(id: 5)
58+
}
59+
```
60+
61+
### Incorrect
62+
63+
```graphql
64+
# eslint @graphql-eslint/match-document-filename: ['error', { fileExtension: '.graphql' }]
65+
66+
# post.gql
67+
type Post {
68+
id: ID!
69+
}
70+
```
71+
72+
### Incorrect
73+
74+
```graphql
75+
# eslint @graphql-eslint/match-document-filename: ['error', { query: 'PascalCase' }]
76+
77+
# user-by-id.gql
78+
query UserById {
79+
userById(id: 5) {
80+
id
81+
name
82+
fullName
83+
}
84+
}
85+
```
86+
87+
## Config Schema
88+
89+
### (array)
90+
91+
The schema defines an array with all elements of the type `object`.
92+
93+
The array object has the following properties:
94+
95+
#### `fileExtension` (string, enum)
96+
97+
This element must be one of the following enum values:
98+
99+
* `.gql`
100+
* `.graphql`
101+
102+
#### `query`
103+
104+
The object must be one of the following types:
105+
106+
* `asString`
107+
* `asObject`
108+
109+
#### `mutation`
110+
111+
The object must be one of the following types:
112+
113+
* `asString`
114+
* `asObject`
115+
116+
#### `subscription`
117+
118+
The object must be one of the following types:
119+
120+
* `asString`
121+
* `asObject`
122+
123+
#### `fragment`
124+
125+
The object must be one of the following types:
126+
127+
* `asString`
128+
* `asObject`
129+
130+
---
131+
132+
# Sub Schemas
133+
134+
The schema defines the following additional types:
135+
136+
## `asString` (string)
137+
138+
One of: `camelCase`, `PascalCase`, `snake_case`, `UPPER_CASE`, `kebab-case`
139+
140+
## `asObject` (object)
141+
142+
Properties of the `asObject` object:
143+
144+
### `style` (string, enum)
145+
146+
This element must be one of the following enum values:
147+
148+
* `camelCase`
149+
* `PascalCase`
150+
* `snake_case`
151+
* `UPPER_CASE`
152+
* `kebab-case`

packages/plugin/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@
3131
"@graphql-tools/import": "^6.3.1",
3232
"@graphql-tools/utils": "^8.0.2",
3333
"graphql-config": "^4.0.1",
34-
"graphql-depth-limit": "1.1.0"
34+
"graphql-depth-limit": "1.1.0",
35+
"lodash.lowercase": "^4.3.0"
3536
},
3637
"devDependencies": {
3738
"@types/eslint": "7.28.0",
3839
"@types/graphql-depth-limit": "1.1.2",
3940
"bob-the-bundler": "1.5.1",
4041
"graphql": "15.5.3",
41-
"typescript": "4.4.2"
42+
"typescript": "4.4.2",
43+
"@types/lodash.camelcase": "^4.3.6"
4244
},
4345
"peerDependencies": {
4446
"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"

packages/plugin/src/rules/avoid-operation-name-prefix.ts

Lines changed: 33 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
1+
import { GraphQLESLintRule } from '../types';
22
import { GraphQLESTreeNode } from '../estree-parser/estree-ast';
33
import { OperationDefinitionNode, FragmentDefinitionNode } from 'graphql';
44

@@ -11,41 +11,6 @@ export type AvoidOperationNamePrefixConfig = [
1111

1212
const AVOID_OPERATION_NAME_PREFIX = 'AVOID_OPERATION_NAME_PREFIX';
1313

14-
function verifyRule(
15-
context: GraphQLESLintRuleContext<AvoidOperationNamePrefixConfig>,
16-
node: GraphQLESTreeNode<OperationDefinitionNode> | GraphQLESTreeNode<FragmentDefinitionNode>
17-
) {
18-
const config = context.options[0] || { keywords: [], caseSensitive: false };
19-
const caseSensitive = config.caseSensitive;
20-
const keywords = config.keywords || [];
21-
22-
if (node && node.name && node.name.value !== '') {
23-
for (const keyword of keywords) {
24-
const testKeyword = caseSensitive ? keyword : keyword.toLowerCase();
25-
const testName = caseSensitive ? node.name.value : node.name.value.toLowerCase();
26-
27-
if (testName.startsWith(testKeyword)) {
28-
context.report({
29-
loc: {
30-
start: {
31-
line: node.name.loc.start.line,
32-
column: node.name.loc.start.column - 1,
33-
},
34-
end: {
35-
line: node.name.loc.start.line,
36-
column: node.name.loc.start.column + testKeyword.length - 1,
37-
},
38-
},
39-
data: {
40-
invalidPrefix: keyword,
41-
},
42-
messageId: AVOID_OPERATION_NAME_PREFIX,
43-
});
44-
}
45-
}
46-
}
47-
}
48-
4914
const rule: GraphQLESLintRule<AvoidOperationNamePrefixConfig> = {
5015
meta: {
5116
type: 'suggestion',
@@ -100,11 +65,38 @@ const rule: GraphQLESLintRule<AvoidOperationNamePrefixConfig> = {
10065
},
10166
create(context) {
10267
return {
103-
OperationDefinition(node) {
104-
verifyRule(context, node);
105-
},
106-
FragmentDefinition(node) {
107-
verifyRule(context, node);
68+
'OperationDefinition, FragmentDefinition'(
69+
node: GraphQLESTreeNode<OperationDefinitionNode | FragmentDefinitionNode>
70+
) {
71+
const config = context.options[0] || { keywords: [], caseSensitive: false };
72+
const caseSensitive = config.caseSensitive;
73+
const keywords = config.keywords || [];
74+
75+
if (node && node.name && node.name.value !== '') {
76+
for (const keyword of keywords) {
77+
const testKeyword = caseSensitive ? keyword : keyword.toLowerCase();
78+
const testName = caseSensitive ? node.name.value : node.name.value.toLowerCase();
79+
80+
if (testName.startsWith(testKeyword)) {
81+
context.report({
82+
loc: {
83+
start: {
84+
line: node.name.loc.start.line,
85+
column: node.name.loc.start.column - 1,
86+
},
87+
end: {
88+
line: node.name.loc.start.line,
89+
column: node.name.loc.start.column + testKeyword.length - 1,
90+
},
91+
},
92+
data: {
93+
invalidPrefix: keyword,
94+
},
95+
messageId: AVOID_OPERATION_NAME_PREFIX,
96+
});
97+
}
98+
}
99+
}
108100
},
109101
};
110102
},

packages/plugin/src/rules/avoid-typename-prefix.ts

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
1-
import { FieldDefinitionNode } from 'graphql';
1+
import {
2+
InterfaceTypeDefinitionNode,
3+
InterfaceTypeExtensionNode,
4+
ObjectTypeDefinitionNode,
5+
ObjectTypeExtensionNode,
6+
} from 'graphql';
27
import { GraphQLESTreeNode } from '../estree-parser';
3-
import { GraphQLESLintRule, GraphQLESLintRuleContext } from '../types';
8+
import { GraphQLESLintRule } from '../types';
49

510
const AVOID_TYPENAME_PREFIX = 'AVOID_TYPENAME_PREFIX';
611

7-
function checkNode(
8-
context: GraphQLESLintRuleContext<any>,
9-
typeName: string,
10-
fields: GraphQLESTreeNode<FieldDefinitionNode>[]
11-
) {
12-
const lowerTypeName = (typeName || '').toLowerCase();
13-
14-
for (const field of fields) {
15-
const fieldName = field.name.value || '';
16-
17-
if (fieldName && lowerTypeName && fieldName.toLowerCase().startsWith(lowerTypeName)) {
18-
context.report({
19-
node: field.name,
20-
data: {
21-
fieldName,
22-
typeName,
23-
},
24-
messageId: AVOID_TYPENAME_PREFIX,
25-
});
26-
}
27-
}
28-
}
29-
3012
const rule: GraphQLESLintRule = {
3113
meta: {
3214
type: 'suggestion',
@@ -60,17 +42,28 @@ const rule: GraphQLESLintRule = {
6042
},
6143
create(context) {
6244
return {
63-
ObjectTypeDefinition(node) {
64-
checkNode(context, node.name.value, node.fields);
65-
},
66-
ObjectTypeExtension(node) {
67-
checkNode(context, node.name.value, node.fields);
68-
},
69-
InterfaceTypeDefinition(node) {
70-
checkNode(context, node.name.value, node.fields);
71-
},
72-
InterfaceTypeExtension(node) {
73-
checkNode(context, node.name.value, node.fields);
45+
'ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension'(
46+
node: GraphQLESTreeNode<
47+
ObjectTypeDefinitionNode | ObjectTypeExtensionNode | InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode
48+
>
49+
) {
50+
const typeName = node.name.value;
51+
const lowerTypeName = (typeName || '').toLowerCase();
52+
53+
for (const field of node.fields) {
54+
const fieldName = field.name.value || '';
55+
56+
if (fieldName && lowerTypeName && fieldName.toLowerCase().startsWith(lowerTypeName)) {
57+
context.report({
58+
node: field.name,
59+
data: {
60+
fieldName,
61+
typeName,
62+
},
63+
messageId: AVOID_TYPENAME_PREFIX,
64+
});
65+
}
66+
}
7467
},
7568
};
7669
},

0 commit comments

Comments
 (0)