Skip to content

Commit 4c20cdd

Browse files
authored
add new option ignoredSelectors for require-description rule, to ignore eslint selectors, e.g. types which ends with Connection or Edge (#2782)
* more * more * more * more * more * more * more
1 parent 98e0b56 commit 4c20cdd

File tree

17 files changed

+381
-114
lines changed

17 files changed

+381
-114
lines changed

.changeset/happy-bottles-warn.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
'@graphql-eslint/eslint-plugin': minor
33
---
44

5-
introduce `forbiddenPattern` and `requiredPattern` options for `naming-convention` rule and
5+
introduce `forbiddenPatterns` and `requiredPatterns` options for `naming-convention` rule and
66
deprecate `forbiddenPrefixes`, `forbiddenSuffixes` and `requiredPrefixes` and `requiredSuffixes`

.changeset/long-chicken-press.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
add new option `ignoredSelectors` for `require-description` rule, to ignore eslint selectors, e.g.
6+
types which ends with `Connection` or `Edge`

packages/plugin/src/rules/naming-convention/index.test.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -527,19 +527,19 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
527527
errors: 2,
528528
},
529529
{
530-
name: 'forbiddenPattern',
530+
name: 'forbiddenPatterns',
531531
code: 'query queryFoo { foo } query getBar { bar }',
532-
options: [{ OperationDefinition: { forbiddenPattern: [/^(get|query)/] } }],
532+
options: [{ OperationDefinition: { forbiddenPatterns: [/^(get|query)/] } }],
533533
errors: 2,
534534
},
535535
{
536-
name: 'requiredPattern',
536+
name: 'requiredPatterns',
537537
code: 'type Test { enabled: Boolean! }',
538538
options: [
539539
{
540540
'FieldDefinition[gqlType.gqlType.name.value=Boolean]': {
541541
style: 'camelCase',
542-
requiredPattern: [/^(is|has)/],
542+
requiredPatterns: [/^(is|has)/],
543543
},
544544
},
545545
],

packages/plugin/src/rules/naming-convention/index.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const schemaOption = {
4848
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
4949
} as const;
5050

51-
const descriptionPrefixesSuffixes = (name: 'forbiddenPattern' | 'requiredPattern') =>
51+
const descriptionPrefixesSuffixes = (name: 'forbiddenPatterns' | 'requiredPatterns') =>
5252
`> [!WARNING]
5353
>
5454
> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`;
@@ -66,14 +66,14 @@ const schema = {
6666
style: { enum: ALLOWED_STYLES },
6767
prefix: { type: 'string' },
6868
suffix: { type: 'string' },
69-
forbiddenPattern: {
69+
forbiddenPatterns: {
7070
...ARRAY_DEFAULT_OPTIONS,
7171
items: {
7272
type: 'object',
7373
},
7474
description: 'Should be of instance of `RegEx`',
7575
},
76-
requiredPattern: {
76+
requiredPatterns: {
7777
...ARRAY_DEFAULT_OPTIONS,
7878
items: {
7979
type: 'object',
@@ -82,19 +82,19 @@ const schema = {
8282
},
8383
forbiddenPrefixes: {
8484
...ARRAY_DEFAULT_OPTIONS,
85-
description: descriptionPrefixesSuffixes('forbiddenPattern'),
85+
description: descriptionPrefixesSuffixes('forbiddenPatterns'),
8686
},
8787
forbiddenSuffixes: {
8888
...ARRAY_DEFAULT_OPTIONS,
89-
description: descriptionPrefixesSuffixes('forbiddenPattern'),
89+
description: descriptionPrefixesSuffixes('forbiddenPatterns'),
9090
},
9191
requiredPrefixes: {
9292
...ARRAY_DEFAULT_OPTIONS,
93-
description: descriptionPrefixesSuffixes('requiredPattern'),
93+
description: descriptionPrefixesSuffixes('requiredPatterns'),
9494
},
9595
requiredSuffixes: {
9696
...ARRAY_DEFAULT_OPTIONS,
97-
description: descriptionPrefixesSuffixes('requiredPattern'),
97+
description: descriptionPrefixesSuffixes('requiredPatterns'),
9898
},
9999
ignorePattern: {
100100
type: 'string',
@@ -118,7 +118,9 @@ const schema = {
118118
kind,
119119
{
120120
...schemaOption,
121-
description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
121+
description: `> [!NOTE]
122+
>
123+
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
122124
},
123125
]),
124126
),
@@ -150,8 +152,8 @@ type PropertySchema = {
150152
style?: AllowedStyle;
151153
suffix?: string;
152154
prefix?: string;
153-
forbiddenPattern?: RegExp[];
154-
requiredPattern?: RegExp[];
155+
forbiddenPatterns?: RegExp[];
156+
requiredPatterns?: RegExp[];
155157
forbiddenPrefixes?: string[];
156158
forbiddenSuffixes?: string[];
157159
requiredPrefixes?: string[];
@@ -375,8 +377,8 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
375377
ignorePattern,
376378
requiredPrefixes,
377379
requiredSuffixes,
378-
forbiddenPattern,
379-
requiredPattern,
380+
forbiddenPatterns,
381+
requiredPatterns,
380382
} = normalisePropertyOption(selector);
381383
const nodeName = node.value;
382384
const error = getError();
@@ -415,16 +417,16 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
415417
renameToNames: [name + suffix],
416418
};
417419
}
418-
const forbidden = forbiddenPattern?.find(pattern => pattern.test(name));
420+
const forbidden = forbiddenPatterns?.find(pattern => pattern.test(name));
419421
if (forbidden) {
420422
return {
421423
errorMessage: `not contain the forbidden pattern "${forbidden}"`,
422424
renameToNames: [name.replace(forbidden, '')],
423425
};
424426
}
425-
if (requiredPattern && !requiredPattern.some(pattern => pattern.test(name))) {
427+
if (requiredPatterns && !requiredPatterns.some(pattern => pattern.test(name))) {
426428
return {
427-
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPattern.map(re => re.source))}`,
429+
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPatterns.map(re => re.source))}`,
428430
renameToNames: [],
429431
};
430432
}

packages/plugin/src/rules/naming-convention/snapshot.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ exports[`naming-convention > invalid > Invalid #10 1`] = `
375375
1 | query Foo { foo } query Bar { bar }
376376
`;
377377

378-
exports[`naming-convention > invalid > forbiddenPattern 1`] = `
378+
exports[`naming-convention > invalid > forbiddenPatterns 1`] = `
379379
#### ⌨️ Code
380380

381381
1 | query queryFoo { foo } query getBar { bar }
@@ -384,7 +384,7 @@ exports[`naming-convention > invalid > forbiddenPattern 1`] = `
384384

385385
{
386386
"OperationDefinition": {
387-
"forbiddenPattern": [
387+
"forbiddenPatterns": [
388388
"/^(get|query)/"
389389
]
390390
}
@@ -1973,7 +1973,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = `
19731973
13 | fragment Test on Test { id }
19741974
`;
19751975

1976-
exports[`naming-convention > invalid > requiredPattern 1`] = `
1976+
exports[`naming-convention > invalid > requiredPatterns 1`] = `
19771977
#### ⌨️ Code
19781978

19791979
1 | type Test { enabled: Boolean! }
@@ -1983,7 +1983,7 @@ exports[`naming-convention > invalid > requiredPattern 1`] = `
19831983
{
19841984
"FieldDefinition[gqlType.gqlType.name.value=Boolean]": {
19851985
"style": "camelCase",
1986-
"requiredPattern": [
1986+
"requiredPatterns": [
19871987
"/^(is|has)/"
19881988
]
19891989
}

packages/plugin/src/rules/no-unused-fields/index.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { FromSchema } from 'json-schema-to-ts';
44
import { ModuleCache } from '../../cache.js';
55
import { SiblingOperations } from '../../siblings.js';
66
import { GraphQLESLintRule, GraphQLESTreeNode } from '../../types.js';
7-
import { requireGraphQLOperations, requireGraphQLSchema } from '../../utils.js';
7+
import { eslintSelectorsTip, requireGraphQLOperations, requireGraphQLSchema } from '../../utils.js';
88

99
const RULE_ID = 'no-unused-fields';
1010

@@ -89,9 +89,7 @@ const schema = {
8989
'```json',
9090
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
9191
'```',
92-
'',
93-
'> These fields are defined by ESLint [`selectors`](https://eslint.org/docs/developer-guide/selectors).',
94-
'> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.',
92+
eslintSelectorsTip,
9593
].join('\n'),
9694
items: {
9795
type: 'string',

packages/plugin/src/rules/require-description/index.test.ts

+41
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,47 @@ ruleTester.run<RuleOptions>('require-description', rule, {
228228
options: [{ rootField: true }],
229229
errors: [{ messageId: RULE_ID }],
230230
},
231+
{
232+
name: 'ignoredSelectors',
233+
options: [
234+
{
235+
types: true,
236+
ignoredSelectors: [
237+
'[type=ObjectTypeDefinition][name.value=PageInfo]',
238+
'[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]',
239+
],
240+
},
241+
],
242+
code: /* GraphQL */ `
243+
type Query {
244+
user: User
245+
}
246+
type User {
247+
id: ID!
248+
name: String!
249+
friends(first: Int, after: String): FriendConnection!
250+
}
251+
type FriendConnection {
252+
edges: [FriendEdge]
253+
pageInfo: PageInfo!
254+
}
255+
type FriendEdge {
256+
cursor: String!
257+
node: Friend!
258+
}
259+
type Friend {
260+
id: ID!
261+
name: String!
262+
}
263+
type PageInfo {
264+
hasPreviousPage: Boolean!
265+
hasNextPage: Boolean!
266+
startCursor: String
267+
endCursor: String
268+
}
269+
`,
270+
errors: 3,
271+
},
231272
],
232273
});
233274

packages/plugin/src/rules/require-description/index.ts

+58-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { ASTKindToNode, Kind, TokenKind } from 'graphql';
22
import { getRootTypeNames } from '@graphql-tools/utils';
33
import { GraphQLESTreeNode } from '../../estree-converter/index.js';
44
import { GraphQLESLintRule, ValueOf } from '../../types.js';
5-
import { getLocation, getNodeName, requireGraphQLSchema, TYPES_KINDS } from '../../utils.js';
5+
import {
6+
ARRAY_DEFAULT_OPTIONS,
7+
eslintSelectorsTip,
8+
getLocation,
9+
getNodeName,
10+
requireGraphQLSchema,
11+
TYPES_KINDS,
12+
} from '../../utils.js';
613

714
export const RULE_ID = 'require-description';
815

@@ -30,18 +37,31 @@ const schema = {
3037
properties: {
3138
types: {
3239
type: 'boolean',
40+
enum: [true],
3341
description: `Includes:\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
3442
},
3543
rootField: {
3644
type: 'boolean',
45+
enum: [true],
3746
description: 'Definitions within `Query`, `Mutation`, and `Subscription` root types.',
3847
},
48+
ignoredSelectors: {
49+
...ARRAY_DEFAULT_OPTIONS,
50+
description: ['Ignore specific selectors', eslintSelectorsTip].join('\n'),
51+
},
3952
...Object.fromEntries(
4053
[...ALLOWED_KINDS].sort().map(kind => {
41-
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
54+
let description = `> [!NOTE]
55+
>
56+
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
4257
if (kind === Kind.OPERATION_DEFINITION) {
43-
description +=
44-
'\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
58+
description += [
59+
'',
60+
'',
61+
'> [!WARNING]',
62+
'>',
63+
'> You must use only comment syntax `#` and not description syntax `"""` or `"`.',
64+
].join('\n');
4565
}
4666
return [kind, { type: 'boolean', description }];
4767
}),
@@ -55,8 +75,9 @@ export type RuleOptions = [
5575
{
5676
[key in AllowedKind]?: boolean;
5777
} & {
58-
types?: boolean;
59-
rootField?: boolean;
78+
types?: true;
79+
rootField?: true;
80+
ignoredSelectors?: string[];
6081
},
6182
];
6283

@@ -115,6 +136,33 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
115136
}
116137
`,
117138
},
139+
{
140+
title: 'Correct',
141+
usage: [
142+
{
143+
ignoredSelectors: [
144+
'[type=ObjectTypeDefinition][name.value=PageInfo]',
145+
'[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]',
146+
],
147+
},
148+
],
149+
code: /* GraphQL */ `
150+
type FriendConnection {
151+
edges: [FriendEdge]
152+
pageInfo: PageInfo!
153+
}
154+
type FriendEdge {
155+
cursor: String!
156+
node: Friend!
157+
}
158+
type PageInfo {
159+
hasPreviousPage: Boolean!
160+
hasNextPage: Boolean!
161+
startCursor: String
162+
endCursor: String
163+
}
164+
`,
165+
},
118166
],
119167
configOptions: [
120168
{
@@ -132,7 +180,7 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
132180
schema,
133181
},
134182
create(context) {
135-
const { types, rootField, ...restOptions } = context.options[0] || {};
183+
const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {};
136184

137185
const kinds = new Set<string>(types ? TYPES_KINDS : []);
138186
for (const [kind, isEnabled] of Object.entries(restOptions)) {
@@ -152,13 +200,10 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
152200
].join(',')})$/] > FieldDefinition`,
153201
);
154202
}
155-
156-
if (!kinds.size) {
157-
throw new Error('At least one kind must be enabled');
203+
let selector = `:matches(${[...kinds]})`;
204+
for (const str of ignoredSelectors) {
205+
selector += `:not(${str})`;
158206
}
159-
160-
const selector = [...kinds].join(',');
161-
162207
return {
163208
[selector](node: SelectorNode) {
164209
let description = '';

0 commit comments

Comments
 (0)