Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading