From 875e67c3e35530598429c98622fc24c85a0defb9 Mon Sep 17 00:00:00 2001 From: Dimitri POSTOLOV Date: Fri, 17 Dec 2021 13:01:56 +0100 Subject: [PATCH] refactor: simplify AST converter refactor: generate .json configs instead typescript fix links use `valueFromASTUntyped` from graphql-js don't export functions from `./estree-parser`, but export `requireGraphQLSchemaFromContext` and `requireSiblingsOperations` --- .eslintrc.js | 1 - README.md | 10 +- docs/custom-rules.md | 16 +- docs/parser-options.md | 8 +- docs/parser.md | 4 +- packages/plugin/package.json | 1 + packages/plugin/src/configs/base.json | 4 + packages/plugin/src/configs/base.ts | 4 - packages/plugin/src/configs/index.ts | 13 -- .../plugin/src/configs/operations-all.json | 24 +++ packages/plugin/src/configs/operations-all.ts | 23 --- .../src/configs/operations-recommended.json | 50 +++++++ .../src/configs/operations-recommended.ts | 50 ------- packages/plugin/src/configs/schema-all.json | 26 ++++ packages/plugin/src/configs/schema-all.ts | 21 --- .../src/configs/schema-recommended.json | 49 +++++++ .../plugin/src/configs/schema-recommended.ts | 50 ------- .../plugin/src/estree-parser/converter.ts | 137 ++++++++---------- .../plugin/src/estree-parser/estree-ast.ts | 43 +++--- packages/plugin/src/estree-parser/utils.ts | 64 +------- packages/plugin/src/index.ts | 10 +- .../plugin/src/rules/graphql-js-validation.ts | 2 + packages/plugin/src/rules/no-root-type.ts | 1 + packages/plugin/src/rules/no-unused-fields.ts | 1 + packages/plugin/src/types.ts | 1 + scripts/constants.ts | 6 - scripts/generate-configs.ts | 92 ++++++------ scripts/generate-docs.ts | 19 +-- 28 files changed, 321 insertions(+), 409 deletions(-) create mode 100644 packages/plugin/src/configs/base.json delete mode 100644 packages/plugin/src/configs/base.ts delete mode 100644 packages/plugin/src/configs/index.ts create mode 100644 packages/plugin/src/configs/operations-all.json delete mode 100644 packages/plugin/src/configs/operations-all.ts create mode 100644 packages/plugin/src/configs/operations-recommended.json delete mode 100644 packages/plugin/src/configs/operations-recommended.ts create mode 100644 packages/plugin/src/configs/schema-all.json delete mode 100644 packages/plugin/src/configs/schema-all.ts create mode 100644 packages/plugin/src/configs/schema-recommended.json delete mode 100644 packages/plugin/src/configs/schema-recommended.ts delete mode 100644 scripts/constants.ts diff --git a/.eslintrc.js b/.eslintrc.js index cae97fb7d39..8248dc4116e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,7 +22,6 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/ban-ts-ignore': 'off', - '@typescript-eslint/ban-types': 'off', 'unicorn/prefer-array-some': 'error', 'unicorn/prefer-includes': 'error', 'unicorn/no-useless-fallback-in-spread': 'error', diff --git a/README.md b/README.md index 36c3d75a093..fe0ef02c90e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This project integrates GraphQL and ESLint, for a better developer experience. ## Key Features - πŸš€ Integrates with ESLint core (as a ESTree parser) -- πŸš€ Works on `.graphql` files, `gql` usages and `/* GraphQL */` magic comments +- πŸš€ Works on `.graphql` files, `gql` usages and `/*Β GraphQLΒ */` magic comments - πŸš€ Lints both GraphQL schema and GraphQL operations - πŸš€ Extended type info for more advanced usages - πŸš€ Supports ESLint directives (for example: `eslint-disable-next-line`) @@ -189,10 +189,10 @@ See [docs/deprecated-rules.md](docs/deprecated-rules.md). |Name|Description| |:-:|-| -|[`schema-recommended`](packages/plugin/src/configs/schema-recommended.ts)|enables recommended rules for schema (SDL) development| -|[`schema-all`](packages/plugin/src/configs/schema-all.ts)|enables all rules for schema (SDL) development, except for those that require `parserOptions.operations` option| -|[`operations-recommended`](packages/plugin/src/configs/operations-recommended.ts) |enables recommended rules for consuming GraphQL (operations) development| -|[`operations-all`](packages/plugin/src/configs/operations-all.ts)|enables all rules for consuming GraphQL (operations) development| +|[`schema-recommended`](packages/plugin/src/configs/schema-recommended.json)|enables recommended rules for schema (SDL) development| +|[`schema-all`](packages/plugin/src/configs/schema-all.json)|enables all rules for schema (SDL) development, except for those that require `parserOptions.operations` option| +|[`operations-recommended`](packages/plugin/src/configs/operations-recommended.json) |enables recommended rules for consuming GraphQL (operations) development| +|[`operations-all`](packages/plugin/src/configs/operations-all.json)|enables all rules for consuming GraphQL (operations) development| > If you are in a project that develops the GraphQL schema, you'll need `schema` rules. diff --git a/docs/custom-rules.md b/docs/custom-rules.md index aa8f5cd1aa7..3a99938373c 100644 --- a/docs/custom-rules.md +++ b/docs/custom-rules.md @@ -2,7 +2,7 @@ To get started with your own rules, start by understanding how [ESLint custom rules works](https://eslint.org/docs/developer-guide/working-with-rules). -`graphql-eslint` converts the [GraphQL AST](https://graphql.org/graphql-js/language/) into [ESTree structure](https://github.com/estree/estree), so it allows you to easily travel the GraphQL AST tree easily. +`graphql-eslint` converts the [GraphQL AST](https://graphql.org/graphql-js/language) into [ESTree structure](https://github.com/estree/estree), so it allows you to easily travel the GraphQL AST tree easily. You can visit any GraphQL AST node in your custom rules, and report this as error. You don't need to have special handlers for code-files, since `graphql-eslint` extracts usages of `gql` and magic `/* GraphQL */` comments automatically, and runs it through the parser, and eventually it knows to adjust errors location to fit in your code files original location. @@ -10,7 +10,7 @@ You can visit any GraphQL AST node in your custom rules, and report this as erro Start by creating a [simple ESLint rule file](https://eslint.org/docs/developer-guide/working-with-rules), and choose the AST nodes you wish to visit. It can either be a [simple AST node `Kind`](https://github.com/graphql/graphql-js/blob/master/src/language/kinds.d.ts) or a complex [ESLint selector](https://eslint.org/docs/developer-guide/selectors) that allows you to travel and filter AST nodes. -We recommend you to read the [graphql-eslint parser documentation](./parser.md) before getting started, to understand the differences between the AST structures. +We recommend you to read the [graphql-eslint parser documentation](parser.md) before getting started, to understand the differences between the AST structures. The `graphql-eslint` comes with a TypeScript wrapper for ESLint rules, and provides a testkit to simplify testing process with GraphQL schemas, so you can use that by importing `GraphQLESLintRule` type. But if you wish to use JavaScript - that's fine :) @@ -49,7 +49,7 @@ You can scan the `packages/plugin/src/rules` directory in this repo for referenc ## Accessing original GraphQL AST nodes Since our parser converts GraphQL AST to ESTree structure, there are some minor differences in the structure of the objects. -If you are using TypeScript, and you typed your rule with `GraphQLESLintRule` - you'll see that each `node` is a bit different from the AST nodes of GraphQL (you can read more about that in [graphql-eslint parser documentation](./parser.md)). +If you are using TypeScript, and you typed your rule with `GraphQLESLintRule` - you'll see that each `node` is a bit different from the AST nodes of GraphQL (you can read more about that in [graphql-eslint parser documentation](parser.md)). If you need access to the original GraphQL AST `node`, you can use `.rawNode()` method on each node you get from the AST structure of ESLint. @@ -104,13 +104,12 @@ import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin' export const rule = { create(context) { - requireGraphQLSchemaFromContext(context) + requireGraphQLSchemaFromContext('your-rule-name', context) return { SelectionSet(node) { const typeInfo = node.typeInfo() - - if (typeInfo && typeInfo.gqlType) { + if (typeInfo.gqlType) { console.log(`The GraphQLOutputType is: ${typeInfo.gqlType}`) } } @@ -119,7 +118,7 @@ export const rule = { } ``` -The structure of the return value of `.typeInfo()` is [defined here](https://github.com/dotansimha/graphql-eslint/blob/master/packages/plugin/src/estree-parser/converter.ts#L38-L46). So based on the `node` you are using, you'll get a different values on `.typeInfo()` result. +The structure of the return value of `.typeInfo()` is [defined here](https://github.com/dotansimha/graphql-eslint/blob/master/packages/plugin/src/estree-parser/converter.ts#L45-L53). So based on the `node` you are using, you'll get a different values on `.typeInfo()` result. ## Testing your rules @@ -141,7 +140,8 @@ ruleTester.runGraphQLTests('my-rule', rule, { ], invalid: [ { - code: 'query invalid { foo }' + code: 'query invalid { foo }', + errors: [{ message: 'Your error message.' }], } ] }) diff --git a/docs/parser-options.md b/docs/parser-options.md index 1e09e014d18..6f6a72eff84 100644 --- a/docs/parser-options.md +++ b/docs/parser-options.md @@ -2,13 +2,13 @@ ### `graphQLParserOptions` -With this configuration, you can specify custom configurations for GraphQL's `parse` method. By default, `graphql-eslint` parser just adds `noLocation: false` to make sure all parsed AST has `location` set, since we need this for tokening and for converting the GraphQL AST into ESTree. +With this configuration, you can specify custom configurations for GraphQL's `parse` method. By default, `graphql-eslint` parser just adds `noLocation: false` to make sure all parsed AST has `location` set, since we need this for tokenizing and for converting the GraphQL AST into ESTree. -You can find the [complete set of options for this object here](https://github.com/graphql/graphql-js/blob/master/src/language/parser.d.ts#L7) +You can find the [complete set of options for this object here](https://github.com/graphql/graphql-js/blob/6e48d16f92b9a6df8638b1486354c6be2537033b/src/language/parser.ts#L73) ### `skipGraphQLConfig` -If you are using [`graphql-config`](https://graphql-config.com/) in your project, the parser will automatically use that to load your default GraphQL schema. +If you are using [`graphql-config`](https://graphql-config.com) in your project, the parser will automatically use that to load your default GraphQL schema. You can disable this behaviour using `skipGraphQLConfig: true` in the `parserOptions`: @@ -82,4 +82,4 @@ If you wish to send additional configuration for the `graphql-tools` loaders tha } ``` -> The configuration here is flexible, and will be sent to `graphql-tools` and it's loaders. So depends on the schema source, the options may vary. [You can read more about these loaders and their configuration here](https://www.graphql-tools.com/docs/api/interfaces/_loaders_graphql_file_src_index_.graphqlfileloaderoptions). +> The configuration here is flexible, and will be sent to `graphql-tools` and it's loaders. So depends on the schema source, the options may vary. [You can read more about these loaders and their configuration here](https://graphql-tools.com/docs/api/interfaces/loaders_graphql_file_src.GraphQLFileLoaderOptions#properties). diff --git a/docs/parser.md b/docs/parser.md index 77c4707b12f..877685aec27 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -40,10 +40,10 @@ Here's a list of changes that the parser performs, in order to make the GraphQL ### Loading GraphQL Schema -If you are using [`graphql-config`](https://graphql-config.com/) in your project, the parser will automatically use that to load your default GraphQL schema (you can disable this behaviour using `skipGraphQLConfig: true` in the `parserOptions`). +If you are using [`graphql-config`](https://graphql-config.com) in your project, the parser will automatically use that to load your default GraphQL schema (you can disable this behaviour using `skipGraphQLConfig: true` in the `parserOptions`). If you are not using `graphql-config`, you can specify `parserOptions.schema` to load your GraphQL schema. The parser uses `graphql-tools` and it's loaders, that means you can either specify a URL, a path to a local `.json` (introspection) file, or a path to a local `.graphql` file(s). You can also use Glob expressions to load multiple files. -[You can find more detail on the `parserOptions` config here](./parser-options.md) +[You can find more detail on the `parserOptions` config here](parser-options.md) Providing the schema will make sure that rules that needs it will be able to access it, and it enriches every converted AST node with `typeInfo`. diff --git a/packages/plugin/package.json b/packages/plugin/package.json index e8cf666abe5..0820589587f 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -57,6 +57,7 @@ }, "buildOptions": { "input": "./src/index.ts", + "copy": "./src/configs", "external": [ "eslint", "graphql", diff --git a/packages/plugin/src/configs/base.json b/packages/plugin/src/configs/base.json new file mode 100644 index 00000000000..bb8ba561f6d --- /dev/null +++ b/packages/plugin/src/configs/base.json @@ -0,0 +1,4 @@ +{ + "parser": "@graphql-eslint/eslint-plugin", + "plugins": ["@graphql-eslint"] +} diff --git a/packages/plugin/src/configs/base.ts b/packages/plugin/src/configs/base.ts deleted file mode 100644 index 3abe834ee5f..00000000000 --- a/packages/plugin/src/configs/base.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - parser: '@graphql-eslint/eslint-plugin', - plugins: ['@graphql-eslint'], -}; diff --git a/packages/plugin/src/configs/index.ts b/packages/plugin/src/configs/index.ts deleted file mode 100644 index ad34eaf0e3b..00000000000 --- a/packages/plugin/src/configs/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -import base from './base'; -import schemaRecommendedConfig from './schema-recommended'; -import schemaAllConfig from './schema-all'; -import operationsRecommendedConfig from './operations-recommended'; -import operationsAllConfig from './operations-all'; - -export const configs = { - base, - 'schema-recommended': schemaRecommendedConfig, - 'schema-all': schemaAllConfig, - 'operations-recommended': operationsRecommendedConfig, - 'operations-all': operationsAllConfig, -}; diff --git a/packages/plugin/src/configs/operations-all.json b/packages/plugin/src/configs/operations-all.json new file mode 100644 index 00000000000..fe19a027898 --- /dev/null +++ b/packages/plugin/src/configs/operations-all.json @@ -0,0 +1,24 @@ +{ + "extends": ["./base.json", "./operations-recommended.json"], + "rules": { + "@graphql-eslint/alphabetize": [ + "error", + { + "selections": ["OperationDefinition", "FragmentDefinition"], + "variables": ["OperationDefinition"], + "arguments": ["Field", "Directive"] + } + ], + "@graphql-eslint/match-document-filename": [ + "error", + { + "query": "kebab-case", + "mutation": "kebab-case", + "subscription": "kebab-case", + "fragment": "kebab-case" + } + ], + "@graphql-eslint/unique-fragment-name": "error", + "@graphql-eslint/unique-operation-name": "error" + } +} diff --git a/packages/plugin/src/configs/operations-all.ts b/packages/plugin/src/configs/operations-all.ts deleted file mode 100644 index baeec878fe8..00000000000 --- a/packages/plugin/src/configs/operations-all.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs` - */ - -export default { - extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/operations-recommended'], - rules: { - '@graphql-eslint/alphabetize': [ - 'error', - { - selections: ['OperationDefinition', 'FragmentDefinition'], - variables: ['OperationDefinition'], - arguments: ['Field', 'Directive'], - }, - ], - '@graphql-eslint/match-document-filename': [ - 'error', - { query: 'kebab-case', mutation: 'kebab-case', subscription: 'kebab-case', fragment: 'kebab-case' }, - ], - '@graphql-eslint/unique-fragment-name': 'error', - '@graphql-eslint/unique-operation-name': 'error', - }, -}; diff --git a/packages/plugin/src/configs/operations-recommended.json b/packages/plugin/src/configs/operations-recommended.json new file mode 100644 index 00000000000..18477941f06 --- /dev/null +++ b/packages/plugin/src/configs/operations-recommended.json @@ -0,0 +1,50 @@ +{ + "extends": "./base.json", + "rules": { + "@graphql-eslint/executable-definitions": "error", + "@graphql-eslint/fields-on-correct-type": "error", + "@graphql-eslint/fragments-on-composite-type": "error", + "@graphql-eslint/known-argument-names": "error", + "@graphql-eslint/known-directives": "error", + "@graphql-eslint/known-fragment-names": "error", + "@graphql-eslint/known-type-names": "error", + "@graphql-eslint/lone-anonymous-operation": "error", + "@graphql-eslint/naming-convention": [ + "error", + { + "VariableDefinition": "camelCase", + "OperationDefinition": { + "style": "PascalCase", + "forbiddenPrefixes": ["Query", "Mutation", "Subscription", "Get"], + "forbiddenSuffixes": ["Query", "Mutation", "Subscription"] + }, + "FragmentDefinition": { + "style": "PascalCase", + "forbiddenPrefixes": ["Fragment"], + "forbiddenSuffixes": ["Fragment"] + } + } + ], + "@graphql-eslint/no-anonymous-operations": "error", + "@graphql-eslint/no-deprecated": "error", + "@graphql-eslint/no-duplicate-fields": "error", + "@graphql-eslint/no-fragment-cycles": "error", + "@graphql-eslint/no-undefined-variables": "error", + "@graphql-eslint/no-unused-fragments": "error", + "@graphql-eslint/no-unused-variables": "error", + "@graphql-eslint/one-field-subscriptions": "error", + "@graphql-eslint/overlapping-fields-can-be-merged": "error", + "@graphql-eslint/possible-fragment-spread": "error", + "@graphql-eslint/provided-required-arguments": "error", + "@graphql-eslint/require-id-when-available": "error", + "@graphql-eslint/scalar-leafs": "error", + "@graphql-eslint/selection-set-depth": ["error", { "maxDepth": 7 }], + "@graphql-eslint/unique-argument-names": "error", + "@graphql-eslint/unique-directive-names-per-location": "error", + "@graphql-eslint/unique-input-field-names": "error", + "@graphql-eslint/unique-variable-names": "error", + "@graphql-eslint/value-literals-of-correct-type": "error", + "@graphql-eslint/variables-are-input-types": "error", + "@graphql-eslint/variables-in-allowed-position": "error" + } +} diff --git a/packages/plugin/src/configs/operations-recommended.ts b/packages/plugin/src/configs/operations-recommended.ts deleted file mode 100644 index 50d0c863c76..00000000000 --- a/packages/plugin/src/configs/operations-recommended.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs` - */ - -export default { - extends: ['plugin:@graphql-eslint/base'], - rules: { - '@graphql-eslint/executable-definitions': 'error', - '@graphql-eslint/fields-on-correct-type': 'error', - '@graphql-eslint/fragments-on-composite-type': 'error', - '@graphql-eslint/known-argument-names': 'error', - '@graphql-eslint/known-directives': 'error', - '@graphql-eslint/known-fragment-names': 'error', - '@graphql-eslint/known-type-names': 'error', - '@graphql-eslint/lone-anonymous-operation': 'error', - '@graphql-eslint/naming-convention': [ - 'error', - { - VariableDefinition: 'camelCase', - OperationDefinition: { - style: 'PascalCase', - forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'], - forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'], - }, - FragmentDefinition: { style: 'PascalCase', forbiddenPrefixes: ['Fragment'], forbiddenSuffixes: ['Fragment'] }, - }, - ], - '@graphql-eslint/no-anonymous-operations': 'error', - '@graphql-eslint/no-deprecated': 'error', - '@graphql-eslint/no-duplicate-fields': 'error', - '@graphql-eslint/no-fragment-cycles': 'error', - '@graphql-eslint/no-undefined-variables': 'error', - '@graphql-eslint/no-unused-fragments': 'error', - '@graphql-eslint/no-unused-variables': 'error', - '@graphql-eslint/one-field-subscriptions': 'error', - '@graphql-eslint/overlapping-fields-can-be-merged': 'error', - '@graphql-eslint/possible-fragment-spread': 'error', - '@graphql-eslint/provided-required-arguments': 'error', - '@graphql-eslint/require-id-when-available': 'error', - '@graphql-eslint/scalar-leafs': 'error', - '@graphql-eslint/selection-set-depth': ['error', { maxDepth: 7 }], - '@graphql-eslint/unique-argument-names': 'error', - '@graphql-eslint/unique-directive-names-per-location': 'error', - '@graphql-eslint/unique-input-field-names': 'error', - '@graphql-eslint/unique-variable-names': 'error', - '@graphql-eslint/value-literals-of-correct-type': 'error', - '@graphql-eslint/variables-are-input-types': 'error', - '@graphql-eslint/variables-in-allowed-position': 'error', - }, -}; diff --git a/packages/plugin/src/configs/schema-all.json b/packages/plugin/src/configs/schema-all.json new file mode 100644 index 00000000000..141cb60583e --- /dev/null +++ b/packages/plugin/src/configs/schema-all.json @@ -0,0 +1,26 @@ +{ + "extends": ["./base.json", "./schema-recommended.json"], + "rules": { + "@graphql-eslint/alphabetize": [ + "error", + { + "fields": [ + "ObjectTypeDefinition", + "InterfaceTypeDefinition", + "InputObjectTypeDefinition" + ], + "values": ["EnumTypeDefinition"], + "arguments": [ + "FieldDefinition", + "Field", + "DirectiveDefinition", + "Directive" + ] + } + ], + "@graphql-eslint/input-name": "error", + "@graphql-eslint/no-scalar-result-type-on-mutation": "error", + "@graphql-eslint/require-deprecation-date": "error", + "@graphql-eslint/require-field-of-type-query-in-mutation-result": "error" + } +} diff --git a/packages/plugin/src/configs/schema-all.ts b/packages/plugin/src/configs/schema-all.ts deleted file mode 100644 index 0fa68f4996c..00000000000 --- a/packages/plugin/src/configs/schema-all.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs` - */ - -export default { - extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/schema-recommended'], - rules: { - '@graphql-eslint/alphabetize': [ - 'error', - { - fields: ['ObjectTypeDefinition', 'InterfaceTypeDefinition', 'InputObjectTypeDefinition'], - values: ['EnumTypeDefinition'], - arguments: ['FieldDefinition', 'Field', 'DirectiveDefinition', 'Directive'], - }, - ], - '@graphql-eslint/input-name': 'error', - '@graphql-eslint/no-scalar-result-type-on-mutation': 'error', - '@graphql-eslint/require-deprecation-date': 'error', - '@graphql-eslint/require-field-of-type-query-in-mutation-result': 'error', - }, -}; diff --git a/packages/plugin/src/configs/schema-recommended.json b/packages/plugin/src/configs/schema-recommended.json new file mode 100644 index 00000000000..0656148cf1a --- /dev/null +++ b/packages/plugin/src/configs/schema-recommended.json @@ -0,0 +1,49 @@ +{ + "extends": "./base.json", + "rules": { + "@graphql-eslint/description-style": "error", + "@graphql-eslint/known-argument-names": "error", + "@graphql-eslint/known-directives": "error", + "@graphql-eslint/known-type-names": "error", + "@graphql-eslint/lone-schema-definition": "error", + "@graphql-eslint/naming-convention": [ + "error", + { + "types": "PascalCase", + "FieldDefinition": "camelCase", + "InputValueDefinition": "camelCase", + "Argument": "camelCase", + "DirectiveDefinition": "camelCase", + "EnumValueDefinition": "UPPER_CASE", + "FieldDefinition[parent.name.value=Query]": { + "forbiddenPrefixes": ["query", "get"], + "forbiddenSuffixes": ["Query"] + }, + "FieldDefinition[parent.name.value=Mutation]": { + "forbiddenPrefixes": ["mutation"], + "forbiddenSuffixes": ["Mutation"] + }, + "FieldDefinition[parent.name.value=Subscription]": { + "forbiddenPrefixes": ["subscription"], + "forbiddenSuffixes": ["Subscription"] + } + } + ], + "@graphql-eslint/no-case-insensitive-enum-values-duplicates": "error", + "@graphql-eslint/no-hashtag-description": "error", + "@graphql-eslint/no-typename-prefix": "error", + "@graphql-eslint/no-unreachable-types": "error", + "@graphql-eslint/provided-required-arguments": "error", + "@graphql-eslint/require-deprecation-reason": "error", + "@graphql-eslint/require-description": [ + "error", + { "types": true, "DirectiveDefinition": true } + ], + "@graphql-eslint/strict-id-in-types": "error", + "@graphql-eslint/unique-directive-names": "error", + "@graphql-eslint/unique-directive-names-per-location": "error", + "@graphql-eslint/unique-field-definition-names": "error", + "@graphql-eslint/unique-operation-types": "error", + "@graphql-eslint/unique-type-names": "error" + } +} diff --git a/packages/plugin/src/configs/schema-recommended.ts b/packages/plugin/src/configs/schema-recommended.ts deleted file mode 100644 index 55c3dc4e82a..00000000000 --- a/packages/plugin/src/configs/schema-recommended.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs` - */ - -export default { - extends: ['plugin:@graphql-eslint/base'], - rules: { - '@graphql-eslint/description-style': 'error', - '@graphql-eslint/known-argument-names': 'error', - '@graphql-eslint/known-directives': 'error', - '@graphql-eslint/known-type-names': 'error', - '@graphql-eslint/lone-schema-definition': 'error', - '@graphql-eslint/naming-convention': [ - 'error', - { - types: 'PascalCase', - FieldDefinition: 'camelCase', - InputValueDefinition: 'camelCase', - Argument: 'camelCase', - DirectiveDefinition: 'camelCase', - EnumValueDefinition: 'UPPER_CASE', - 'FieldDefinition[parent.name.value=Query]': { - forbiddenPrefixes: ['query', 'get'], - forbiddenSuffixes: ['Query'], - }, - 'FieldDefinition[parent.name.value=Mutation]': { - forbiddenPrefixes: ['mutation'], - forbiddenSuffixes: ['Mutation'], - }, - 'FieldDefinition[parent.name.value=Subscription]': { - forbiddenPrefixes: ['subscription'], - forbiddenSuffixes: ['Subscription'], - }, - }, - ], - '@graphql-eslint/no-case-insensitive-enum-values-duplicates': 'error', - '@graphql-eslint/no-hashtag-description': 'error', - '@graphql-eslint/no-typename-prefix': 'error', - '@graphql-eslint/no-unreachable-types': 'error', - '@graphql-eslint/provided-required-arguments': 'error', - '@graphql-eslint/require-deprecation-reason': 'error', - '@graphql-eslint/require-description': ['error', { types: true, DirectiveDefinition: true }], - '@graphql-eslint/strict-id-in-types': 'error', - '@graphql-eslint/unique-directive-names': 'error', - '@graphql-eslint/unique-directive-names-per-location': 'error', - '@graphql-eslint/unique-field-definition-names': 'error', - '@graphql-eslint/unique-operation-types': 'error', - '@graphql-eslint/unique-type-names': 'error', - }, -}; diff --git a/packages/plugin/src/estree-parser/converter.ts b/packages/plugin/src/estree-parser/converter.ts index b635885a165..5c3aa69302b 100644 --- a/packages/plugin/src/estree-parser/converter.ts +++ b/packages/plugin/src/estree-parser/converter.ts @@ -1,17 +1,17 @@ -import type { SourceLocation } from 'estree'; -import { convertDescription, extractCommentsFromAst } from './utils'; -import { GraphQLESTreeNode, SafeGraphQLType } from './estree-ast'; import { ASTNode, TypeNode, TypeInfo, visit, visitWithTypeInfo, - Location, Kind, DocumentNode, ASTVisitor, + Location, } from 'graphql'; +import { SourceLocation, Comment } from 'estree'; +import { extractCommentsFromAst } from './utils'; +import { GraphQLESTreeNode, TypeInformation } from './estree-ast'; export function convertToESTree(node: T, typeInfo?: TypeInfo) { const visitor: ASTVisitor = { leave: convertNode(typeInfo) }; @@ -21,8 +21,8 @@ export function convertToESTree(node: T, typeInfo?: TypeInfo) }; } -function hasTypeField(obj: any): obj is T & { readonly type: TypeNode } { - return obj && !!(obj as any).type; +function hasTypeField(node: T): node is T & { readonly type: TypeNode } { + return 'type' in node && Boolean(node.type); } function convertLocation(location: Location): SourceLocation { @@ -51,84 +51,61 @@ function convertLocation(location: Location): SourceLocation { return loc; } -const convertNode = (typeInfo?: TypeInfo) => ( - node: T, - key: string | number, - parent: any -): GraphQLESTreeNode => { - const calculatedTypeInfo = typeInfo - ? { - argument: typeInfo.getArgument(), - defaultValue: typeInfo.getDefaultValue(), - directive: typeInfo.getDirective(), - enumValue: typeInfo.getEnumValue(), - fieldDef: typeInfo.getFieldDef(), - inputType: typeInfo.getInputType(), - parentInputType: typeInfo.getParentInputType(), - parentType: typeInfo.getParentType(), - gqlType: typeInfo.getType(), - } - : {}; - - const commonFields = { - typeInfo: () => calculatedTypeInfo, - leadingComments: convertDescription(node), - loc: convertLocation(node.loc), - range: [node.loc.start, node.loc.end], - }; +const convertNode = + (typeInfo?: TypeInfo) => + (node: T, key: string | number, parent: any): GraphQLESTreeNode => { + const leadingComments: Comment[] = + 'description' in node && node.description + ? [ + { + type: node.description.block ? 'Block' : 'Line', + value: node.description.value, + }, + ] + : []; - if (hasTypeField(node)) { - const { type: gqlType, loc: gqlLocation, ...rest } = node; - const typeFieldSafe: SafeGraphQLType = { - ...rest, - gqlType, - } as SafeGraphQLType; - const estreeNode: GraphQLESTreeNode = ({ - ...typeFieldSafe, - ...commonFields, - type: node.kind, - rawNode: () => { - if (!parent || key === undefined) { - if (node && (node as any).definitions) { - return { - loc: gqlLocation, - kind: Kind.DOCUMENT, - definitions: (node as any).definitions.map(d => d.rawNode()), - }; - } - - return node; + const calculatedTypeInfo: TypeInformation | Record = typeInfo + ? { + argument: typeInfo.getArgument(), + defaultValue: typeInfo.getDefaultValue(), + directive: typeInfo.getDirective(), + enumValue: typeInfo.getEnumValue(), + fieldDef: typeInfo.getFieldDef(), + inputType: typeInfo.getInputType(), + parentInputType: typeInfo.getParentInputType(), + parentType: typeInfo.getParentType(), + gqlType: typeInfo.getType(), } + : {}; + const rawNode = () => { + if (parent && key !== undefined) { return parent[key]; - }, - } as any) as GraphQLESTreeNode; - - return estreeNode; - } else { - const { loc: gqlLocation, ...rest } = node; - const typeFieldSafe: SafeGraphQLType = rest as SafeGraphQLType; - const estreeNode: GraphQLESTreeNode = ({ - ...typeFieldSafe, - ...commonFields, - type: node.kind, - rawNode: () => { - if (!parent || key === undefined) { - if (node && (node as any).definitions) { - return { - loc: gqlLocation, - kind: Kind.DOCUMENT, - definitions: (node as any).definitions.map(d => d.rawNode()), - }; + } + return node.kind === Kind.DOCUMENT + ? { + kind: node.kind, + loc: node.loc, + definitions: node.definitions.map(d => (d as any).rawNode()), } + : node; + }; - return node; - } - - return parent[key]; - }, - } as any) as GraphQLESTreeNode; + const commonFields = { + ...node, + type: node.kind, + loc: convertLocation(node.loc), + range: [node.loc.start, node.loc.end], + leadingComments, + // Use function to prevent RangeError: Maximum call stack size exceeded + typeInfo: () => calculatedTypeInfo, + rawNode, + }; - return estreeNode; - } -}; + return hasTypeField(node) + ? ({ + ...commonFields, + gqlType: node.type, + } as any as GraphQLESTreeNode) + : (commonFields as any as GraphQLESTreeNode); + }; diff --git a/packages/plugin/src/estree-parser/estree-ast.ts b/packages/plugin/src/estree-parser/estree-ast.ts index 2ec6e9c73c3..27b5e40a1f4 100644 --- a/packages/plugin/src/estree-parser/estree-ast.ts +++ b/packages/plugin/src/estree-parser/estree-ast.ts @@ -1,34 +1,33 @@ import { ASTNode, TypeInfo, TypeNode, ValueNode } from 'graphql'; import { BaseNode } from 'estree'; -export type SafeGraphQLType = Omit< +type SafeGraphQLType = Omit< T extends { readonly type: TypeNode } ? Omit & { readonly gqlType: TypeNode } : T, 'loc' >; -export type SingleESTreeNode = T extends ASTNode | ValueNode - ? SafeGraphQLType & - Pick & { - type: T['kind']; - } & (WithTypeInfo extends true - ? { - typeInfo?: () => { - argument?: ReturnType; - defaultValue?: ReturnType; - directive?: ReturnType; - enumValue?: ReturnType; - fieldDef?: ReturnType; - inputType?: ReturnType; - parentInputType?: ReturnType; - parentType?: ReturnType; - gqlType?: ReturnType; - }; - } - : {}) - : T; +export type TypeInformation = { + argument: ReturnType; + defaultValue: ReturnType; + directive: ReturnType; + enumValue: ReturnType; + fieldDef: ReturnType; + inputType: ReturnType; + parentInputType: ReturnType; + parentType: ReturnType; + gqlType: ReturnType; +}; + +type SingleESTreeNode = SafeGraphQLType & + Pick & { + type: T['kind']; + // eslint-disable-next-line @typescript-eslint/ban-types -- Record don't work + typeInfo: () => WithTypeInfo extends true ? TypeInformation : {}; + rawNode: () => T; + }; export type GraphQLESTreeNode = T extends ASTNode | ValueNode - ? { rawNode: () => T } & { + ? { [K in keyof SingleESTreeNode]: SingleESTreeNode[K] extends ReadonlyArray< infer Nested > diff --git a/packages/plugin/src/estree-parser/utils.ts b/packages/plugin/src/estree-parser/utils.ts index 45036b5c6fd..d01357c8de1 100644 --- a/packages/plugin/src/estree-parser/utils.ts +++ b/packages/plugin/src/estree-parser/utils.ts @@ -1,9 +1,5 @@ import { - Kind, Location, - ValueNode, - StringValueNode, - ASTNode, TokenKind, GraphQLOutputType, GraphQLNamedType, @@ -15,49 +11,16 @@ import { } from 'graphql'; import type { Comment } from 'estree'; import type { AST } from 'eslint'; -import type { GraphQLESTreeNode } from './estree-ast'; +import { valueFromASTUntyped } from 'graphql/utilities/valueFromASTUntyped'; -export default function keyValMap( - list: ReadonlyArray, - keyFn: (item: T) => string, - valFn: (item: T) => V -): Record { - return list.reduce((map, item) => { - map[keyFn(item)] = valFn(item); - return map; - }, Object.create(null)); -} - -export function valueFromNode(valueNode: GraphQLESTreeNode, variables?: Record): any { - switch (valueNode.type) { - case Kind.NULL: - return null; - case Kind.INT: - return parseInt(valueNode.value, 10); - case Kind.FLOAT: - return parseFloat(valueNode.value); - case Kind.STRING: - case Kind.ENUM: - case Kind.BOOLEAN: - return valueNode.value; - case Kind.LIST: - return valueNode.values.map(node => valueFromNode(node, variables)); - case Kind.OBJECT: - return keyValMap( - valueNode.fields, - field => field.name.value, - field => valueFromNode(field.value, variables) - ); - case Kind.VARIABLE: - return variables?.[valueNode.name.value]; - } -} +export const valueFromNode = (...args: Parameters): any => { + return valueFromASTUntyped(...args); +}; export function getBaseType(type: GraphQLOutputType): GraphQLNamedType { if (isNonNullType(type) || isListType(type)) { return getBaseType(type.ofType); } - return type; } @@ -162,22 +125,3 @@ export function extractCommentsFromAst(loc: Location): Comment[] { } return comments; } - -export function isNodeWithDescription( - obj: T -): obj is T & { readonly description?: StringValueNode } { - return (obj as any)?.description; -} - -export function convertDescription(node: T): Comment[] { - if (isNodeWithDescription(node)) { - return [ - { - type: node.description.block ? 'Block' : 'Line', - value: node.description.value, - }, - ]; - } - - return []; -} diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index c526f9289e6..07f1dcefc41 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -1,7 +1,13 @@ -export { configs } from './configs'; export { rules } from './rules'; export { processors } from './processors'; export * from './parser'; export * from './types'; -export * from './estree-parser'; export * from './testkit'; +export { requireGraphQLSchemaFromContext, requireSiblingsOperations } from './utils'; + +export const configs = Object.fromEntries( + ['schema-recommended', 'schema-all', 'operations-recommended', 'operations-all'].map(configName => [ + configName, + { extends: `./configs/${configName}.json` }, + ]) +); diff --git a/packages/plugin/src/rules/graphql-js-validation.ts b/packages/plugin/src/rules/graphql-js-validation.ts index a65c2693bc1..38abb93568d 100644 --- a/packages/plugin/src/rules/graphql-js-validation.ts +++ b/packages/plugin/src/rules/graphql-js-validation.ts @@ -368,6 +368,7 @@ export const GRAPHQL_JS_VALIDATIONS: Record = Object. description: `A type extension is only valid if the type is defined and has the same kind.`, recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed requiresSchema: true, + isDisabledForAllConfig: true, }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', { category: ['Schema', 'Operations'], @@ -402,6 +403,7 @@ export const GRAPHQL_JS_VALIDATIONS: Record = Object. category: 'Schema', description: `A GraphQL enum type is only valid if all its values are uniquely named.`, recommended: false, + isDisabledForAllConfig: true, }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', { category: 'Schema', diff --git a/packages/plugin/src/rules/no-root-type.ts b/packages/plugin/src/rules/no-root-type.ts index 9e308fb117f..6603d2a66ca 100644 --- a/packages/plugin/src/rules/no-root-type.ts +++ b/packages/plugin/src/rules/no-root-type.ts @@ -15,6 +15,7 @@ const rule: GraphQLESLintRule<[NoRootTypeConfig]> = { description: 'Disallow using root types `mutation` and/or `subscription`.', url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-root-type.md', requiresSchema: true, + isDisabledForAllConfig: true, examples: [ { title: 'Incorrect', diff --git a/packages/plugin/src/rules/no-unused-fields.ts b/packages/plugin/src/rules/no-unused-fields.ts index dae7536bdfa..912c80ba410 100644 --- a/packages/plugin/src/rules/no-unused-fields.ts +++ b/packages/plugin/src/rules/no-unused-fields.ts @@ -15,6 +15,7 @@ const rule: GraphQLESLintRule = { url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`, requiresSiblings: true, requiresSchema: true, + isDisabledForAllConfig: true, examples: [ { title: 'Incorrect', diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts index a8f23c6b6d6..0c1c81d69ff 100644 --- a/packages/plugin/src/types.ts +++ b/packages/plugin/src/types.ts @@ -67,6 +67,7 @@ export type RuleDocsInfo = { operations?: T; }; graphQLJSRuleName?: string; + isDisabledForAllConfig?: true; }; }; diff --git a/scripts/constants.ts b/scripts/constants.ts deleted file mode 100644 index 0c6eb4cc9ec..00000000000 --- a/scripts/constants.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const DISABLED_RULES_FOR_ALL_CONFIG = new Set([ - 'no-root-type', - 'no-unused-fields', - 'possible-type-extension', - 'unique-enum-value-names', -]); diff --git a/scripts/generate-configs.ts b/scripts/generate-configs.ts index 29b26ddcc68..11f171ddd3c 100644 --- a/scripts/generate-configs.ts +++ b/scripts/generate-configs.ts @@ -4,30 +4,40 @@ import { format, resolveConfig } from 'prettier'; import chalk from 'chalk'; import { camelCase } from '../packages/plugin/src/utils'; import { CategoryType, GraphQLESLintRule } from '../packages/plugin/src'; -import { DISABLED_RULES_FOR_ALL_CONFIG } from './constants'; const BR = ''; -const prettierOptions = { - parser: 'typescript', - ...resolveConfig.sync(__dirname), -}; +const prettierOptions = resolveConfig.sync(__dirname); const SRC_PATH = join(process.cwd(), 'packages/plugin/src'); const IGNORE_FILES = ['index.ts', 'graphql-js-validation.ts']; -function writeFormattedFile(filePath: string, typeScriptCode: string): void { - const code = [ - '/*', - ' * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`', - ' */', - BR, - typeScriptCode, - ].join('\n'); +type WriteFile = { + (filePath: `${string}.ts`, code: string): void; + (filePath: `configs/${string}.json`, code: Record): void; +}; + +const writeFormattedFile: WriteFile = (filePath, code): void => { + const isJson = filePath.endsWith('.json'); + + const formattedCode = isJson + ? format(JSON.stringify(code), { + parser: 'json', + printWidth: 80, + }) + : [ + '/*', + ' * 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`', + ' */', + BR, + format(code, { + ...prettierOptions, + parser: 'typescript', + }), + ].join('\n'); - const formattedCode = format(code, prettierOptions); writeFileSync(join(SRC_PATH, filePath), formattedCode); // eslint-disable-next-line no-console console.log(`βœ… ${chalk.green(filePath)} file generated`); -} +}; const ruleFilenames = readdirSync(join(SRC_PATH, 'rules')) .filter(filename => filename.endsWith('.ts') && !IGNORE_FILES.includes(filename)) @@ -78,42 +88,30 @@ async function generateConfigs(): Promise { return Object.fromEntries( filteredRules - .filter(ruleId => !DISABLED_RULES_FOR_ALL_CONFIG.has(ruleId)) + .filter(ruleId => !rules[ruleId].meta.docs.isDisabledForAllConfig) .map(ruleId => [`@graphql-eslint/${ruleId}`, getRuleOptions(ruleId, rules[ruleId])]) ); }; - writeFormattedFile( - 'configs/schema-recommended.ts', - `export default ${JSON.stringify({ - extends: ['plugin:@graphql-eslint/base'], - rules: getRulesConfig('Schema', true), - })}` - ); - - writeFormattedFile( - 'configs/operations-recommended.ts', - `export default ${JSON.stringify({ - extends: ['plugin:@graphql-eslint/base'], - rules: getRulesConfig('Operations', true), - })}` - ); - - writeFormattedFile( - 'configs/schema-all.ts', - `export default ${JSON.stringify({ - extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/schema-recommended'], - rules: getRulesConfig('Schema', false), - })}` - ); - - writeFormattedFile( - 'configs/operations-all.ts', - `export default ${JSON.stringify({ - extends: ['plugin:@graphql-eslint/base', 'plugin:@graphql-eslint/operations-recommended'], - rules: getRulesConfig('Operations', false), - })}` - ); + writeFormattedFile('configs/schema-recommended.json', { + extends: './base.json', + rules: getRulesConfig('Schema', true), + }); + + writeFormattedFile('configs/operations-recommended.json', { + extends: './base.json', + rules: getRulesConfig('Operations', true), + }); + + writeFormattedFile('configs/schema-all.json', { + extends: ['./base.json', './schema-recommended.json'], + rules: getRulesConfig('Schema', false), + }); + + writeFormattedFile('configs/operations-all.json', { + extends: ['./base.json', './operations-recommended.json'], + rules: getRulesConfig('Operations', false), + }); } generateRules(); diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 93a5b6c3f18..c3f43ce0896 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -1,10 +1,10 @@ -import { writeFileSync, existsSync } from 'fs'; +import { writeFileSync } from 'fs'; import { resolve } from 'path'; import dedent from 'dedent'; import md from 'json-schema-to-markdown'; import { format } from 'prettier'; +import { asArray } from '@graphql-tools/utils'; import { rules } from '../packages/plugin/src'; -import { DISABLED_RULES_FOR_ALL_CONFIG } from './constants'; const BR = ''; const NBSP = ' '; @@ -50,7 +50,7 @@ function generateDocs(): void { if (deprecated) { blocks.push(`- ❗ DEPRECATED ❗`); } - const categories = Array.isArray(docs.category) ? docs.category : [docs.category]; + const categories = asArray(docs.category); if (docs.recommended) { const configNames = categories.map(category => `"plugin:@graphql-eslint/${category.toLowerCase()}-recommended"`); blocks.push( @@ -122,12 +122,10 @@ function generateDocs(): void { `- [Test source](https://github.com/graphql/graphql-js/tree/main/src/validation/__tests__/${graphQLJSRuleName}Rule-test.ts)` ); } else { - blocks.push(`- [Rule source](../../packages/plugin/src/rules/${ruleName}.ts)`); - const testPath = `packages/plugin/tests/${ruleName}.spec.ts`; - const isTestExists = existsSync(resolve(process.cwd(), testPath)); - if (isTestExists) { - blocks.push(`- [Test source](../../${testPath})`); - } + blocks.push( + `- [Rule source](../../packages/plugin/src/rules/${ruleName}.ts)`, + `- [Test source](../../packages/plugin/tests/${ruleName}.spec.ts)` + ); } blocks.push(BR); @@ -143,12 +141,11 @@ function generateDocs(): void { .map(([ruleName, rule]) => { const link = `[${ruleName}](rules/${ruleName}.md)`; const { docs } = rule.meta; - const isDisabled = DISABLED_RULES_FOR_ALL_CONFIG.has(ruleName); return [ link, docs.description.split('\n')[0], - isDisabled ? '' : docs.recommended ? '![recommended][]' : '![all][]', + docs.isDisabledForAllConfig ? '' : docs.recommended ? '![recommended][]' : '![all][]', docs.graphQLJSRuleName ? Icon.GRAPHQL_JS : Icon.GRAPHQL_ESLINT, ]; });