Skip to content

Commit

Permalink
add new option ignoredSelectors for require-description rule, to …
Browse files Browse the repository at this point in the history
…ignore eslint selectors, e.g. types which ends with `Connection` or `Edge` (#2782)

* more

* more

* more

* more

* more

* more

* more
  • Loading branch information
dimaMachina authored Nov 29, 2024
1 parent 98e0b56 commit 4c20cdd
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 114 deletions.
2 changes: 1 addition & 1 deletion .changeset/happy-bottles-warn.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
'@graphql-eslint/eslint-plugin': minor
---

introduce `forbiddenPattern` and `requiredPattern` options for `naming-convention` rule and
introduce `forbiddenPatterns` and `requiredPatterns` options for `naming-convention` rule and
deprecate `forbiddenPrefixes`, `forbiddenSuffixes` and `requiredPrefixes` and `requiredSuffixes`
6 changes: 6 additions & 0 deletions .changeset/long-chicken-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-eslint/eslint-plugin': minor
---

add new option `ignoredSelectors` for `require-description` rule, to ignore eslint selectors, e.g.
types which ends with `Connection` or `Edge`
8 changes: 4 additions & 4 deletions packages/plugin/src/rules/naming-convention/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,19 +527,19 @@ ruleTester.run<RuleOptions>('naming-convention', rule, {
errors: 2,
},
{
name: 'forbiddenPattern',
name: 'forbiddenPatterns',
code: 'query queryFoo { foo } query getBar { bar }',
options: [{ OperationDefinition: { forbiddenPattern: [/^(get|query)/] } }],
options: [{ OperationDefinition: { forbiddenPatterns: [/^(get|query)/] } }],
errors: 2,
},
{
name: 'requiredPattern',
name: 'requiredPatterns',
code: 'type Test { enabled: Boolean! }',
options: [
{
'FieldDefinition[gqlType.gqlType.name.value=Boolean]': {
style: 'camelCase',
requiredPattern: [/^(is|has)/],
requiredPatterns: [/^(is|has)/],
},
},
],
Expand Down
32 changes: 17 additions & 15 deletions packages/plugin/src/rules/naming-convention/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const schemaOption = {
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
} as const;

const descriptionPrefixesSuffixes = (name: 'forbiddenPattern' | 'requiredPattern') =>
const descriptionPrefixesSuffixes = (name: 'forbiddenPatterns' | 'requiredPatterns') =>
`> [!WARNING]
>
> This option is deprecated and will be removed in the next major release. Use [\`${name}\`](#${name.toLowerCase()}-array) instead.`;
Expand All @@ -66,14 +66,14 @@ const schema = {
style: { enum: ALLOWED_STYLES },
prefix: { type: 'string' },
suffix: { type: 'string' },
forbiddenPattern: {
forbiddenPatterns: {
...ARRAY_DEFAULT_OPTIONS,
items: {
type: 'object',
},
description: 'Should be of instance of `RegEx`',
},
requiredPattern: {
requiredPatterns: {
...ARRAY_DEFAULT_OPTIONS,
items: {
type: 'object',
Expand All @@ -82,19 +82,19 @@ const schema = {
},
forbiddenPrefixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('forbiddenPattern'),
description: descriptionPrefixesSuffixes('forbiddenPatterns'),
},
forbiddenSuffixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('forbiddenPattern'),
description: descriptionPrefixesSuffixes('forbiddenPatterns'),
},
requiredPrefixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('requiredPattern'),
description: descriptionPrefixesSuffixes('requiredPatterns'),
},
requiredSuffixes: {
...ARRAY_DEFAULT_OPTIONS,
description: descriptionPrefixesSuffixes('requiredPattern'),
description: descriptionPrefixesSuffixes('requiredPatterns'),
},
ignorePattern: {
type: 'string',
Expand All @@ -118,7 +118,9 @@ const schema = {
kind,
{
...schemaOption,
description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
description: `> [!NOTE]
>
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
},
]),
),
Expand Down Expand Up @@ -150,8 +152,8 @@ type PropertySchema = {
style?: AllowedStyle;
suffix?: string;
prefix?: string;
forbiddenPattern?: RegExp[];
requiredPattern?: RegExp[];
forbiddenPatterns?: RegExp[];
requiredPatterns?: RegExp[];
forbiddenPrefixes?: string[];
forbiddenSuffixes?: string[];
requiredPrefixes?: string[];
Expand Down Expand Up @@ -375,8 +377,8 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
ignorePattern,
requiredPrefixes,
requiredSuffixes,
forbiddenPattern,
requiredPattern,
forbiddenPatterns,
requiredPatterns,
} = normalisePropertyOption(selector);
const nodeName = node.value;
const error = getError();
Expand Down Expand Up @@ -415,16 +417,16 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
renameToNames: [name + suffix],
};
}
const forbidden = forbiddenPattern?.find(pattern => pattern.test(name));
const forbidden = forbiddenPatterns?.find(pattern => pattern.test(name));
if (forbidden) {
return {
errorMessage: `not contain the forbidden pattern "${forbidden}"`,
renameToNames: [name.replace(forbidden, '')],
};
}
if (requiredPattern && !requiredPattern.some(pattern => pattern.test(name))) {
if (requiredPatterns && !requiredPatterns.some(pattern => pattern.test(name))) {
return {
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPattern.map(re => re.source))}`,
errorMessage: `contain the required pattern: ${englishJoinWords(requiredPatterns.map(re => re.source))}`,
renameToNames: [],
};
}
Expand Down
8 changes: 4 additions & 4 deletions packages/plugin/src/rules/naming-convention/snapshot.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ exports[`naming-convention > invalid > Invalid #10 1`] = `
1 | query Foo { foo } query Bar { bar }
`;

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

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

{
"OperationDefinition": {
"forbiddenPattern": [
"forbiddenPatterns": [
"/^(get|query)/"
]
}
Expand Down Expand Up @@ -1973,7 +1973,7 @@ exports[`naming-convention > invalid > operations-recommended config 1`] = `
13 | fragment Test on Test { id }
`;

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

1 | type Test { enabled: Boolean! }
Expand All @@ -1983,7 +1983,7 @@ exports[`naming-convention > invalid > requiredPattern 1`] = `
{
"FieldDefinition[gqlType.gqlType.name.value=Boolean]": {
"style": "camelCase",
"requiredPattern": [
"requiredPatterns": [
"/^(is|has)/"
]
}
Expand Down
6 changes: 2 additions & 4 deletions packages/plugin/src/rules/no-unused-fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { FromSchema } from 'json-schema-to-ts';
import { ModuleCache } from '../../cache.js';
import { SiblingOperations } from '../../siblings.js';
import { GraphQLESLintRule, GraphQLESTreeNode } from '../../types.js';
import { requireGraphQLOperations, requireGraphQLSchema } from '../../utils.js';
import { eslintSelectorsTip, requireGraphQLOperations, requireGraphQLSchema } from '../../utils.js';

const RULE_ID = 'no-unused-fields';

Expand Down Expand Up @@ -89,9 +89,7 @@ const schema = {
'```json',
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
'```',
'',
'> These fields are defined by ESLint [`selectors`](https://eslint.org/docs/developer-guide/selectors).',
'> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.',
eslintSelectorsTip,
].join('\n'),
items: {
type: 'string',
Expand Down
41 changes: 41 additions & 0 deletions packages/plugin/src/rules/require-description/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,47 @@ ruleTester.run<RuleOptions>('require-description', rule, {
options: [{ rootField: true }],
errors: [{ messageId: RULE_ID }],
},
{
name: 'ignoredSelectors',
options: [
{
types: true,
ignoredSelectors: [
'[type=ObjectTypeDefinition][name.value=PageInfo]',
'[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]',
],
},
],
code: /* GraphQL */ `
type Query {
user: User
}
type User {
id: ID!
name: String!
friends(first: Int, after: String): FriendConnection!
}
type FriendConnection {
edges: [FriendEdge]
pageInfo: PageInfo!
}
type FriendEdge {
cursor: String!
node: Friend!
}
type Friend {
id: ID!
name: String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
`,
errors: 3,
},
],
});

Expand Down
71 changes: 58 additions & 13 deletions packages/plugin/src/rules/require-description/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { ASTKindToNode, Kind, TokenKind } from 'graphql';
import { getRootTypeNames } from '@graphql-tools/utils';
import { GraphQLESTreeNode } from '../../estree-converter/index.js';
import { GraphQLESLintRule, ValueOf } from '../../types.js';
import { getLocation, getNodeName, requireGraphQLSchema, TYPES_KINDS } from '../../utils.js';
import {
ARRAY_DEFAULT_OPTIONS,
eslintSelectorsTip,
getLocation,
getNodeName,
requireGraphQLSchema,
TYPES_KINDS,
} from '../../utils.js';

export const RULE_ID = 'require-description';

Expand Down Expand Up @@ -30,18 +37,31 @@ const schema = {
properties: {
types: {
type: 'boolean',
enum: [true],
description: `Includes:\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
},
rootField: {
type: 'boolean',
enum: [true],
description: 'Definitions within `Query`, `Mutation`, and `Subscription` root types.',
},
ignoredSelectors: {
...ARRAY_DEFAULT_OPTIONS,
description: ['Ignore specific selectors', eslintSelectorsTip].join('\n'),
},
...Object.fromEntries(
[...ALLOWED_KINDS].sort().map(kind => {
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
let description = `> [!NOTE]
>
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
if (kind === Kind.OPERATION_DEFINITION) {
description +=
'\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
description += [
'',
'',
'> [!WARNING]',
'>',
'> You must use only comment syntax `#` and not description syntax `"""` or `"`.',
].join('\n');
}
return [kind, { type: 'boolean', description }];
}),
Expand All @@ -55,8 +75,9 @@ export type RuleOptions = [
{
[key in AllowedKind]?: boolean;
} & {
types?: boolean;
rootField?: boolean;
types?: true;
rootField?: true;
ignoredSelectors?: string[];
},
];

Expand Down Expand Up @@ -115,6 +136,33 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
}
`,
},
{
title: 'Correct',
usage: [
{
ignoredSelectors: [
'[type=ObjectTypeDefinition][name.value=PageInfo]',
'[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]',
],
},
],
code: /* GraphQL */ `
type FriendConnection {
edges: [FriendEdge]
pageInfo: PageInfo!
}
type FriendEdge {
cursor: String!
node: Friend!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
`,
},
],
configOptions: [
{
Expand All @@ -132,7 +180,7 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
schema,
},
create(context) {
const { types, rootField, ...restOptions } = context.options[0] || {};
const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {};

const kinds = new Set<string>(types ? TYPES_KINDS : []);
for (const [kind, isEnabled] of Object.entries(restOptions)) {
Expand All @@ -152,13 +200,10 @@ export const rule: GraphQLESLintRule<RuleOptions> = {
].join(',')})$/] > FieldDefinition`,
);
}

if (!kinds.size) {
throw new Error('At least one kind must be enabled');
let selector = `:matches(${[...kinds]})`;
for (const str of ignoredSelectors) {
selector += `:not(${str})`;
}

const selector = [...kinds].join(',');

return {
[selector](node: SelectorNode) {
let description = '';
Expand Down
Loading

0 comments on commit 4c20cdd

Please sign in to comment.