@graphql-eslint/eslint-plugin 4.0.0-alpha-20220821140530-e968cfc → 4.0.0-alpha-20230801163310-8bc4340
Sign up to get free protection for your applications and to get access to all the features.
- package/LICENSE +21 -0
- package/README.md +13 -253
- package/cjs/cache.d.ts +12 -0
- package/cjs/cache.js +62 -0
- package/cjs/configs/index.d.ts +148 -0
- package/cjs/configs/index.js +49 -0
- package/cjs/configs/operations-all.d.ts +22 -0
- package/cjs/configs/operations-all.js +27 -0
- package/cjs/configs/operations-recommended.d.ts +52 -0
- package/{configs/operations-recommended.json → cjs/configs/operations-recommended.js} +16 -14
- package/cjs/configs/relay.d.ts +12 -0
- package/{configs/relay.json → cjs/configs/relay.js} +6 -4
- package/cjs/configs/schema-all.d.ts +19 -0
- package/cjs/configs/schema-all.js +21 -0
- package/cjs/configs/schema-recommended.d.ts +49 -0
- package/{configs/schema-recommended.json → cjs/configs/schema-recommended.js} +19 -20
- package/cjs/documents.d.ts +6 -0
- package/cjs/documents.js +81 -0
- package/cjs/estree-converter/converter.d.ts +8 -0
- package/cjs/estree-converter/converter.js +83 -0
- package/cjs/estree-converter/index.d.ts +8 -0
- package/cjs/estree-converter/index.js +26 -0
- package/cjs/estree-converter/types.d.ts +42 -0
- package/cjs/estree-converter/types.js +16 -0
- package/cjs/estree-converter/utils.d.ts +18 -0
- package/cjs/estree-converter/utils.js +135 -0
- package/cjs/flat-configs.d.ts +260 -0
- package/cjs/flat-configs.js +60 -0
- package/cjs/graphql-config.d.ts +13 -0
- package/cjs/graphql-config.js +86 -0
- package/cjs/index.d.ts +22 -0
- package/cjs/index.js +49 -0
- package/cjs/parser.d.ts +12 -0
- package/cjs/parser.js +103 -0
- package/cjs/processor.d.ts +9 -0
- package/cjs/processor.js +127 -0
- package/cjs/rules/alphabetize.d.ts +84 -0
- package/cjs/rules/alphabetize.js +395 -0
- package/cjs/rules/description-style.d.ts +28 -0
- package/cjs/rules/description-style.js +109 -0
- package/cjs/rules/graphql-js-validation.d.ts +12 -0
- package/cjs/rules/graphql-js-validation.js +669 -0
- package/cjs/rules/index.d.ts +125 -0
- package/cjs/rules/index.js +99 -0
- package/cjs/rules/input-name.d.ts +43 -0
- package/cjs/rules/input-name.js +170 -0
- package/cjs/rules/lone-executable-definition.d.ts +34 -0
- package/cjs/rules/lone-executable-definition.js +119 -0
- package/cjs/rules/match-document-filename.d.ts +80 -0
- package/cjs/rules/match-document-filename.js +282 -0
- package/cjs/rules/naming-convention.d.ts +107 -0
- package/cjs/rules/naming-convention.js +434 -0
- package/cjs/rules/no-anonymous-operations.d.ts +12 -0
- package/cjs/rules/no-anonymous-operations.js +98 -0
- package/cjs/rules/no-case-insensitive-enum-values-duplicates.d.ts +12 -0
- package/cjs/rules/no-case-insensitive-enum-values-duplicates.js +96 -0
- package/cjs/rules/no-deprecated.d.ts +12 -0
- package/cjs/rules/no-deprecated.js +157 -0
- package/cjs/rules/no-duplicate-fields.d.ts +12 -0
- package/cjs/rules/no-duplicate-fields.js +146 -0
- package/cjs/rules/no-hashtag-description.d.ts +13 -0
- package/cjs/rules/no-hashtag-description.js +140 -0
- package/cjs/rules/no-one-place-fragments.d.ts +12 -0
- package/cjs/rules/no-one-place-fragments.js +113 -0
- package/cjs/rules/no-root-type.d.ts +33 -0
- package/cjs/rules/no-root-type.js +113 -0
- package/cjs/rules/no-scalar-result-type-on-mutation.d.ts +12 -0
- package/cjs/rules/no-scalar-result-type-on-mutation.js +100 -0
- package/cjs/rules/no-typename-prefix.d.ts +12 -0
- package/cjs/rules/no-typename-prefix.js +98 -0
- package/cjs/rules/no-unreachable-types.d.ts +12 -0
- package/cjs/rules/no-unreachable-types.js +199 -0
- package/cjs/rules/no-unused-fields.d.ts +12 -0
- package/cjs/rules/no-unused-fields.js +157 -0
- package/cjs/rules/relay-arguments.d.ts +29 -0
- package/cjs/rules/relay-arguments.js +149 -0
- package/cjs/rules/relay-connection-types.d.ts +13 -0
- package/cjs/rules/relay-connection-types.js +142 -0
- package/cjs/rules/relay-edge-types.d.ts +39 -0
- package/cjs/rules/relay-edge-types.js +212 -0
- package/cjs/rules/relay-page-info.d.ts +12 -0
- package/cjs/rules/relay-page-info.js +121 -0
- package/cjs/rules/require-deprecation-date.d.ts +26 -0
- package/cjs/rules/require-deprecation-date.js +164 -0
- package/cjs/rules/require-deprecation-reason.d.ts +12 -0
- package/cjs/rules/require-deprecation-reason.js +93 -0
- package/cjs/rules/require-description.d.ts +23 -0
- package/cjs/rules/require-description.js +205 -0
- package/cjs/rules/require-field-of-type-query-in-mutation-result.d.ts +12 -0
- package/cjs/rules/require-field-of-type-query-in-mutation-result.js +102 -0
- package/cjs/rules/require-id-when-available.d.ts +44 -0
- package/cjs/rules/require-id-when-available.js +241 -0
- package/cjs/rules/require-import-fragment.d.ts +12 -0
- package/cjs/rules/require-import-fragment.js +166 -0
- package/cjs/rules/require-nullable-fields-with-oneof.d.ts +12 -0
- package/cjs/rules/require-nullable-fields-with-oneof.js +92 -0
- package/cjs/rules/require-nullable-result-in-root.d.ts +12 -0
- package/cjs/rules/require-nullable-result-in-root.js +109 -0
- package/cjs/rules/require-type-pattern-with-oneof.d.ts +12 -0
- package/cjs/rules/require-type-pattern-with-oneof.js +91 -0
- package/cjs/rules/selection-set-depth.d.ts +36 -0
- package/cjs/rules/selection-set-depth.js +175 -0
- package/cjs/rules/strict-id-in-types.d.ts +65 -0
- package/cjs/rules/strict-id-in-types.js +186 -0
- package/cjs/rules/unique-fragment-name.d.ts +13 -0
- package/cjs/rules/unique-fragment-name.js +118 -0
- package/cjs/rules/unique-operation-name.d.ts +12 -0
- package/cjs/rules/unique-operation-name.js +95 -0
- package/cjs/schema.d.ts +12 -0
- package/cjs/schema.js +65 -0
- package/cjs/siblings.d.ts +8 -0
- package/cjs/siblings.js +136 -0
- package/cjs/types-8d5f4ae0.d.ts +107 -0
- package/cjs/types.d.ts +8 -0
- package/cjs/types.js +16 -0
- package/cjs/utils.d.ts +44 -0
- package/cjs/utils.js +205 -0
- package/esm/cache.d.mts +12 -0
- package/esm/cache.js +29 -0
- package/esm/chunk-BMTV3EA2.js +8 -0
- package/esm/configs/index.d.mts +148 -0
- package/esm/configs/index.js +16 -0
- package/esm/configs/operations-all.d.mts +22 -0
- package/esm/configs/operations-all.js +34 -0
- package/esm/configs/operations-recommended.d.mts +52 -0
- package/esm/configs/operations-recommended.js +59 -0
- package/esm/configs/relay.d.mts +12 -0
- package/esm/configs/relay.js +18 -0
- package/esm/configs/schema-all.d.mts +19 -0
- package/esm/configs/schema-all.js +28 -0
- package/esm/configs/schema-recommended.d.mts +49 -0
- package/esm/configs/schema-recommended.js +55 -0
- package/esm/documents.d.mts +6 -0
- package/esm/documents.js +48 -0
- package/esm/estree-converter/converter.d.mts +8 -0
- package/esm/estree-converter/converter.js +65 -0
- package/esm/estree-converter/index.d.mts +8 -0
- package/esm/estree-converter/index.js +3 -0
- package/esm/estree-converter/types.d.mts +42 -0
- package/esm/estree-converter/types.js +0 -0
- package/esm/estree-converter/utils.d.mts +18 -0
- package/esm/estree-converter/utils.js +114 -0
- package/esm/flat-configs.d.mts +260 -0
- package/esm/flat-configs.js +37 -0
- package/esm/graphql-config.d.mts +13 -0
- package/esm/graphql-config.js +55 -0
- package/esm/index.d.mts +22 -0
- package/esm/index.js +18 -0
- package/esm/package.json +1 -0
- package/esm/parser.d.mts +12 -0
- package/esm/parser.js +70 -0
- package/esm/processor.d.mts +9 -0
- package/esm/processor.js +106 -0
- package/esm/rules/alphabetize.d.mts +84 -0
- package/esm/rules/alphabetize.js +364 -0
- package/esm/rules/description-style.d.mts +28 -0
- package/esm/rules/description-style.js +86 -0
- package/esm/rules/graphql-js-validation.d.mts +12 -0
- package/esm/rules/graphql-js-validation.js +658 -0
- package/esm/rules/index.d.mts +125 -0
- package/esm/rules/index.js +76 -0
- package/esm/rules/input-name.d.mts +43 -0
- package/esm/rules/input-name.js +149 -0
- package/esm/rules/lone-executable-definition.d.mts +34 -0
- package/esm/rules/lone-executable-definition.js +96 -0
- package/esm/rules/match-document-filename.d.mts +80 -0
- package/esm/rules/match-document-filename.js +263 -0
- package/esm/rules/naming-convention.d.mts +107 -0
- package/esm/rules/naming-convention.js +417 -0
- package/esm/rules/no-anonymous-operations.d.mts +12 -0
- package/esm/rules/no-anonymous-operations.js +75 -0
- package/esm/rules/no-case-insensitive-enum-values-duplicates.d.mts +12 -0
- package/esm/rules/no-case-insensitive-enum-values-duplicates.js +73 -0
- package/esm/rules/no-deprecated.d.mts +12 -0
- package/esm/rules/no-deprecated.js +134 -0
- package/esm/rules/no-duplicate-fields.d.mts +12 -0
- package/esm/rules/no-duplicate-fields.js +123 -0
- package/esm/rules/no-hashtag-description.d.mts +13 -0
- package/esm/rules/no-hashtag-description.js +116 -0
- package/esm/rules/no-one-place-fragments.d.mts +12 -0
- package/esm/rules/no-one-place-fragments.js +90 -0
- package/esm/rules/no-root-type.d.mts +33 -0
- package/esm/rules/no-root-type.js +90 -0
- package/esm/rules/no-scalar-result-type-on-mutation.d.mts +12 -0
- package/esm/rules/no-scalar-result-type-on-mutation.js +77 -0
- package/esm/rules/no-typename-prefix.d.mts +12 -0
- package/esm/rules/no-typename-prefix.js +75 -0
- package/esm/rules/no-unreachable-types.d.mts +12 -0
- package/esm/rules/no-unreachable-types.js +171 -0
- package/esm/rules/no-unused-fields.d.mts +12 -0
- package/esm/rules/no-unused-fields.js +134 -0
- package/esm/rules/relay-arguments.d.mts +29 -0
- package/esm/rules/relay-arguments.js +126 -0
- package/esm/rules/relay-connection-types.d.mts +13 -0
- package/esm/rules/relay-connection-types.js +118 -0
- package/esm/rules/relay-edge-types.d.mts +39 -0
- package/esm/rules/relay-edge-types.js +194 -0
- package/esm/rules/relay-page-info.d.mts +12 -0
- package/esm/rules/relay-page-info.js +98 -0
- package/esm/rules/require-deprecation-date.d.mts +26 -0
- package/esm/rules/require-deprecation-date.js +141 -0
- package/esm/rules/require-deprecation-reason.d.mts +12 -0
- package/esm/rules/require-deprecation-reason.js +70 -0
- package/esm/rules/require-description.d.mts +23 -0
- package/esm/rules/require-description.js +186 -0
- package/esm/rules/require-field-of-type-query-in-mutation-result.d.mts +12 -0
- package/esm/rules/require-field-of-type-query-in-mutation-result.js +79 -0
- package/esm/rules/require-id-when-available.d.mts +44 -0
- package/esm/rules/require-id-when-available.js +231 -0
- package/esm/rules/require-import-fragment.d.mts +12 -0
- package/esm/rules/require-import-fragment.js +133 -0
- package/esm/rules/require-nullable-fields-with-oneof.d.mts +12 -0
- package/esm/rules/require-nullable-fields-with-oneof.js +69 -0
- package/esm/rules/require-nullable-result-in-root.d.mts +12 -0
- package/esm/rules/require-nullable-result-in-root.js +86 -0
- package/esm/rules/require-type-pattern-with-oneof.d.mts +12 -0
- package/esm/rules/require-type-pattern-with-oneof.js +68 -0
- package/esm/rules/selection-set-depth.d.mts +36 -0
- package/esm/rules/selection-set-depth.js +142 -0
- package/esm/rules/strict-id-in-types.d.mts +65 -0
- package/esm/rules/strict-id-in-types.js +169 -0
- package/esm/rules/unique-fragment-name.d.mts +13 -0
- package/esm/rules/unique-fragment-name.js +94 -0
- package/esm/rules/unique-operation-name.d.mts +12 -0
- package/esm/rules/unique-operation-name.js +72 -0
- package/esm/schema.d.mts +12 -0
- package/esm/schema.js +32 -0
- package/esm/siblings.d.mts +8 -0
- package/esm/siblings.js +116 -0
- package/esm/types-ace77d86.d.ts +107 -0
- package/esm/types.d.mts +8 -0
- package/esm/types.js +0 -0
- package/esm/utils.d.mts +44 -0
- package/esm/utils.js +155 -0
- package/package.json +47 -34
- package/configs/base.json +0 -4
- package/configs/operations-all.json +0 -24
- package/configs/schema-all.json +0 -26
- package/docs/README.md +0 -75
- package/docs/custom-rules.md +0 -148
- package/docs/deprecated-rules.md +0 -21
- package/docs/parser-options.md +0 -85
- package/docs/parser.md +0 -49
- package/docs/rules/alphabetize.md +0 -178
- package/docs/rules/description-style.md +0 -54
- package/docs/rules/executable-definitions.md +0 -17
- package/docs/rules/fields-on-correct-type.md +0 -17
- package/docs/rules/fragments-on-composite-type.md +0 -17
- package/docs/rules/input-name.md +0 -76
- package/docs/rules/known-argument-names.md +0 -17
- package/docs/rules/known-directives.md +0 -44
- package/docs/rules/known-fragment-names.md +0 -69
- package/docs/rules/known-type-names.md +0 -17
- package/docs/rules/lone-anonymous-operation.md +0 -17
- package/docs/rules/lone-schema-definition.md +0 -17
- package/docs/rules/match-document-filename.md +0 -156
- package/docs/rules/naming-convention.md +0 -300
- package/docs/rules/no-anonymous-operations.md +0 -39
- package/docs/rules/no-case-insensitive-enum-values-duplicates.md +0 -43
- package/docs/rules/no-deprecated.md +0 -85
- package/docs/rules/no-duplicate-fields.md +0 -65
- package/docs/rules/no-fragment-cycles.md +0 -17
- package/docs/rules/no-hashtag-description.md +0 -59
- package/docs/rules/no-root-type.md +0 -53
- package/docs/rules/no-scalar-result-type-on-mutation.md +0 -37
- package/docs/rules/no-typename-prefix.md +0 -39
- package/docs/rules/no-undefined-variables.md +0 -17
- package/docs/rules/no-unreachable-types.md +0 -49
- package/docs/rules/no-unused-fields.md +0 -62
- package/docs/rules/no-unused-fragments.md +0 -17
- package/docs/rules/no-unused-variables.md +0 -17
- package/docs/rules/one-field-subscriptions.md +0 -17
- package/docs/rules/overlapping-fields-can-be-merged.md +0 -17
- package/docs/rules/possible-fragment-spread.md +0 -17
- package/docs/rules/possible-type-extension.md +0 -15
- package/docs/rules/provided-required-arguments.md +0 -17
- package/docs/rules/relay-arguments.md +0 -57
- package/docs/rules/relay-connection-types.md +0 -42
- package/docs/rules/relay-edge-types.md +0 -56
- package/docs/rules/relay-page-info.md +0 -32
- package/docs/rules/require-deprecation-date.md +0 -56
- package/docs/rules/require-deprecation-reason.md +0 -47
- package/docs/rules/require-description.md +0 -115
- package/docs/rules/require-field-of-type-query-in-mutation-result.md +0 -47
- package/docs/rules/require-id-when-available.md +0 -88
- package/docs/rules/scalar-leafs.md +0 -17
- package/docs/rules/selection-set-depth.md +0 -76
- package/docs/rules/strict-id-in-types.md +0 -130
- package/docs/rules/unique-argument-names.md +0 -17
- package/docs/rules/unique-directive-names-per-location.md +0 -17
- package/docs/rules/unique-directive-names.md +0 -17
- package/docs/rules/unique-enum-value-names.md +0 -15
- package/docs/rules/unique-field-definition-names.md +0 -17
- package/docs/rules/unique-fragment-name.md +0 -51
- package/docs/rules/unique-input-field-names.md +0 -17
- package/docs/rules/unique-operation-name.md +0 -55
- package/docs/rules/unique-operation-types.md +0 -17
- package/docs/rules/unique-type-names.md +0 -17
- package/docs/rules/unique-variable-names.md +0 -17
- package/docs/rules/value-literals-of-correct-type.md +0 -17
- package/docs/rules/variables-are-input-types.md +0 -17
- package/docs/rules/variables-in-allowed-position.md +0 -17
- package/estree-converter/converter.d.ts +0 -3
- package/estree-converter/index.d.ts +0 -3
- package/estree-converter/types.d.ts +0 -40
- package/estree-converter/utils.d.ts +0 -13
- package/graphql-config.d.ts +0 -3
- package/index.d.ts +0 -16
- package/index.js +0 -4653
- package/index.mjs +0 -4641
- package/parser.d.ts +0 -2
- package/processor.d.ts +0 -7
- package/rules/alphabetize.d.ts +0 -16
- package/rules/description-style.d.ts +0 -6
- package/rules/graphql-js-validation.d.ts +0 -2
- package/rules/index.d.ts +0 -41
- package/rules/input-name.d.ts +0 -9
- package/rules/match-document-filename.d.ts +0 -18
- package/rules/naming-convention.d.ts +0 -37
- package/rules/no-anonymous-operations.d.ts +0 -3
- package/rules/no-case-insensitive-enum-values-duplicates.d.ts +0 -3
- package/rules/no-deprecated.d.ts +0 -3
- package/rules/no-duplicate-fields.d.ts +0 -3
- package/rules/no-hashtag-description.d.ts +0 -3
- package/rules/no-root-type.d.ts +0 -7
- package/rules/no-scalar-result-type-on-mutation.d.ts +0 -3
- package/rules/no-typename-prefix.d.ts +0 -3
- package/rules/no-unreachable-types.d.ts +0 -3
- package/rules/no-unused-fields.d.ts +0 -3
- package/rules/relay-arguments.d.ts +0 -6
- package/rules/relay-connection-types.d.ts +0 -5
- package/rules/relay-edge-types.d.ts +0 -8
- package/rules/relay-page-info.d.ts +0 -3
- package/rules/require-deprecation-date.d.ts +0 -5
- package/rules/require-deprecation-reason.d.ts +0 -3
- package/rules/require-description.d.ts +0 -11
- package/rules/require-field-of-type-query-in-mutation-result.d.ts +0 -3
- package/rules/require-id-when-available.d.ts +0 -6
- package/rules/selection-set-depth.d.ts +0 -7
- package/rules/strict-id-in-types.d.ts +0 -11
- package/rules/unique-fragment-name.d.ts +0 -6
- package/rules/unique-operation-name.d.ts +0 -3
- package/schema.d.ts +0 -3
- package/sibling-operations.d.ts +0 -21
- package/testkit.d.ts +0 -27
- package/types.d.ts +0 -79
- package/utils.d.ts +0 -39
package/index.mjs
DELETED
@@ -1,4641 +0,0 @@
|
|
1
|
-
import { parseCode } from '@graphql-tools/graphql-tag-pluck';
|
2
|
-
import { Kind, visit, validate, TokenKind, isScalarType, DirectiveLocation, isInterfaceType, TypeInfo, visitWithTypeInfo, isObjectType as isObjectType$1, Source, isNonNullType, isListType, GraphQLObjectType, GraphQLInterfaceType, GraphQLSchema, GraphQLError } from 'graphql';
|
3
|
-
import { validateSDL } from 'graphql/validation/validate';
|
4
|
-
import { statSync, existsSync, readFileSync } from 'fs';
|
5
|
-
import { dirname, extname, basename, relative, resolve } from 'path';
|
6
|
-
import lowerCase from 'lodash.lowercase';
|
7
|
-
import chalk from 'chalk';
|
8
|
-
import { getDocumentNodeFromSchema, asArray, parseGraphQLSDL } from '@graphql-tools/utils';
|
9
|
-
import { valueFromASTUntyped } from 'graphql/utilities/valueFromASTUntyped';
|
10
|
-
import depthLimit from 'graphql-depth-limit';
|
11
|
-
import debugFactory from 'debug';
|
12
|
-
import fastGlob from 'fast-glob';
|
13
|
-
import { loadConfigSync, GraphQLConfig } from 'graphql-config';
|
14
|
-
import { CodeFileLoader } from '@graphql-tools/code-file-loader';
|
15
|
-
import { RuleTester, Linter } from 'eslint';
|
16
|
-
import { codeFrameColumns } from '@babel/code-frame';
|
17
|
-
|
18
|
-
const RELEVANT_KEYWORDS = ['gql', 'graphql', '/* GraphQL */'];
|
19
|
-
const blocksMap = new Map();
|
20
|
-
const processor = {
|
21
|
-
supportsAutofix: true,
|
22
|
-
preprocess(code, filePath) {
|
23
|
-
if (RELEVANT_KEYWORDS.every(keyword => !code.includes(keyword))) {
|
24
|
-
return [code];
|
25
|
-
}
|
26
|
-
const extractedDocuments = parseCode({
|
27
|
-
code,
|
28
|
-
filePath,
|
29
|
-
options: {
|
30
|
-
globalGqlIdentifierName: ['gql', 'graphql'],
|
31
|
-
skipIndent: true,
|
32
|
-
},
|
33
|
-
});
|
34
|
-
const blocks = extractedDocuments.map(item => ({
|
35
|
-
filename: 'document.graphql',
|
36
|
-
text: item.content,
|
37
|
-
lineOffset: item.loc.start.line - 1,
|
38
|
-
offset: item.start + 1,
|
39
|
-
}));
|
40
|
-
blocksMap.set(filePath, blocks);
|
41
|
-
return [...blocks, code /* source code must be provided and be last */];
|
42
|
-
},
|
43
|
-
postprocess(messages, filePath) {
|
44
|
-
const blocks = blocksMap.get(filePath) || [];
|
45
|
-
for (let i = 0; i < blocks.length; i += 1) {
|
46
|
-
const { lineOffset, offset } = blocks[i];
|
47
|
-
for (const message of messages[i]) {
|
48
|
-
message.line += lineOffset;
|
49
|
-
// endLine can not exist if only `loc: { start, column }` was provided to context.report
|
50
|
-
if (typeof message.endLine === 'number') {
|
51
|
-
message.endLine += lineOffset;
|
52
|
-
}
|
53
|
-
if (message.fix) {
|
54
|
-
message.fix.range[0] += offset;
|
55
|
-
message.fix.range[1] += offset;
|
56
|
-
}
|
57
|
-
for (const suggestion of message.suggestions || []) {
|
58
|
-
suggestion.fix.range[0] += offset;
|
59
|
-
suggestion.fix.range[1] += offset;
|
60
|
-
}
|
61
|
-
}
|
62
|
-
}
|
63
|
-
return messages.flat();
|
64
|
-
},
|
65
|
-
};
|
66
|
-
|
67
|
-
function requireSiblingsOperations(ruleId, context) {
|
68
|
-
const { siblingOperations } = context.parserServices;
|
69
|
-
if (!siblingOperations.available) {
|
70
|
-
throw new Error(`Rule \`${ruleId}\` requires \`parserOptions.operations\` to be set and loaded. See https://bit.ly/graphql-eslint-operations for more info`);
|
71
|
-
}
|
72
|
-
return siblingOperations;
|
73
|
-
}
|
74
|
-
function requireGraphQLSchemaFromContext(ruleId, context) {
|
75
|
-
const { schema } = context.parserServices;
|
76
|
-
if (!schema) {
|
77
|
-
throw new Error(`Rule \`${ruleId}\` requires \`parserOptions.schema\` to be set and loaded. See https://bit.ly/graphql-eslint-schema for more info`);
|
78
|
-
}
|
79
|
-
else if (schema instanceof Error) {
|
80
|
-
throw schema;
|
81
|
-
}
|
82
|
-
return schema;
|
83
|
-
}
|
84
|
-
const logger = {
|
85
|
-
// eslint-disable-next-line no-console
|
86
|
-
error: (...args) => console.error(chalk.red('error'), '[graphql-eslint]', chalk(...args)),
|
87
|
-
// eslint-disable-next-line no-console
|
88
|
-
warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
|
89
|
-
};
|
90
|
-
const normalizePath = (path) => (path || '').replace(/\\/g, '/');
|
91
|
-
/**
|
92
|
-
* https://github.com/prettier/eslint-plugin-prettier/blob/76bd45ece6d56eb52f75db6b4a1efdd2efb56392/eslint-plugin-prettier.js#L71
|
93
|
-
* Given a filepath, get the nearest path that is a regular file.
|
94
|
-
* The filepath provided by eslint may be a virtual filepath rather than a file
|
95
|
-
* on disk. This attempts to transform a virtual path into an on-disk path
|
96
|
-
*/
|
97
|
-
const getOnDiskFilepath = (filepath) => {
|
98
|
-
try {
|
99
|
-
if (statSync(filepath).isFile()) {
|
100
|
-
return filepath;
|
101
|
-
}
|
102
|
-
}
|
103
|
-
catch (err) {
|
104
|
-
// https://github.com/eslint/eslint/issues/11989
|
105
|
-
if (err.code === 'ENOTDIR') {
|
106
|
-
return getOnDiskFilepath(dirname(filepath));
|
107
|
-
}
|
108
|
-
}
|
109
|
-
return filepath;
|
110
|
-
};
|
111
|
-
const getTypeName = (node) => ('type' in node ? getTypeName(node.type) : node.name.value);
|
112
|
-
const TYPES_KINDS = [
|
113
|
-
Kind.OBJECT_TYPE_DEFINITION,
|
114
|
-
Kind.INTERFACE_TYPE_DEFINITION,
|
115
|
-
Kind.ENUM_TYPE_DEFINITION,
|
116
|
-
Kind.SCALAR_TYPE_DEFINITION,
|
117
|
-
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
118
|
-
Kind.UNION_TYPE_DEFINITION,
|
119
|
-
];
|
120
|
-
const pascalCase = (str) => lowerCase(str)
|
121
|
-
.split(' ')
|
122
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
123
|
-
.join('');
|
124
|
-
const camelCase = (str) => {
|
125
|
-
const result = pascalCase(str);
|
126
|
-
return result.charAt(0).toLowerCase() + result.slice(1);
|
127
|
-
};
|
128
|
-
const convertCase = (style, str) => {
|
129
|
-
switch (style) {
|
130
|
-
case 'camelCase':
|
131
|
-
return camelCase(str);
|
132
|
-
case 'PascalCase':
|
133
|
-
return pascalCase(str);
|
134
|
-
case 'snake_case':
|
135
|
-
return lowerCase(str).replace(/ /g, '_');
|
136
|
-
case 'UPPER_CASE':
|
137
|
-
return lowerCase(str).replace(/ /g, '_').toUpperCase();
|
138
|
-
case 'kebab-case':
|
139
|
-
return lowerCase(str).replace(/ /g, '-');
|
140
|
-
}
|
141
|
-
};
|
142
|
-
function getLocation(start, fieldName = '') {
|
143
|
-
const { line, column } = start;
|
144
|
-
return {
|
145
|
-
start: {
|
146
|
-
line,
|
147
|
-
column,
|
148
|
-
},
|
149
|
-
end: {
|
150
|
-
line,
|
151
|
-
column: column + fieldName.length,
|
152
|
-
},
|
153
|
-
};
|
154
|
-
}
|
155
|
-
const REPORT_ON_FIRST_CHARACTER = { column: 0, line: 1 };
|
156
|
-
const ARRAY_DEFAULT_OPTIONS = {
|
157
|
-
type: 'array',
|
158
|
-
uniqueItems: true,
|
159
|
-
minItems: 1,
|
160
|
-
items: {
|
161
|
-
type: 'string',
|
162
|
-
},
|
163
|
-
};
|
164
|
-
const englishJoinWords = words => new Intl.ListFormat('en-US', { type: 'disjunction' }).format(words);
|
165
|
-
|
166
|
-
function validateDocument(context, schema = null, documentNode, rule) {
|
167
|
-
if (documentNode.definitions.length === 0) {
|
168
|
-
return;
|
169
|
-
}
|
170
|
-
try {
|
171
|
-
const validationErrors = schema
|
172
|
-
? validate(schema, documentNode, [rule])
|
173
|
-
: validateSDL(documentNode, null, [rule]);
|
174
|
-
for (const error of validationErrors) {
|
175
|
-
const { line, column } = error.locations[0];
|
176
|
-
const sourceCode = context.getSourceCode();
|
177
|
-
const { tokens } = sourceCode.ast;
|
178
|
-
const token = tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
|
179
|
-
let loc = {
|
180
|
-
line,
|
181
|
-
column: column - 1,
|
182
|
-
};
|
183
|
-
if (token) {
|
184
|
-
loc =
|
185
|
-
// if cursor on `@` symbol than use next node
|
186
|
-
token.type === '@' ? sourceCode.getNodeByRangeIndex(token.range[1] + 1).loc : token.loc;
|
187
|
-
}
|
188
|
-
context.report({
|
189
|
-
loc,
|
190
|
-
message: error.message,
|
191
|
-
});
|
192
|
-
}
|
193
|
-
}
|
194
|
-
catch (e) {
|
195
|
-
context.report({
|
196
|
-
loc: REPORT_ON_FIRST_CHARACTER,
|
197
|
-
message: e.message,
|
198
|
-
});
|
199
|
-
}
|
200
|
-
}
|
201
|
-
const getFragmentDefsAndFragmentSpreads = (node) => {
|
202
|
-
const fragmentDefs = new Set();
|
203
|
-
const fragmentSpreads = new Set();
|
204
|
-
const visitor = {
|
205
|
-
FragmentDefinition(node) {
|
206
|
-
fragmentDefs.add(node.name.value);
|
207
|
-
},
|
208
|
-
FragmentSpread(node) {
|
209
|
-
fragmentSpreads.add(node.name.value);
|
210
|
-
},
|
211
|
-
};
|
212
|
-
visit(node, visitor);
|
213
|
-
return { fragmentDefs, fragmentSpreads };
|
214
|
-
};
|
215
|
-
const getMissingFragments = (node) => {
|
216
|
-
const { fragmentDefs, fragmentSpreads } = getFragmentDefsAndFragmentSpreads(node);
|
217
|
-
return [...fragmentSpreads].filter(name => !fragmentDefs.has(name));
|
218
|
-
};
|
219
|
-
const handleMissingFragments = ({ ruleId, context, node }) => {
|
220
|
-
const missingFragments = getMissingFragments(node);
|
221
|
-
if (missingFragments.length > 0) {
|
222
|
-
const siblings = requireSiblingsOperations(ruleId, context);
|
223
|
-
const fragmentsToAdd = [];
|
224
|
-
for (const fragmentName of missingFragments) {
|
225
|
-
const [foundFragment] = siblings.getFragment(fragmentName).map(source => source.document);
|
226
|
-
if (foundFragment) {
|
227
|
-
fragmentsToAdd.push(foundFragment);
|
228
|
-
}
|
229
|
-
}
|
230
|
-
if (fragmentsToAdd.length > 0) {
|
231
|
-
// recall fn to make sure to add fragments inside fragments
|
232
|
-
return handleMissingFragments({
|
233
|
-
ruleId,
|
234
|
-
context,
|
235
|
-
node: {
|
236
|
-
kind: Kind.DOCUMENT,
|
237
|
-
definitions: [...node.definitions, ...fragmentsToAdd],
|
238
|
-
},
|
239
|
-
});
|
240
|
-
}
|
241
|
-
}
|
242
|
-
return node;
|
243
|
-
};
|
244
|
-
const validationToRule = (ruleId, ruleName, docs, getDocumentNode, schema = []) => {
|
245
|
-
let ruleFn = null;
|
246
|
-
try {
|
247
|
-
ruleFn = require(`graphql/validation/rules/${ruleName}Rule`)[`${ruleName}Rule`];
|
248
|
-
}
|
249
|
-
catch (_a) {
|
250
|
-
try {
|
251
|
-
ruleFn = require(`graphql/validation/rules/${ruleName}`)[`${ruleName}Rule`];
|
252
|
-
}
|
253
|
-
catch (_b) {
|
254
|
-
ruleFn = require('graphql/validation')[`${ruleName}Rule`];
|
255
|
-
}
|
256
|
-
}
|
257
|
-
return {
|
258
|
-
[ruleId]: {
|
259
|
-
meta: {
|
260
|
-
docs: {
|
261
|
-
recommended: true,
|
262
|
-
...docs,
|
263
|
-
graphQLJSRuleName: ruleName,
|
264
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${ruleId}.md`,
|
265
|
-
description: `${docs.description}\n\n> This rule is a wrapper around a \`graphql-js\` validation function.`,
|
266
|
-
},
|
267
|
-
schema,
|
268
|
-
},
|
269
|
-
create(context) {
|
270
|
-
if (!ruleFn) {
|
271
|
-
logger.warn(`Rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql" version you are using. Skipping…`);
|
272
|
-
return {};
|
273
|
-
}
|
274
|
-
return {
|
275
|
-
Document(node) {
|
276
|
-
const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
|
277
|
-
const documentNode = getDocumentNode
|
278
|
-
? getDocumentNode({ ruleId, context, node: node.rawNode() })
|
279
|
-
: node.rawNode();
|
280
|
-
validateDocument(context, schema, documentNode, ruleFn);
|
281
|
-
},
|
282
|
-
};
|
283
|
-
},
|
284
|
-
},
|
285
|
-
};
|
286
|
-
};
|
287
|
-
const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-definitions', 'ExecutableDefinitions', {
|
288
|
-
category: 'Operations',
|
289
|
-
description: 'A GraphQL document is only valid for execution if all definitions are either operation or fragment definitions.',
|
290
|
-
requiresSchema: true,
|
291
|
-
}), validationToRule('fields-on-correct-type', 'FieldsOnCorrectType', {
|
292
|
-
category: 'Operations',
|
293
|
-
description: 'A GraphQL document is only valid if all fields selected are defined by the parent type, or are an allowed meta field such as `__typename`.',
|
294
|
-
requiresSchema: true,
|
295
|
-
}), validationToRule('fragments-on-composite-type', 'FragmentsOnCompositeTypes', {
|
296
|
-
category: 'Operations',
|
297
|
-
description: 'Fragments use a type condition to determine if they apply, since fragments can only be spread into a composite type (object, interface, or union), the type condition must also be a composite type.',
|
298
|
-
requiresSchema: true,
|
299
|
-
}), validationToRule('known-argument-names', 'KnownArgumentNames', {
|
300
|
-
category: ['Schema', 'Operations'],
|
301
|
-
description: 'A GraphQL field is only valid if all supplied arguments are defined by that field.',
|
302
|
-
requiresSchema: true,
|
303
|
-
}), validationToRule('known-directives', 'KnownDirectives', {
|
304
|
-
category: ['Schema', 'Operations'],
|
305
|
-
description: 'A GraphQL document is only valid if all `@directive`s are known by the schema and legally positioned.',
|
306
|
-
requiresSchema: true,
|
307
|
-
examples: [
|
308
|
-
{
|
309
|
-
title: 'Valid',
|
310
|
-
usage: [{ ignoreClientDirectives: ['client'] }],
|
311
|
-
code: /* GraphQL */ `
|
312
|
-
{
|
313
|
-
product {
|
314
|
-
someClientField @client
|
315
|
-
}
|
316
|
-
}
|
317
|
-
`,
|
318
|
-
},
|
319
|
-
],
|
320
|
-
}, ({ context, node: documentNode }) => {
|
321
|
-
const { ignoreClientDirectives = [] } = context.options[0] || {};
|
322
|
-
if (ignoreClientDirectives.length === 0) {
|
323
|
-
return documentNode;
|
324
|
-
}
|
325
|
-
const filterDirectives = (node) => ({
|
326
|
-
...node,
|
327
|
-
directives: node.directives.filter(directive => !ignoreClientDirectives.includes(directive.name.value)),
|
328
|
-
});
|
329
|
-
return visit(documentNode, {
|
330
|
-
Field: filterDirectives,
|
331
|
-
OperationDefinition: filterDirectives,
|
332
|
-
});
|
333
|
-
}, {
|
334
|
-
type: 'array',
|
335
|
-
maxItems: 1,
|
336
|
-
items: {
|
337
|
-
type: 'object',
|
338
|
-
additionalProperties: false,
|
339
|
-
required: ['ignoreClientDirectives'],
|
340
|
-
properties: {
|
341
|
-
ignoreClientDirectives: ARRAY_DEFAULT_OPTIONS,
|
342
|
-
},
|
343
|
-
},
|
344
|
-
}), validationToRule('known-fragment-names', 'KnownFragmentNames', {
|
345
|
-
category: 'Operations',
|
346
|
-
description: 'A GraphQL document is only valid if all `...Fragment` fragment spreads refer to fragments defined in the same document.',
|
347
|
-
requiresSchema: true,
|
348
|
-
requiresSiblings: true,
|
349
|
-
examples: [
|
350
|
-
{
|
351
|
-
title: 'Incorrect',
|
352
|
-
code: /* GraphQL */ `
|
353
|
-
query {
|
354
|
-
user {
|
355
|
-
id
|
356
|
-
...UserFields # fragment not defined in the document
|
357
|
-
}
|
358
|
-
}
|
359
|
-
`,
|
360
|
-
},
|
361
|
-
{
|
362
|
-
title: 'Correct',
|
363
|
-
code: /* GraphQL */ `
|
364
|
-
fragment UserFields on User {
|
365
|
-
firstName
|
366
|
-
lastName
|
367
|
-
}
|
368
|
-
|
369
|
-
query {
|
370
|
-
user {
|
371
|
-
id
|
372
|
-
...UserFields
|
373
|
-
}
|
374
|
-
}
|
375
|
-
`,
|
376
|
-
},
|
377
|
-
{
|
378
|
-
title: 'Correct (`UserFields` fragment located in a separate file)',
|
379
|
-
code: /* GraphQL */ `
|
380
|
-
# user.gql
|
381
|
-
query {
|
382
|
-
user {
|
383
|
-
id
|
384
|
-
...UserFields
|
385
|
-
}
|
386
|
-
}
|
387
|
-
|
388
|
-
# user-fields.gql
|
389
|
-
fragment UserFields on User {
|
390
|
-
id
|
391
|
-
}
|
392
|
-
`,
|
393
|
-
},
|
394
|
-
],
|
395
|
-
}, handleMissingFragments), validationToRule('known-type-names', 'KnownTypeNames', {
|
396
|
-
category: ['Schema', 'Operations'],
|
397
|
-
description: 'A GraphQL document is only valid if referenced types (specifically variable definitions and fragment conditions) are defined by the type schema.',
|
398
|
-
requiresSchema: true,
|
399
|
-
}), validationToRule('lone-anonymous-operation', 'LoneAnonymousOperation', {
|
400
|
-
category: 'Operations',
|
401
|
-
description: 'A GraphQL document is only valid if when it contains an anonymous operation (the query short-hand) that it contains only that one operation definition.',
|
402
|
-
requiresSchema: true,
|
403
|
-
}), validationToRule('lone-schema-definition', 'LoneSchemaDefinition', {
|
404
|
-
category: 'Schema',
|
405
|
-
description: 'A GraphQL document is only valid if it contains only one schema definition.',
|
406
|
-
}), validationToRule('no-fragment-cycles', 'NoFragmentCycles', {
|
407
|
-
category: 'Operations',
|
408
|
-
description: 'A GraphQL fragment is only valid when it does not have cycles in fragments usage.',
|
409
|
-
requiresSchema: true,
|
410
|
-
}), validationToRule('no-undefined-variables', 'NoUndefinedVariables', {
|
411
|
-
category: 'Operations',
|
412
|
-
description: 'A GraphQL operation is only valid if all variables encountered, both directly and via fragment spreads, are defined by that operation.',
|
413
|
-
requiresSchema: true,
|
414
|
-
requiresSiblings: true,
|
415
|
-
}, handleMissingFragments), validationToRule('no-unused-fragments', 'NoUnusedFragments', {
|
416
|
-
category: 'Operations',
|
417
|
-
description: 'A GraphQL document is only valid if all fragment definitions are spread within operations, or spread within other fragments spread within operations.',
|
418
|
-
requiresSchema: true,
|
419
|
-
requiresSiblings: true,
|
420
|
-
}, ({ ruleId, context, node }) => {
|
421
|
-
const siblings = requireSiblingsOperations(ruleId, context);
|
422
|
-
const FilePathToDocumentsMap = [...siblings.getOperations(), ...siblings.getFragments()].reduce((map, { filePath, document }) => {
|
423
|
-
var _a;
|
424
|
-
(_a = map[filePath]) !== null && _a !== void 0 ? _a : (map[filePath] = []);
|
425
|
-
map[filePath].push(document);
|
426
|
-
return map;
|
427
|
-
}, Object.create(null));
|
428
|
-
const getParentNode = (currentFilePath, node) => {
|
429
|
-
const { fragmentDefs } = getFragmentDefsAndFragmentSpreads(node);
|
430
|
-
if (fragmentDefs.size === 0) {
|
431
|
-
return node;
|
432
|
-
}
|
433
|
-
// skip iteration over documents for current filepath
|
434
|
-
delete FilePathToDocumentsMap[currentFilePath];
|
435
|
-
for (const [filePath, documents] of Object.entries(FilePathToDocumentsMap)) {
|
436
|
-
const missingFragments = getMissingFragments({
|
437
|
-
kind: Kind.DOCUMENT,
|
438
|
-
definitions: documents,
|
439
|
-
});
|
440
|
-
const isCurrentFileImportFragment = missingFragments.some(fragment => fragmentDefs.has(fragment));
|
441
|
-
if (isCurrentFileImportFragment) {
|
442
|
-
return getParentNode(filePath, {
|
443
|
-
kind: Kind.DOCUMENT,
|
444
|
-
definitions: [...node.definitions, ...documents],
|
445
|
-
});
|
446
|
-
}
|
447
|
-
}
|
448
|
-
return node;
|
449
|
-
};
|
450
|
-
return getParentNode(context.getFilename(), node);
|
451
|
-
}), validationToRule('no-unused-variables', 'NoUnusedVariables', {
|
452
|
-
category: 'Operations',
|
453
|
-
description: 'A GraphQL operation is only valid if all variables defined by an operation are used, either directly or within a spread fragment.',
|
454
|
-
requiresSchema: true,
|
455
|
-
requiresSiblings: true,
|
456
|
-
}, handleMissingFragments), validationToRule('overlapping-fields-can-be-merged', 'OverlappingFieldsCanBeMerged', {
|
457
|
-
category: 'Operations',
|
458
|
-
description: 'A selection set is only valid if all fields (including spreading any fragments) either correspond to distinct response names or can be merged without ambiguity.',
|
459
|
-
requiresSchema: true,
|
460
|
-
}), validationToRule('possible-fragment-spread', 'PossibleFragmentSpreads', {
|
461
|
-
category: 'Operations',
|
462
|
-
description: 'A fragment spread is only valid if the type condition could ever possibly be true: if there is a non-empty intersection of the possible parent types, and possible types which pass the type condition.',
|
463
|
-
requiresSchema: true,
|
464
|
-
}), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
|
465
|
-
category: 'Schema',
|
466
|
-
description: 'A type extension is only valid if the type is defined and has the same kind.',
|
467
|
-
// TODO: add in graphql-eslint v4
|
468
|
-
recommended: false,
|
469
|
-
requiresSchema: true,
|
470
|
-
isDisabledForAllConfig: true,
|
471
|
-
}), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
|
472
|
-
category: ['Schema', 'Operations'],
|
473
|
-
description: 'A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.',
|
474
|
-
requiresSchema: true,
|
475
|
-
}), validationToRule('scalar-leafs', 'ScalarLeafs', {
|
476
|
-
category: 'Operations',
|
477
|
-
description: 'A GraphQL document is valid only if all leaf fields (fields without sub selections) are of scalar or enum types.',
|
478
|
-
requiresSchema: true,
|
479
|
-
}), validationToRule('one-field-subscriptions', 'SingleFieldSubscriptions', {
|
480
|
-
category: 'Operations',
|
481
|
-
description: 'A GraphQL subscription is valid only if it contains a single root field.',
|
482
|
-
requiresSchema: true,
|
483
|
-
}), validationToRule('unique-argument-names', 'UniqueArgumentNames', {
|
484
|
-
category: 'Operations',
|
485
|
-
description: 'A GraphQL field or directive is only valid if all supplied arguments are uniquely named.',
|
486
|
-
requiresSchema: true,
|
487
|
-
}), validationToRule('unique-directive-names', 'UniqueDirectiveNames', {
|
488
|
-
category: 'Schema',
|
489
|
-
description: 'A GraphQL document is only valid if all defined directives have unique names.',
|
490
|
-
}), validationToRule('unique-directive-names-per-location', 'UniqueDirectivesPerLocation', {
|
491
|
-
category: ['Schema', 'Operations'],
|
492
|
-
description: 'A GraphQL document is only valid if all non-repeatable directives at a given location are uniquely named.',
|
493
|
-
requiresSchema: true,
|
494
|
-
}), validationToRule('unique-enum-value-names', 'UniqueEnumValueNames', {
|
495
|
-
category: 'Schema',
|
496
|
-
description: 'A GraphQL enum type is only valid if all its values are uniquely named.',
|
497
|
-
recommended: false,
|
498
|
-
isDisabledForAllConfig: true,
|
499
|
-
}), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
|
500
|
-
category: 'Schema',
|
501
|
-
description: 'A GraphQL complex type is only valid if all its fields are uniquely named.',
|
502
|
-
}), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
|
503
|
-
category: 'Operations',
|
504
|
-
description: 'A GraphQL input object value is only valid if all supplied fields are uniquely named.',
|
505
|
-
}), validationToRule('unique-operation-types', 'UniqueOperationTypes', {
|
506
|
-
category: 'Schema',
|
507
|
-
description: 'A GraphQL document is only valid if it has only one type per operation.',
|
508
|
-
}), validationToRule('unique-type-names', 'UniqueTypeNames', {
|
509
|
-
category: 'Schema',
|
510
|
-
description: 'A GraphQL document is only valid if all defined types have unique names.',
|
511
|
-
}), validationToRule('unique-variable-names', 'UniqueVariableNames', {
|
512
|
-
category: 'Operations',
|
513
|
-
description: 'A GraphQL operation is only valid if all its variables are uniquely named.',
|
514
|
-
requiresSchema: true,
|
515
|
-
}), validationToRule('value-literals-of-correct-type', 'ValuesOfCorrectType', {
|
516
|
-
category: 'Operations',
|
517
|
-
description: 'A GraphQL document is only valid if all value literals are of the type expected at their position.',
|
518
|
-
requiresSchema: true,
|
519
|
-
}), validationToRule('variables-are-input-types', 'VariablesAreInputTypes', {
|
520
|
-
category: 'Operations',
|
521
|
-
description: 'A GraphQL operation is only valid if all the variables it defines are of input types (scalar, enum, or input object).',
|
522
|
-
requiresSchema: true,
|
523
|
-
}), validationToRule('variables-in-allowed-position', 'VariablesInAllowedPosition', {
|
524
|
-
category: 'Operations',
|
525
|
-
description: 'Variables passed to field arguments conform to type.',
|
526
|
-
requiresSchema: true,
|
527
|
-
}));
|
528
|
-
|
529
|
-
const RULE_ID = 'alphabetize';
|
530
|
-
const fieldsEnum = [
|
531
|
-
Kind.OBJECT_TYPE_DEFINITION,
|
532
|
-
Kind.INTERFACE_TYPE_DEFINITION,
|
533
|
-
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
534
|
-
];
|
535
|
-
const valuesEnum = [Kind.ENUM_TYPE_DEFINITION];
|
536
|
-
const selectionsEnum = [
|
537
|
-
Kind.OPERATION_DEFINITION,
|
538
|
-
Kind.FRAGMENT_DEFINITION,
|
539
|
-
];
|
540
|
-
const variablesEnum = [Kind.OPERATION_DEFINITION];
|
541
|
-
const argumentsEnum = [
|
542
|
-
Kind.FIELD_DEFINITION,
|
543
|
-
Kind.FIELD,
|
544
|
-
Kind.DIRECTIVE_DEFINITION,
|
545
|
-
Kind.DIRECTIVE,
|
546
|
-
];
|
547
|
-
const rule = {
|
548
|
-
meta: {
|
549
|
-
type: 'suggestion',
|
550
|
-
fixable: 'code',
|
551
|
-
docs: {
|
552
|
-
category: ['Schema', 'Operations'],
|
553
|
-
description: 'Enforce arrange in alphabetical order for type fields, enum values, input object fields, operation selections and more.',
|
554
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
555
|
-
examples: [
|
556
|
-
{
|
557
|
-
title: 'Incorrect',
|
558
|
-
usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
|
559
|
-
code: /* GraphQL */ `
|
560
|
-
type User {
|
561
|
-
password: String
|
562
|
-
firstName: String! # should be before "password"
|
563
|
-
age: Int # should be before "firstName"
|
564
|
-
lastName: String!
|
565
|
-
}
|
566
|
-
`,
|
567
|
-
},
|
568
|
-
{
|
569
|
-
title: 'Correct',
|
570
|
-
usage: [{ fields: [Kind.OBJECT_TYPE_DEFINITION] }],
|
571
|
-
code: /* GraphQL */ `
|
572
|
-
type User {
|
573
|
-
age: Int
|
574
|
-
firstName: String!
|
575
|
-
lastName: String!
|
576
|
-
password: String
|
577
|
-
}
|
578
|
-
`,
|
579
|
-
},
|
580
|
-
{
|
581
|
-
title: 'Incorrect',
|
582
|
-
usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
|
583
|
-
code: /* GraphQL */ `
|
584
|
-
enum Role {
|
585
|
-
SUPER_ADMIN
|
586
|
-
ADMIN # should be before "SUPER_ADMIN"
|
587
|
-
USER
|
588
|
-
GOD # should be before "USER"
|
589
|
-
}
|
590
|
-
`,
|
591
|
-
},
|
592
|
-
{
|
593
|
-
title: 'Correct',
|
594
|
-
usage: [{ values: [Kind.ENUM_TYPE_DEFINITION] }],
|
595
|
-
code: /* GraphQL */ `
|
596
|
-
enum Role {
|
597
|
-
ADMIN
|
598
|
-
GOD
|
599
|
-
SUPER_ADMIN
|
600
|
-
USER
|
601
|
-
}
|
602
|
-
`,
|
603
|
-
},
|
604
|
-
{
|
605
|
-
title: 'Incorrect',
|
606
|
-
usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
|
607
|
-
code: /* GraphQL */ `
|
608
|
-
query {
|
609
|
-
me {
|
610
|
-
firstName
|
611
|
-
lastName
|
612
|
-
email # should be before "lastName"
|
613
|
-
}
|
614
|
-
}
|
615
|
-
`,
|
616
|
-
},
|
617
|
-
{
|
618
|
-
title: 'Correct',
|
619
|
-
usage: [{ selections: [Kind.OPERATION_DEFINITION] }],
|
620
|
-
code: /* GraphQL */ `
|
621
|
-
query {
|
622
|
-
me {
|
623
|
-
email
|
624
|
-
firstName
|
625
|
-
lastName
|
626
|
-
}
|
627
|
-
}
|
628
|
-
`,
|
629
|
-
},
|
630
|
-
],
|
631
|
-
configOptions: {
|
632
|
-
schema: [
|
633
|
-
{
|
634
|
-
fields: fieldsEnum,
|
635
|
-
values: valuesEnum,
|
636
|
-
arguments: argumentsEnum,
|
637
|
-
// TODO: add in graphql-eslint v4
|
638
|
-
// definitions: true,
|
639
|
-
},
|
640
|
-
],
|
641
|
-
operations: [
|
642
|
-
{
|
643
|
-
selections: selectionsEnum,
|
644
|
-
variables: variablesEnum,
|
645
|
-
arguments: [Kind.FIELD, Kind.DIRECTIVE],
|
646
|
-
},
|
647
|
-
],
|
648
|
-
},
|
649
|
-
},
|
650
|
-
messages: {
|
651
|
-
[RULE_ID]: '`{{ currName }}` should be before {{ prevName }}.',
|
652
|
-
},
|
653
|
-
schema: {
|
654
|
-
type: 'array',
|
655
|
-
minItems: 1,
|
656
|
-
maxItems: 1,
|
657
|
-
items: {
|
658
|
-
type: 'object',
|
659
|
-
additionalProperties: false,
|
660
|
-
minProperties: 1,
|
661
|
-
properties: {
|
662
|
-
fields: {
|
663
|
-
...ARRAY_DEFAULT_OPTIONS,
|
664
|
-
items: {
|
665
|
-
enum: fieldsEnum,
|
666
|
-
},
|
667
|
-
description: 'Fields of `type`, `interface`, and `input`.',
|
668
|
-
},
|
669
|
-
values: {
|
670
|
-
...ARRAY_DEFAULT_OPTIONS,
|
671
|
-
items: {
|
672
|
-
enum: valuesEnum,
|
673
|
-
},
|
674
|
-
description: 'Values of `enum`.',
|
675
|
-
},
|
676
|
-
selections: {
|
677
|
-
...ARRAY_DEFAULT_OPTIONS,
|
678
|
-
items: {
|
679
|
-
enum: selectionsEnum,
|
680
|
-
},
|
681
|
-
description: 'Selections of `fragment` and operations `query`, `mutation` and `subscription`.',
|
682
|
-
},
|
683
|
-
variables: {
|
684
|
-
...ARRAY_DEFAULT_OPTIONS,
|
685
|
-
items: {
|
686
|
-
enum: variablesEnum,
|
687
|
-
},
|
688
|
-
description: 'Variables of operations `query`, `mutation` and `subscription`.',
|
689
|
-
},
|
690
|
-
arguments: {
|
691
|
-
...ARRAY_DEFAULT_OPTIONS,
|
692
|
-
items: {
|
693
|
-
enum: argumentsEnum,
|
694
|
-
},
|
695
|
-
description: 'Arguments of fields and directives.',
|
696
|
-
},
|
697
|
-
definitions: {
|
698
|
-
type: 'boolean',
|
699
|
-
description: 'Definitions – `type`, `interface`, `enum`, `scalar`, `input`, `union` and `directive`.',
|
700
|
-
default: false,
|
701
|
-
},
|
702
|
-
},
|
703
|
-
},
|
704
|
-
},
|
705
|
-
},
|
706
|
-
create(context) {
|
707
|
-
var _a, _b, _c, _d, _e;
|
708
|
-
const sourceCode = context.getSourceCode();
|
709
|
-
function isNodeAndCommentOnSameLine(node, comment) {
|
710
|
-
return node.loc.end.line === comment.loc.start.line;
|
711
|
-
}
|
712
|
-
function getBeforeComments(node) {
|
713
|
-
const commentsBefore = sourceCode.getCommentsBefore(node);
|
714
|
-
if (commentsBefore.length === 0) {
|
715
|
-
return [];
|
716
|
-
}
|
717
|
-
const tokenBefore = sourceCode.getTokenBefore(node);
|
718
|
-
if (tokenBefore) {
|
719
|
-
return commentsBefore.filter(comment => !isNodeAndCommentOnSameLine(tokenBefore, comment));
|
720
|
-
}
|
721
|
-
const filteredComments = [];
|
722
|
-
const nodeLine = node.loc.start.line;
|
723
|
-
// Break on comment that not attached to node
|
724
|
-
for (let i = commentsBefore.length - 1; i >= 0; i -= 1) {
|
725
|
-
const comment = commentsBefore[i];
|
726
|
-
if (nodeLine - comment.loc.start.line - filteredComments.length > 1) {
|
727
|
-
break;
|
728
|
-
}
|
729
|
-
filteredComments.unshift(comment);
|
730
|
-
}
|
731
|
-
return filteredComments;
|
732
|
-
}
|
733
|
-
function getRangeWithComments(node) {
|
734
|
-
if (node.kind === Kind.VARIABLE) {
|
735
|
-
node = node.parent;
|
736
|
-
}
|
737
|
-
const [firstBeforeComment] = getBeforeComments(node);
|
738
|
-
const [firstAfterComment] = sourceCode.getCommentsAfter(node);
|
739
|
-
const from = firstBeforeComment || node;
|
740
|
-
const to = firstAfterComment && isNodeAndCommentOnSameLine(node, firstAfterComment) ? firstAfterComment : node;
|
741
|
-
return [from.range[0], to.range[1]];
|
742
|
-
}
|
743
|
-
function checkNodes(nodes) {
|
744
|
-
var _a, _b, _c, _d;
|
745
|
-
// Starts from 1, ignore nodes.length <= 1
|
746
|
-
for (let i = 1; i < nodes.length; i += 1) {
|
747
|
-
const currNode = nodes[i];
|
748
|
-
const currName = ('alias' in currNode && ((_a = currNode.alias) === null || _a === void 0 ? void 0 : _a.value)) || ('name' in currNode && ((_b = currNode.name) === null || _b === void 0 ? void 0 : _b.value));
|
749
|
-
if (!currName) {
|
750
|
-
// we don't move unnamed current nodes
|
751
|
-
continue;
|
752
|
-
}
|
753
|
-
const prevNode = nodes[i - 1];
|
754
|
-
const prevName = ('alias' in prevNode && ((_c = prevNode.alias) === null || _c === void 0 ? void 0 : _c.value)) || ('name' in prevNode && ((_d = prevNode.name) === null || _d === void 0 ? void 0 : _d.value));
|
755
|
-
if (prevName) {
|
756
|
-
// Compare with lexicographic order
|
757
|
-
const compareResult = prevName.localeCompare(currName);
|
758
|
-
const shouldSort = compareResult === 1;
|
759
|
-
if (!shouldSort) {
|
760
|
-
const isSameName = compareResult === 0;
|
761
|
-
if (!isSameName || !prevNode.kind.endsWith('Extension') || currNode.kind.endsWith('Extension')) {
|
762
|
-
continue;
|
763
|
-
}
|
764
|
-
}
|
765
|
-
}
|
766
|
-
context.report({
|
767
|
-
node: ('alias' in currNode && currNode.alias) || currNode.name,
|
768
|
-
messageId: RULE_ID,
|
769
|
-
data: {
|
770
|
-
currName,
|
771
|
-
prevName: prevName ? `\`${prevName}\`` : lowerCase(prevNode.kind),
|
772
|
-
},
|
773
|
-
*fix(fixer) {
|
774
|
-
const prevRange = getRangeWithComments(prevNode);
|
775
|
-
const currRange = getRangeWithComments(currNode);
|
776
|
-
yield fixer.replaceTextRange(prevRange, sourceCode.getText({ range: currRange }));
|
777
|
-
yield fixer.replaceTextRange(currRange, sourceCode.getText({ range: prevRange }));
|
778
|
-
},
|
779
|
-
});
|
780
|
-
}
|
781
|
-
}
|
782
|
-
const opts = context.options[0];
|
783
|
-
const fields = new Set((_a = opts.fields) !== null && _a !== void 0 ? _a : []);
|
784
|
-
const listeners = {};
|
785
|
-
const kinds = [
|
786
|
-
fields.has(Kind.OBJECT_TYPE_DEFINITION) && [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION],
|
787
|
-
fields.has(Kind.INTERFACE_TYPE_DEFINITION) && [Kind.INTERFACE_TYPE_DEFINITION, Kind.INTERFACE_TYPE_EXTENSION],
|
788
|
-
fields.has(Kind.INPUT_OBJECT_TYPE_DEFINITION) && [
|
789
|
-
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
790
|
-
Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
791
|
-
],
|
792
|
-
]
|
793
|
-
.filter(Boolean)
|
794
|
-
.flat();
|
795
|
-
const fieldsSelector = kinds.join(',');
|
796
|
-
const hasEnumValues = ((_b = opts.values) === null || _b === void 0 ? void 0 : _b[0]) === Kind.ENUM_TYPE_DEFINITION;
|
797
|
-
const selectionsSelector = (_c = opts.selections) === null || _c === void 0 ? void 0 : _c.join(',');
|
798
|
-
const hasVariables = ((_d = opts.variables) === null || _d === void 0 ? void 0 : _d[0]) === Kind.OPERATION_DEFINITION;
|
799
|
-
const argumentsSelector = (_e = opts.arguments) === null || _e === void 0 ? void 0 : _e.join(',');
|
800
|
-
if (fieldsSelector) {
|
801
|
-
listeners[fieldsSelector] = (node) => {
|
802
|
-
checkNodes(node.fields);
|
803
|
-
};
|
804
|
-
}
|
805
|
-
if (hasEnumValues) {
|
806
|
-
const enumValuesSelector = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION].join(',');
|
807
|
-
listeners[enumValuesSelector] = (node) => {
|
808
|
-
checkNodes(node.values);
|
809
|
-
};
|
810
|
-
}
|
811
|
-
if (selectionsSelector) {
|
812
|
-
listeners[`:matches(${selectionsSelector}) SelectionSet`] = (node) => {
|
813
|
-
checkNodes(node.selections);
|
814
|
-
};
|
815
|
-
}
|
816
|
-
if (hasVariables) {
|
817
|
-
listeners.OperationDefinition = (node) => {
|
818
|
-
checkNodes(node.variableDefinitions.map(varDef => varDef.variable));
|
819
|
-
};
|
820
|
-
}
|
821
|
-
if (argumentsSelector) {
|
822
|
-
listeners[argumentsSelector] = (node) => {
|
823
|
-
checkNodes(node.arguments);
|
824
|
-
};
|
825
|
-
}
|
826
|
-
if (opts.definitions) {
|
827
|
-
listeners.Document = node => {
|
828
|
-
checkNodes(node.definitions);
|
829
|
-
};
|
830
|
-
}
|
831
|
-
return listeners;
|
832
|
-
},
|
833
|
-
};
|
834
|
-
|
835
|
-
const rule$1 = {
|
836
|
-
meta: {
|
837
|
-
type: 'suggestion',
|
838
|
-
hasSuggestions: true,
|
839
|
-
docs: {
|
840
|
-
examples: [
|
841
|
-
{
|
842
|
-
title: 'Incorrect',
|
843
|
-
usage: [{ style: 'inline' }],
|
844
|
-
code: /* GraphQL */ `
|
845
|
-
""" Description """
|
846
|
-
type someTypeName {
|
847
|
-
# ...
|
848
|
-
}
|
849
|
-
`,
|
850
|
-
},
|
851
|
-
{
|
852
|
-
title: 'Correct',
|
853
|
-
usage: [{ style: 'inline' }],
|
854
|
-
code: /* GraphQL */ `
|
855
|
-
" Description "
|
856
|
-
type someTypeName {
|
857
|
-
# ...
|
858
|
-
}
|
859
|
-
`,
|
860
|
-
},
|
861
|
-
],
|
862
|
-
description: 'Require all comments to follow the same style (either block or inline).',
|
863
|
-
category: 'Schema',
|
864
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/description-style.md',
|
865
|
-
recommended: true,
|
866
|
-
},
|
867
|
-
schema: [
|
868
|
-
{
|
869
|
-
type: 'object',
|
870
|
-
additionalProperties: false,
|
871
|
-
properties: {
|
872
|
-
style: {
|
873
|
-
enum: ['block', 'inline'],
|
874
|
-
default: 'block',
|
875
|
-
},
|
876
|
-
},
|
877
|
-
},
|
878
|
-
],
|
879
|
-
},
|
880
|
-
create(context) {
|
881
|
-
const { style = 'block' } = context.options[0] || {};
|
882
|
-
const isBlock = style === 'block';
|
883
|
-
return {
|
884
|
-
[`.description[type=StringValue][block!=${isBlock}]`](node) {
|
885
|
-
context.report({
|
886
|
-
loc: isBlock ? node.loc : node.loc.start,
|
887
|
-
message: `Unexpected ${isBlock ? 'inline' : 'block'} description.`,
|
888
|
-
suggest: [
|
889
|
-
{
|
890
|
-
desc: `Change to ${isBlock ? 'block' : 'inline'} style description`,
|
891
|
-
fix(fixer) {
|
892
|
-
const sourceCode = context.getSourceCode();
|
893
|
-
const originalText = sourceCode.getText(node);
|
894
|
-
const newText = isBlock
|
895
|
-
? originalText.replace(/(^")|("$)/g, '"""')
|
896
|
-
: originalText.replace(/(^""")|("""$)/g, '"').replace(/\s+/g, ' ');
|
897
|
-
return fixer.replaceText(node, newText);
|
898
|
-
},
|
899
|
-
},
|
900
|
-
],
|
901
|
-
});
|
902
|
-
},
|
903
|
-
};
|
904
|
-
},
|
905
|
-
};
|
906
|
-
|
907
|
-
const isObjectType = (node) => [Kind.OBJECT_TYPE_DEFINITION, Kind.OBJECT_TYPE_EXTENSION].includes(node.type);
|
908
|
-
const isQueryType = (node) => isObjectType(node) && node.name.value === 'Query';
|
909
|
-
const isMutationType = (node) => isObjectType(node) && node.name.value === 'Mutation';
|
910
|
-
const rule$2 = {
|
911
|
-
meta: {
|
912
|
-
type: 'suggestion',
|
913
|
-
hasSuggestions: true,
|
914
|
-
docs: {
|
915
|
-
description: 'Require mutation argument to be always called "input" and input type to be called Mutation name + "Input".\nUsing the same name for all input parameters will make your schemas easier to consume and more predictable. Using the same name as mutation for InputType will make it easier to find mutations that InputType belongs to.',
|
916
|
-
category: 'Schema',
|
917
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/input-name.md',
|
918
|
-
examples: [
|
919
|
-
{
|
920
|
-
title: 'Incorrect',
|
921
|
-
usage: [{ checkInputType: true }],
|
922
|
-
code: /* GraphQL */ `
|
923
|
-
type Mutation {
|
924
|
-
SetMessage(message: InputMessage): String
|
925
|
-
}
|
926
|
-
`,
|
927
|
-
},
|
928
|
-
{
|
929
|
-
title: 'Correct (with checkInputType)',
|
930
|
-
usage: [{ checkInputType: true }],
|
931
|
-
code: /* GraphQL */ `
|
932
|
-
type Mutation {
|
933
|
-
SetMessage(input: SetMessageInput): String
|
934
|
-
}
|
935
|
-
`,
|
936
|
-
},
|
937
|
-
{
|
938
|
-
title: 'Correct (without checkInputType)',
|
939
|
-
usage: [{ checkInputType: false }],
|
940
|
-
code: /* GraphQL */ `
|
941
|
-
type Mutation {
|
942
|
-
SetMessage(input: AnyInputTypeName): String
|
943
|
-
}
|
944
|
-
`,
|
945
|
-
},
|
946
|
-
],
|
947
|
-
},
|
948
|
-
schema: [
|
949
|
-
{
|
950
|
-
type: 'object',
|
951
|
-
additionalProperties: false,
|
952
|
-
properties: {
|
953
|
-
checkInputType: {
|
954
|
-
type: 'boolean',
|
955
|
-
default: false,
|
956
|
-
description: 'Check that the input type name follows the convention <mutationName>Input',
|
957
|
-
},
|
958
|
-
caseSensitiveInputType: {
|
959
|
-
type: 'boolean',
|
960
|
-
default: true,
|
961
|
-
description: 'Allow for case discrepancies in the input type name',
|
962
|
-
},
|
963
|
-
checkQueries: {
|
964
|
-
type: 'boolean',
|
965
|
-
default: false,
|
966
|
-
description: 'Apply the rule to Queries',
|
967
|
-
},
|
968
|
-
checkMutations: {
|
969
|
-
type: 'boolean',
|
970
|
-
default: true,
|
971
|
-
description: 'Apply the rule to Mutations',
|
972
|
-
},
|
973
|
-
},
|
974
|
-
},
|
975
|
-
],
|
976
|
-
},
|
977
|
-
create(context) {
|
978
|
-
const options = {
|
979
|
-
checkInputType: false,
|
980
|
-
caseSensitiveInputType: true,
|
981
|
-
checkQueries: false,
|
982
|
-
checkMutations: true,
|
983
|
-
...context.options[0],
|
984
|
-
};
|
985
|
-
const shouldCheckType = node => (options.checkMutations && isMutationType(node)) || (options.checkQueries && isQueryType(node));
|
986
|
-
const listeners = {
|
987
|
-
'FieldDefinition > InputValueDefinition[name.value!=input] > Name'(node) {
|
988
|
-
if (shouldCheckType(node.parent.parent.parent)) {
|
989
|
-
const inputName = node.value;
|
990
|
-
context.report({
|
991
|
-
node,
|
992
|
-
message: `Input \`${inputName}\` should be called \`input\`.`,
|
993
|
-
suggest: [
|
994
|
-
{
|
995
|
-
desc: 'Rename to `input`',
|
996
|
-
fix: fixer => fixer.replaceText(node, 'input'),
|
997
|
-
},
|
998
|
-
],
|
999
|
-
});
|
1000
|
-
}
|
1001
|
-
},
|
1002
|
-
};
|
1003
|
-
if (options.checkInputType) {
|
1004
|
-
listeners['FieldDefinition > InputValueDefinition NamedType'] = (node) => {
|
1005
|
-
const findInputType = item => {
|
1006
|
-
let currentNode = item;
|
1007
|
-
while (currentNode.type !== Kind.INPUT_VALUE_DEFINITION) {
|
1008
|
-
currentNode = currentNode.parent;
|
1009
|
-
}
|
1010
|
-
return currentNode;
|
1011
|
-
};
|
1012
|
-
const inputValueNode = findInputType(node);
|
1013
|
-
if (shouldCheckType(inputValueNode.parent.parent)) {
|
1014
|
-
const mutationName = `${inputValueNode.parent.name.value}Input`;
|
1015
|
-
const name = node.name.value;
|
1016
|
-
if ((options.caseSensitiveInputType && node.name.value !== mutationName) ||
|
1017
|
-
name.toLowerCase() !== mutationName.toLowerCase()) {
|
1018
|
-
context.report({
|
1019
|
-
node: node.name,
|
1020
|
-
message: `Input type \`${name}\` name should be \`${mutationName}\`.`,
|
1021
|
-
suggest: [
|
1022
|
-
{
|
1023
|
-
desc: `Rename to \`${mutationName}\``,
|
1024
|
-
fix: fixer => fixer.replaceText(node, mutationName),
|
1025
|
-
},
|
1026
|
-
],
|
1027
|
-
});
|
1028
|
-
}
|
1029
|
-
}
|
1030
|
-
};
|
1031
|
-
}
|
1032
|
-
return listeners;
|
1033
|
-
},
|
1034
|
-
};
|
1035
|
-
|
1036
|
-
const MATCH_EXTENSION = 'MATCH_EXTENSION';
|
1037
|
-
const MATCH_STYLE = 'MATCH_STYLE';
|
1038
|
-
const ACCEPTED_EXTENSIONS = ['.gql', '.graphql'];
|
1039
|
-
const CASE_STYLES = [
|
1040
|
-
'camelCase',
|
1041
|
-
'PascalCase',
|
1042
|
-
'snake_case',
|
1043
|
-
'UPPER_CASE',
|
1044
|
-
'kebab-case',
|
1045
|
-
'matchDocumentStyle',
|
1046
|
-
];
|
1047
|
-
const schemaOption = {
|
1048
|
-
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1049
|
-
};
|
1050
|
-
const rule$3 = {
|
1051
|
-
meta: {
|
1052
|
-
type: 'suggestion',
|
1053
|
-
docs: {
|
1054
|
-
category: 'Operations',
|
1055
|
-
description: 'This rule allows you to enforce that the file name should match the operation name.',
|
1056
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/match-document-filename.md',
|
1057
|
-
examples: [
|
1058
|
-
{
|
1059
|
-
title: 'Correct',
|
1060
|
-
usage: [{ fileExtension: '.gql' }],
|
1061
|
-
code: /* GraphQL */ `
|
1062
|
-
# user.gql
|
1063
|
-
type User {
|
1064
|
-
id: ID!
|
1065
|
-
}
|
1066
|
-
`,
|
1067
|
-
},
|
1068
|
-
{
|
1069
|
-
title: 'Correct',
|
1070
|
-
usage: [{ query: 'snake_case' }],
|
1071
|
-
code: /* GraphQL */ `
|
1072
|
-
# user_by_id.gql
|
1073
|
-
query UserById {
|
1074
|
-
userById(id: 5) {
|
1075
|
-
id
|
1076
|
-
name
|
1077
|
-
fullName
|
1078
|
-
}
|
1079
|
-
}
|
1080
|
-
`,
|
1081
|
-
},
|
1082
|
-
{
|
1083
|
-
title: 'Correct',
|
1084
|
-
usage: [{ fragment: { style: 'kebab-case', suffix: '.fragment' } }],
|
1085
|
-
code: /* GraphQL */ `
|
1086
|
-
# user-fields.fragment.gql
|
1087
|
-
fragment user_fields on User {
|
1088
|
-
id
|
1089
|
-
email
|
1090
|
-
}
|
1091
|
-
`,
|
1092
|
-
},
|
1093
|
-
{
|
1094
|
-
title: 'Correct',
|
1095
|
-
usage: [{ mutation: { style: 'PascalCase', suffix: 'Mutation' } }],
|
1096
|
-
code: /* GraphQL */ `
|
1097
|
-
# DeleteUserMutation.gql
|
1098
|
-
mutation DELETE_USER {
|
1099
|
-
deleteUser(id: 5)
|
1100
|
-
}
|
1101
|
-
`,
|
1102
|
-
},
|
1103
|
-
{
|
1104
|
-
title: 'Incorrect',
|
1105
|
-
usage: [{ fileExtension: '.graphql' }],
|
1106
|
-
code: /* GraphQL */ `
|
1107
|
-
# post.gql
|
1108
|
-
type Post {
|
1109
|
-
id: ID!
|
1110
|
-
}
|
1111
|
-
`,
|
1112
|
-
},
|
1113
|
-
{
|
1114
|
-
title: 'Incorrect',
|
1115
|
-
usage: [{ query: 'PascalCase' }],
|
1116
|
-
code: /* GraphQL */ `
|
1117
|
-
# user-by-id.gql
|
1118
|
-
query UserById {
|
1119
|
-
userById(id: 5) {
|
1120
|
-
id
|
1121
|
-
name
|
1122
|
-
fullName
|
1123
|
-
}
|
1124
|
-
}
|
1125
|
-
`,
|
1126
|
-
},
|
1127
|
-
{
|
1128
|
-
title: 'Correct',
|
1129
|
-
usage: [{ fragment: { style: 'kebab-case', suffix: 'Mutation', prefix: 'mutation.' } }],
|
1130
|
-
name: 'mutation.add-alert.graphql',
|
1131
|
-
code: /* GraphQL */ `
|
1132
|
-
# mutation.add-alert.graphql
|
1133
|
-
mutation addAlert($input: AddAlertInput!) {
|
1134
|
-
addAlert(input: $input) {
|
1135
|
-
...AlertFields
|
1136
|
-
}
|
1137
|
-
}
|
1138
|
-
`,
|
1139
|
-
},
|
1140
|
-
{
|
1141
|
-
title: 'Correct',
|
1142
|
-
usage: [{ fragment: { prefix: 'query.' } }],
|
1143
|
-
name: 'query.me.graphql',
|
1144
|
-
code: /* GraphQL */ `
|
1145
|
-
# query.me.graphql
|
1146
|
-
query me {
|
1147
|
-
me {
|
1148
|
-
...UserFields
|
1149
|
-
}
|
1150
|
-
}
|
1151
|
-
`,
|
1152
|
-
},
|
1153
|
-
],
|
1154
|
-
configOptions: [
|
1155
|
-
{
|
1156
|
-
query: 'kebab-case',
|
1157
|
-
mutation: 'kebab-case',
|
1158
|
-
subscription: 'kebab-case',
|
1159
|
-
fragment: 'kebab-case',
|
1160
|
-
},
|
1161
|
-
],
|
1162
|
-
},
|
1163
|
-
messages: {
|
1164
|
-
[MATCH_EXTENSION]: 'File extension "{{ fileExtension }}" don\'t match extension "{{ expectedFileExtension }}"',
|
1165
|
-
[MATCH_STYLE]: 'Unexpected filename "{{ filename }}". Rename it to "{{ expectedFilename }}"',
|
1166
|
-
},
|
1167
|
-
schema: {
|
1168
|
-
definitions: {
|
1169
|
-
asString: {
|
1170
|
-
enum: CASE_STYLES,
|
1171
|
-
description: `One of: ${CASE_STYLES.map(t => `\`${t}\``).join(', ')}`,
|
1172
|
-
},
|
1173
|
-
asObject: {
|
1174
|
-
type: 'object',
|
1175
|
-
additionalProperties: false,
|
1176
|
-
minProperties: 1,
|
1177
|
-
properties: {
|
1178
|
-
style: { enum: CASE_STYLES },
|
1179
|
-
suffix: { type: 'string' },
|
1180
|
-
prefix: { type: 'string' },
|
1181
|
-
},
|
1182
|
-
},
|
1183
|
-
},
|
1184
|
-
type: 'array',
|
1185
|
-
minItems: 1,
|
1186
|
-
maxItems: 1,
|
1187
|
-
items: {
|
1188
|
-
type: 'object',
|
1189
|
-
additionalProperties: false,
|
1190
|
-
minProperties: 1,
|
1191
|
-
properties: {
|
1192
|
-
fileExtension: { enum: ACCEPTED_EXTENSIONS },
|
1193
|
-
query: schemaOption,
|
1194
|
-
mutation: schemaOption,
|
1195
|
-
subscription: schemaOption,
|
1196
|
-
fragment: schemaOption,
|
1197
|
-
},
|
1198
|
-
},
|
1199
|
-
},
|
1200
|
-
},
|
1201
|
-
create(context) {
|
1202
|
-
const options = context.options[0] || {
|
1203
|
-
fileExtension: null,
|
1204
|
-
};
|
1205
|
-
const filePath = context.getFilename();
|
1206
|
-
const isVirtualFile = !existsSync(filePath);
|
1207
|
-
if (process.env.NODE_ENV !== 'test' && isVirtualFile) {
|
1208
|
-
// Skip validation for code files
|
1209
|
-
return {};
|
1210
|
-
}
|
1211
|
-
const fileExtension = extname(filePath);
|
1212
|
-
const filename = basename(filePath, fileExtension);
|
1213
|
-
return {
|
1214
|
-
Document(documentNode) {
|
1215
|
-
var _a;
|
1216
|
-
if (options.fileExtension && options.fileExtension !== fileExtension) {
|
1217
|
-
context.report({
|
1218
|
-
loc: REPORT_ON_FIRST_CHARACTER,
|
1219
|
-
messageId: MATCH_EXTENSION,
|
1220
|
-
data: {
|
1221
|
-
fileExtension,
|
1222
|
-
expectedFileExtension: options.fileExtension,
|
1223
|
-
},
|
1224
|
-
});
|
1225
|
-
}
|
1226
|
-
const firstOperation = documentNode.definitions.find(n => n.kind === Kind.OPERATION_DEFINITION);
|
1227
|
-
const firstFragment = documentNode.definitions.find(n => n.kind === Kind.FRAGMENT_DEFINITION);
|
1228
|
-
const node = firstOperation || firstFragment;
|
1229
|
-
if (!node) {
|
1230
|
-
return;
|
1231
|
-
}
|
1232
|
-
const docName = (_a = node.name) === null || _a === void 0 ? void 0 : _a.value;
|
1233
|
-
if (!docName) {
|
1234
|
-
return;
|
1235
|
-
}
|
1236
|
-
const docType = 'operation' in node ? node.operation : 'fragment';
|
1237
|
-
let option = options[docType];
|
1238
|
-
if (!option) {
|
1239
|
-
// Config not provided
|
1240
|
-
return;
|
1241
|
-
}
|
1242
|
-
if (typeof option === 'string') {
|
1243
|
-
option = { style: option };
|
1244
|
-
}
|
1245
|
-
const expectedExtension = options.fileExtension || fileExtension;
|
1246
|
-
let expectedFilename = option.prefix || '';
|
1247
|
-
if (option.style) {
|
1248
|
-
expectedFilename += option.style === 'matchDocumentStyle' ? docName : convertCase(option.style, docName);
|
1249
|
-
}
|
1250
|
-
else {
|
1251
|
-
expectedFilename += filename;
|
1252
|
-
}
|
1253
|
-
expectedFilename += (option.suffix || '') + expectedExtension;
|
1254
|
-
const filenameWithExtension = filename + expectedExtension;
|
1255
|
-
if (expectedFilename !== filenameWithExtension) {
|
1256
|
-
context.report({
|
1257
|
-
loc: REPORT_ON_FIRST_CHARACTER,
|
1258
|
-
messageId: MATCH_STYLE,
|
1259
|
-
data: {
|
1260
|
-
expectedFilename,
|
1261
|
-
filename: filenameWithExtension,
|
1262
|
-
},
|
1263
|
-
});
|
1264
|
-
}
|
1265
|
-
},
|
1266
|
-
};
|
1267
|
-
},
|
1268
|
-
};
|
1269
|
-
|
1270
|
-
const KindToDisplayName = {
|
1271
|
-
// types
|
1272
|
-
[Kind.OBJECT_TYPE_DEFINITION]: 'Type',
|
1273
|
-
[Kind.INTERFACE_TYPE_DEFINITION]: 'Interface',
|
1274
|
-
[Kind.ENUM_TYPE_DEFINITION]: 'Enumerator',
|
1275
|
-
[Kind.SCALAR_TYPE_DEFINITION]: 'Scalar',
|
1276
|
-
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'Input type',
|
1277
|
-
[Kind.UNION_TYPE_DEFINITION]: 'Union',
|
1278
|
-
// fields
|
1279
|
-
[Kind.FIELD_DEFINITION]: 'Field',
|
1280
|
-
[Kind.INPUT_VALUE_DEFINITION]: 'Input property',
|
1281
|
-
[Kind.ARGUMENT]: 'Argument',
|
1282
|
-
[Kind.DIRECTIVE_DEFINITION]: 'Directive',
|
1283
|
-
// rest
|
1284
|
-
[Kind.ENUM_VALUE_DEFINITION]: 'Enumeration value',
|
1285
|
-
[Kind.OPERATION_DEFINITION]: 'Operation',
|
1286
|
-
[Kind.FRAGMENT_DEFINITION]: 'Fragment',
|
1287
|
-
[Kind.VARIABLE_DEFINITION]: 'Variable',
|
1288
|
-
};
|
1289
|
-
const StyleToRegex = {
|
1290
|
-
camelCase: /^[a-z][\dA-Za-z]*$/,
|
1291
|
-
PascalCase: /^[A-Z][\dA-Za-z]*$/,
|
1292
|
-
snake_case: /^[a-z][\d_a-z]*[\da-z]*$/,
|
1293
|
-
UPPER_CASE: /^[A-Z][\dA-Z_]*[\dA-Z]*$/,
|
1294
|
-
};
|
1295
|
-
const ALLOWED_KINDS = Object.keys(KindToDisplayName).sort();
|
1296
|
-
const ALLOWED_STYLES = Object.keys(StyleToRegex);
|
1297
|
-
const schemaOption$1 = {
|
1298
|
-
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asObject' }],
|
1299
|
-
};
|
1300
|
-
const rule$4 = {
|
1301
|
-
meta: {
|
1302
|
-
type: 'suggestion',
|
1303
|
-
docs: {
|
1304
|
-
description: 'Require names to follow specified conventions.',
|
1305
|
-
category: ['Schema', 'Operations'],
|
1306
|
-
recommended: true,
|
1307
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/naming-convention.md',
|
1308
|
-
examples: [
|
1309
|
-
{
|
1310
|
-
title: 'Incorrect',
|
1311
|
-
usage: [{ types: 'PascalCase', FieldDefinition: 'camelCase' }],
|
1312
|
-
code: /* GraphQL */ `
|
1313
|
-
type user {
|
1314
|
-
first_name: String!
|
1315
|
-
}
|
1316
|
-
`,
|
1317
|
-
},
|
1318
|
-
{
|
1319
|
-
title: 'Incorrect',
|
1320
|
-
usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }],
|
1321
|
-
code: /* GraphQL */ `
|
1322
|
-
fragment UserFragment on User {
|
1323
|
-
# ...
|
1324
|
-
}
|
1325
|
-
`,
|
1326
|
-
},
|
1327
|
-
{
|
1328
|
-
title: 'Incorrect',
|
1329
|
-
usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }],
|
1330
|
-
code: /* GraphQL */ `
|
1331
|
-
type Query {
|
1332
|
-
getUsers: [User!]!
|
1333
|
-
}
|
1334
|
-
`,
|
1335
|
-
},
|
1336
|
-
{
|
1337
|
-
title: 'Correct',
|
1338
|
-
usage: [{ types: 'PascalCase', FieldDefinition: 'camelCase' }],
|
1339
|
-
code: /* GraphQL */ `
|
1340
|
-
type User {
|
1341
|
-
firstName: String
|
1342
|
-
}
|
1343
|
-
`,
|
1344
|
-
},
|
1345
|
-
{
|
1346
|
-
title: 'Correct',
|
1347
|
-
usage: [{ FragmentDefinition: { style: 'PascalCase', forbiddenSuffixes: ['Fragment'] } }],
|
1348
|
-
code: /* GraphQL */ `
|
1349
|
-
fragment UserFields on User {
|
1350
|
-
# ...
|
1351
|
-
}
|
1352
|
-
`,
|
1353
|
-
},
|
1354
|
-
{
|
1355
|
-
title: 'Correct',
|
1356
|
-
usage: [{ 'FieldDefinition[parent.name.value=Query]': { forbiddenPrefixes: ['get'] } }],
|
1357
|
-
code: /* GraphQL */ `
|
1358
|
-
type Query {
|
1359
|
-
users: [User!]!
|
1360
|
-
}
|
1361
|
-
`,
|
1362
|
-
},
|
1363
|
-
{
|
1364
|
-
title: 'Correct',
|
1365
|
-
usage: [{ FieldDefinition: { style: 'camelCase', ignorePattern: '^(EAN13|UPC|UK)' } }],
|
1366
|
-
code: /* GraphQL */ `
|
1367
|
-
type Product {
|
1368
|
-
EAN13: String
|
1369
|
-
UPC: String
|
1370
|
-
UKFlag: String
|
1371
|
-
}
|
1372
|
-
`,
|
1373
|
-
},
|
1374
|
-
],
|
1375
|
-
configOptions: {
|
1376
|
-
schema: [
|
1377
|
-
{
|
1378
|
-
types: 'PascalCase',
|
1379
|
-
FieldDefinition: 'camelCase',
|
1380
|
-
InputValueDefinition: 'camelCase',
|
1381
|
-
Argument: 'camelCase',
|
1382
|
-
DirectiveDefinition: 'camelCase',
|
1383
|
-
EnumValueDefinition: 'UPPER_CASE',
|
1384
|
-
'FieldDefinition[parent.name.value=Query]': {
|
1385
|
-
forbiddenPrefixes: ['query', 'get'],
|
1386
|
-
forbiddenSuffixes: ['Query'],
|
1387
|
-
},
|
1388
|
-
'FieldDefinition[parent.name.value=Mutation]': {
|
1389
|
-
forbiddenPrefixes: ['mutation'],
|
1390
|
-
forbiddenSuffixes: ['Mutation'],
|
1391
|
-
},
|
1392
|
-
'FieldDefinition[parent.name.value=Subscription]': {
|
1393
|
-
forbiddenPrefixes: ['subscription'],
|
1394
|
-
forbiddenSuffixes: ['Subscription'],
|
1395
|
-
},
|
1396
|
-
},
|
1397
|
-
],
|
1398
|
-
operations: [
|
1399
|
-
{
|
1400
|
-
VariableDefinition: 'camelCase',
|
1401
|
-
OperationDefinition: {
|
1402
|
-
style: 'PascalCase',
|
1403
|
-
forbiddenPrefixes: ['Query', 'Mutation', 'Subscription', 'Get'],
|
1404
|
-
forbiddenSuffixes: ['Query', 'Mutation', 'Subscription'],
|
1405
|
-
},
|
1406
|
-
FragmentDefinition: {
|
1407
|
-
style: 'PascalCase',
|
1408
|
-
forbiddenPrefixes: ['Fragment'],
|
1409
|
-
forbiddenSuffixes: ['Fragment'],
|
1410
|
-
},
|
1411
|
-
},
|
1412
|
-
],
|
1413
|
-
},
|
1414
|
-
},
|
1415
|
-
hasSuggestions: true,
|
1416
|
-
schema: {
|
1417
|
-
definitions: {
|
1418
|
-
asString: {
|
1419
|
-
enum: ALLOWED_STYLES,
|
1420
|
-
description: `One of: ${ALLOWED_STYLES.map(t => `\`${t}\``).join(', ')}`,
|
1421
|
-
},
|
1422
|
-
asObject: {
|
1423
|
-
type: 'object',
|
1424
|
-
additionalProperties: false,
|
1425
|
-
properties: {
|
1426
|
-
style: { enum: ALLOWED_STYLES },
|
1427
|
-
prefix: { type: 'string' },
|
1428
|
-
suffix: { type: 'string' },
|
1429
|
-
forbiddenPrefixes: ARRAY_DEFAULT_OPTIONS,
|
1430
|
-
forbiddenSuffixes: ARRAY_DEFAULT_OPTIONS,
|
1431
|
-
ignorePattern: {
|
1432
|
-
type: 'string',
|
1433
|
-
description: 'Option to skip validation of some words, e.g. acronyms',
|
1434
|
-
},
|
1435
|
-
},
|
1436
|
-
},
|
1437
|
-
},
|
1438
|
-
type: 'array',
|
1439
|
-
maxItems: 1,
|
1440
|
-
items: {
|
1441
|
-
type: 'object',
|
1442
|
-
additionalProperties: false,
|
1443
|
-
properties: {
|
1444
|
-
types: {
|
1445
|
-
...schemaOption$1,
|
1446
|
-
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
|
1447
|
-
},
|
1448
|
-
...Object.fromEntries(ALLOWED_KINDS.map(kind => [
|
1449
|
-
kind,
|
1450
|
-
{
|
1451
|
-
...schemaOption$1,
|
1452
|
-
description: `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`,
|
1453
|
-
},
|
1454
|
-
])),
|
1455
|
-
allowLeadingUnderscore: {
|
1456
|
-
type: 'boolean',
|
1457
|
-
default: false,
|
1458
|
-
},
|
1459
|
-
allowTrailingUnderscore: {
|
1460
|
-
type: 'boolean',
|
1461
|
-
default: false,
|
1462
|
-
},
|
1463
|
-
},
|
1464
|
-
patternProperties: {
|
1465
|
-
[`^(${ALLOWED_KINDS.join('|')})(.+)?$`]: schemaOption$1,
|
1466
|
-
},
|
1467
|
-
description: [
|
1468
|
-
"> It's possible to use a [`selector`](https://eslint.org/docs/developer-guide/selectors) that starts with allowed `ASTNode` names which are described below.",
|
1469
|
-
'>',
|
1470
|
-
'> Paste or drop code into the editor in [ASTExplorer](https://astexplorer.net) and inspect the generated AST to compose your selector.',
|
1471
|
-
'>',
|
1472
|
-
'> Example: pattern property `FieldDefinition[parent.name.value=Query]` will match only fields for type `Query`.',
|
1473
|
-
].join('\n'),
|
1474
|
-
},
|
1475
|
-
},
|
1476
|
-
},
|
1477
|
-
create(context) {
|
1478
|
-
const options = context.options[0] || {};
|
1479
|
-
const { allowLeadingUnderscore, allowTrailingUnderscore, types, ...restOptions } = options;
|
1480
|
-
function normalisePropertyOption(kind) {
|
1481
|
-
const style = restOptions[kind] || types;
|
1482
|
-
return typeof style === 'object' ? style : { style };
|
1483
|
-
}
|
1484
|
-
function report(node, message, suggestedName) {
|
1485
|
-
context.report({
|
1486
|
-
node,
|
1487
|
-
message,
|
1488
|
-
suggest: [
|
1489
|
-
{
|
1490
|
-
desc: `Rename to \`${suggestedName}\``,
|
1491
|
-
fix: fixer => fixer.replaceText(node, suggestedName),
|
1492
|
-
},
|
1493
|
-
],
|
1494
|
-
});
|
1495
|
-
}
|
1496
|
-
const checkNode = (selector) => (n) => {
|
1497
|
-
const { name: node } = n.kind === Kind.VARIABLE_DEFINITION ? n.variable : n;
|
1498
|
-
if (!node) {
|
1499
|
-
return;
|
1500
|
-
}
|
1501
|
-
const { prefix, suffix, forbiddenPrefixes, forbiddenSuffixes, style, ignorePattern } = normalisePropertyOption(selector);
|
1502
|
-
const nodeType = KindToDisplayName[n.kind] || n.kind;
|
1503
|
-
const nodeName = node.value;
|
1504
|
-
const error = getError();
|
1505
|
-
if (error) {
|
1506
|
-
const { errorMessage, renameToName } = error;
|
1507
|
-
const [leadingUnderscores] = nodeName.match(/^_*/);
|
1508
|
-
const [trailingUnderscores] = nodeName.match(/_*$/);
|
1509
|
-
const suggestedName = leadingUnderscores + renameToName + trailingUnderscores;
|
1510
|
-
report(node, `${nodeType} "${nodeName}" should ${errorMessage}`, suggestedName);
|
1511
|
-
}
|
1512
|
-
function getError() {
|
1513
|
-
const name = nodeName.replace(/(^_+)|(_+$)/g, '');
|
1514
|
-
if (ignorePattern && new RegExp(ignorePattern, 'u').test(name)) {
|
1515
|
-
return;
|
1516
|
-
}
|
1517
|
-
if (prefix && !name.startsWith(prefix)) {
|
1518
|
-
return {
|
1519
|
-
errorMessage: `have "${prefix}" prefix`,
|
1520
|
-
renameToName: prefix + name,
|
1521
|
-
};
|
1522
|
-
}
|
1523
|
-
if (suffix && !name.endsWith(suffix)) {
|
1524
|
-
return {
|
1525
|
-
errorMessage: `have "${suffix}" suffix`,
|
1526
|
-
renameToName: name + suffix,
|
1527
|
-
};
|
1528
|
-
}
|
1529
|
-
const forbiddenPrefix = forbiddenPrefixes === null || forbiddenPrefixes === void 0 ? void 0 : forbiddenPrefixes.find(prefix => name.startsWith(prefix));
|
1530
|
-
if (forbiddenPrefix) {
|
1531
|
-
return {
|
1532
|
-
errorMessage: `not have "${forbiddenPrefix}" prefix`,
|
1533
|
-
renameToName: name.replace(new RegExp(`^${forbiddenPrefix}`), ''),
|
1534
|
-
};
|
1535
|
-
}
|
1536
|
-
const forbiddenSuffix = forbiddenSuffixes === null || forbiddenSuffixes === void 0 ? void 0 : forbiddenSuffixes.find(suffix => name.endsWith(suffix));
|
1537
|
-
if (forbiddenSuffix) {
|
1538
|
-
return {
|
1539
|
-
errorMessage: `not have "${forbiddenSuffix}" suffix`,
|
1540
|
-
renameToName: name.replace(new RegExp(`${forbiddenSuffix}$`), ''),
|
1541
|
-
};
|
1542
|
-
}
|
1543
|
-
// Style is optional
|
1544
|
-
if (!style) {
|
1545
|
-
return;
|
1546
|
-
}
|
1547
|
-
const caseRegex = StyleToRegex[style];
|
1548
|
-
if (!caseRegex.test(name)) {
|
1549
|
-
return {
|
1550
|
-
errorMessage: `be in ${style} format`,
|
1551
|
-
renameToName: convertCase(style, name),
|
1552
|
-
};
|
1553
|
-
}
|
1554
|
-
}
|
1555
|
-
};
|
1556
|
-
const checkUnderscore = (isLeading) => (node) => {
|
1557
|
-
const suggestedName = node.value.replace(isLeading ? /^_+/ : /_+$/, '');
|
1558
|
-
report(node, `${isLeading ? 'Leading' : 'Trailing'} underscores are not allowed`, suggestedName);
|
1559
|
-
};
|
1560
|
-
const listeners = {};
|
1561
|
-
if (!allowLeadingUnderscore) {
|
1562
|
-
listeners['Name[value=/^_/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1563
|
-
checkUnderscore(true);
|
1564
|
-
}
|
1565
|
-
if (!allowTrailingUnderscore) {
|
1566
|
-
listeners['Name[value=/_$/]:matches([parent.kind!=Field], [parent.kind=Field][parent.alias])'] =
|
1567
|
-
checkUnderscore(false);
|
1568
|
-
}
|
1569
|
-
const selectors = new Set([types && TYPES_KINDS, Object.keys(restOptions)].flat().filter(Boolean));
|
1570
|
-
for (const selector of selectors) {
|
1571
|
-
listeners[selector] = checkNode(selector);
|
1572
|
-
}
|
1573
|
-
return listeners;
|
1574
|
-
},
|
1575
|
-
};
|
1576
|
-
|
1577
|
-
const RULE_ID$1 = 'no-anonymous-operations';
|
1578
|
-
const rule$5 = {
|
1579
|
-
meta: {
|
1580
|
-
type: 'suggestion',
|
1581
|
-
hasSuggestions: true,
|
1582
|
-
docs: {
|
1583
|
-
category: 'Operations',
|
1584
|
-
description: 'Require name for your GraphQL operations. This is useful since most GraphQL client libraries are using the operation name for caching purposes.',
|
1585
|
-
recommended: true,
|
1586
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$1}.md`,
|
1587
|
-
examples: [
|
1588
|
-
{
|
1589
|
-
title: 'Incorrect',
|
1590
|
-
code: /* GraphQL */ `
|
1591
|
-
query {
|
1592
|
-
# ...
|
1593
|
-
}
|
1594
|
-
`,
|
1595
|
-
},
|
1596
|
-
{
|
1597
|
-
title: 'Correct',
|
1598
|
-
code: /* GraphQL */ `
|
1599
|
-
query user {
|
1600
|
-
# ...
|
1601
|
-
}
|
1602
|
-
`,
|
1603
|
-
},
|
1604
|
-
],
|
1605
|
-
},
|
1606
|
-
messages: {
|
1607
|
-
[RULE_ID$1]: 'Anonymous GraphQL operations are forbidden. Make sure to name your {{ operation }}!',
|
1608
|
-
},
|
1609
|
-
schema: [],
|
1610
|
-
},
|
1611
|
-
create(context) {
|
1612
|
-
return {
|
1613
|
-
'OperationDefinition[name=undefined]'(node) {
|
1614
|
-
const [firstSelection] = node.selectionSet.selections;
|
1615
|
-
const suggestedName = firstSelection.kind === Kind.FIELD ? (firstSelection.alias || firstSelection.name).value : node.operation;
|
1616
|
-
context.report({
|
1617
|
-
loc: getLocation(node.loc.start, node.operation),
|
1618
|
-
messageId: RULE_ID$1,
|
1619
|
-
data: {
|
1620
|
-
operation: node.operation,
|
1621
|
-
},
|
1622
|
-
suggest: [
|
1623
|
-
{
|
1624
|
-
desc: `Rename to \`${suggestedName}\``,
|
1625
|
-
fix(fixer) {
|
1626
|
-
const sourceCode = context.getSourceCode();
|
1627
|
-
const hasQueryKeyword = sourceCode.getText({ range: [node.range[0], node.range[0] + 1] }) !== '{';
|
1628
|
-
return fixer.insertTextAfterRange([node.range[0], node.range[0] + (hasQueryKeyword ? node.operation.length : 0)], `${hasQueryKeyword ? '' : 'query'} ${suggestedName}${hasQueryKeyword ? '' : ' '}`);
|
1629
|
-
},
|
1630
|
-
},
|
1631
|
-
],
|
1632
|
-
});
|
1633
|
-
},
|
1634
|
-
};
|
1635
|
-
},
|
1636
|
-
};
|
1637
|
-
|
1638
|
-
const rule$6 = {
|
1639
|
-
meta: {
|
1640
|
-
type: 'suggestion',
|
1641
|
-
hasSuggestions: true,
|
1642
|
-
docs: {
|
1643
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-case-insensitive-enum-values-duplicates.md',
|
1644
|
-
category: 'Schema',
|
1645
|
-
recommended: true,
|
1646
|
-
description: 'Disallow case-insensitive enum values duplicates.',
|
1647
|
-
examples: [
|
1648
|
-
{
|
1649
|
-
title: 'Incorrect',
|
1650
|
-
code: /* GraphQL */ `
|
1651
|
-
enum MyEnum {
|
1652
|
-
Value
|
1653
|
-
VALUE
|
1654
|
-
ValuE
|
1655
|
-
}
|
1656
|
-
`,
|
1657
|
-
},
|
1658
|
-
{
|
1659
|
-
title: 'Correct',
|
1660
|
-
code: /* GraphQL */ `
|
1661
|
-
enum MyEnum {
|
1662
|
-
Value1
|
1663
|
-
Value2
|
1664
|
-
Value3
|
1665
|
-
}
|
1666
|
-
`,
|
1667
|
-
},
|
1668
|
-
],
|
1669
|
-
},
|
1670
|
-
schema: [],
|
1671
|
-
},
|
1672
|
-
create(context) {
|
1673
|
-
const selector = [Kind.ENUM_TYPE_DEFINITION, Kind.ENUM_TYPE_EXTENSION].join(',');
|
1674
|
-
return {
|
1675
|
-
[selector](node) {
|
1676
|
-
const duplicates = node.values.filter((item, index, array) => array.findIndex(v => v.name.value.toLowerCase() === item.name.value.toLowerCase()) !== index);
|
1677
|
-
for (const duplicate of duplicates) {
|
1678
|
-
const enumName = duplicate.name.value;
|
1679
|
-
context.report({
|
1680
|
-
node: duplicate.name,
|
1681
|
-
message: `Case-insensitive enum values duplicates are not allowed! Found: \`${enumName}\`.`,
|
1682
|
-
suggest: [
|
1683
|
-
{
|
1684
|
-
desc: `Remove \`${enumName}\` enum value`,
|
1685
|
-
fix: fixer => fixer.remove(duplicate),
|
1686
|
-
},
|
1687
|
-
],
|
1688
|
-
});
|
1689
|
-
}
|
1690
|
-
},
|
1691
|
-
};
|
1692
|
-
},
|
1693
|
-
};
|
1694
|
-
|
1695
|
-
const RULE_ID$2 = 'no-deprecated';
|
1696
|
-
const rule$7 = {
|
1697
|
-
meta: {
|
1698
|
-
type: 'suggestion',
|
1699
|
-
hasSuggestions: true,
|
1700
|
-
docs: {
|
1701
|
-
category: 'Operations',
|
1702
|
-
description: 'Enforce that deprecated fields or enum values are not in use by operations.',
|
1703
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
|
1704
|
-
requiresSchema: true,
|
1705
|
-
examples: [
|
1706
|
-
{
|
1707
|
-
title: 'Incorrect (field)',
|
1708
|
-
code: /* GraphQL */ `
|
1709
|
-
# In your schema
|
1710
|
-
type User {
|
1711
|
-
id: ID!
|
1712
|
-
name: String! @deprecated(reason: "old field, please use fullName instead")
|
1713
|
-
fullName: String!
|
1714
|
-
}
|
1715
|
-
|
1716
|
-
# Query
|
1717
|
-
query user {
|
1718
|
-
user {
|
1719
|
-
name # This is deprecated, so you'll get an error
|
1720
|
-
}
|
1721
|
-
}
|
1722
|
-
`,
|
1723
|
-
},
|
1724
|
-
{
|
1725
|
-
title: 'Incorrect (enum value)',
|
1726
|
-
code: /* GraphQL */ `
|
1727
|
-
# In your schema
|
1728
|
-
type Mutation {
|
1729
|
-
changeSomething(type: SomeType): Boolean!
|
1730
|
-
}
|
1731
|
-
|
1732
|
-
enum SomeType {
|
1733
|
-
NEW
|
1734
|
-
OLD @deprecated(reason: "old field, please use NEW instead")
|
1735
|
-
}
|
1736
|
-
|
1737
|
-
# Mutation
|
1738
|
-
mutation {
|
1739
|
-
changeSomething(
|
1740
|
-
type: OLD # This is deprecated, so you'll get an error
|
1741
|
-
) {
|
1742
|
-
...
|
1743
|
-
}
|
1744
|
-
}
|
1745
|
-
`,
|
1746
|
-
},
|
1747
|
-
{
|
1748
|
-
title: 'Correct',
|
1749
|
-
code: /* GraphQL */ `
|
1750
|
-
# In your schema
|
1751
|
-
type User {
|
1752
|
-
id: ID!
|
1753
|
-
name: String! @deprecated(reason: "old field, please use fullName instead")
|
1754
|
-
fullName: String!
|
1755
|
-
}
|
1756
|
-
|
1757
|
-
# Query
|
1758
|
-
query user {
|
1759
|
-
user {
|
1760
|
-
id
|
1761
|
-
fullName
|
1762
|
-
}
|
1763
|
-
}
|
1764
|
-
`,
|
1765
|
-
},
|
1766
|
-
],
|
1767
|
-
recommended: true,
|
1768
|
-
},
|
1769
|
-
messages: {
|
1770
|
-
[RULE_ID$2]: 'This {{ type }} is marked as deprecated in your GraphQL schema (reason: {{ reason }})',
|
1771
|
-
},
|
1772
|
-
schema: [],
|
1773
|
-
},
|
1774
|
-
create(context) {
|
1775
|
-
requireGraphQLSchemaFromContext(RULE_ID$2, context);
|
1776
|
-
function report(node, reason) {
|
1777
|
-
const nodeName = node.kind === Kind.ENUM ? node.value : node.name.value;
|
1778
|
-
const nodeType = node.kind === Kind.ENUM ? 'enum value' : 'field';
|
1779
|
-
context.report({
|
1780
|
-
node,
|
1781
|
-
messageId: RULE_ID$2,
|
1782
|
-
data: {
|
1783
|
-
type: nodeType,
|
1784
|
-
reason,
|
1785
|
-
},
|
1786
|
-
suggest: [
|
1787
|
-
{
|
1788
|
-
desc: `Remove \`${nodeName}\` ${nodeType}`,
|
1789
|
-
fix: fixer => fixer.remove(node),
|
1790
|
-
},
|
1791
|
-
],
|
1792
|
-
});
|
1793
|
-
}
|
1794
|
-
return {
|
1795
|
-
EnumValue(node) {
|
1796
|
-
var _a;
|
1797
|
-
const typeInfo = node.typeInfo();
|
1798
|
-
const reason = (_a = typeInfo.enumValue) === null || _a === void 0 ? void 0 : _a.deprecationReason;
|
1799
|
-
if (reason) {
|
1800
|
-
report(node, reason);
|
1801
|
-
}
|
1802
|
-
},
|
1803
|
-
Field(node) {
|
1804
|
-
var _a;
|
1805
|
-
const typeInfo = node.typeInfo();
|
1806
|
-
const reason = (_a = typeInfo.fieldDef) === null || _a === void 0 ? void 0 : _a.deprecationReason;
|
1807
|
-
if (reason) {
|
1808
|
-
report(node, reason);
|
1809
|
-
}
|
1810
|
-
},
|
1811
|
-
};
|
1812
|
-
},
|
1813
|
-
};
|
1814
|
-
|
1815
|
-
const RULE_ID$3 = 'no-duplicate-fields';
|
1816
|
-
const rule$8 = {
|
1817
|
-
meta: {
|
1818
|
-
type: 'suggestion',
|
1819
|
-
hasSuggestions: true,
|
1820
|
-
docs: {
|
1821
|
-
description: 'Checks for duplicate fields in selection set, variables in operation definition, or in arguments set of a field.',
|
1822
|
-
category: 'Operations',
|
1823
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$3}.md`,
|
1824
|
-
recommended: true,
|
1825
|
-
examples: [
|
1826
|
-
{
|
1827
|
-
title: 'Incorrect',
|
1828
|
-
code: /* GraphQL */ `
|
1829
|
-
query {
|
1830
|
-
user {
|
1831
|
-
name
|
1832
|
-
email
|
1833
|
-
name # duplicate field
|
1834
|
-
}
|
1835
|
-
}
|
1836
|
-
`,
|
1837
|
-
},
|
1838
|
-
{
|
1839
|
-
title: 'Incorrect',
|
1840
|
-
code: /* GraphQL */ `
|
1841
|
-
query {
|
1842
|
-
users(
|
1843
|
-
first: 100
|
1844
|
-
skip: 50
|
1845
|
-
after: "cji629tngfgou0b73kt7vi5jo"
|
1846
|
-
first: 100 # duplicate argument
|
1847
|
-
) {
|
1848
|
-
id
|
1849
|
-
}
|
1850
|
-
}
|
1851
|
-
`,
|
1852
|
-
},
|
1853
|
-
{
|
1854
|
-
title: 'Incorrect',
|
1855
|
-
code: /* GraphQL */ `
|
1856
|
-
query (
|
1857
|
-
$first: Int!
|
1858
|
-
$first: Int! # duplicate variable
|
1859
|
-
) {
|
1860
|
-
users(first: $first, skip: 50) {
|
1861
|
-
id
|
1862
|
-
}
|
1863
|
-
}
|
1864
|
-
`,
|
1865
|
-
},
|
1866
|
-
],
|
1867
|
-
},
|
1868
|
-
messages: {
|
1869
|
-
[RULE_ID$3]: '{{ type }} `{{ fieldName }}` defined multiple times.',
|
1870
|
-
},
|
1871
|
-
schema: [],
|
1872
|
-
},
|
1873
|
-
create(context) {
|
1874
|
-
function checkNode(usedFields, node) {
|
1875
|
-
const fieldName = node.value;
|
1876
|
-
if (usedFields.has(fieldName)) {
|
1877
|
-
const { parent } = node;
|
1878
|
-
context.report({
|
1879
|
-
node,
|
1880
|
-
messageId: RULE_ID$3,
|
1881
|
-
data: {
|
1882
|
-
type: parent.type,
|
1883
|
-
fieldName,
|
1884
|
-
},
|
1885
|
-
suggest: [
|
1886
|
-
{
|
1887
|
-
desc: `Remove \`${fieldName}\` ${parent.type.toLowerCase()}`,
|
1888
|
-
fix(fixer) {
|
1889
|
-
return fixer.remove((parent.type === Kind.VARIABLE ? parent.parent : parent));
|
1890
|
-
},
|
1891
|
-
},
|
1892
|
-
],
|
1893
|
-
});
|
1894
|
-
}
|
1895
|
-
else {
|
1896
|
-
usedFields.add(fieldName);
|
1897
|
-
}
|
1898
|
-
}
|
1899
|
-
return {
|
1900
|
-
OperationDefinition(node) {
|
1901
|
-
const set = new Set();
|
1902
|
-
for (const varDef of node.variableDefinitions) {
|
1903
|
-
checkNode(set, varDef.variable.name);
|
1904
|
-
}
|
1905
|
-
},
|
1906
|
-
Field(node) {
|
1907
|
-
const set = new Set();
|
1908
|
-
for (const arg of node.arguments) {
|
1909
|
-
checkNode(set, arg.name);
|
1910
|
-
}
|
1911
|
-
},
|
1912
|
-
SelectionSet(node) {
|
1913
|
-
const set = new Set();
|
1914
|
-
for (const selection of node.selections) {
|
1915
|
-
if (selection.kind === Kind.FIELD) {
|
1916
|
-
checkNode(set, selection.alias || selection.name);
|
1917
|
-
}
|
1918
|
-
}
|
1919
|
-
},
|
1920
|
-
};
|
1921
|
-
},
|
1922
|
-
};
|
1923
|
-
|
1924
|
-
const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
|
1925
|
-
const rule$9 = {
|
1926
|
-
meta: {
|
1927
|
-
type: 'suggestion',
|
1928
|
-
hasSuggestions: true,
|
1929
|
-
schema: [],
|
1930
|
-
messages: {
|
1931
|
-
[HASHTAG_COMMENT]: 'Using hashtag `#` for adding GraphQL descriptions is not allowed. Prefer using `"""` for multiline, or `"` for a single line description.',
|
1932
|
-
},
|
1933
|
-
docs: {
|
1934
|
-
description: 'Requires to use `"""` or `"` for adding a GraphQL description instead of `#`.\nAllows to use hashtag for comments, as long as it\'s not attached to an AST definition.',
|
1935
|
-
category: 'Schema',
|
1936
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-hashtag-description.md',
|
1937
|
-
examples: [
|
1938
|
-
{
|
1939
|
-
title: 'Incorrect',
|
1940
|
-
code: /* GraphQL */ `
|
1941
|
-
# Represents a user
|
1942
|
-
type User {
|
1943
|
-
id: ID!
|
1944
|
-
name: String
|
1945
|
-
}
|
1946
|
-
`,
|
1947
|
-
},
|
1948
|
-
{
|
1949
|
-
title: 'Correct',
|
1950
|
-
code: /* GraphQL */ `
|
1951
|
-
" Represents a user "
|
1952
|
-
type User {
|
1953
|
-
id: ID!
|
1954
|
-
name: String
|
1955
|
-
}
|
1956
|
-
`,
|
1957
|
-
},
|
1958
|
-
{
|
1959
|
-
title: 'Correct',
|
1960
|
-
code: /* GraphQL */ `
|
1961
|
-
# This file defines the basic User type.
|
1962
|
-
# This comment is valid because it's not attached specifically to an AST object.
|
1963
|
-
|
1964
|
-
" Represents a user "
|
1965
|
-
type User {
|
1966
|
-
id: ID! # This one is also valid, since it comes after the AST object
|
1967
|
-
name: String
|
1968
|
-
}
|
1969
|
-
`,
|
1970
|
-
},
|
1971
|
-
],
|
1972
|
-
recommended: true,
|
1973
|
-
},
|
1974
|
-
},
|
1975
|
-
create(context) {
|
1976
|
-
const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
|
1977
|
-
return {
|
1978
|
-
[selector](node) {
|
1979
|
-
const rawNode = node.rawNode();
|
1980
|
-
let token = rawNode.loc.startToken;
|
1981
|
-
while (token) {
|
1982
|
-
const { kind, prev, next, value, line, column } = token;
|
1983
|
-
if (kind === TokenKind.COMMENT && prev && next) {
|
1984
|
-
const isEslintComment = value.trimStart().startsWith('eslint');
|
1985
|
-
const linesAfter = next.line - line;
|
1986
|
-
if (!isEslintComment && line !== prev.line && next.kind === TokenKind.NAME && linesAfter < 2) {
|
1987
|
-
context.report({
|
1988
|
-
messageId: HASHTAG_COMMENT,
|
1989
|
-
loc: {
|
1990
|
-
line,
|
1991
|
-
column: column - 1,
|
1992
|
-
},
|
1993
|
-
suggest: ['"""', '"'].map(descriptionSyntax => ({
|
1994
|
-
desc: `Replace with \`${descriptionSyntax}\` description syntax`,
|
1995
|
-
fix: fixer => fixer.replaceTextRange([token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join('')),
|
1996
|
-
})),
|
1997
|
-
});
|
1998
|
-
}
|
1999
|
-
}
|
2000
|
-
token = next;
|
2001
|
-
}
|
2002
|
-
},
|
2003
|
-
};
|
2004
|
-
},
|
2005
|
-
};
|
2006
|
-
|
2007
|
-
const ROOT_TYPES = ['mutation', 'subscription'];
|
2008
|
-
const rule$a = {
|
2009
|
-
meta: {
|
2010
|
-
type: 'suggestion',
|
2011
|
-
hasSuggestions: true,
|
2012
|
-
docs: {
|
2013
|
-
category: 'Schema',
|
2014
|
-
description: 'Disallow using root types `mutation` and/or `subscription`.',
|
2015
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-root-type.md',
|
2016
|
-
requiresSchema: true,
|
2017
|
-
isDisabledForAllConfig: true,
|
2018
|
-
examples: [
|
2019
|
-
{
|
2020
|
-
title: 'Incorrect',
|
2021
|
-
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2022
|
-
code: /* GraphQL */ `
|
2023
|
-
type Mutation {
|
2024
|
-
createUser(input: CreateUserInput!): User!
|
2025
|
-
}
|
2026
|
-
`,
|
2027
|
-
},
|
2028
|
-
{
|
2029
|
-
title: 'Correct',
|
2030
|
-
usage: [{ disallow: ['mutation', 'subscription'] }],
|
2031
|
-
code: /* GraphQL */ `
|
2032
|
-
type Query {
|
2033
|
-
users: [User!]!
|
2034
|
-
}
|
2035
|
-
`,
|
2036
|
-
},
|
2037
|
-
],
|
2038
|
-
},
|
2039
|
-
schema: {
|
2040
|
-
type: 'array',
|
2041
|
-
minItems: 1,
|
2042
|
-
maxItems: 1,
|
2043
|
-
items: {
|
2044
|
-
type: 'object',
|
2045
|
-
additionalProperties: false,
|
2046
|
-
required: ['disallow'],
|
2047
|
-
properties: {
|
2048
|
-
disallow: {
|
2049
|
-
...ARRAY_DEFAULT_OPTIONS,
|
2050
|
-
items: {
|
2051
|
-
enum: ROOT_TYPES,
|
2052
|
-
},
|
2053
|
-
},
|
2054
|
-
},
|
2055
|
-
},
|
2056
|
-
},
|
2057
|
-
},
|
2058
|
-
create(context) {
|
2059
|
-
const schema = requireGraphQLSchemaFromContext('no-root-type', context);
|
2060
|
-
const disallow = new Set(context.options[0].disallow);
|
2061
|
-
const rootTypeNames = [
|
2062
|
-
disallow.has('mutation') && schema.getMutationType(),
|
2063
|
-
disallow.has('subscription') && schema.getSubscriptionType(),
|
2064
|
-
]
|
2065
|
-
.filter(Boolean)
|
2066
|
-
.map(type => type.name)
|
2067
|
-
.join('|');
|
2068
|
-
if (!rootTypeNames) {
|
2069
|
-
return {};
|
2070
|
-
}
|
2071
|
-
const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension) > .name[value=/^(${rootTypeNames})$/]`;
|
2072
|
-
return {
|
2073
|
-
[selector](node) {
|
2074
|
-
const typeName = node.value;
|
2075
|
-
context.report({
|
2076
|
-
node,
|
2077
|
-
message: `Root type \`${typeName}\` is forbidden.`,
|
2078
|
-
suggest: [
|
2079
|
-
{
|
2080
|
-
desc: `Remove \`${typeName}\` type`,
|
2081
|
-
fix: fixer => fixer.remove(node.parent),
|
2082
|
-
},
|
2083
|
-
],
|
2084
|
-
});
|
2085
|
-
},
|
2086
|
-
};
|
2087
|
-
},
|
2088
|
-
};
|
2089
|
-
|
2090
|
-
const RULE_ID$4 = 'no-scalar-result-type-on-mutation';
|
2091
|
-
const rule$b = {
|
2092
|
-
meta: {
|
2093
|
-
type: 'suggestion',
|
2094
|
-
hasSuggestions: true,
|
2095
|
-
docs: {
|
2096
|
-
category: 'Schema',
|
2097
|
-
description: 'Avoid scalar result type on mutation type to make sure to return a valid state.',
|
2098
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$4}.md`,
|
2099
|
-
requiresSchema: true,
|
2100
|
-
examples: [
|
2101
|
-
{
|
2102
|
-
title: 'Incorrect',
|
2103
|
-
code: /* GraphQL */ `
|
2104
|
-
type Mutation {
|
2105
|
-
createUser: Boolean
|
2106
|
-
}
|
2107
|
-
`,
|
2108
|
-
},
|
2109
|
-
{
|
2110
|
-
title: 'Correct',
|
2111
|
-
code: /* GraphQL */ `
|
2112
|
-
type Mutation {
|
2113
|
-
createUser: User!
|
2114
|
-
}
|
2115
|
-
`,
|
2116
|
-
},
|
2117
|
-
],
|
2118
|
-
},
|
2119
|
-
schema: [],
|
2120
|
-
},
|
2121
|
-
create(context) {
|
2122
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$4, context);
|
2123
|
-
const mutationType = schema.getMutationType();
|
2124
|
-
if (!mutationType) {
|
2125
|
-
return {};
|
2126
|
-
}
|
2127
|
-
const selector = [
|
2128
|
-
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
|
2129
|
-
'> FieldDefinition > .gqlType Name',
|
2130
|
-
].join(' ');
|
2131
|
-
return {
|
2132
|
-
[selector](node) {
|
2133
|
-
const typeName = node.value;
|
2134
|
-
const graphQLType = schema.getType(typeName);
|
2135
|
-
if (isScalarType(graphQLType)) {
|
2136
|
-
context.report({
|
2137
|
-
node,
|
2138
|
-
message: `Unexpected scalar result type \`${typeName}\`.`,
|
2139
|
-
suggest: [
|
2140
|
-
{
|
2141
|
-
desc: `Remove \`${typeName}\``,
|
2142
|
-
fix: fixer => fixer.remove(node),
|
2143
|
-
},
|
2144
|
-
],
|
2145
|
-
});
|
2146
|
-
}
|
2147
|
-
},
|
2148
|
-
};
|
2149
|
-
},
|
2150
|
-
};
|
2151
|
-
|
2152
|
-
const NO_TYPENAME_PREFIX = 'NO_TYPENAME_PREFIX';
|
2153
|
-
const rule$c = {
|
2154
|
-
meta: {
|
2155
|
-
type: 'suggestion',
|
2156
|
-
hasSuggestions: true,
|
2157
|
-
docs: {
|
2158
|
-
category: 'Schema',
|
2159
|
-
description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
|
2160
|
-
recommended: true,
|
2161
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-typename-prefix.md',
|
2162
|
-
examples: [
|
2163
|
-
{
|
2164
|
-
title: 'Incorrect',
|
2165
|
-
code: /* GraphQL */ `
|
2166
|
-
type User {
|
2167
|
-
userId: ID!
|
2168
|
-
}
|
2169
|
-
`,
|
2170
|
-
},
|
2171
|
-
{
|
2172
|
-
title: 'Correct',
|
2173
|
-
code: /* GraphQL */ `
|
2174
|
-
type User {
|
2175
|
-
id: ID!
|
2176
|
-
}
|
2177
|
-
`,
|
2178
|
-
},
|
2179
|
-
],
|
2180
|
-
},
|
2181
|
-
messages: {
|
2182
|
-
[NO_TYPENAME_PREFIX]: 'Field "{{ fieldName }}" starts with the name of the parent type "{{ typeName }}"',
|
2183
|
-
},
|
2184
|
-
schema: [],
|
2185
|
-
},
|
2186
|
-
create(context) {
|
2187
|
-
return {
|
2188
|
-
'ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension'(node) {
|
2189
|
-
const typeName = node.name.value;
|
2190
|
-
const lowerTypeName = typeName.toLowerCase();
|
2191
|
-
for (const field of node.fields) {
|
2192
|
-
const fieldName = field.name.value;
|
2193
|
-
if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
|
2194
|
-
context.report({
|
2195
|
-
data: {
|
2196
|
-
fieldName,
|
2197
|
-
typeName,
|
2198
|
-
},
|
2199
|
-
messageId: NO_TYPENAME_PREFIX,
|
2200
|
-
node: field.name,
|
2201
|
-
suggest: [
|
2202
|
-
{
|
2203
|
-
desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
|
2204
|
-
fix: fixer => fixer.replaceText(field.name, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
|
2205
|
-
},
|
2206
|
-
],
|
2207
|
-
});
|
2208
|
-
}
|
2209
|
-
}
|
2210
|
-
},
|
2211
|
-
};
|
2212
|
-
},
|
2213
|
-
};
|
2214
|
-
|
2215
|
-
const RULE_ID$5 = 'no-unreachable-types';
|
2216
|
-
const KINDS = [
|
2217
|
-
Kind.DIRECTIVE_DEFINITION,
|
2218
|
-
Kind.OBJECT_TYPE_DEFINITION,
|
2219
|
-
Kind.OBJECT_TYPE_EXTENSION,
|
2220
|
-
Kind.INTERFACE_TYPE_DEFINITION,
|
2221
|
-
Kind.INTERFACE_TYPE_EXTENSION,
|
2222
|
-
Kind.SCALAR_TYPE_DEFINITION,
|
2223
|
-
Kind.SCALAR_TYPE_EXTENSION,
|
2224
|
-
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
2225
|
-
Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
2226
|
-
Kind.UNION_TYPE_DEFINITION,
|
2227
|
-
Kind.UNION_TYPE_EXTENSION,
|
2228
|
-
Kind.ENUM_TYPE_DEFINITION,
|
2229
|
-
Kind.ENUM_TYPE_EXTENSION,
|
2230
|
-
];
|
2231
|
-
let reachableTypesCache;
|
2232
|
-
const RequestDirectiveLocations = new Set([
|
2233
|
-
DirectiveLocation.QUERY,
|
2234
|
-
DirectiveLocation.MUTATION,
|
2235
|
-
DirectiveLocation.SUBSCRIPTION,
|
2236
|
-
DirectiveLocation.FIELD,
|
2237
|
-
DirectiveLocation.FRAGMENT_DEFINITION,
|
2238
|
-
DirectiveLocation.FRAGMENT_SPREAD,
|
2239
|
-
DirectiveLocation.INLINE_FRAGMENT,
|
2240
|
-
DirectiveLocation.VARIABLE_DEFINITION,
|
2241
|
-
]);
|
2242
|
-
function getReachableTypes(schema) {
|
2243
|
-
// We don't want cache reachableTypes on test environment
|
2244
|
-
// Otherwise reachableTypes will be same for all tests
|
2245
|
-
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
|
2246
|
-
return reachableTypesCache;
|
2247
|
-
}
|
2248
|
-
const reachableTypes = new Set();
|
2249
|
-
const collect = (node) => {
|
2250
|
-
const typeName = getTypeName(node);
|
2251
|
-
if (reachableTypes.has(typeName)) {
|
2252
|
-
return;
|
2253
|
-
}
|
2254
|
-
reachableTypes.add(typeName);
|
2255
|
-
const type = schema.getType(typeName) || schema.getDirective(typeName);
|
2256
|
-
if (isInterfaceType(type)) {
|
2257
|
-
const { objects, interfaces } = schema.getImplementations(type);
|
2258
|
-
for (const { astNode } of [...objects, ...interfaces]) {
|
2259
|
-
visit(astNode, visitor);
|
2260
|
-
}
|
2261
|
-
}
|
2262
|
-
else if (type.astNode) {
|
2263
|
-
// astNode can be undefined for ID, String, Boolean
|
2264
|
-
visit(type.astNode, visitor);
|
2265
|
-
}
|
2266
|
-
};
|
2267
|
-
const visitor = {
|
2268
|
-
InterfaceTypeDefinition: collect,
|
2269
|
-
ObjectTypeDefinition: collect,
|
2270
|
-
InputValueDefinition: collect,
|
2271
|
-
UnionTypeDefinition: collect,
|
2272
|
-
FieldDefinition: collect,
|
2273
|
-
Directive: collect,
|
2274
|
-
NamedType: collect,
|
2275
|
-
};
|
2276
|
-
for (const type of [
|
2277
|
-
schema,
|
2278
|
-
schema.getQueryType(),
|
2279
|
-
schema.getMutationType(),
|
2280
|
-
schema.getSubscriptionType(),
|
2281
|
-
]) {
|
2282
|
-
// if schema don't have Query type, schema.astNode will be undefined
|
2283
|
-
if (type === null || type === void 0 ? void 0 : type.astNode) {
|
2284
|
-
visit(type.astNode, visitor);
|
2285
|
-
}
|
2286
|
-
}
|
2287
|
-
for (const node of schema.getDirectives()) {
|
2288
|
-
if (node.locations.some(location => RequestDirectiveLocations.has(location))) {
|
2289
|
-
reachableTypes.add(node.name);
|
2290
|
-
}
|
2291
|
-
}
|
2292
|
-
reachableTypesCache = reachableTypes;
|
2293
|
-
return reachableTypesCache;
|
2294
|
-
}
|
2295
|
-
const rule$d = {
|
2296
|
-
meta: {
|
2297
|
-
messages: {
|
2298
|
-
[RULE_ID$5]: '{{ type }} `{{ typeName }}` is unreachable.',
|
2299
|
-
},
|
2300
|
-
docs: {
|
2301
|
-
description: 'Requires all types to be reachable at some level by root level fields.',
|
2302
|
-
category: 'Schema',
|
2303
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$5}.md`,
|
2304
|
-
requiresSchema: true,
|
2305
|
-
examples: [
|
2306
|
-
{
|
2307
|
-
title: 'Incorrect',
|
2308
|
-
code: /* GraphQL */ `
|
2309
|
-
type User {
|
2310
|
-
id: ID!
|
2311
|
-
name: String
|
2312
|
-
}
|
2313
|
-
|
2314
|
-
type Query {
|
2315
|
-
me: String
|
2316
|
-
}
|
2317
|
-
`,
|
2318
|
-
},
|
2319
|
-
{
|
2320
|
-
title: 'Correct',
|
2321
|
-
code: /* GraphQL */ `
|
2322
|
-
type User {
|
2323
|
-
id: ID!
|
2324
|
-
name: String
|
2325
|
-
}
|
2326
|
-
|
2327
|
-
type Query {
|
2328
|
-
me: User
|
2329
|
-
}
|
2330
|
-
`,
|
2331
|
-
},
|
2332
|
-
],
|
2333
|
-
recommended: true,
|
2334
|
-
},
|
2335
|
-
type: 'suggestion',
|
2336
|
-
schema: [],
|
2337
|
-
hasSuggestions: true,
|
2338
|
-
},
|
2339
|
-
create(context) {
|
2340
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$5, context);
|
2341
|
-
const reachableTypes = getReachableTypes(schema);
|
2342
|
-
return {
|
2343
|
-
[`:matches(${KINDS}) > .name`](node) {
|
2344
|
-
const typeName = node.value;
|
2345
|
-
if (!reachableTypes.has(typeName)) {
|
2346
|
-
const type = lowerCase(node.parent.kind.replace(/(Extension|Definition)$/, ''));
|
2347
|
-
context.report({
|
2348
|
-
node,
|
2349
|
-
messageId: RULE_ID$5,
|
2350
|
-
data: {
|
2351
|
-
type: type[0].toUpperCase() + type.slice(1),
|
2352
|
-
typeName,
|
2353
|
-
},
|
2354
|
-
suggest: [
|
2355
|
-
{
|
2356
|
-
desc: `Remove \`${typeName}\``,
|
2357
|
-
fix: fixer => fixer.remove(node.parent),
|
2358
|
-
},
|
2359
|
-
],
|
2360
|
-
});
|
2361
|
-
}
|
2362
|
-
},
|
2363
|
-
};
|
2364
|
-
},
|
2365
|
-
};
|
2366
|
-
|
2367
|
-
const RULE_ID$6 = 'no-unused-fields';
|
2368
|
-
let usedFieldsCache;
|
2369
|
-
function getUsedFields(schema, operations) {
|
2370
|
-
// We don't want cache usedFields on test environment
|
2371
|
-
// Otherwise usedFields will be same for all tests
|
2372
|
-
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
|
2373
|
-
return usedFieldsCache;
|
2374
|
-
}
|
2375
|
-
const usedFields = Object.create(null);
|
2376
|
-
const typeInfo = new TypeInfo(schema);
|
2377
|
-
const visitor = visitWithTypeInfo(typeInfo, {
|
2378
|
-
Field(node) {
|
2379
|
-
var _a;
|
2380
|
-
const fieldDef = typeInfo.getFieldDef();
|
2381
|
-
if (!fieldDef) {
|
2382
|
-
// skip visiting this node if field is not defined in schema
|
2383
|
-
return false;
|
2384
|
-
}
|
2385
|
-
const parentTypeName = typeInfo.getParentType().name;
|
2386
|
-
const fieldName = node.name.value;
|
2387
|
-
(_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
|
2388
|
-
usedFields[parentTypeName].add(fieldName);
|
2389
|
-
},
|
2390
|
-
});
|
2391
|
-
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
|
2392
|
-
for (const { document } of allDocuments) {
|
2393
|
-
visit(document, visitor);
|
2394
|
-
}
|
2395
|
-
usedFieldsCache = usedFields;
|
2396
|
-
return usedFieldsCache;
|
2397
|
-
}
|
2398
|
-
const rule$e = {
|
2399
|
-
meta: {
|
2400
|
-
messages: {
|
2401
|
-
[RULE_ID$6]: 'Field "{{fieldName}}" is unused',
|
2402
|
-
},
|
2403
|
-
docs: {
|
2404
|
-
description: 'Requires all fields to be used at some level by siblings operations.',
|
2405
|
-
category: 'Schema',
|
2406
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$6}.md`,
|
2407
|
-
requiresSiblings: true,
|
2408
|
-
requiresSchema: true,
|
2409
|
-
isDisabledForAllConfig: true,
|
2410
|
-
examples: [
|
2411
|
-
{
|
2412
|
-
title: 'Incorrect',
|
2413
|
-
code: /* GraphQL */ `
|
2414
|
-
type User {
|
2415
|
-
id: ID!
|
2416
|
-
name: String
|
2417
|
-
someUnusedField: String
|
2418
|
-
}
|
2419
|
-
|
2420
|
-
type Query {
|
2421
|
-
me: User
|
2422
|
-
}
|
2423
|
-
|
2424
|
-
query {
|
2425
|
-
me {
|
2426
|
-
id
|
2427
|
-
name
|
2428
|
-
}
|
2429
|
-
}
|
2430
|
-
`,
|
2431
|
-
},
|
2432
|
-
{
|
2433
|
-
title: 'Correct',
|
2434
|
-
code: /* GraphQL */ `
|
2435
|
-
type User {
|
2436
|
-
id: ID!
|
2437
|
-
name: String
|
2438
|
-
}
|
2439
|
-
|
2440
|
-
type Query {
|
2441
|
-
me: User
|
2442
|
-
}
|
2443
|
-
|
2444
|
-
query {
|
2445
|
-
me {
|
2446
|
-
id
|
2447
|
-
name
|
2448
|
-
}
|
2449
|
-
}
|
2450
|
-
`,
|
2451
|
-
},
|
2452
|
-
],
|
2453
|
-
},
|
2454
|
-
type: 'suggestion',
|
2455
|
-
schema: [],
|
2456
|
-
hasSuggestions: true,
|
2457
|
-
},
|
2458
|
-
create(context) {
|
2459
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$6, context);
|
2460
|
-
const siblingsOperations = requireSiblingsOperations(RULE_ID$6, context);
|
2461
|
-
const usedFields = getUsedFields(schema, siblingsOperations);
|
2462
|
-
return {
|
2463
|
-
FieldDefinition(node) {
|
2464
|
-
var _a;
|
2465
|
-
const fieldName = node.name.value;
|
2466
|
-
const parentTypeName = node.parent.name.value;
|
2467
|
-
const isUsed = (_a = usedFields[parentTypeName]) === null || _a === void 0 ? void 0 : _a.has(fieldName);
|
2468
|
-
if (isUsed) {
|
2469
|
-
return;
|
2470
|
-
}
|
2471
|
-
context.report({
|
2472
|
-
node: node.name,
|
2473
|
-
messageId: RULE_ID$6,
|
2474
|
-
data: { fieldName },
|
2475
|
-
suggest: [
|
2476
|
-
{
|
2477
|
-
desc: `Remove \`${fieldName}\` field`,
|
2478
|
-
fix(fixer) {
|
2479
|
-
const sourceCode = context.getSourceCode();
|
2480
|
-
const tokenBefore = sourceCode.getTokenBefore(node);
|
2481
|
-
const tokenAfter = sourceCode.getTokenAfter(node);
|
2482
|
-
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
|
2483
|
-
return fixer.remove((isEmptyType ? node.parent : node));
|
2484
|
-
},
|
2485
|
-
},
|
2486
|
-
],
|
2487
|
-
});
|
2488
|
-
},
|
2489
|
-
};
|
2490
|
-
},
|
2491
|
-
};
|
2492
|
-
|
2493
|
-
const RULE_ID$7 = 'relay-arguments';
|
2494
|
-
const MISSING_ARGUMENTS = 'MISSING_ARGUMENTS';
|
2495
|
-
const rule$f = {
|
2496
|
-
meta: {
|
2497
|
-
type: 'problem',
|
2498
|
-
docs: {
|
2499
|
-
category: 'Schema',
|
2500
|
-
description: [
|
2501
|
-
'Set of rules to follow Relay specification for Arguments.',
|
2502
|
-
'',
|
2503
|
-
'- A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both',
|
2504
|
-
'',
|
2505
|
-
'Forward pagination arguments',
|
2506
|
-
'',
|
2507
|
-
'- `first` takes a non-negative integer',
|
2508
|
-
'- `after` takes the Cursor type',
|
2509
|
-
'',
|
2510
|
-
'Backward pagination arguments',
|
2511
|
-
'',
|
2512
|
-
'- `last` takes a non-negative integer',
|
2513
|
-
'- `before` takes the Cursor type',
|
2514
|
-
].join('\n'),
|
2515
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$7}.md`,
|
2516
|
-
examples: [
|
2517
|
-
{
|
2518
|
-
title: 'Incorrect',
|
2519
|
-
code: /* GraphQL */ `
|
2520
|
-
type User {
|
2521
|
-
posts: PostConnection
|
2522
|
-
}
|
2523
|
-
`,
|
2524
|
-
},
|
2525
|
-
{
|
2526
|
-
title: 'Correct',
|
2527
|
-
code: /* GraphQL */ `
|
2528
|
-
type User {
|
2529
|
-
posts(after: String, first: Int, before: String, last: Int): PostConnection
|
2530
|
-
}
|
2531
|
-
`,
|
2532
|
-
},
|
2533
|
-
],
|
2534
|
-
isDisabledForAllConfig: true,
|
2535
|
-
},
|
2536
|
-
messages: {
|
2537
|
-
[MISSING_ARGUMENTS]: 'A field that returns a Connection type must include forward pagination arguments (`first` and `after`), backward pagination arguments (`last` and `before`), or both.',
|
2538
|
-
},
|
2539
|
-
schema: {
|
2540
|
-
type: 'array',
|
2541
|
-
maxItems: 1,
|
2542
|
-
items: {
|
2543
|
-
type: 'object',
|
2544
|
-
additionalProperties: false,
|
2545
|
-
minProperties: 1,
|
2546
|
-
properties: {
|
2547
|
-
includeBoth: {
|
2548
|
-
type: 'boolean',
|
2549
|
-
default: true,
|
2550
|
-
description: 'Enforce including both forward and backward pagination arguments',
|
2551
|
-
},
|
2552
|
-
},
|
2553
|
-
},
|
2554
|
-
},
|
2555
|
-
},
|
2556
|
-
create(context) {
|
2557
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$7, context);
|
2558
|
-
const { includeBoth = true } = context.options[0] || {};
|
2559
|
-
return {
|
2560
|
-
'FieldDefinition > .gqlType Name[value=/Connection$/]'(node) {
|
2561
|
-
let fieldNode = node.parent;
|
2562
|
-
while (fieldNode.kind !== Kind.FIELD_DEFINITION) {
|
2563
|
-
fieldNode = fieldNode.parent;
|
2564
|
-
}
|
2565
|
-
const args = Object.fromEntries(fieldNode.arguments.map(argument => [argument.name.value, argument]));
|
2566
|
-
const hasForwardPagination = Boolean(args.first && args.after);
|
2567
|
-
const hasBackwardPagination = Boolean(args.last && args.before);
|
2568
|
-
if (!hasForwardPagination && !hasBackwardPagination) {
|
2569
|
-
context.report({
|
2570
|
-
node: fieldNode.name,
|
2571
|
-
messageId: MISSING_ARGUMENTS,
|
2572
|
-
});
|
2573
|
-
return;
|
2574
|
-
}
|
2575
|
-
function checkField(typeName, argumentName) {
|
2576
|
-
const argument = args[argumentName];
|
2577
|
-
const hasArgument = Boolean(argument);
|
2578
|
-
let type = argument;
|
2579
|
-
if (hasArgument && type.gqlType.kind === Kind.NON_NULL_TYPE) {
|
2580
|
-
type = type.gqlType;
|
2581
|
-
}
|
2582
|
-
const isAllowedNonNullType = hasArgument &&
|
2583
|
-
type.gqlType.kind === Kind.NAMED_TYPE &&
|
2584
|
-
(type.gqlType.name.value === typeName ||
|
2585
|
-
(typeName === 'String' && isScalarType(schema.getType(type.gqlType.name.value))));
|
2586
|
-
if (!isAllowedNonNullType) {
|
2587
|
-
const returnType = typeName === 'String' ? 'String or Scalar' : typeName;
|
2588
|
-
context.report({
|
2589
|
-
node: (argument || fieldNode).name,
|
2590
|
-
message: hasArgument
|
2591
|
-
? `Argument \`${argumentName}\` must return ${returnType}.`
|
2592
|
-
: `Field \`${fieldNode.name.value}\` must contain an argument \`${argumentName}\`, that return ${returnType}.`,
|
2593
|
-
});
|
2594
|
-
}
|
2595
|
-
}
|
2596
|
-
if (includeBoth || args.first || args.after) {
|
2597
|
-
checkField('Int', 'first');
|
2598
|
-
checkField('String', 'after');
|
2599
|
-
}
|
2600
|
-
if (includeBoth || args.last || args.before) {
|
2601
|
-
checkField('Int', 'last');
|
2602
|
-
checkField('String', 'before');
|
2603
|
-
}
|
2604
|
-
},
|
2605
|
-
};
|
2606
|
-
},
|
2607
|
-
};
|
2608
|
-
|
2609
|
-
const MUST_BE_OBJECT_TYPE = 'MUST_BE_OBJECT_TYPE';
|
2610
|
-
const MUST_CONTAIN_FIELD_EDGES = 'MUST_CONTAIN_FIELD_EDGES';
|
2611
|
-
const MUST_CONTAIN_FIELD_PAGE_INFO = 'MUST_CONTAIN_FIELD_PAGE_INFO';
|
2612
|
-
const MUST_HAVE_CONNECTION_SUFFIX = 'MUST_HAVE_CONNECTION_SUFFIX';
|
2613
|
-
const EDGES_FIELD_MUST_RETURN_LIST_TYPE = 'EDGES_FIELD_MUST_RETURN_LIST_TYPE';
|
2614
|
-
const PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE = 'PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE';
|
2615
|
-
const NON_OBJECT_TYPES = [
|
2616
|
-
Kind.SCALAR_TYPE_DEFINITION,
|
2617
|
-
Kind.UNION_TYPE_DEFINITION,
|
2618
|
-
Kind.UNION_TYPE_EXTENSION,
|
2619
|
-
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
2620
|
-
Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
2621
|
-
Kind.ENUM_TYPE_DEFINITION,
|
2622
|
-
Kind.ENUM_TYPE_EXTENSION,
|
2623
|
-
Kind.INTERFACE_TYPE_DEFINITION,
|
2624
|
-
Kind.INTERFACE_TYPE_EXTENSION,
|
2625
|
-
];
|
2626
|
-
const notConnectionTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=/Connection$/] > .name`;
|
2627
|
-
const hasEdgesField = (node) => node.fields.some(field => field.name.value === 'edges');
|
2628
|
-
const hasPageInfoField = (node) => node.fields.some(field => field.name.value === 'pageInfo');
|
2629
|
-
const rule$g = {
|
2630
|
-
meta: {
|
2631
|
-
type: 'problem',
|
2632
|
-
docs: {
|
2633
|
-
category: 'Schema',
|
2634
|
-
description: [
|
2635
|
-
'Set of rules to follow Relay specification for Connection types.',
|
2636
|
-
'',
|
2637
|
-
'- Any type whose name ends in "Connection" is considered by spec to be a `Connection type`',
|
2638
|
-
'- Connection type must be an Object type',
|
2639
|
-
'- Connection type must contain a field `edges` that return a list type that wraps an edge type',
|
2640
|
-
'- Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type',
|
2641
|
-
].join('\n'),
|
2642
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/relay-connection-types.md',
|
2643
|
-
isDisabledForAllConfig: true,
|
2644
|
-
examples: [
|
2645
|
-
{
|
2646
|
-
title: 'Incorrect',
|
2647
|
-
code: /* GraphQL */ `
|
2648
|
-
type UserPayload { # should be an Object type with \`Connection\` suffix
|
2649
|
-
edges: UserEdge! # should return a list type
|
2650
|
-
pageInfo: PageInfo # should return a non-null \`PageInfo\` Object type
|
2651
|
-
}
|
2652
|
-
`,
|
2653
|
-
},
|
2654
|
-
{
|
2655
|
-
title: 'Correct',
|
2656
|
-
code: /* GraphQL */ `
|
2657
|
-
type UserConnection {
|
2658
|
-
edges: [UserEdge]
|
2659
|
-
pageInfo: PageInfo!
|
2660
|
-
}
|
2661
|
-
`,
|
2662
|
-
},
|
2663
|
-
],
|
2664
|
-
},
|
2665
|
-
messages: {
|
2666
|
-
// Connection types
|
2667
|
-
[MUST_BE_OBJECT_TYPE]: 'Connection type must be an Object type.',
|
2668
|
-
[MUST_HAVE_CONNECTION_SUFFIX]: 'Connection type must have `Connection` suffix.',
|
2669
|
-
[MUST_CONTAIN_FIELD_EDGES]: 'Connection type must contain a field `edges` that return a list type.',
|
2670
|
-
[MUST_CONTAIN_FIELD_PAGE_INFO]: 'Connection type must contain a field `pageInfo` that return a non-null `PageInfo` Object type.',
|
2671
|
-
[EDGES_FIELD_MUST_RETURN_LIST_TYPE]: '`edges` field must return a list type.',
|
2672
|
-
[PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE]: '`pageInfo` field must return a non-null `PageInfo` Object type.',
|
2673
|
-
},
|
2674
|
-
schema: [],
|
2675
|
-
},
|
2676
|
-
create(context) {
|
2677
|
-
return {
|
2678
|
-
[notConnectionTypesSelector](node) {
|
2679
|
-
context.report({ node, messageId: MUST_BE_OBJECT_TYPE });
|
2680
|
-
},
|
2681
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value!=/Connection$/]'(node) {
|
2682
|
-
if (hasEdgesField(node) && hasPageInfoField(node)) {
|
2683
|
-
context.report({ node: node.name, messageId: MUST_HAVE_CONNECTION_SUFFIX });
|
2684
|
-
}
|
2685
|
-
},
|
2686
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/]'(node) {
|
2687
|
-
if (!hasEdgesField(node)) {
|
2688
|
-
context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_EDGES });
|
2689
|
-
}
|
2690
|
-
if (!hasPageInfoField(node)) {
|
2691
|
-
context.report({ node: node.name, messageId: MUST_CONTAIN_FIELD_PAGE_INFO });
|
2692
|
-
}
|
2693
|
-
},
|
2694
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType'(node) {
|
2695
|
-
const isListType = node.kind === Kind.LIST_TYPE || (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE);
|
2696
|
-
if (!isListType) {
|
2697
|
-
context.report({ node, messageId: EDGES_FIELD_MUST_RETURN_LIST_TYPE });
|
2698
|
-
}
|
2699
|
-
},
|
2700
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=pageInfo] > .gqlType'(node) {
|
2701
|
-
const isNonNullPageInfoType = node.kind === Kind.NON_NULL_TYPE &&
|
2702
|
-
node.gqlType.kind === Kind.NAMED_TYPE &&
|
2703
|
-
node.gqlType.name.value === 'PageInfo';
|
2704
|
-
if (!isNonNullPageInfoType) {
|
2705
|
-
context.report({ node, messageId: PAGE_INFO_FIELD_MUST_RETURN_NON_NULL_TYPE });
|
2706
|
-
}
|
2707
|
-
},
|
2708
|
-
};
|
2709
|
-
},
|
2710
|
-
};
|
2711
|
-
|
2712
|
-
const RULE_ID$8 = 'relay-edge-types';
|
2713
|
-
const MESSAGE_MUST_BE_OBJECT_TYPE = 'MESSAGE_MUST_BE_OBJECT_TYPE';
|
2714
|
-
const MESSAGE_MISSING_EDGE_SUFFIX = 'MESSAGE_MISSING_EDGE_SUFFIX';
|
2715
|
-
const MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE = 'MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE';
|
2716
|
-
const MESSAGE_SHOULD_IMPLEMENTS_NODE = 'MESSAGE_SHOULD_IMPLEMENTS_NODE';
|
2717
|
-
let edgeTypesCache;
|
2718
|
-
function getEdgeTypes(schema) {
|
2719
|
-
// We don't want cache edgeTypes on test environment
|
2720
|
-
// Otherwise edgeTypes will be same for all tests
|
2721
|
-
if (process.env.NODE_ENV !== 'test' && edgeTypesCache) {
|
2722
|
-
return edgeTypesCache;
|
2723
|
-
}
|
2724
|
-
const edgeTypes = new Set();
|
2725
|
-
const visitor = {
|
2726
|
-
ObjectTypeDefinition(node) {
|
2727
|
-
const typeName = node.name.value;
|
2728
|
-
const hasConnectionSuffix = typeName.endsWith('Connection');
|
2729
|
-
if (!hasConnectionSuffix) {
|
2730
|
-
return;
|
2731
|
-
}
|
2732
|
-
const edges = node.fields.find(field => field.name.value === 'edges');
|
2733
|
-
if (edges) {
|
2734
|
-
const edgesTypeName = getTypeName(edges);
|
2735
|
-
const edgesType = schema.getType(edgesTypeName);
|
2736
|
-
if (isObjectType$1(edgesType)) {
|
2737
|
-
edgeTypes.add(edgesTypeName);
|
2738
|
-
}
|
2739
|
-
}
|
2740
|
-
},
|
2741
|
-
};
|
2742
|
-
const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
|
2743
|
-
visit(astNode, visitor);
|
2744
|
-
edgeTypesCache = edgeTypes;
|
2745
|
-
return edgeTypesCache;
|
2746
|
-
}
|
2747
|
-
const rule$h = {
|
2748
|
-
meta: {
|
2749
|
-
type: 'problem',
|
2750
|
-
docs: {
|
2751
|
-
category: 'Schema',
|
2752
|
-
description: [
|
2753
|
-
'Set of rules to follow Relay specification for Edge types.',
|
2754
|
-
'',
|
2755
|
-
"- A type that is returned in list form by a connection type's `edges` field is considered by this spec to be an Edge type",
|
2756
|
-
'- Edge type must be an Object type',
|
2757
|
-
'- Edge type must contain a field `node` that return either Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types. Notably, this field cannot return a list',
|
2758
|
-
'- Edge type must contain a field `cursor` that return either String, Scalar, or a non-null wrapper around one of those types',
|
2759
|
-
'- Edge type name must end in "Edge" _(optional)_',
|
2760
|
-
"- Edge type's field `node` must implement `Node` interface _(optional)_",
|
2761
|
-
'- A list type should only wrap an edge type _(optional)_',
|
2762
|
-
].join('\n'),
|
2763
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$8}.md`,
|
2764
|
-
isDisabledForAllConfig: true,
|
2765
|
-
requiresSchema: true,
|
2766
|
-
examples: [
|
2767
|
-
{
|
2768
|
-
title: 'Correct',
|
2769
|
-
code: /* GraphQL */ `
|
2770
|
-
type UserConnection {
|
2771
|
-
edges: [UserEdge]
|
2772
|
-
pageInfo: PageInfo!
|
2773
|
-
}
|
2774
|
-
`,
|
2775
|
-
},
|
2776
|
-
],
|
2777
|
-
},
|
2778
|
-
messages: {
|
2779
|
-
[MESSAGE_MUST_BE_OBJECT_TYPE]: 'Edge type must be an Object type.',
|
2780
|
-
[MESSAGE_MISSING_EDGE_SUFFIX]: 'Edge type must have "Edge" suffix.',
|
2781
|
-
[MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE]: 'A list type should only wrap an edge type.',
|
2782
|
-
[MESSAGE_SHOULD_IMPLEMENTS_NODE]: "Edge type's field `node` must implement `Node` interface.",
|
2783
|
-
},
|
2784
|
-
schema: {
|
2785
|
-
type: 'array',
|
2786
|
-
maxItems: 1,
|
2787
|
-
items: {
|
2788
|
-
type: 'object',
|
2789
|
-
additionalProperties: false,
|
2790
|
-
minProperties: 1,
|
2791
|
-
properties: {
|
2792
|
-
withEdgeSuffix: {
|
2793
|
-
type: 'boolean',
|
2794
|
-
default: true,
|
2795
|
-
description: 'Edge type name must end in "Edge".',
|
2796
|
-
},
|
2797
|
-
shouldImplementNode: {
|
2798
|
-
type: 'boolean',
|
2799
|
-
default: true,
|
2800
|
-
description: "Edge type's field `node` must implement `Node` interface.",
|
2801
|
-
},
|
2802
|
-
listTypeCanWrapOnlyEdgeType: {
|
2803
|
-
type: 'boolean',
|
2804
|
-
default: true,
|
2805
|
-
description: 'A list type should only wrap an edge type.',
|
2806
|
-
},
|
2807
|
-
},
|
2808
|
-
},
|
2809
|
-
},
|
2810
|
-
},
|
2811
|
-
create(context) {
|
2812
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$8, context);
|
2813
|
-
const edgeTypes = getEdgeTypes(schema);
|
2814
|
-
const options = {
|
2815
|
-
withEdgeSuffix: true,
|
2816
|
-
shouldImplementNode: true,
|
2817
|
-
listTypeCanWrapOnlyEdgeType: true,
|
2818
|
-
...context.options[0],
|
2819
|
-
};
|
2820
|
-
const isNamedOrNonNullNamed = (node) => node.kind === Kind.NAMED_TYPE || (node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.NAMED_TYPE);
|
2821
|
-
const checkNodeField = (node) => {
|
2822
|
-
const nodeField = node.fields.find(field => field.name.value === 'node');
|
2823
|
-
const message = 'return either a Scalar, Enum, Object, Interface, Union, or a non-null wrapper around one of those types.';
|
2824
|
-
if (!nodeField) {
|
2825
|
-
context.report({
|
2826
|
-
node: node.name,
|
2827
|
-
message: `Edge type must contain a field \`node\` that ${message}`,
|
2828
|
-
});
|
2829
|
-
}
|
2830
|
-
else if (!isNamedOrNonNullNamed(nodeField.gqlType)) {
|
2831
|
-
context.report({ node: nodeField.name, message: `Field \`node\` must ${message}` });
|
2832
|
-
}
|
2833
|
-
else if (options.shouldImplementNode) {
|
2834
|
-
const nodeReturnTypeName = getTypeName(nodeField.gqlType.rawNode());
|
2835
|
-
const type = schema.getType(nodeReturnTypeName);
|
2836
|
-
if (!isObjectType$1(type)) {
|
2837
|
-
return;
|
2838
|
-
}
|
2839
|
-
const implementsNode = type.astNode.interfaces.some(n => n.name.value === 'Node');
|
2840
|
-
if (!implementsNode) {
|
2841
|
-
context.report({ node: node.name, messageId: MESSAGE_SHOULD_IMPLEMENTS_NODE });
|
2842
|
-
}
|
2843
|
-
}
|
2844
|
-
};
|
2845
|
-
const checkCursorField = (node) => {
|
2846
|
-
const cursorField = node.fields.find(field => field.name.value === 'cursor');
|
2847
|
-
const message = 'return either a String, Scalar, or a non-null wrapper wrapper around one of those types.';
|
2848
|
-
if (!cursorField) {
|
2849
|
-
context.report({
|
2850
|
-
node: node.name,
|
2851
|
-
message: `Edge type must contain a field \`cursor\` that ${message}`,
|
2852
|
-
});
|
2853
|
-
return;
|
2854
|
-
}
|
2855
|
-
const typeName = getTypeName(cursorField.rawNode());
|
2856
|
-
if (!isNamedOrNonNullNamed(cursorField.gqlType) ||
|
2857
|
-
(typeName !== 'String' && !isScalarType(schema.getType(typeName)))) {
|
2858
|
-
context.report({ node: cursorField.name, message: `Field \`cursor\` must ${message}` });
|
2859
|
-
}
|
2860
|
-
};
|
2861
|
-
const listeners = {
|
2862
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/Connection$/] > FieldDefinition[name.value=edges] > .gqlType Name'(node) {
|
2863
|
-
const type = schema.getType(node.value);
|
2864
|
-
if (!isObjectType$1(type)) {
|
2865
|
-
context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE });
|
2866
|
-
}
|
2867
|
-
},
|
2868
|
-
':matches(ObjectTypeDefinition, ObjectTypeExtension)'(node) {
|
2869
|
-
const typeName = node.name.value;
|
2870
|
-
if (edgeTypes.has(typeName)) {
|
2871
|
-
checkNodeField(node);
|
2872
|
-
checkCursorField(node);
|
2873
|
-
if (options.withEdgeSuffix && !typeName.endsWith('Edge')) {
|
2874
|
-
context.report({ node: node.name, messageId: MESSAGE_MISSING_EDGE_SUFFIX });
|
2875
|
-
}
|
2876
|
-
}
|
2877
|
-
},
|
2878
|
-
};
|
2879
|
-
if (options.listTypeCanWrapOnlyEdgeType) {
|
2880
|
-
listeners['FieldDefinition > .gqlType'] = (node) => {
|
2881
|
-
if (node.kind === Kind.LIST_TYPE ||
|
2882
|
-
(node.kind === Kind.NON_NULL_TYPE && node.gqlType.kind === Kind.LIST_TYPE)) {
|
2883
|
-
const typeName = getTypeName(node.rawNode());
|
2884
|
-
if (!edgeTypes.has(typeName)) {
|
2885
|
-
context.report({ node, messageId: MESSAGE_LIST_TYPE_ONLY_EDGE_TYPE });
|
2886
|
-
}
|
2887
|
-
}
|
2888
|
-
};
|
2889
|
-
}
|
2890
|
-
return listeners;
|
2891
|
-
},
|
2892
|
-
};
|
2893
|
-
|
2894
|
-
const RULE_ID$9 = 'relay-page-info';
|
2895
|
-
const MESSAGE_MUST_EXIST = 'MESSAGE_MUST_EXIST';
|
2896
|
-
const MESSAGE_MUST_BE_OBJECT_TYPE$1 = 'MESSAGE_MUST_BE_OBJECT_TYPE';
|
2897
|
-
const notPageInfoTypesSelector = `:matches(${NON_OBJECT_TYPES})[name.value=PageInfo] > .name`;
|
2898
|
-
let hasPageInfoChecked = false;
|
2899
|
-
const rule$i = {
|
2900
|
-
meta: {
|
2901
|
-
type: 'problem',
|
2902
|
-
docs: {
|
2903
|
-
category: 'Schema',
|
2904
|
-
description: [
|
2905
|
-
'Set of rules to follow Relay specification for `PageInfo` object.',
|
2906
|
-
'',
|
2907
|
-
'- `PageInfo` must be an Object type',
|
2908
|
-
'- `PageInfo` must contain fields `hasPreviousPage` and `hasNextPage`, that return non-null Boolean',
|
2909
|
-
'- `PageInfo` must contain fields `startCursor` and `endCursor`, that return either String or Scalar, which can be null if there are no results',
|
2910
|
-
].join('\n'),
|
2911
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$9}.md`,
|
2912
|
-
examples: [
|
2913
|
-
{
|
2914
|
-
title: 'Correct',
|
2915
|
-
code: /* GraphQL */ `
|
2916
|
-
type PageInfo {
|
2917
|
-
hasPreviousPage: Boolean!
|
2918
|
-
hasNextPage: Boolean!
|
2919
|
-
startCursor: String
|
2920
|
-
endCursor: String
|
2921
|
-
}
|
2922
|
-
`,
|
2923
|
-
},
|
2924
|
-
],
|
2925
|
-
isDisabledForAllConfig: true,
|
2926
|
-
requiresSchema: true,
|
2927
|
-
},
|
2928
|
-
messages: {
|
2929
|
-
[MESSAGE_MUST_EXIST]: 'The server must provide a `PageInfo` object.',
|
2930
|
-
[MESSAGE_MUST_BE_OBJECT_TYPE$1]: '`PageInfo` must be an Object type.',
|
2931
|
-
},
|
2932
|
-
schema: [],
|
2933
|
-
},
|
2934
|
-
create(context) {
|
2935
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$9, context);
|
2936
|
-
if (process.env.NODE_ENV === 'test' || !hasPageInfoChecked) {
|
2937
|
-
const pageInfoType = schema.getType('PageInfo');
|
2938
|
-
if (!pageInfoType) {
|
2939
|
-
context.report({
|
2940
|
-
loc: REPORT_ON_FIRST_CHARACTER,
|
2941
|
-
messageId: MESSAGE_MUST_EXIST,
|
2942
|
-
});
|
2943
|
-
}
|
2944
|
-
hasPageInfoChecked = true;
|
2945
|
-
}
|
2946
|
-
return {
|
2947
|
-
[notPageInfoTypesSelector](node) {
|
2948
|
-
context.report({ node, messageId: MESSAGE_MUST_BE_OBJECT_TYPE$1 });
|
2949
|
-
},
|
2950
|
-
'ObjectTypeDefinition[name.value=PageInfo]'(node) {
|
2951
|
-
const fieldMap = Object.fromEntries(node.fields.map(field => [field.name.value, field]));
|
2952
|
-
const checkField = (fieldName, typeName) => {
|
2953
|
-
const field = fieldMap[fieldName];
|
2954
|
-
let isAllowedType = false;
|
2955
|
-
if (field) {
|
2956
|
-
const type = field.gqlType;
|
2957
|
-
if (typeName === 'Boolean') {
|
2958
|
-
isAllowedType =
|
2959
|
-
type.kind === Kind.NON_NULL_TYPE &&
|
2960
|
-
type.gqlType.kind === Kind.NAMED_TYPE &&
|
2961
|
-
type.gqlType.name.value === 'Boolean';
|
2962
|
-
}
|
2963
|
-
else if (type.kind === Kind.NAMED_TYPE) {
|
2964
|
-
isAllowedType = type.name.value === 'String' || isScalarType(schema.getType(type.name.value));
|
2965
|
-
}
|
2966
|
-
}
|
2967
|
-
if (!isAllowedType) {
|
2968
|
-
const returnType = typeName === 'Boolean'
|
2969
|
-
? 'non-null Boolean'
|
2970
|
-
: 'either String or Scalar, which can be null if there are no results';
|
2971
|
-
context.report({
|
2972
|
-
node: field ? field.name : node.name,
|
2973
|
-
message: field
|
2974
|
-
? `Field \`${fieldName}\` must return ${returnType}.`
|
2975
|
-
: `\`PageInfo\` must contain a field \`${fieldName}\`, that return ${returnType}.`,
|
2976
|
-
});
|
2977
|
-
}
|
2978
|
-
};
|
2979
|
-
checkField('hasPreviousPage', 'Boolean');
|
2980
|
-
checkField('hasNextPage', 'Boolean');
|
2981
|
-
checkField('startCursor', 'String');
|
2982
|
-
checkField('endCursor', 'String');
|
2983
|
-
},
|
2984
|
-
};
|
2985
|
-
},
|
2986
|
-
};
|
2987
|
-
|
2988
|
-
const valueFromNode = (...args) => {
|
2989
|
-
return valueFromASTUntyped(...args);
|
2990
|
-
};
|
2991
|
-
function getBaseType(type) {
|
2992
|
-
if (isNonNullType(type) || isListType(type)) {
|
2993
|
-
return getBaseType(type.ofType);
|
2994
|
-
}
|
2995
|
-
return type;
|
2996
|
-
}
|
2997
|
-
function convertToken(token, type) {
|
2998
|
-
const { line, column, end, start, value } = token;
|
2999
|
-
return {
|
3000
|
-
type,
|
3001
|
-
value,
|
3002
|
-
/*
|
3003
|
-
* ESLint has 0-based column number
|
3004
|
-
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
3005
|
-
*/
|
3006
|
-
loc: {
|
3007
|
-
start: {
|
3008
|
-
line,
|
3009
|
-
column: column - 1,
|
3010
|
-
},
|
3011
|
-
end: {
|
3012
|
-
line,
|
3013
|
-
column: column - 1 + (end - start),
|
3014
|
-
},
|
3015
|
-
},
|
3016
|
-
range: [start, end],
|
3017
|
-
};
|
3018
|
-
}
|
3019
|
-
function getLexer(source) {
|
3020
|
-
// GraphQL v14
|
3021
|
-
const gqlLanguage = require('graphql/language');
|
3022
|
-
if (gqlLanguage && gqlLanguage.createLexer) {
|
3023
|
-
return gqlLanguage.createLexer(source, {});
|
3024
|
-
}
|
3025
|
-
// GraphQL v15
|
3026
|
-
const { Lexer: LexerCls } = require('graphql');
|
3027
|
-
if (LexerCls && typeof LexerCls === 'function') {
|
3028
|
-
return new LexerCls(source);
|
3029
|
-
}
|
3030
|
-
throw new Error('Unsupported GraphQL version! Please make sure to use GraphQL v14 or newer!');
|
3031
|
-
}
|
3032
|
-
function extractTokens(filePath, code) {
|
3033
|
-
const source = new Source(code, filePath);
|
3034
|
-
const lexer = getLexer(source);
|
3035
|
-
const tokens = [];
|
3036
|
-
let token = lexer.advance();
|
3037
|
-
while (token && token.kind !== TokenKind.EOF) {
|
3038
|
-
const result = convertToken(token, token.kind);
|
3039
|
-
tokens.push(result);
|
3040
|
-
token = lexer.advance();
|
3041
|
-
}
|
3042
|
-
return tokens;
|
3043
|
-
}
|
3044
|
-
function extractComments(loc) {
|
3045
|
-
if (!loc) {
|
3046
|
-
return [];
|
3047
|
-
}
|
3048
|
-
const comments = [];
|
3049
|
-
let token = loc.startToken;
|
3050
|
-
while (token) {
|
3051
|
-
if (token.kind === TokenKind.COMMENT) {
|
3052
|
-
const comment = convertToken(token,
|
3053
|
-
// `eslint-disable` directive works only with `Block` type comment
|
3054
|
-
token.value.trimStart().startsWith('eslint') ? 'Block' : 'Line');
|
3055
|
-
comments.push(comment);
|
3056
|
-
}
|
3057
|
-
token = token.next;
|
3058
|
-
}
|
3059
|
-
return comments;
|
3060
|
-
}
|
3061
|
-
function convertLocation(location) {
|
3062
|
-
const { startToken, endToken, source, start, end } = location;
|
3063
|
-
/*
|
3064
|
-
* ESLint has 0-based column number
|
3065
|
-
* https://eslint.org/docs/developer-guide/working-with-rules#contextreport
|
3066
|
-
*/
|
3067
|
-
const loc = {
|
3068
|
-
start: {
|
3069
|
-
/*
|
3070
|
-
* Kind.Document has startToken: { line: 0, column: 0 }, we set line as 1 and column as 0
|
3071
|
-
*/
|
3072
|
-
line: startToken.line === 0 ? 1 : startToken.line,
|
3073
|
-
column: startToken.column === 0 ? 0 : startToken.column - 1,
|
3074
|
-
},
|
3075
|
-
end: {
|
3076
|
-
line: endToken.line,
|
3077
|
-
column: endToken.column - 1,
|
3078
|
-
},
|
3079
|
-
source: source.body,
|
3080
|
-
};
|
3081
|
-
if (loc.start.column === loc.end.column) {
|
3082
|
-
loc.end.column += end - start;
|
3083
|
-
}
|
3084
|
-
return loc;
|
3085
|
-
}
|
3086
|
-
|
3087
|
-
function convertToESTree(node, schema) {
|
3088
|
-
const typeInfo = schema ? new TypeInfo(schema) : null;
|
3089
|
-
const visitor = {
|
3090
|
-
leave(node, key, parent) {
|
3091
|
-
const leadingComments = 'description' in node && node.description
|
3092
|
-
? [
|
3093
|
-
{
|
3094
|
-
type: node.description.block ? 'Block' : 'Line',
|
3095
|
-
value: node.description.value,
|
3096
|
-
},
|
3097
|
-
]
|
3098
|
-
: [];
|
3099
|
-
const calculatedTypeInfo = typeInfo
|
3100
|
-
? {
|
3101
|
-
argument: typeInfo.getArgument(),
|
3102
|
-
defaultValue: typeInfo.getDefaultValue(),
|
3103
|
-
directive: typeInfo.getDirective(),
|
3104
|
-
enumValue: typeInfo.getEnumValue(),
|
3105
|
-
fieldDef: typeInfo.getFieldDef(),
|
3106
|
-
inputType: typeInfo.getInputType(),
|
3107
|
-
parentInputType: typeInfo.getParentInputType(),
|
3108
|
-
parentType: typeInfo.getParentType(),
|
3109
|
-
gqlType: typeInfo.getType(),
|
3110
|
-
}
|
3111
|
-
: {};
|
3112
|
-
const rawNode = () => {
|
3113
|
-
if (parent && key !== undefined) {
|
3114
|
-
return parent[key];
|
3115
|
-
}
|
3116
|
-
return node.kind === Kind.DOCUMENT
|
3117
|
-
? {
|
3118
|
-
...node,
|
3119
|
-
definitions: node.definitions.map(definition => definition.rawNode()),
|
3120
|
-
}
|
3121
|
-
: node;
|
3122
|
-
};
|
3123
|
-
const commonFields = {
|
3124
|
-
...node,
|
3125
|
-
type: node.kind,
|
3126
|
-
loc: convertLocation(node.loc),
|
3127
|
-
range: [node.loc.start, node.loc.end],
|
3128
|
-
leadingComments,
|
3129
|
-
// Use function to prevent RangeError: Maximum call stack size exceeded
|
3130
|
-
typeInfo: () => calculatedTypeInfo,
|
3131
|
-
rawNode,
|
3132
|
-
};
|
3133
|
-
return 'type' in node
|
3134
|
-
? {
|
3135
|
-
...commonFields,
|
3136
|
-
gqlType: node.type,
|
3137
|
-
}
|
3138
|
-
: commonFields;
|
3139
|
-
},
|
3140
|
-
};
|
3141
|
-
return visit(node, typeInfo ? visitWithTypeInfo(typeInfo, visitor) : visitor);
|
3142
|
-
}
|
3143
|
-
|
3144
|
-
// eslint-disable-next-line unicorn/better-regex
|
3145
|
-
const DATE_REGEX = /^\d{2}\/\d{2}\/\d{4}$/;
|
3146
|
-
const MESSAGE_REQUIRE_DATE = 'MESSAGE_REQUIRE_DATE';
|
3147
|
-
const MESSAGE_INVALID_FORMAT = 'MESSAGE_INVALID_FORMAT';
|
3148
|
-
const MESSAGE_INVALID_DATE = 'MESSAGE_INVALID_DATE';
|
3149
|
-
const MESSAGE_CAN_BE_REMOVED = 'MESSAGE_CAN_BE_REMOVED';
|
3150
|
-
const rule$j = {
|
3151
|
-
meta: {
|
3152
|
-
type: 'suggestion',
|
3153
|
-
hasSuggestions: true,
|
3154
|
-
docs: {
|
3155
|
-
category: 'Schema',
|
3156
|
-
description: 'Require deletion date on `@deprecated` directive. Suggest removing deprecated things after deprecated date.',
|
3157
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/require-deprecation-date.md',
|
3158
|
-
examples: [
|
3159
|
-
{
|
3160
|
-
title: 'Incorrect',
|
3161
|
-
code: /* GraphQL */ `
|
3162
|
-
type User {
|
3163
|
-
firstname: String @deprecated
|
3164
|
-
firstName: String
|
3165
|
-
}
|
3166
|
-
`,
|
3167
|
-
},
|
3168
|
-
{
|
3169
|
-
title: 'Incorrect',
|
3170
|
-
code: /* GraphQL */ `
|
3171
|
-
type User {
|
3172
|
-
firstname: String @deprecated(reason: "Use 'firstName' instead")
|
3173
|
-
firstName: String
|
3174
|
-
}
|
3175
|
-
`,
|
3176
|
-
},
|
3177
|
-
{
|
3178
|
-
title: 'Correct',
|
3179
|
-
code: /* GraphQL */ `
|
3180
|
-
type User {
|
3181
|
-
firstname: String @deprecated(reason: "Use 'firstName' instead", deletionDate: "25/12/2022")
|
3182
|
-
firstName: String
|
3183
|
-
}
|
3184
|
-
`,
|
3185
|
-
},
|
3186
|
-
],
|
3187
|
-
},
|
3188
|
-
messages: {
|
3189
|
-
[MESSAGE_REQUIRE_DATE]: 'Directive "@deprecated" must have a deletion date',
|
3190
|
-
[MESSAGE_INVALID_FORMAT]: 'Deletion date must be in format "DD/MM/YYYY"',
|
3191
|
-
[MESSAGE_INVALID_DATE]: 'Invalid "{{ deletionDate }}" deletion date',
|
3192
|
-
[MESSAGE_CAN_BE_REMOVED]: '"{{ nodeName }}" сan be removed',
|
3193
|
-
},
|
3194
|
-
schema: [
|
3195
|
-
{
|
3196
|
-
type: 'object',
|
3197
|
-
additionalProperties: false,
|
3198
|
-
properties: {
|
3199
|
-
argumentName: {
|
3200
|
-
type: 'string',
|
3201
|
-
},
|
3202
|
-
},
|
3203
|
-
},
|
3204
|
-
],
|
3205
|
-
},
|
3206
|
-
create(context) {
|
3207
|
-
return {
|
3208
|
-
'Directive[name.value=deprecated]'(node) {
|
3209
|
-
var _a;
|
3210
|
-
const argName = ((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.argumentName) || 'deletionDate';
|
3211
|
-
const deletionDateNode = node.arguments.find(arg => arg.name.value === argName);
|
3212
|
-
if (!deletionDateNode) {
|
3213
|
-
context.report({
|
3214
|
-
node: node.name,
|
3215
|
-
messageId: MESSAGE_REQUIRE_DATE,
|
3216
|
-
});
|
3217
|
-
return;
|
3218
|
-
}
|
3219
|
-
const deletionDate = valueFromNode(deletionDateNode.value);
|
3220
|
-
const isValidDate = DATE_REGEX.test(deletionDate);
|
3221
|
-
if (!isValidDate) {
|
3222
|
-
context.report({ node: deletionDateNode.value, messageId: MESSAGE_INVALID_FORMAT });
|
3223
|
-
return;
|
3224
|
-
}
|
3225
|
-
let [day, month, year] = deletionDate.split('/');
|
3226
|
-
day = day.padStart(2, '0');
|
3227
|
-
month = month.padStart(2, '0');
|
3228
|
-
const deletionDateInMS = Date.parse(`${year}-${month}-${day}`);
|
3229
|
-
if (Number.isNaN(deletionDateInMS)) {
|
3230
|
-
context.report({
|
3231
|
-
node: deletionDateNode.value,
|
3232
|
-
messageId: MESSAGE_INVALID_DATE,
|
3233
|
-
data: {
|
3234
|
-
deletionDate,
|
3235
|
-
},
|
3236
|
-
});
|
3237
|
-
return;
|
3238
|
-
}
|
3239
|
-
const canRemove = Date.now() > deletionDateInMS;
|
3240
|
-
if (canRemove) {
|
3241
|
-
const { parent } = node;
|
3242
|
-
const nodeName = parent.name.value;
|
3243
|
-
context.report({
|
3244
|
-
node: parent.name,
|
3245
|
-
messageId: MESSAGE_CAN_BE_REMOVED,
|
3246
|
-
data: { nodeName },
|
3247
|
-
suggest: [
|
3248
|
-
{
|
3249
|
-
desc: `Remove \`${nodeName}\``,
|
3250
|
-
fix: fixer => fixer.remove(parent),
|
3251
|
-
},
|
3252
|
-
],
|
3253
|
-
});
|
3254
|
-
}
|
3255
|
-
},
|
3256
|
-
};
|
3257
|
-
},
|
3258
|
-
};
|
3259
|
-
|
3260
|
-
const rule$k = {
|
3261
|
-
meta: {
|
3262
|
-
docs: {
|
3263
|
-
description: 'Require all deprecation directives to specify a reason.',
|
3264
|
-
category: 'Schema',
|
3265
|
-
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/require-deprecation-reason.md',
|
3266
|
-
recommended: true,
|
3267
|
-
examples: [
|
3268
|
-
{
|
3269
|
-
title: 'Incorrect',
|
3270
|
-
code: /* GraphQL */ `
|
3271
|
-
type MyType {
|
3272
|
-
name: String @deprecated
|
3273
|
-
}
|
3274
|
-
`,
|
3275
|
-
},
|
3276
|
-
{
|
3277
|
-
title: 'Incorrect',
|
3278
|
-
code: /* GraphQL */ `
|
3279
|
-
type MyType {
|
3280
|
-
name: String @deprecated(reason: "")
|
3281
|
-
}
|
3282
|
-
`,
|
3283
|
-
},
|
3284
|
-
{
|
3285
|
-
title: 'Correct',
|
3286
|
-
code: /* GraphQL */ `
|
3287
|
-
type MyType {
|
3288
|
-
name: String @deprecated(reason: "no longer relevant, please use fullName field")
|
3289
|
-
}
|
3290
|
-
`,
|
3291
|
-
},
|
3292
|
-
],
|
3293
|
-
},
|
3294
|
-
type: 'suggestion',
|
3295
|
-
schema: [],
|
3296
|
-
},
|
3297
|
-
create(context) {
|
3298
|
-
return {
|
3299
|
-
'Directive[name.value=deprecated]'(node) {
|
3300
|
-
const reasonArgument = node.arguments.find(arg => arg.name.value === 'reason');
|
3301
|
-
const value = reasonArgument && String(valueFromNode(reasonArgument.value)).trim();
|
3302
|
-
if (!value) {
|
3303
|
-
context.report({
|
3304
|
-
node: node.name,
|
3305
|
-
message: 'Directive "@deprecated" must have a reason!',
|
3306
|
-
});
|
3307
|
-
}
|
3308
|
-
},
|
3309
|
-
};
|
3310
|
-
},
|
3311
|
-
};
|
3312
|
-
|
3313
|
-
const RULE_ID$a = 'require-description';
|
3314
|
-
const ALLOWED_KINDS$1 = [
|
3315
|
-
...TYPES_KINDS,
|
3316
|
-
Kind.DIRECTIVE_DEFINITION,
|
3317
|
-
Kind.FIELD_DEFINITION,
|
3318
|
-
Kind.INPUT_VALUE_DEFINITION,
|
3319
|
-
Kind.ENUM_VALUE_DEFINITION,
|
3320
|
-
Kind.OPERATION_DEFINITION,
|
3321
|
-
];
|
3322
|
-
function getNodeName(node) {
|
3323
|
-
const DisplayNodeNameMap = {
|
3324
|
-
[Kind.OBJECT_TYPE_DEFINITION]: 'type',
|
3325
|
-
[Kind.INTERFACE_TYPE_DEFINITION]: 'interface',
|
3326
|
-
[Kind.ENUM_TYPE_DEFINITION]: 'enum',
|
3327
|
-
[Kind.SCALAR_TYPE_DEFINITION]: 'scalar',
|
3328
|
-
[Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
|
3329
|
-
[Kind.UNION_TYPE_DEFINITION]: 'union',
|
3330
|
-
[Kind.DIRECTIVE_DEFINITION]: 'directive',
|
3331
|
-
};
|
3332
|
-
switch (node.kind) {
|
3333
|
-
case Kind.OBJECT_TYPE_DEFINITION:
|
3334
|
-
case Kind.INTERFACE_TYPE_DEFINITION:
|
3335
|
-
case Kind.ENUM_TYPE_DEFINITION:
|
3336
|
-
case Kind.SCALAR_TYPE_DEFINITION:
|
3337
|
-
case Kind.INPUT_OBJECT_TYPE_DEFINITION:
|
3338
|
-
case Kind.UNION_TYPE_DEFINITION:
|
3339
|
-
return `${DisplayNodeNameMap[node.kind]} ${node.name.value}`;
|
3340
|
-
case Kind.DIRECTIVE_DEFINITION:
|
3341
|
-
return `${DisplayNodeNameMap[node.kind]} @${node.name.value}`;
|
3342
|
-
case Kind.FIELD_DEFINITION:
|
3343
|
-
case Kind.INPUT_VALUE_DEFINITION:
|
3344
|
-
case Kind.ENUM_VALUE_DEFINITION:
|
3345
|
-
return `${node.parent.name.value}.${node.name.value}`;
|
3346
|
-
case Kind.OPERATION_DEFINITION:
|
3347
|
-
return node.name ? `${node.operation} ${node.name.value}` : node.operation;
|
3348
|
-
}
|
3349
|
-
}
|
3350
|
-
const rule$l = {
|
3351
|
-
meta: {
|
3352
|
-
docs: {
|
3353
|
-
category: 'Schema',
|
3354
|
-
description: 'Enforce descriptions in type definitions and operations.',
|
3355
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$a}.md`,
|
3356
|
-
examples: [
|
3357
|
-
{
|
3358
|
-
title: 'Incorrect',
|
3359
|
-
usage: [{ types: true, FieldDefinition: true }],
|
3360
|
-
code: /* GraphQL */ `
|
3361
|
-
type someTypeName {
|
3362
|
-
name: String
|
3363
|
-
}
|
3364
|
-
`,
|
3365
|
-
},
|
3366
|
-
{
|
3367
|
-
title: 'Correct',
|
3368
|
-
usage: [{ types: true, FieldDefinition: true }],
|
3369
|
-
code: /* GraphQL */ `
|
3370
|
-
"""
|
3371
|
-
Some type description
|
3372
|
-
"""
|
3373
|
-
type someTypeName {
|
3374
|
-
"""
|
3375
|
-
Name description
|
3376
|
-
"""
|
3377
|
-
name: String
|
3378
|
-
}
|
3379
|
-
`,
|
3380
|
-
},
|
3381
|
-
{
|
3382
|
-
title: 'Correct',
|
3383
|
-
usage: [{ OperationDefinition: true }],
|
3384
|
-
code: /* GraphQL */ `
|
3385
|
-
# Create a new user
|
3386
|
-
mutation createUser {
|
3387
|
-
# ...
|
3388
|
-
}
|
3389
|
-
`,
|
3390
|
-
},
|
3391
|
-
],
|
3392
|
-
configOptions: [
|
3393
|
-
{
|
3394
|
-
types: true,
|
3395
|
-
[Kind.DIRECTIVE_DEFINITION]: true,
|
3396
|
-
},
|
3397
|
-
],
|
3398
|
-
recommended: true,
|
3399
|
-
},
|
3400
|
-
type: 'suggestion',
|
3401
|
-
messages: {
|
3402
|
-
[RULE_ID$a]: 'Description is required for `{{ nodeName }}`.',
|
3403
|
-
},
|
3404
|
-
schema: {
|
3405
|
-
type: 'array',
|
3406
|
-
minItems: 1,
|
3407
|
-
maxItems: 1,
|
3408
|
-
items: {
|
3409
|
-
type: 'object',
|
3410
|
-
additionalProperties: false,
|
3411
|
-
minProperties: 1,
|
3412
|
-
properties: {
|
3413
|
-
types: {
|
3414
|
-
type: 'boolean',
|
3415
|
-
description: `Includes:\n\n${TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
|
3416
|
-
},
|
3417
|
-
...Object.fromEntries([...ALLOWED_KINDS$1].sort().map(kind => {
|
3418
|
-
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
|
3419
|
-
if (kind === Kind.OPERATION_DEFINITION) {
|
3420
|
-
description += '\n\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
|
3421
|
-
}
|
3422
|
-
return [kind, { type: 'boolean', description }];
|
3423
|
-
})),
|
3424
|
-
},
|
3425
|
-
},
|
3426
|
-
},
|
3427
|
-
},
|
3428
|
-
create(context) {
|
3429
|
-
const { types, ...restOptions } = context.options[0] || {};
|
3430
|
-
const kinds = new Set(types ? TYPES_KINDS : []);
|
3431
|
-
for (const [kind, isEnabled] of Object.entries(restOptions)) {
|
3432
|
-
if (isEnabled) {
|
3433
|
-
kinds.add(kind);
|
3434
|
-
}
|
3435
|
-
else {
|
3436
|
-
kinds.delete(kind);
|
3437
|
-
}
|
3438
|
-
}
|
3439
|
-
const selector = [...kinds].join(',');
|
3440
|
-
return {
|
3441
|
-
[selector](node) {
|
3442
|
-
var _a;
|
3443
|
-
let description = '';
|
3444
|
-
const isOperation = node.kind === Kind.OPERATION_DEFINITION;
|
3445
|
-
if (isOperation) {
|
3446
|
-
const rawNode = node.rawNode();
|
3447
|
-
const { prev, line } = rawNode.loc.startToken;
|
3448
|
-
if (prev.kind === TokenKind.COMMENT) {
|
3449
|
-
const value = prev.value.trim();
|
3450
|
-
const linesBefore = line - prev.line;
|
3451
|
-
if (!value.startsWith('eslint') && linesBefore === 1) {
|
3452
|
-
description = value;
|
3453
|
-
}
|
3454
|
-
}
|
3455
|
-
}
|
3456
|
-
else {
|
3457
|
-
description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
|
3458
|
-
}
|
3459
|
-
if (description.length === 0) {
|
3460
|
-
context.report({
|
3461
|
-
loc: isOperation ? getLocation(node.loc.start, node.operation) : node.name.loc,
|
3462
|
-
messageId: RULE_ID$a,
|
3463
|
-
data: {
|
3464
|
-
nodeName: getNodeName(node),
|
3465
|
-
},
|
3466
|
-
});
|
3467
|
-
}
|
3468
|
-
},
|
3469
|
-
};
|
3470
|
-
},
|
3471
|
-
};
|
3472
|
-
|
3473
|
-
const RULE_ID$b = 'require-field-of-type-query-in-mutation-result';
|
3474
|
-
const rule$m = {
|
3475
|
-
meta: {
|
3476
|
-
type: 'suggestion',
|
3477
|
-
docs: {
|
3478
|
-
category: 'Schema',
|
3479
|
-
description: 'Allow the client in one round-trip not only to call mutation but also to get a wagon of data to update their application.\n> Currently, no errors are reported for result type `union`, `interface` and `scalar`.',
|
3480
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$b}.md`,
|
3481
|
-
requiresSchema: true,
|
3482
|
-
examples: [
|
3483
|
-
{
|
3484
|
-
title: 'Incorrect',
|
3485
|
-
code: /* GraphQL */ `
|
3486
|
-
type User { ... }
|
3487
|
-
|
3488
|
-
type Mutation {
|
3489
|
-
createUser: User!
|
3490
|
-
}
|
3491
|
-
`,
|
3492
|
-
},
|
3493
|
-
{
|
3494
|
-
title: 'Correct',
|
3495
|
-
code: /* GraphQL */ `
|
3496
|
-
type User { ... }
|
3497
|
-
|
3498
|
-
type Query { ... }
|
3499
|
-
|
3500
|
-
type CreateUserPayload {
|
3501
|
-
user: User!
|
3502
|
-
query: Query!
|
3503
|
-
}
|
3504
|
-
|
3505
|
-
type Mutation {
|
3506
|
-
createUser: CreateUserPayload!
|
3507
|
-
}
|
3508
|
-
`,
|
3509
|
-
},
|
3510
|
-
],
|
3511
|
-
},
|
3512
|
-
schema: [],
|
3513
|
-
},
|
3514
|
-
create(context) {
|
3515
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$b, context);
|
3516
|
-
const mutationType = schema.getMutationType();
|
3517
|
-
const queryType = schema.getQueryType();
|
3518
|
-
if (!mutationType || !queryType) {
|
3519
|
-
return {};
|
3520
|
-
}
|
3521
|
-
const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}] > FieldDefinition > .gqlType Name`;
|
3522
|
-
return {
|
3523
|
-
[selector](node) {
|
3524
|
-
const typeName = node.value;
|
3525
|
-
const graphQLType = schema.getType(typeName);
|
3526
|
-
if (isObjectType$1(graphQLType)) {
|
3527
|
-
const { fields } = graphQLType.astNode;
|
3528
|
-
const hasQueryType = fields.some(field => getTypeName(field) === queryType.name);
|
3529
|
-
if (!hasQueryType) {
|
3530
|
-
context.report({
|
3531
|
-
node,
|
3532
|
-
message: `Mutation result type "${graphQLType.name}" must contain field of type "${queryType.name}"`,
|
3533
|
-
});
|
3534
|
-
}
|
3535
|
-
}
|
3536
|
-
},
|
3537
|
-
};
|
3538
|
-
},
|
3539
|
-
};
|
3540
|
-
|
3541
|
-
const RULE_ID$c = 'require-id-when-available';
|
3542
|
-
const DEFAULT_ID_FIELD_NAME = 'id';
|
3543
|
-
const rule$n = {
|
3544
|
-
meta: {
|
3545
|
-
type: 'suggestion',
|
3546
|
-
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
|
3547
|
-
hasSuggestions: true,
|
3548
|
-
docs: {
|
3549
|
-
category: 'Operations',
|
3550
|
-
description: 'Enforce selecting specific fields when they are available on the GraphQL type.',
|
3551
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$c}.md`,
|
3552
|
-
requiresSchema: true,
|
3553
|
-
requiresSiblings: true,
|
3554
|
-
examples: [
|
3555
|
-
{
|
3556
|
-
title: 'Incorrect',
|
3557
|
-
code: /* GraphQL */ `
|
3558
|
-
# In your schema
|
3559
|
-
type User {
|
3560
|
-
id: ID!
|
3561
|
-
name: String!
|
3562
|
-
}
|
3563
|
-
|
3564
|
-
# Query
|
3565
|
-
query {
|
3566
|
-
user {
|
3567
|
-
name
|
3568
|
-
}
|
3569
|
-
}
|
3570
|
-
`,
|
3571
|
-
},
|
3572
|
-
{
|
3573
|
-
title: 'Correct',
|
3574
|
-
code: /* GraphQL */ `
|
3575
|
-
# In your schema
|
3576
|
-
type User {
|
3577
|
-
id: ID!
|
3578
|
-
name: String!
|
3579
|
-
}
|
3580
|
-
|
3581
|
-
# Query
|
3582
|
-
query {
|
3583
|
-
user {
|
3584
|
-
id
|
3585
|
-
name
|
3586
|
-
}
|
3587
|
-
}
|
3588
|
-
|
3589
|
-
# Selecting \`id\` with an alias is also valid
|
3590
|
-
query {
|
3591
|
-
user {
|
3592
|
-
id: name
|
3593
|
-
}
|
3594
|
-
}
|
3595
|
-
`,
|
3596
|
-
},
|
3597
|
-
],
|
3598
|
-
recommended: true,
|
3599
|
-
},
|
3600
|
-
messages: {
|
3601
|
-
[RULE_ID$c]: "Field{{ pluralSuffix }} {{ fieldName }} must be selected when it's available on a type.\nInclude it in your selection set{{ addition }}.",
|
3602
|
-
},
|
3603
|
-
schema: {
|
3604
|
-
definitions: {
|
3605
|
-
asString: {
|
3606
|
-
type: 'string',
|
3607
|
-
},
|
3608
|
-
asArray: ARRAY_DEFAULT_OPTIONS,
|
3609
|
-
},
|
3610
|
-
type: 'array',
|
3611
|
-
maxItems: 1,
|
3612
|
-
items: {
|
3613
|
-
type: 'object',
|
3614
|
-
additionalProperties: false,
|
3615
|
-
properties: {
|
3616
|
-
fieldName: {
|
3617
|
-
oneOf: [{ $ref: '#/definitions/asString' }, { $ref: '#/definitions/asArray' }],
|
3618
|
-
default: DEFAULT_ID_FIELD_NAME,
|
3619
|
-
},
|
3620
|
-
},
|
3621
|
-
},
|
3622
|
-
},
|
3623
|
-
},
|
3624
|
-
create(context) {
|
3625
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$c, context);
|
3626
|
-
const siblings = requireSiblingsOperations(RULE_ID$c, context);
|
3627
|
-
const { fieldName = DEFAULT_ID_FIELD_NAME } = context.options[0] || {};
|
3628
|
-
const idNames = asArray(fieldName);
|
3629
|
-
// Check selections only in OperationDefinition,
|
3630
|
-
// skip selections of OperationDefinition and InlineFragment
|
3631
|
-
const selector = 'OperationDefinition SelectionSet[parent.kind!=/(^OperationDefinition|InlineFragment)$/]';
|
3632
|
-
const typeInfo = new TypeInfo(schema);
|
3633
|
-
function checkFragments(node) {
|
3634
|
-
for (const selection of node.selections) {
|
3635
|
-
if (selection.kind !== Kind.FRAGMENT_SPREAD) {
|
3636
|
-
continue;
|
3637
|
-
}
|
3638
|
-
const [foundSpread] = siblings.getFragment(selection.name.value);
|
3639
|
-
if (!foundSpread) {
|
3640
|
-
continue;
|
3641
|
-
}
|
3642
|
-
const checkedFragmentSpreads = new Set();
|
3643
|
-
const visitor = visitWithTypeInfo(typeInfo, {
|
3644
|
-
SelectionSet(node, key, parent) {
|
3645
|
-
if (parent.kind === Kind.FRAGMENT_DEFINITION) {
|
3646
|
-
checkedFragmentSpreads.add(parent.name.value);
|
3647
|
-
}
|
3648
|
-
else if (parent.kind !== Kind.INLINE_FRAGMENT) {
|
3649
|
-
checkSelections(node, typeInfo.getType(), selection.loc.start, parent, checkedFragmentSpreads);
|
3650
|
-
}
|
3651
|
-
},
|
3652
|
-
});
|
3653
|
-
visit(foundSpread.document, visitor);
|
3654
|
-
}
|
3655
|
-
}
|
3656
|
-
function checkSelections(node, type,
|
3657
|
-
// Fragment can be placed in separate file
|
3658
|
-
// Provide actual fragment spread location instead of location in fragment
|
3659
|
-
loc,
|
3660
|
-
// Can't access to node.parent in GraphQL AST.Node, so pass as argument
|
3661
|
-
parent, checkedFragmentSpreads = new Set()) {
|
3662
|
-
const rawType = getBaseType(type);
|
3663
|
-
const isObjectType = rawType instanceof GraphQLObjectType;
|
3664
|
-
const isInterfaceType = rawType instanceof GraphQLInterfaceType;
|
3665
|
-
if (!isObjectType && !isInterfaceType) {
|
3666
|
-
return;
|
3667
|
-
}
|
3668
|
-
const fields = rawType.getFields();
|
3669
|
-
const hasIdFieldInType = idNames.some(name => fields[name]);
|
3670
|
-
if (!hasIdFieldInType) {
|
3671
|
-
return;
|
3672
|
-
}
|
3673
|
-
function hasIdField({ selections }) {
|
3674
|
-
return selections.some(selection => {
|
3675
|
-
if (selection.kind === Kind.FIELD) {
|
3676
|
-
if (selection.alias && idNames.includes(selection.alias.value)) {
|
3677
|
-
return true;
|
3678
|
-
}
|
3679
|
-
return idNames.includes(selection.name.value);
|
3680
|
-
}
|
3681
|
-
if (selection.kind === Kind.INLINE_FRAGMENT) {
|
3682
|
-
return hasIdField(selection.selectionSet);
|
3683
|
-
}
|
3684
|
-
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
3685
|
-
const [foundSpread] = siblings.getFragment(selection.name.value);
|
3686
|
-
if (foundSpread) {
|
3687
|
-
const fragmentSpread = foundSpread.document;
|
3688
|
-
checkedFragmentSpreads.add(fragmentSpread.name.value);
|
3689
|
-
return hasIdField(fragmentSpread.selectionSet);
|
3690
|
-
}
|
3691
|
-
}
|
3692
|
-
return false;
|
3693
|
-
});
|
3694
|
-
}
|
3695
|
-
const hasId = hasIdField(node);
|
3696
|
-
checkFragments(node);
|
3697
|
-
if (hasId) {
|
3698
|
-
return;
|
3699
|
-
}
|
3700
|
-
const pluralSuffix = idNames.length > 1 ? 's' : '';
|
3701
|
-
const fieldName = englishJoinWords(idNames.map(name => `\`${(parent.alias || parent.name).value}.${name}\``));
|
3702
|
-
const addition = checkedFragmentSpreads.size === 0
|
3703
|
-
? ''
|
3704
|
-
: ` or add to used fragment${checkedFragmentSpreads.size > 1 ? 's' : ''} ${englishJoinWords([...checkedFragmentSpreads].map(name => `\`${name}\``))}`;
|
3705
|
-
const problem = {
|
3706
|
-
loc,
|
3707
|
-
messageId: RULE_ID$c,
|
3708
|
-
data: {
|
3709
|
-
pluralSuffix,
|
3710
|
-
fieldName,
|
3711
|
-
addition,
|
3712
|
-
},
|
3713
|
-
};
|
3714
|
-
// Don't provide suggestions for selections in fragments as fragment can be in a separate file
|
3715
|
-
if ('type' in node) {
|
3716
|
-
problem.suggest = idNames.map(idName => ({
|
3717
|
-
desc: `Add \`${idName}\` selection`,
|
3718
|
-
fix: fixer => fixer.insertTextBefore(node.selections[0], `${idName} `),
|
3719
|
-
}));
|
3720
|
-
}
|
3721
|
-
context.report(problem);
|
3722
|
-
}
|
3723
|
-
return {
|
3724
|
-
[selector](node) {
|
3725
|
-
const typeInfo = node.typeInfo();
|
3726
|
-
if (typeInfo.gqlType) {
|
3727
|
-
checkSelections(node, typeInfo.gqlType, node.loc.start, node.parent);
|
3728
|
-
}
|
3729
|
-
},
|
3730
|
-
};
|
3731
|
-
},
|
3732
|
-
};
|
3733
|
-
|
3734
|
-
const RULE_ID$d = 'selection-set-depth';
|
3735
|
-
const rule$o = {
|
3736
|
-
meta: {
|
3737
|
-
type: 'suggestion',
|
3738
|
-
// eslint-disable-next-line eslint-plugin/require-meta-has-suggestions -- optional since we can't provide fixes for fragments located in separate files
|
3739
|
-
hasSuggestions: true,
|
3740
|
-
docs: {
|
3741
|
-
category: 'Operations',
|
3742
|
-
description: 'Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://npmjs.com/package/graphql-depth-limit).',
|
3743
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$d}.md`,
|
3744
|
-
requiresSiblings: true,
|
3745
|
-
examples: [
|
3746
|
-
{
|
3747
|
-
title: 'Incorrect',
|
3748
|
-
usage: [{ maxDepth: 1 }],
|
3749
|
-
code: `
|
3750
|
-
query deep2 {
|
3751
|
-
viewer { # Level 0
|
3752
|
-
albums { # Level 1
|
3753
|
-
title # Level 2
|
3754
|
-
}
|
3755
|
-
}
|
3756
|
-
}
|
3757
|
-
`,
|
3758
|
-
},
|
3759
|
-
{
|
3760
|
-
title: 'Correct',
|
3761
|
-
usage: [{ maxDepth: 4 }],
|
3762
|
-
code: `
|
3763
|
-
query deep2 {
|
3764
|
-
viewer { # Level 0
|
3765
|
-
albums { # Level 1
|
3766
|
-
title # Level 2
|
3767
|
-
}
|
3768
|
-
}
|
3769
|
-
}
|
3770
|
-
`,
|
3771
|
-
},
|
3772
|
-
{
|
3773
|
-
title: 'Correct (ignored field)',
|
3774
|
-
usage: [{ maxDepth: 1, ignore: ['albums'] }],
|
3775
|
-
code: `
|
3776
|
-
query deep2 {
|
3777
|
-
viewer { # Level 0
|
3778
|
-
albums { # Level 1
|
3779
|
-
title # Level 2
|
3780
|
-
}
|
3781
|
-
}
|
3782
|
-
}
|
3783
|
-
`,
|
3784
|
-
},
|
3785
|
-
],
|
3786
|
-
recommended: true,
|
3787
|
-
configOptions: [{ maxDepth: 7 }],
|
3788
|
-
},
|
3789
|
-
schema: {
|
3790
|
-
type: 'array',
|
3791
|
-
minItems: 1,
|
3792
|
-
maxItems: 1,
|
3793
|
-
items: {
|
3794
|
-
type: 'object',
|
3795
|
-
additionalProperties: false,
|
3796
|
-
required: ['maxDepth'],
|
3797
|
-
properties: {
|
3798
|
-
maxDepth: {
|
3799
|
-
type: 'number',
|
3800
|
-
},
|
3801
|
-
ignore: ARRAY_DEFAULT_OPTIONS,
|
3802
|
-
},
|
3803
|
-
},
|
3804
|
-
},
|
3805
|
-
},
|
3806
|
-
create(context) {
|
3807
|
-
let siblings = null;
|
3808
|
-
try {
|
3809
|
-
siblings = requireSiblingsOperations(RULE_ID$d, context);
|
3810
|
-
}
|
3811
|
-
catch (_a) {
|
3812
|
-
logger.warn(`Rule "${RULE_ID$d}" works best with siblings operations loaded. For more info: https://bit.ly/graphql-eslint-operations`);
|
3813
|
-
}
|
3814
|
-
const { maxDepth, ignore = [] } = context.options[0];
|
3815
|
-
const checkFn = depthLimit(maxDepth, { ignore });
|
3816
|
-
return {
|
3817
|
-
'OperationDefinition, FragmentDefinition'(node) {
|
3818
|
-
try {
|
3819
|
-
const rawNode = node.rawNode();
|
3820
|
-
const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
|
3821
|
-
const document = {
|
3822
|
-
kind: Kind.DOCUMENT,
|
3823
|
-
definitions: [rawNode, ...fragmentsInUse],
|
3824
|
-
};
|
3825
|
-
checkFn({
|
3826
|
-
getDocument: () => document,
|
3827
|
-
reportError(error) {
|
3828
|
-
const { line, column } = error.locations[0];
|
3829
|
-
const ancestors = context.getAncestors();
|
3830
|
-
const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
|
3831
|
-
context.report({
|
3832
|
-
loc: {
|
3833
|
-
line,
|
3834
|
-
column: column - 1,
|
3835
|
-
},
|
3836
|
-
message: error.message,
|
3837
|
-
// Don't provide suggestions for fragment that can be in a separate file
|
3838
|
-
...(token && {
|
3839
|
-
suggest: [
|
3840
|
-
{
|
3841
|
-
desc: 'Remove selections',
|
3842
|
-
fix(fixer) {
|
3843
|
-
const sourceCode = context.getSourceCode();
|
3844
|
-
const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]);
|
3845
|
-
const parentNode = foundNode.parent.parent;
|
3846
|
-
return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
|
3847
|
-
},
|
3848
|
-
},
|
3849
|
-
],
|
3850
|
-
}),
|
3851
|
-
});
|
3852
|
-
},
|
3853
|
-
});
|
3854
|
-
}
|
3855
|
-
catch (e) {
|
3856
|
-
logger.warn(`Rule "${RULE_ID$d}" check failed due to a missing siblings operations. For more info: https://bit.ly/graphql-eslint-operations`, e);
|
3857
|
-
}
|
3858
|
-
},
|
3859
|
-
};
|
3860
|
-
},
|
3861
|
-
};
|
3862
|
-
|
3863
|
-
const RULE_ID$e = 'strict-id-in-types';
|
3864
|
-
const rule$p = {
|
3865
|
-
meta: {
|
3866
|
-
type: 'suggestion',
|
3867
|
-
docs: {
|
3868
|
-
description: 'Requires output types to have one unique identifier unless they do not have a logical one. Exceptions can be used to ignore output types that do not have unique identifiers.',
|
3869
|
-
category: 'Schema',
|
3870
|
-
recommended: true,
|
3871
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$e}.md`,
|
3872
|
-
requiresSchema: true,
|
3873
|
-
examples: [
|
3874
|
-
{
|
3875
|
-
title: 'Incorrect',
|
3876
|
-
usage: [
|
3877
|
-
{
|
3878
|
-
acceptedIdNames: ['id', '_id'],
|
3879
|
-
acceptedIdTypes: ['ID'],
|
3880
|
-
exceptions: { suffixes: ['Payload'] },
|
3881
|
-
},
|
3882
|
-
],
|
3883
|
-
code: /* GraphQL */ `
|
3884
|
-
# Incorrect field name
|
3885
|
-
type InvalidFieldName {
|
3886
|
-
key: ID!
|
3887
|
-
}
|
3888
|
-
|
3889
|
-
# Incorrect field type
|
3890
|
-
type InvalidFieldType {
|
3891
|
-
id: String!
|
3892
|
-
}
|
3893
|
-
|
3894
|
-
# Incorrect exception suffix
|
3895
|
-
type InvalidSuffixResult {
|
3896
|
-
data: String!
|
3897
|
-
}
|
3898
|
-
|
3899
|
-
# Too many unique identifiers. Must only contain one.
|
3900
|
-
type InvalidFieldName {
|
3901
|
-
id: ID!
|
3902
|
-
_id: ID!
|
3903
|
-
}
|
3904
|
-
`,
|
3905
|
-
},
|
3906
|
-
{
|
3907
|
-
title: 'Correct',
|
3908
|
-
usage: [
|
3909
|
-
{
|
3910
|
-
acceptedIdNames: ['id', '_id'],
|
3911
|
-
acceptedIdTypes: ['ID'],
|
3912
|
-
exceptions: { types: ['Error'], suffixes: ['Payload'] },
|
3913
|
-
},
|
3914
|
-
],
|
3915
|
-
code: /* GraphQL */ `
|
3916
|
-
type User {
|
3917
|
-
id: ID!
|
3918
|
-
}
|
3919
|
-
|
3920
|
-
type Post {
|
3921
|
-
_id: ID!
|
3922
|
-
}
|
3923
|
-
|
3924
|
-
type CreateUserPayload {
|
3925
|
-
data: String!
|
3926
|
-
}
|
3927
|
-
|
3928
|
-
type Error {
|
3929
|
-
message: String!
|
3930
|
-
}
|
3931
|
-
`,
|
3932
|
-
},
|
3933
|
-
],
|
3934
|
-
},
|
3935
|
-
schema: {
|
3936
|
-
type: 'array',
|
3937
|
-
maxItems: 1,
|
3938
|
-
items: {
|
3939
|
-
type: 'object',
|
3940
|
-
additionalProperties: false,
|
3941
|
-
properties: {
|
3942
|
-
acceptedIdNames: {
|
3943
|
-
...ARRAY_DEFAULT_OPTIONS,
|
3944
|
-
default: ['id'],
|
3945
|
-
},
|
3946
|
-
acceptedIdTypes: {
|
3947
|
-
...ARRAY_DEFAULT_OPTIONS,
|
3948
|
-
default: ['ID'],
|
3949
|
-
},
|
3950
|
-
exceptions: {
|
3951
|
-
type: 'object',
|
3952
|
-
properties: {
|
3953
|
-
types: {
|
3954
|
-
...ARRAY_DEFAULT_OPTIONS,
|
3955
|
-
description: 'This is used to exclude types with names that match one of the specified values.',
|
3956
|
-
},
|
3957
|
-
suffixes: {
|
3958
|
-
...ARRAY_DEFAULT_OPTIONS,
|
3959
|
-
description: 'This is used to exclude types with names with suffixes that match one of the specified values.',
|
3960
|
-
},
|
3961
|
-
},
|
3962
|
-
},
|
3963
|
-
},
|
3964
|
-
},
|
3965
|
-
},
|
3966
|
-
},
|
3967
|
-
create(context) {
|
3968
|
-
const options = {
|
3969
|
-
acceptedIdNames: ['id'],
|
3970
|
-
acceptedIdTypes: ['ID'],
|
3971
|
-
exceptions: {},
|
3972
|
-
...context.options[0],
|
3973
|
-
};
|
3974
|
-
const schema = requireGraphQLSchemaFromContext(RULE_ID$e, context);
|
3975
|
-
const rootTypeNames = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]
|
3976
|
-
.filter(Boolean)
|
3977
|
-
.map(type => type.name);
|
3978
|
-
const selector = `ObjectTypeDefinition[name.value!=/^(${rootTypeNames.join('|')})$/]`;
|
3979
|
-
return {
|
3980
|
-
[selector](node) {
|
3981
|
-
var _a, _b;
|
3982
|
-
const typeName = node.name.value;
|
3983
|
-
const shouldIgnoreNode = ((_a = options.exceptions.types) === null || _a === void 0 ? void 0 : _a.includes(typeName)) ||
|
3984
|
-
((_b = options.exceptions.suffixes) === null || _b === void 0 ? void 0 : _b.some(suffix => typeName.endsWith(suffix)));
|
3985
|
-
if (shouldIgnoreNode) {
|
3986
|
-
return;
|
3987
|
-
}
|
3988
|
-
const validIds = node.fields.filter(field => {
|
3989
|
-
const fieldNode = field.rawNode();
|
3990
|
-
const isValidIdName = options.acceptedIdNames.includes(fieldNode.name.value);
|
3991
|
-
// To be a valid type, it must be non-null and one of the accepted types.
|
3992
|
-
let isValidIdType = false;
|
3993
|
-
if (fieldNode.type.kind === Kind.NON_NULL_TYPE && fieldNode.type.type.kind === Kind.NAMED_TYPE) {
|
3994
|
-
isValidIdType = options.acceptedIdTypes.includes(fieldNode.type.type.name.value);
|
3995
|
-
}
|
3996
|
-
return isValidIdName && isValidIdType;
|
3997
|
-
});
|
3998
|
-
// Usually, there should be only one unique identifier field per type.
|
3999
|
-
// Some clients allow multiple fields to be used. If more people need this,
|
4000
|
-
// we can extend this rule later.
|
4001
|
-
if (validIds.length !== 1) {
|
4002
|
-
const pluralNamesSuffix = options.acceptedIdNames.length > 1 ? 's' : '';
|
4003
|
-
const pluralTypesSuffix = options.acceptedIdTypes.length > 1 ? 's' : '';
|
4004
|
-
context.report({
|
4005
|
-
node: node.name,
|
4006
|
-
message: `${typeName} must have exactly one non-nullable unique identifier. Accepted name${pluralNamesSuffix}: ${englishJoinWords(options.acceptedIdNames)}. Accepted type${pluralTypesSuffix}: ${englishJoinWords(options.acceptedIdTypes)}.`,
|
4007
|
-
});
|
4008
|
-
}
|
4009
|
-
},
|
4010
|
-
};
|
4011
|
-
},
|
4012
|
-
};
|
4013
|
-
|
4014
|
-
const RULE_ID$f = 'unique-fragment-name';
|
4015
|
-
const checkNode = (context, node, ruleId) => {
|
4016
|
-
const documentName = node.name.value;
|
4017
|
-
const siblings = requireSiblingsOperations(ruleId, context);
|
4018
|
-
const siblingDocuments = node.kind === Kind.FRAGMENT_DEFINITION ? siblings.getFragment(documentName) : siblings.getOperation(documentName);
|
4019
|
-
const filepath = context.getFilename();
|
4020
|
-
const conflictingDocuments = siblingDocuments.filter(f => {
|
4021
|
-
var _a;
|
4022
|
-
const isSameName = ((_a = f.document.name) === null || _a === void 0 ? void 0 : _a.value) === documentName;
|
4023
|
-
const isSamePath = normalizePath(f.filePath) === normalizePath(filepath);
|
4024
|
-
return isSameName && !isSamePath;
|
4025
|
-
});
|
4026
|
-
if (conflictingDocuments.length > 0) {
|
4027
|
-
context.report({
|
4028
|
-
messageId: ruleId,
|
4029
|
-
data: {
|
4030
|
-
documentName,
|
4031
|
-
summary: conflictingDocuments
|
4032
|
-
.map(f => `\t${relative(process.cwd(), getOnDiskFilepath(f.filePath))}`)
|
4033
|
-
.join('\n'),
|
4034
|
-
},
|
4035
|
-
node: node.name,
|
4036
|
-
});
|
4037
|
-
}
|
4038
|
-
};
|
4039
|
-
const rule$q = {
|
4040
|
-
meta: {
|
4041
|
-
type: 'suggestion',
|
4042
|
-
docs: {
|
4043
|
-
category: 'Operations',
|
4044
|
-
description: 'Enforce unique fragment names across your project.',
|
4045
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$f}.md`,
|
4046
|
-
requiresSiblings: true,
|
4047
|
-
examples: [
|
4048
|
-
{
|
4049
|
-
title: 'Incorrect',
|
4050
|
-
code: /* GraphQL */ `
|
4051
|
-
# user.fragment.graphql
|
4052
|
-
fragment UserFields on User {
|
4053
|
-
id
|
4054
|
-
name
|
4055
|
-
fullName
|
4056
|
-
}
|
4057
|
-
|
4058
|
-
# user-fields.graphql
|
4059
|
-
fragment UserFields on User {
|
4060
|
-
id
|
4061
|
-
}
|
4062
|
-
`,
|
4063
|
-
},
|
4064
|
-
{
|
4065
|
-
title: 'Correct',
|
4066
|
-
code: /* GraphQL */ `
|
4067
|
-
# user.fragment.graphql
|
4068
|
-
fragment AllUserFields on User {
|
4069
|
-
id
|
4070
|
-
name
|
4071
|
-
fullName
|
4072
|
-
}
|
4073
|
-
|
4074
|
-
# user-fields.graphql
|
4075
|
-
fragment UserFields on User {
|
4076
|
-
id
|
4077
|
-
}
|
4078
|
-
`,
|
4079
|
-
},
|
4080
|
-
],
|
4081
|
-
},
|
4082
|
-
messages: {
|
4083
|
-
[RULE_ID$f]: 'Fragment named "{{ documentName }}" already defined in:\n{{ summary }}',
|
4084
|
-
},
|
4085
|
-
schema: [],
|
4086
|
-
},
|
4087
|
-
create(context) {
|
4088
|
-
return {
|
4089
|
-
FragmentDefinition(node) {
|
4090
|
-
checkNode(context, node, RULE_ID$f);
|
4091
|
-
},
|
4092
|
-
};
|
4093
|
-
},
|
4094
|
-
};
|
4095
|
-
|
4096
|
-
const RULE_ID$g = 'unique-operation-name';
|
4097
|
-
const rule$r = {
|
4098
|
-
meta: {
|
4099
|
-
type: 'suggestion',
|
4100
|
-
docs: {
|
4101
|
-
category: 'Operations',
|
4102
|
-
description: 'Enforce unique operation names across your project.',
|
4103
|
-
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID$g}.md`,
|
4104
|
-
requiresSiblings: true,
|
4105
|
-
examples: [
|
4106
|
-
{
|
4107
|
-
title: 'Incorrect',
|
4108
|
-
code: /* GraphQL */ `
|
4109
|
-
# foo.query.graphql
|
4110
|
-
query user {
|
4111
|
-
user {
|
4112
|
-
id
|
4113
|
-
}
|
4114
|
-
}
|
4115
|
-
|
4116
|
-
# bar.query.graphql
|
4117
|
-
query user {
|
4118
|
-
me {
|
4119
|
-
id
|
4120
|
-
}
|
4121
|
-
}
|
4122
|
-
`,
|
4123
|
-
},
|
4124
|
-
{
|
4125
|
-
title: 'Correct',
|
4126
|
-
code: /* GraphQL */ `
|
4127
|
-
# foo.query.graphql
|
4128
|
-
query user {
|
4129
|
-
user {
|
4130
|
-
id
|
4131
|
-
}
|
4132
|
-
}
|
4133
|
-
|
4134
|
-
# bar.query.graphql
|
4135
|
-
query me {
|
4136
|
-
me {
|
4137
|
-
id
|
4138
|
-
}
|
4139
|
-
}
|
4140
|
-
`,
|
4141
|
-
},
|
4142
|
-
],
|
4143
|
-
},
|
4144
|
-
messages: {
|
4145
|
-
[RULE_ID$g]: 'Operation named "{{ documentName }}" already defined in:\n{{ summary }}',
|
4146
|
-
},
|
4147
|
-
schema: [],
|
4148
|
-
},
|
4149
|
-
create(context) {
|
4150
|
-
return {
|
4151
|
-
'OperationDefinition[name!=undefined]'(node) {
|
4152
|
-
checkNode(context, node, RULE_ID$g);
|
4153
|
-
},
|
4154
|
-
};
|
4155
|
-
},
|
4156
|
-
};
|
4157
|
-
|
4158
|
-
/*
|
4159
|
-
* 🚨 IMPORTANT! Do not manually modify this file. Run: `yarn generate-configs`
|
4160
|
-
*/
|
4161
|
-
const rules = {
|
4162
|
-
...GRAPHQL_JS_VALIDATIONS,
|
4163
|
-
alphabetize: rule,
|
4164
|
-
'description-style': rule$1,
|
4165
|
-
'input-name': rule$2,
|
4166
|
-
'match-document-filename': rule$3,
|
4167
|
-
'naming-convention': rule$4,
|
4168
|
-
'no-anonymous-operations': rule$5,
|
4169
|
-
'no-case-insensitive-enum-values-duplicates': rule$6,
|
4170
|
-
'no-deprecated': rule$7,
|
4171
|
-
'no-duplicate-fields': rule$8,
|
4172
|
-
'no-hashtag-description': rule$9,
|
4173
|
-
'no-root-type': rule$a,
|
4174
|
-
'no-scalar-result-type-on-mutation': rule$b,
|
4175
|
-
'no-typename-prefix': rule$c,
|
4176
|
-
'no-unreachable-types': rule$d,
|
4177
|
-
'no-unused-fields': rule$e,
|
4178
|
-
'relay-arguments': rule$f,
|
4179
|
-
'relay-connection-types': rule$g,
|
4180
|
-
'relay-edge-types': rule$h,
|
4181
|
-
'relay-page-info': rule$i,
|
4182
|
-
'require-deprecation-date': rule$j,
|
4183
|
-
'require-deprecation-reason': rule$k,
|
4184
|
-
'require-description': rule$l,
|
4185
|
-
'require-field-of-type-query-in-mutation-result': rule$m,
|
4186
|
-
'require-id-when-available': rule$n,
|
4187
|
-
'selection-set-depth': rule$o,
|
4188
|
-
'strict-id-in-types': rule$p,
|
4189
|
-
'unique-fragment-name': rule$q,
|
4190
|
-
'unique-operation-name': rule$r,
|
4191
|
-
};
|
4192
|
-
|
4193
|
-
const schemaCache = new Map();
|
4194
|
-
const debug = debugFactory('graphql-eslint:schema');
|
4195
|
-
function getSchema(projectForFile, options = {}) {
|
4196
|
-
const schemaKey = asArray(projectForFile.schema).sort().join(',');
|
4197
|
-
if (!schemaKey) {
|
4198
|
-
return null;
|
4199
|
-
}
|
4200
|
-
if (schemaCache.has(schemaKey)) {
|
4201
|
-
return schemaCache.get(schemaKey);
|
4202
|
-
}
|
4203
|
-
let schema;
|
4204
|
-
try {
|
4205
|
-
debug('Loading schema from %o', projectForFile.schema);
|
4206
|
-
schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', options.schemaOptions);
|
4207
|
-
if (debug.enabled) {
|
4208
|
-
debug('Schema loaded: %o', schema instanceof GraphQLSchema);
|
4209
|
-
const schemaPaths = fastGlob.sync(projectForFile.schema, {
|
4210
|
-
absolute: true,
|
4211
|
-
});
|
4212
|
-
debug('Schema pointers %O', schemaPaths);
|
4213
|
-
}
|
4214
|
-
}
|
4215
|
-
catch (error) {
|
4216
|
-
error.message = chalk.red(`Error while loading schema: ${error.message}`);
|
4217
|
-
schema = error;
|
4218
|
-
}
|
4219
|
-
schemaCache.set(schemaKey, schema);
|
4220
|
-
return schema;
|
4221
|
-
}
|
4222
|
-
|
4223
|
-
const debug$1 = debugFactory('graphql-eslint:operations');
|
4224
|
-
const handleVirtualPath = (documents) => {
|
4225
|
-
const filepathMap = Object.create(null);
|
4226
|
-
return documents.map(source => {
|
4227
|
-
var _a;
|
4228
|
-
const { location } = source;
|
4229
|
-
if (['.gql', '.graphql'].some(extension => location.endsWith(extension))) {
|
4230
|
-
return source;
|
4231
|
-
}
|
4232
|
-
(_a = filepathMap[location]) !== null && _a !== void 0 ? _a : (filepathMap[location] = -1);
|
4233
|
-
const index = (filepathMap[location] += 1);
|
4234
|
-
return {
|
4235
|
-
...source,
|
4236
|
-
location: resolve(location, `${index}_document.graphql`),
|
4237
|
-
};
|
4238
|
-
});
|
4239
|
-
};
|
4240
|
-
const operationsCache = new Map();
|
4241
|
-
const siblingOperationsCache = new Map();
|
4242
|
-
const getSiblings = (projectForFile) => {
|
4243
|
-
const documentsKey = asArray(projectForFile.documents).sort().join(',');
|
4244
|
-
if (!documentsKey) {
|
4245
|
-
return [];
|
4246
|
-
}
|
4247
|
-
let siblings = operationsCache.get(documentsKey);
|
4248
|
-
if (!siblings) {
|
4249
|
-
debug$1('Loading operations from %o', projectForFile.documents);
|
4250
|
-
const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
|
4251
|
-
skipGraphQLImport: true,
|
4252
|
-
});
|
4253
|
-
if (debug$1.enabled) {
|
4254
|
-
debug$1('Loaded %d operations', documents.length);
|
4255
|
-
const operationsPaths = fastGlob.sync(projectForFile.documents, {
|
4256
|
-
absolute: true,
|
4257
|
-
});
|
4258
|
-
debug$1('Operations pointers %O', operationsPaths);
|
4259
|
-
}
|
4260
|
-
siblings = handleVirtualPath(documents);
|
4261
|
-
operationsCache.set(documentsKey, siblings);
|
4262
|
-
}
|
4263
|
-
return siblings;
|
4264
|
-
};
|
4265
|
-
function getSiblingOperations(projectForFile) {
|
4266
|
-
const siblings = getSiblings(projectForFile);
|
4267
|
-
if (siblings.length === 0) {
|
4268
|
-
let printed = false;
|
4269
|
-
const noopWarn = () => {
|
4270
|
-
if (!printed) {
|
4271
|
-
logger.warn('getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!');
|
4272
|
-
printed = true;
|
4273
|
-
}
|
4274
|
-
return [];
|
4275
|
-
};
|
4276
|
-
return {
|
4277
|
-
available: false,
|
4278
|
-
getFragment: noopWarn,
|
4279
|
-
getFragments: noopWarn,
|
4280
|
-
getFragmentByType: noopWarn,
|
4281
|
-
getFragmentsInUse: noopWarn,
|
4282
|
-
getOperation: noopWarn,
|
4283
|
-
getOperations: noopWarn,
|
4284
|
-
getOperationByType: noopWarn,
|
4285
|
-
};
|
4286
|
-
}
|
4287
|
-
// Since the siblings array is cached, we can use it as cache key.
|
4288
|
-
// We should get the same array reference each time we get
|
4289
|
-
// to this point for the same graphql project
|
4290
|
-
if (siblingOperationsCache.has(siblings)) {
|
4291
|
-
return siblingOperationsCache.get(siblings);
|
4292
|
-
}
|
4293
|
-
let fragmentsCache = null;
|
4294
|
-
const getFragments = () => {
|
4295
|
-
if (fragmentsCache === null) {
|
4296
|
-
const result = [];
|
4297
|
-
for (const source of siblings) {
|
4298
|
-
for (const definition of source.document.definitions) {
|
4299
|
-
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
|
4300
|
-
result.push({
|
4301
|
-
filePath: source.location,
|
4302
|
-
document: definition,
|
4303
|
-
});
|
4304
|
-
}
|
4305
|
-
}
|
4306
|
-
}
|
4307
|
-
fragmentsCache = result;
|
4308
|
-
}
|
4309
|
-
return fragmentsCache;
|
4310
|
-
};
|
4311
|
-
let cachedOperations = null;
|
4312
|
-
const getOperations = () => {
|
4313
|
-
if (cachedOperations === null) {
|
4314
|
-
const result = [];
|
4315
|
-
for (const source of siblings) {
|
4316
|
-
for (const definition of source.document.definitions) {
|
4317
|
-
if (definition.kind === Kind.OPERATION_DEFINITION) {
|
4318
|
-
result.push({
|
4319
|
-
filePath: source.location,
|
4320
|
-
document: definition,
|
4321
|
-
});
|
4322
|
-
}
|
4323
|
-
}
|
4324
|
-
}
|
4325
|
-
cachedOperations = result;
|
4326
|
-
}
|
4327
|
-
return cachedOperations;
|
4328
|
-
};
|
4329
|
-
const getFragment = (name) => getFragments().filter(f => { var _a; return ((_a = f.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; });
|
4330
|
-
const collectFragments = (selectable, recursive, collected = new Map()) => {
|
4331
|
-
visit(selectable, {
|
4332
|
-
FragmentSpread(spread) {
|
4333
|
-
const fragmentName = spread.name.value;
|
4334
|
-
const [fragment] = getFragment(fragmentName);
|
4335
|
-
if (!fragment) {
|
4336
|
-
logger.warn(`Unable to locate fragment named "${fragmentName}", please make sure it's loaded using "parserOptions.operations"`);
|
4337
|
-
return;
|
4338
|
-
}
|
4339
|
-
if (!collected.has(fragmentName)) {
|
4340
|
-
collected.set(fragmentName, fragment.document);
|
4341
|
-
if (recursive) {
|
4342
|
-
collectFragments(fragment.document, recursive, collected);
|
4343
|
-
}
|
4344
|
-
}
|
4345
|
-
},
|
4346
|
-
});
|
4347
|
-
return collected;
|
4348
|
-
};
|
4349
|
-
const siblingOperations = {
|
4350
|
-
available: true,
|
4351
|
-
getFragment,
|
4352
|
-
getFragments,
|
4353
|
-
getFragmentByType: typeName => getFragments().filter(f => { var _a, _b; return ((_b = (_a = f.document.typeCondition) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.value) === typeName; }),
|
4354
|
-
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
|
4355
|
-
getOperation: name => getOperations().filter(o => { var _a; return ((_a = o.document.name) === null || _a === void 0 ? void 0 : _a.value) === name; }),
|
4356
|
-
getOperations,
|
4357
|
-
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
|
4358
|
-
};
|
4359
|
-
siblingOperationsCache.set(siblings, siblingOperations);
|
4360
|
-
return siblingOperations;
|
4361
|
-
}
|
4362
|
-
|
4363
|
-
const debug$2 = debugFactory('graphql-eslint:graphql-config');
|
4364
|
-
let graphQLConfig;
|
4365
|
-
function loadGraphQLConfig(options) {
|
4366
|
-
// We don't want cache config on test environment
|
4367
|
-
// Otherwise schema and documents will be same for all tests
|
4368
|
-
if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
|
4369
|
-
return graphQLConfig;
|
4370
|
-
}
|
4371
|
-
const onDiskConfig = options.skipGraphQLConfig
|
4372
|
-
? null
|
4373
|
-
: loadConfigSync({
|
4374
|
-
// load config relative to the file being linted
|
4375
|
-
rootDir: options.filePath ? dirname(options.filePath) : undefined,
|
4376
|
-
throwOnEmpty: false,
|
4377
|
-
throwOnMissing: false,
|
4378
|
-
extensions: [addCodeFileLoaderExtension],
|
4379
|
-
});
|
4380
|
-
debug$2('options.skipGraphQLConfig: %o', options.skipGraphQLConfig);
|
4381
|
-
if (onDiskConfig) {
|
4382
|
-
debug$2('Graphql-config path %o', onDiskConfig.filepath);
|
4383
|
-
}
|
4384
|
-
const configOptions = options.projects
|
4385
|
-
? { projects: options.projects }
|
4386
|
-
: {
|
4387
|
-
schema: (options.schema || ''),
|
4388
|
-
documents: options.documents || options.operations,
|
4389
|
-
extensions: options.extensions,
|
4390
|
-
include: options.include,
|
4391
|
-
exclude: options.exclude,
|
4392
|
-
};
|
4393
|
-
graphQLConfig =
|
4394
|
-
onDiskConfig ||
|
4395
|
-
new GraphQLConfig({
|
4396
|
-
config: configOptions,
|
4397
|
-
filepath: 'virtual-config',
|
4398
|
-
}, [addCodeFileLoaderExtension]);
|
4399
|
-
return graphQLConfig;
|
4400
|
-
}
|
4401
|
-
const addCodeFileLoaderExtension = api => {
|
4402
|
-
api.loaders.schema.register(new CodeFileLoader());
|
4403
|
-
api.loaders.documents.register(new CodeFileLoader());
|
4404
|
-
return { name: 'graphql-eslint-loaders' };
|
4405
|
-
};
|
4406
|
-
|
4407
|
-
const debug$3 = debugFactory('graphql-eslint:parser');
|
4408
|
-
debug$3('cwd %o', process.cwd());
|
4409
|
-
function parseForESLint(code, options = {}) {
|
4410
|
-
try {
|
4411
|
-
const filePath = options.filePath || '';
|
4412
|
-
const realFilepath = filePath && getOnDiskFilepath(filePath);
|
4413
|
-
const gqlConfig = loadGraphQLConfig(options);
|
4414
|
-
const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
|
4415
|
-
const schema = getSchema(projectForFile, options);
|
4416
|
-
const siblingOperations = getSiblingOperations(projectForFile);
|
4417
|
-
const { document } = parseGraphQLSDL(filePath, code, {
|
4418
|
-
...options.graphQLParserOptions,
|
4419
|
-
noLocation: false,
|
4420
|
-
});
|
4421
|
-
const comments = extractComments(document.loc);
|
4422
|
-
const tokens = extractTokens(filePath, code);
|
4423
|
-
const rootTree = convertToESTree(document, schema instanceof GraphQLSchema ? schema : null);
|
4424
|
-
return {
|
4425
|
-
services: {
|
4426
|
-
schema,
|
4427
|
-
siblingOperations,
|
4428
|
-
},
|
4429
|
-
ast: {
|
4430
|
-
comments,
|
4431
|
-
tokens,
|
4432
|
-
loc: rootTree.loc,
|
4433
|
-
range: rootTree.range,
|
4434
|
-
type: 'Program',
|
4435
|
-
sourceType: 'script',
|
4436
|
-
body: [rootTree],
|
4437
|
-
},
|
4438
|
-
};
|
4439
|
-
}
|
4440
|
-
catch (error) {
|
4441
|
-
error.message = `[graphql-eslint] ${error.message}`;
|
4442
|
-
// In case of GraphQL parser error, we report it to ESLint as a parser error that matches the requirements
|
4443
|
-
// of ESLint. This will make sure to display it correctly in IDEs and lint results.
|
4444
|
-
if (error instanceof GraphQLError) {
|
4445
|
-
const eslintError = {
|
4446
|
-
index: error.positions[0],
|
4447
|
-
lineNumber: error.locations[0].line,
|
4448
|
-
column: error.locations[0].column - 1,
|
4449
|
-
message: error.message,
|
4450
|
-
};
|
4451
|
-
throw eslintError;
|
4452
|
-
}
|
4453
|
-
throw error;
|
4454
|
-
}
|
4455
|
-
}
|
4456
|
-
|
4457
|
-
/* eslint-env jest */
|
4458
|
-
function indentCode(code, indent = 4) {
|
4459
|
-
return code.replace(/^/gm, ' '.repeat(indent));
|
4460
|
-
}
|
4461
|
-
// A simple version of `SourceCodeFixer.applyFixes`
|
4462
|
-
// https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
|
4463
|
-
function applyFix(code, { range, text }) {
|
4464
|
-
return [code.slice(0, range[0]), text, code.slice(range[1])].join('');
|
4465
|
-
}
|
4466
|
-
class GraphQLRuleTester extends RuleTester {
|
4467
|
-
constructor(parserOptions = {}) {
|
4468
|
-
const config = {
|
4469
|
-
parser: require.resolve('@graphql-eslint/eslint-plugin'),
|
4470
|
-
parserOptions: {
|
4471
|
-
...parserOptions,
|
4472
|
-
skipGraphQLConfig: true,
|
4473
|
-
},
|
4474
|
-
};
|
4475
|
-
super(config);
|
4476
|
-
this.config = config;
|
4477
|
-
}
|
4478
|
-
fromMockFile(path) {
|
4479
|
-
return readFileSync(resolve(__dirname, `../tests/mocks/${path}`), 'utf-8');
|
4480
|
-
}
|
4481
|
-
runGraphQLTests(ruleId, rule, tests) {
|
4482
|
-
const ruleTests = Linter.version.startsWith('8')
|
4483
|
-
? tests
|
4484
|
-
: {
|
4485
|
-
valid: tests.valid.map(test => {
|
4486
|
-
if (typeof test === 'string') {
|
4487
|
-
return test;
|
4488
|
-
}
|
4489
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
4490
|
-
const { name, ...testCaseOptions } = test;
|
4491
|
-
return testCaseOptions;
|
4492
|
-
}),
|
4493
|
-
invalid: tests.invalid.map(test => {
|
4494
|
-
// ESLint 7 throws an error on CI - Unexpected top-level property "name"
|
4495
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
4496
|
-
const { name, ...testCaseOptions } = test;
|
4497
|
-
return testCaseOptions;
|
4498
|
-
}),
|
4499
|
-
};
|
4500
|
-
super.run(ruleId, rule, ruleTests);
|
4501
|
-
const linter = new Linter();
|
4502
|
-
linter.defineRule(ruleId, rule);
|
4503
|
-
const hasOnlyTest = [...tests.valid, ...tests.invalid].some(t => typeof t !== 'string' && t.only);
|
4504
|
-
// for (const [index, testCase] of tests.valid.entries()) {
|
4505
|
-
// const { name, code, filename, only }: RuleTester.ValidTestCase =
|
4506
|
-
// typeof testCase === 'string' ? { code: testCase } : testCase;
|
4507
|
-
//
|
4508
|
-
// if (hasOnlyTest && !only) {
|
4509
|
-
// continue;
|
4510
|
-
// }
|
4511
|
-
//
|
4512
|
-
// const verifyConfig = getVerifyConfig(ruleId, this.config, testCase);
|
4513
|
-
// defineParser(linter, verifyConfig.parser);
|
4514
|
-
//
|
4515
|
-
// const messages = linter.verify(code, verifyConfig, { filename });
|
4516
|
-
// const codeFrame = printCode(code, { line: 0, column: 0 });
|
4517
|
-
//
|
4518
|
-
// it(name || `Valid #${index + 1}\n${codeFrame}`, () => {
|
4519
|
-
// expect(messages).toEqual([]);
|
4520
|
-
// });
|
4521
|
-
// }
|
4522
|
-
for (const [idx, testCase] of tests.invalid.entries()) {
|
4523
|
-
const { only, filename, options, name } = testCase;
|
4524
|
-
if (hasOnlyTest && !only) {
|
4525
|
-
continue;
|
4526
|
-
}
|
4527
|
-
const code = removeTrailingBlankLines(testCase.code);
|
4528
|
-
const verifyConfig = getVerifyConfig(ruleId, this.config, testCase);
|
4529
|
-
defineParser(linter, verifyConfig.parser);
|
4530
|
-
const messages = linter.verify(code, verifyConfig, filename);
|
4531
|
-
if (messages.length === 0) {
|
4532
|
-
throw new Error('Invalid case should have at least one error.');
|
4533
|
-
}
|
4534
|
-
const codeFrame = indentCode(printCode(code, { line: 0, column: 0 }));
|
4535
|
-
const messageForSnapshot = ['#### ⌨️ Code', codeFrame];
|
4536
|
-
if (options) {
|
4537
|
-
const opts = JSON.stringify(options, null, 2).slice(1, -1);
|
4538
|
-
messageForSnapshot.push('#### ⚙️ Options', indentCode(removeTrailingBlankLines(opts), 2));
|
4539
|
-
}
|
4540
|
-
for (const [index, message] of messages.entries()) {
|
4541
|
-
if (message.fatal) {
|
4542
|
-
throw new Error(message.message);
|
4543
|
-
}
|
4544
|
-
const codeWithMessage = printCode(code, message, 1);
|
4545
|
-
messageForSnapshot.push(printWithIndex('#### ❌ Error', index, messages.length), indentCode(codeWithMessage));
|
4546
|
-
const { suggestions } = message;
|
4547
|
-
// Don't print suggestions in snapshots for too big codes
|
4548
|
-
if (suggestions && (code.match(/\n/g) || '').length < 1000) {
|
4549
|
-
for (const [i, suggestion] of message.suggestions.entries()) {
|
4550
|
-
const title = printWithIndex('#### 💡 Suggestion', i, suggestions.length, suggestion.desc);
|
4551
|
-
const output = applyFix(code, suggestion.fix);
|
4552
|
-
const codeFrame = printCode(output, { line: 0, column: 0 });
|
4553
|
-
messageForSnapshot.push(title, indentCode(codeFrame, 2));
|
4554
|
-
}
|
4555
|
-
}
|
4556
|
-
}
|
4557
|
-
if (rule.meta.fixable) {
|
4558
|
-
const { fixed, output } = linter.verifyAndFix(code, verifyConfig, filename);
|
4559
|
-
if (fixed) {
|
4560
|
-
messageForSnapshot.push('#### 🔧 Autofix output', indentCode(printCode(output)));
|
4561
|
-
}
|
4562
|
-
}
|
4563
|
-
it(name || `Invalid #${idx + 1}`, () => {
|
4564
|
-
expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
|
4565
|
-
});
|
4566
|
-
}
|
4567
|
-
}
|
4568
|
-
}
|
4569
|
-
function removeTrailingBlankLines(text) {
|
4570
|
-
return text.replace(/^\s*\n/, '').trimEnd();
|
4571
|
-
}
|
4572
|
-
function printWithIndex(title, index, total, description) {
|
4573
|
-
if (total > 1) {
|
4574
|
-
title += ` ${index + 1}/${total}`;
|
4575
|
-
}
|
4576
|
-
if (description) {
|
4577
|
-
title += `: ${description}`;
|
4578
|
-
}
|
4579
|
-
return title;
|
4580
|
-
}
|
4581
|
-
function getVerifyConfig(ruleId, testerConfig, testCase) {
|
4582
|
-
const { parser = testerConfig.parser, parserOptions, options } = testCase;
|
4583
|
-
return {
|
4584
|
-
...testerConfig,
|
4585
|
-
parser,
|
4586
|
-
parserOptions: {
|
4587
|
-
...testerConfig.parserOptions,
|
4588
|
-
...parserOptions,
|
4589
|
-
},
|
4590
|
-
rules: {
|
4591
|
-
[ruleId]: Array.isArray(options) ? ['error', ...options] : 'error',
|
4592
|
-
},
|
4593
|
-
};
|
4594
|
-
}
|
4595
|
-
const parsers = new WeakMap();
|
4596
|
-
function defineParser(linter, parser) {
|
4597
|
-
if (!parser) {
|
4598
|
-
return;
|
4599
|
-
}
|
4600
|
-
if (!parsers.has(linter)) {
|
4601
|
-
parsers.set(linter, new Set());
|
4602
|
-
}
|
4603
|
-
const defined = parsers.get(linter);
|
4604
|
-
if (!defined.has(parser)) {
|
4605
|
-
defined.add(parser);
|
4606
|
-
linter.defineParser(parser, require(parser));
|
4607
|
-
}
|
4608
|
-
}
|
4609
|
-
function printCode(code, result = {}, linesOffset = Number.POSITIVE_INFINITY) {
|
4610
|
-
const { line, column, endLine, endColumn, message } = result;
|
4611
|
-
const location = {};
|
4612
|
-
if (typeof line === 'number' && typeof column === 'number') {
|
4613
|
-
location.start = {
|
4614
|
-
line,
|
4615
|
-
column,
|
4616
|
-
};
|
4617
|
-
}
|
4618
|
-
if (typeof endLine === 'number' && typeof endColumn === 'number') {
|
4619
|
-
location.end = {
|
4620
|
-
line: endLine,
|
4621
|
-
column: endColumn,
|
4622
|
-
};
|
4623
|
-
}
|
4624
|
-
return codeFrameColumns(code, location, {
|
4625
|
-
linesAbove: linesOffset,
|
4626
|
-
linesBelow: linesOffset,
|
4627
|
-
message,
|
4628
|
-
});
|
4629
|
-
}
|
4630
|
-
|
4631
|
-
const processors = { graphql: processor };
|
4632
|
-
const configs = Object.fromEntries([
|
4633
|
-
// Configs to extend from `configs` directory
|
4634
|
-
'schema-recommended',
|
4635
|
-
'schema-all',
|
4636
|
-
'operations-recommended',
|
4637
|
-
'operations-all',
|
4638
|
-
'relay',
|
4639
|
-
].map(configName => [configName, { extends: `./configs/${configName}.json` }]));
|
4640
|
-
|
4641
|
-
export { GraphQLRuleTester, configs, parseForESLint, processors, requireGraphQLSchemaFromContext, requireSiblingsOperations, rules };
|