Skip to content

Commit 5e1bbe6

Browse files
authored
NEW RULE: avoid-duplicate-fields (#245)
* NEW RULE: avoid-duplicate-fields * added more tests
1 parent 4942b58 commit 5e1bbe6

File tree

6 files changed

+251
-2
lines changed

6 files changed

+251
-2
lines changed

.changeset/clean-apricots-attend.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: avoid-duplicate-fields

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
- [`require-description`](./rules/require-description.md)
1616
- [`require-id-when-available`](./rules/require-id-when-available.md)
1717
- [`description-style`](./rules/description-style.md)
18-
- [`prettier`](./rules/prettier.md)
18+
- [`avoid-duplicate-fields`](./rules/avoid-duplicate-fields.md)
1919
- [`naming-convention`](./rules/naming-convention.md)
2020
- [`input-name`](./rules/input-name.md)
21+
- [`prettier`](./rules/prettier.md)
2122
- [`executable-definitions`](./rules/executable-definitions.md)
2223
- [`fields-on-correct-type`](./rules/fields-on-correct-type.md)
2324
- [`fragments-on-composite-type`](./rules/fragments-on-composite-type.md)

docs/rules/avoid-duplicate-fields.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# `avoid-duplicate-fields`
2+
3+
- Category: `Stylistic Issues`
4+
- Rule name: `@graphql-eslint/avoid-duplicate-fields`
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+
Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.
9+
10+
## Usage Examples
11+
12+
### Incorrect
13+
14+
```graphql
15+
# eslint @graphql-eslint/avoid-duplicate-fields: ["error"]
16+
17+
query getUserDetails {
18+
user {
19+
name # first
20+
email
21+
name # second
22+
}
23+
}
24+
```
25+
26+
### Incorrect
27+
28+
```graphql
29+
# eslint @graphql-eslint/avoid-duplicate-fields: ["error"]
30+
31+
query getUsers {
32+
users(
33+
first: 100
34+
skip: 50
35+
after: "cji629tngfgou0b73kt7vi5jo"
36+
first: 100 # duplicate argument
37+
) {
38+
id
39+
}
40+
}
41+
```
42+
43+
### Incorrect
44+
45+
```graphql
46+
# eslint @graphql-eslint/avoid-duplicate-fields: ["error"]
47+
48+
query getUsers($first: Int!, $first: Int!) {
49+
# Duplicate variable
50+
users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
51+
id
52+
}
53+
}
54+
```
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { Kind } from 'graphql';
2+
import { GraphQLESLintRule } from '../types';
3+
4+
const AVOID_DUPLICATE_FIELDS = 'AVOID_DUPLICATE_FIELDS';
5+
6+
const ensureUnique = () => {
7+
const set = new Set<string>();
8+
9+
return {
10+
add: (item: string, onError: () => void) => {
11+
if (set.has(item)) {
12+
onError();
13+
} else {
14+
set.add(item);
15+
}
16+
},
17+
};
18+
};
19+
20+
const rule: GraphQLESLintRule<[], false> = {
21+
meta: {
22+
type: 'suggestion',
23+
docs: {
24+
requiresSchema: false,
25+
requiresSiblings: false,
26+
description:
27+
'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
28+
category: 'Stylistic Issues',
29+
url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/avoid-duplicate-fields.md',
30+
examples: [
31+
{
32+
title: 'Incorrect',
33+
code: /* GraphQL */ `
34+
query getUserDetails {
35+
user {
36+
name # first
37+
email
38+
name # second
39+
}
40+
}
41+
`,
42+
},
43+
{
44+
title: 'Incorrect',
45+
code: /* GraphQL */ `
46+
query getUsers {
47+
users(
48+
first: 100
49+
skip: 50
50+
after: "cji629tngfgou0b73kt7vi5jo"
51+
first: 100 # duplicate argument
52+
) {
53+
id
54+
}
55+
}
56+
`,
57+
},
58+
{
59+
title: 'Incorrect',
60+
code: /* GraphQL */ `
61+
query getUsers($first: Int!, $first: Int!) {
62+
# Duplicate variable
63+
users(first: 100, skip: 50, after: "cji629tngfgou0b73kt7vi5jo") {
64+
id
65+
}
66+
}
67+
`,
68+
},
69+
],
70+
},
71+
messages: {
72+
[AVOID_DUPLICATE_FIELDS]: `{{ type }} "{{ fieldName }}" defined multiple times.`,
73+
},
74+
},
75+
create(context) {
76+
return {
77+
OperationDefinition(node) {
78+
const uniqueCheck = ensureUnique();
79+
80+
for (const arg of node.variableDefinitions || []) {
81+
uniqueCheck.add(arg.variable.name.value, () => {
82+
context.report({
83+
messageId: AVOID_DUPLICATE_FIELDS,
84+
data: {
85+
type: 'Operation variable',
86+
fieldName: arg.variable.name.value,
87+
},
88+
node: arg,
89+
});
90+
});
91+
}
92+
},
93+
Field(node) {
94+
const uniqueCheck = ensureUnique();
95+
96+
for (const arg of node.arguments || []) {
97+
uniqueCheck.add(arg.name.value, () => {
98+
context.report({
99+
messageId: AVOID_DUPLICATE_FIELDS,
100+
data: {
101+
type: 'Field argument',
102+
fieldName: arg.name.value,
103+
},
104+
node: arg,
105+
});
106+
});
107+
}
108+
},
109+
SelectionSet(node) {
110+
const uniqueCheck = ensureUnique();
111+
112+
for (const selection of node.selections || []) {
113+
if (selection.kind === Kind.FIELD) {
114+
const nameToCheck = selection.alias?.value || selection.name.value;
115+
116+
uniqueCheck.add(nameToCheck, () => {
117+
context.report({
118+
messageId: AVOID_DUPLICATE_FIELDS,
119+
data: {
120+
type: 'Field',
121+
fieldName: nameToCheck,
122+
},
123+
node: selection,
124+
});
125+
});
126+
}
127+
}
128+
},
129+
};
130+
},
131+
};
132+
133+
export default rule;

packages/plugin/src/rules/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import uniqueOperationName from './unique-operation-name';
1515
import noDeprecated from './no-deprecated';
1616
import noHashtagDescription from './no-hashtag-description';
1717
import selectionSetDepth from './selection-set-depth';
18+
import avoidDuplicateFields from './avoid-duplicate-fields';
1819
import { GraphQLESLintRule } from '../types';
1920
import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';
2021

@@ -33,8 +34,9 @@ export const rules: Record<string, GraphQLESLintRule> = {
3334
'require-description': requireDescription,
3435
'require-id-when-available': requireIdWhenAvailable,
3536
'description-style': descriptionStyle,
36-
prettier: prettier,
37+
'avoid-duplicate-fields': avoidDuplicateFields,
3738
'naming-convention': namingConvention,
3839
'input-name': inputName,
40+
prettier,
3941
...GRAPHQL_JS_VALIDATIONS,
4042
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { GraphQLRuleTester } from '../src/testkit';
2+
import rule from '../src/rules/avoid-duplicate-fields';
3+
4+
const ruleTester = new GraphQLRuleTester();
5+
6+
ruleTester.runGraphQLTests('avoid-duplicate-fields', rule, {
7+
valid: [],
8+
invalid: [
9+
{
10+
code: /* GraphQL */ `
11+
query test($v: String, $t: String, $v: String) {
12+
id
13+
}
14+
`,
15+
errors: [{ message: 'Operation variable "v" defined multiple times.' }],
16+
},
17+
{
18+
code: /* GraphQL */ `
19+
query test {
20+
users(first: 100, after: 10, filter: "test", first: 50) {
21+
id
22+
}
23+
}
24+
`,
25+
errors: [{ message: 'Field argument "first" defined multiple times.' }],
26+
},
27+
{
28+
code: /* GraphQL */ `
29+
query test {
30+
users {
31+
id
32+
name
33+
email
34+
name
35+
}
36+
}
37+
`,
38+
errors: [{ message: 'Field "name" defined multiple times.' }],
39+
},
40+
{
41+
code: /* GraphQL */ `
42+
query test {
43+
users {
44+
id
45+
name
46+
email
47+
email: somethingElse
48+
}
49+
}
50+
`,
51+
errors: [{ message: 'Field "email" defined multiple times.' }],
52+
},
53+
],
54+
});

0 commit comments

Comments
 (0)