@graphql-eslint/eslint-plugin 3.14.0-alpha-20221222211539-5e993f5 → 3.14.0-alpha-20221223011223-bd3e820
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cjs/configs/index.js +10 -10
- package/cjs/documents.js +5 -5
- package/cjs/estree-converter/converter.js +2 -2
- package/cjs/estree-converter/index.js +3 -3
- package/cjs/estree-converter/utils.js +2 -2
- package/cjs/flat-configs.js +36 -0
- package/cjs/index.js +16 -14
- package/cjs/parser.js +13 -13
- package/cjs/processor.js +2 -2
- package/cjs/rules/alphabetize.js +7 -7
- package/cjs/rules/graphql-js-validation.js +9 -9
- package/cjs/rules/index.js +66 -66
- package/cjs/rules/lone-executable-definition.js +4 -4
- package/cjs/rules/match-document-filename.js +4 -4
- package/cjs/rules/naming-convention.js +6 -6
- package/cjs/rules/no-anonymous-operations.js +2 -2
- package/cjs/rules/no-deprecated.js +2 -2
- package/cjs/rules/no-one-place-fragments.js +3 -4
- package/cjs/rules/no-root-type.js +3 -3
- package/cjs/rules/no-scalar-result-type-on-mutation.js +2 -2
- package/cjs/rules/no-unreachable-types.js +3 -3
- package/cjs/rules/no-unused-fields.js +3 -3
- package/cjs/rules/relay-arguments.js +2 -2
- package/cjs/rules/relay-edge-types.js +6 -6
- package/cjs/rules/relay-page-info.js +5 -5
- package/cjs/rules/require-deprecation-date.js +2 -2
- package/cjs/rules/require-deprecation-reason.js +2 -2
- package/cjs/rules/require-description.js +8 -8
- package/cjs/rules/require-field-of-type-query-in-mutation-result.js +3 -3
- package/cjs/rules/require-id-when-available.js +8 -8
- package/cjs/rules/selection-set-depth.js +5 -5
- package/cjs/rules/strict-id-in-types.js +7 -7
- package/cjs/rules/unique-fragment-name.js +4 -4
- package/cjs/rules/unique-operation-name.js +2 -2
- package/cjs/schema.js +2 -2
- package/esm/cache.js +25 -0
- package/esm/configs/base.js +4 -0
- package/esm/configs/index.js +12 -0
- package/esm/configs/operations-all.js +29 -0
- package/esm/configs/operations-recommended.js +53 -0
- package/esm/configs/relay.js +9 -0
- package/esm/configs/schema-all.js +22 -0
- package/esm/configs/schema-recommended.js +49 -0
- package/esm/documents.js +144 -0
- package/esm/estree-converter/converter.js +58 -0
- package/esm/estree-converter/index.js +3 -0
- package/esm/estree-converter/types.js +1 -0
- package/esm/estree-converter/utils.js +102 -0
- package/esm/flat-configs.js +33 -0
- package/esm/graphql-config.js +49 -0
- package/esm/index.js +9 -0
- package/esm/package.json +1 -0
- package/esm/parser.js +56 -0
- package/esm/processor.js +75 -0
- package/esm/rules/alphabetize.js +344 -0
- package/esm/rules/description-style.js +75 -0
- package/esm/rules/graphql-js-validation.js +498 -0
- package/esm/rules/index.js +71 -0
- package/esm/rules/input-name.js +133 -0
- package/esm/rules/lone-executable-definition.js +85 -0
- package/esm/rules/match-document-filename.js +232 -0
- package/esm/rules/naming-convention.js +307 -0
- package/esm/rules/no-anonymous-operations.js +64 -0
- package/esm/rules/no-case-insensitive-enum-values-duplicates.js +58 -0
- package/esm/rules/no-deprecated.js +121 -0
- package/esm/rules/no-duplicate-fields.js +109 -0
- package/esm/rules/no-hashtag-description.js +86 -0
- package/esm/rules/no-one-place-fragments.js +80 -0
- package/esm/rules/no-root-type.js +83 -0
- package/esm/rules/no-scalar-result-type-on-mutation.js +63 -0
- package/esm/rules/no-typename-prefix.js +62 -0
- package/esm/rules/no-unreachable-types.js +154 -0
- package/esm/rules/no-unused-fields.js +127 -0
- package/esm/rules/relay-arguments.js +118 -0
- package/esm/rules/relay-connection-types.js +104 -0
- package/esm/rules/relay-edge-types.js +186 -0
- package/esm/rules/relay-page-info.js +97 -0
- package/esm/rules/require-deprecation-date.js +120 -0
- package/esm/rules/require-deprecation-reason.js +53 -0
- package/esm/rules/require-description.js +190 -0
- package/esm/rules/require-field-of-type-query-in-mutation-result.js +69 -0
- package/esm/rules/require-id-when-available.js +196 -0
- package/esm/rules/require-nullable-fields-with-oneof.js +58 -0
- package/esm/rules/require-type-pattern-with-oneof.js +57 -0
- package/esm/rules/selection-set-depth.js +131 -0
- package/esm/rules/strict-id-in-types.js +159 -0
- package/esm/rules/unique-fragment-name.js +86 -0
- package/esm/rules/unique-operation-name.js +62 -0
- package/esm/schema.js +37 -0
- package/esm/testkit.js +181 -0
- package/esm/types.js +1 -0
- package/esm/utils.js +83 -0
- package/package.json +10 -1
- package/typings/estree-converter/converter.d.cts +1 -1
- package/typings/estree-converter/converter.d.ts +1 -1
- package/typings/estree-converter/index.d.cts +3 -3
- package/typings/estree-converter/index.d.ts +3 -3
- package/typings/estree-converter/types.d.cts +3 -3
- package/typings/estree-converter/types.d.ts +3 -3
- package/typings/estree-converter/utils.d.cts +2 -2
- package/typings/estree-converter/utils.d.ts +2 -2
- package/typings/flat-configs.d.cts +248 -0
- package/typings/flat-configs.d.ts +248 -0
- package/typings/graphql-config.d.cts +1 -1
- package/typings/graphql-config.d.ts +1 -1
- package/typings/index.d.cts +8 -7
- package/typings/index.d.ts +8 -7
- package/typings/parser.d.cts +1 -1
- package/typings/parser.d.ts +1 -1
- package/typings/rules/alphabetize.d.cts +1 -1
- package/typings/rules/alphabetize.d.ts +1 -1
- package/typings/rules/description-style.d.cts +1 -1
- package/typings/rules/description-style.d.ts +1 -1
- package/typings/rules/graphql-js-validation.d.cts +1 -1
- package/typings/rules/graphql-js-validation.d.ts +1 -1
- package/typings/rules/index.d.cts +45 -45
- package/typings/rules/index.d.ts +45 -45
- package/typings/rules/input-name.d.cts +1 -1
- package/typings/rules/input-name.d.ts +1 -1
- package/typings/rules/lone-executable-definition.d.cts +1 -1
- package/typings/rules/lone-executable-definition.d.ts +1 -1
- package/typings/rules/match-document-filename.d.cts +2 -2
- package/typings/rules/match-document-filename.d.ts +2 -2
- package/typings/rules/naming-convention.d.cts +1 -1
- package/typings/rules/naming-convention.d.ts +1 -1
- package/typings/rules/no-anonymous-operations.d.cts +1 -1
- package/typings/rules/no-anonymous-operations.d.ts +1 -1
- package/typings/rules/no-case-insensitive-enum-values-duplicates.d.cts +1 -1
- package/typings/rules/no-case-insensitive-enum-values-duplicates.d.ts +1 -1
- package/typings/rules/no-deprecated.d.cts +1 -1
- package/typings/rules/no-deprecated.d.ts +1 -1
- package/typings/rules/no-duplicate-fields.d.cts +1 -1
- package/typings/rules/no-duplicate-fields.d.ts +1 -1
- package/typings/rules/no-hashtag-description.d.cts +1 -1
- package/typings/rules/no-hashtag-description.d.ts +1 -1
- package/typings/rules/no-one-place-fragments.d.cts +1 -1
- package/typings/rules/no-one-place-fragments.d.ts +1 -1
- package/typings/rules/no-root-type.d.cts +1 -1
- package/typings/rules/no-root-type.d.ts +1 -1
- package/typings/rules/no-scalar-result-type-on-mutation.d.cts +1 -1
- package/typings/rules/no-scalar-result-type-on-mutation.d.ts +1 -1
- package/typings/rules/no-typename-prefix.d.cts +1 -1
- package/typings/rules/no-typename-prefix.d.ts +1 -1
- package/typings/rules/no-unreachable-types.d.cts +1 -1
- package/typings/rules/no-unreachable-types.d.ts +1 -1
- package/typings/rules/no-unused-fields.d.cts +1 -1
- package/typings/rules/no-unused-fields.d.ts +1 -1
- package/typings/rules/relay-arguments.d.cts +1 -1
- package/typings/rules/relay-arguments.d.ts +1 -1
- package/typings/rules/relay-connection-types.d.cts +1 -1
- package/typings/rules/relay-connection-types.d.ts +1 -1
- package/typings/rules/relay-edge-types.d.cts +1 -1
- package/typings/rules/relay-edge-types.d.ts +1 -1
- package/typings/rules/relay-page-info.d.cts +1 -1
- package/typings/rules/relay-page-info.d.ts +1 -1
- package/typings/rules/require-deprecation-date.d.cts +1 -1
- package/typings/rules/require-deprecation-date.d.ts +1 -1
- package/typings/rules/require-deprecation-reason.d.cts +1 -1
- package/typings/rules/require-deprecation-reason.d.ts +1 -1
- package/typings/rules/require-description.d.cts +1 -1
- package/typings/rules/require-description.d.ts +1 -1
- package/typings/rules/require-field-of-type-query-in-mutation-result.d.cts +1 -1
- package/typings/rules/require-field-of-type-query-in-mutation-result.d.ts +1 -1
- package/typings/rules/require-id-when-available.d.cts +1 -1
- package/typings/rules/require-id-when-available.d.ts +1 -1
- package/typings/rules/require-nullable-fields-with-oneof.d.cts +1 -1
- package/typings/rules/require-nullable-fields-with-oneof.d.ts +1 -1
- package/typings/rules/require-type-pattern-with-oneof.d.cts +1 -1
- package/typings/rules/require-type-pattern-with-oneof.d.ts +1 -1
- package/typings/rules/selection-set-depth.d.cts +1 -1
- package/typings/rules/selection-set-depth.d.ts +1 -1
- package/typings/rules/strict-id-in-types.d.cts +1 -1
- package/typings/rules/strict-id-in-types.d.ts +1 -1
- package/typings/rules/unique-fragment-name.d.cts +2 -2
- package/typings/rules/unique-fragment-name.d.ts +2 -2
- package/typings/rules/unique-operation-name.d.cts +1 -1
- package/typings/rules/unique-operation-name.d.ts +1 -1
- package/typings/schema.d.cts +1 -1
- package/typings/schema.d.ts +1 -1
- package/typings/testkit.d.cts +3 -3
- package/typings/testkit.d.ts +3 -3
- package/typings/types.d.cts +2 -2
- package/typings/types.d.ts +2 -2
- package/typings/utils.d.cts +2 -2
- package/typings/utils.d.ts +2 -2
@@ -0,0 +1,86 @@
|
|
1
|
+
import { TokenKind } from 'graphql';
|
2
|
+
const HASHTAG_COMMENT = 'HASHTAG_COMMENT';
|
3
|
+
export const rule = {
|
4
|
+
meta: {
|
5
|
+
type: 'suggestion',
|
6
|
+
hasSuggestions: true,
|
7
|
+
schema: [],
|
8
|
+
messages: {
|
9
|
+
[HASHTAG_COMMENT]: 'Using hashtag `#` for adding GraphQL descriptions is not allowed. Prefer using `"""` for multiline, or `"` for a single line description.',
|
10
|
+
},
|
11
|
+
docs: {
|
12
|
+
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.',
|
13
|
+
category: 'Schema',
|
14
|
+
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-hashtag-description.md',
|
15
|
+
examples: [
|
16
|
+
{
|
17
|
+
title: 'Incorrect',
|
18
|
+
code: /* GraphQL */ `
|
19
|
+
# Represents a user
|
20
|
+
type User {
|
21
|
+
id: ID!
|
22
|
+
name: String
|
23
|
+
}
|
24
|
+
`,
|
25
|
+
},
|
26
|
+
{
|
27
|
+
title: 'Correct',
|
28
|
+
code: /* GraphQL */ `
|
29
|
+
" Represents a user "
|
30
|
+
type User {
|
31
|
+
id: ID!
|
32
|
+
name: String
|
33
|
+
}
|
34
|
+
`,
|
35
|
+
},
|
36
|
+
{
|
37
|
+
title: 'Correct',
|
38
|
+
code: /* GraphQL */ `
|
39
|
+
# This file defines the basic User type.
|
40
|
+
# This comment is valid because it's not attached specifically to an AST object.
|
41
|
+
|
42
|
+
" Represents a user "
|
43
|
+
type User {
|
44
|
+
id: ID! # This one is also valid, since it comes after the AST object
|
45
|
+
name: String
|
46
|
+
}
|
47
|
+
`,
|
48
|
+
},
|
49
|
+
],
|
50
|
+
recommended: true,
|
51
|
+
},
|
52
|
+
},
|
53
|
+
create(context) {
|
54
|
+
const selector = 'Document[definitions.0.kind!=/^(OperationDefinition|FragmentDefinition)$/]';
|
55
|
+
return {
|
56
|
+
[selector](node) {
|
57
|
+
const rawNode = node.rawNode();
|
58
|
+
let token = rawNode.loc.startToken;
|
59
|
+
while (token) {
|
60
|
+
const { kind, prev, next, value, line, column } = token;
|
61
|
+
if (kind === TokenKind.COMMENT && prev && next) {
|
62
|
+
const isEslintComment = value.trimStart().startsWith('eslint');
|
63
|
+
const linesAfter = next.line - line;
|
64
|
+
if (!isEslintComment &&
|
65
|
+
line !== prev.line &&
|
66
|
+
next.kind === TokenKind.NAME &&
|
67
|
+
linesAfter < 2) {
|
68
|
+
context.report({
|
69
|
+
messageId: HASHTAG_COMMENT,
|
70
|
+
loc: {
|
71
|
+
line,
|
72
|
+
column: column - 1,
|
73
|
+
},
|
74
|
+
suggest: ['"""', '"'].map(descriptionSyntax => ({
|
75
|
+
desc: `Replace with \`${descriptionSyntax}\` description syntax`,
|
76
|
+
fix: fixer => fixer.replaceTextRange([token.start, token.end], [descriptionSyntax, value.trim(), descriptionSyntax].join('')),
|
77
|
+
})),
|
78
|
+
});
|
79
|
+
}
|
80
|
+
}
|
81
|
+
token = next;
|
82
|
+
}
|
83
|
+
},
|
84
|
+
};
|
85
|
+
},
|
86
|
+
};
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import { CWD, requireSiblingsOperations } from '../utils.js';
|
2
|
+
import { relative } from 'path';
|
3
|
+
import { visit } from 'graphql';
|
4
|
+
const RULE_ID = 'no-one-place-fragments';
|
5
|
+
export const rule = {
|
6
|
+
meta: {
|
7
|
+
type: 'suggestion',
|
8
|
+
docs: {
|
9
|
+
category: 'Operations',
|
10
|
+
description: 'Disallow fragments that are used only in one place.',
|
11
|
+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
12
|
+
examples: [
|
13
|
+
{
|
14
|
+
title: 'Incorrect',
|
15
|
+
code: /* GraphQL */ `
|
16
|
+
fragment UserFields on User {
|
17
|
+
id
|
18
|
+
}
|
19
|
+
|
20
|
+
{
|
21
|
+
user {
|
22
|
+
...UserFields
|
23
|
+
friends {
|
24
|
+
...UserFields
|
25
|
+
}
|
26
|
+
}
|
27
|
+
}
|
28
|
+
`,
|
29
|
+
},
|
30
|
+
{
|
31
|
+
title: 'Correct',
|
32
|
+
code: /* GraphQL */ `
|
33
|
+
fragment UserFields on User {
|
34
|
+
id
|
35
|
+
}
|
36
|
+
|
37
|
+
{
|
38
|
+
user {
|
39
|
+
...UserFields
|
40
|
+
}
|
41
|
+
}
|
42
|
+
`,
|
43
|
+
},
|
44
|
+
],
|
45
|
+
requiresSiblings: true,
|
46
|
+
},
|
47
|
+
messages: {
|
48
|
+
[RULE_ID]: 'Fragment `{{fragmentName}}` used only once. Inline him in "{{filePath}}".',
|
49
|
+
},
|
50
|
+
schema: [],
|
51
|
+
},
|
52
|
+
create(context) {
|
53
|
+
const operations = requireSiblingsOperations(RULE_ID, context);
|
54
|
+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
|
55
|
+
const usedFragmentsMap = Object.create(null);
|
56
|
+
for (const { document, filePath } of allDocuments) {
|
57
|
+
const relativeFilePath = relative(CWD, filePath);
|
58
|
+
visit(document, {
|
59
|
+
FragmentSpread({ name }) {
|
60
|
+
const spreadName = name.value;
|
61
|
+
usedFragmentsMap[spreadName] || (usedFragmentsMap[spreadName] = []);
|
62
|
+
usedFragmentsMap[spreadName].push(relativeFilePath);
|
63
|
+
},
|
64
|
+
});
|
65
|
+
}
|
66
|
+
return {
|
67
|
+
'FragmentDefinition > Name'(node) {
|
68
|
+
const fragmentName = node.value;
|
69
|
+
const fragmentUsage = usedFragmentsMap[fragmentName];
|
70
|
+
if (fragmentUsage.length === 1) {
|
71
|
+
context.report({
|
72
|
+
node,
|
73
|
+
messageId: RULE_ID,
|
74
|
+
data: { fragmentName, filePath: fragmentUsage[0] },
|
75
|
+
});
|
76
|
+
}
|
77
|
+
},
|
78
|
+
};
|
79
|
+
},
|
80
|
+
};
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import { ARRAY_DEFAULT_OPTIONS, requireGraphQLSchemaFromContext } from '../utils.js';
|
2
|
+
const schema = {
|
3
|
+
type: 'array',
|
4
|
+
minItems: 1,
|
5
|
+
maxItems: 1,
|
6
|
+
items: {
|
7
|
+
type: 'object',
|
8
|
+
additionalProperties: false,
|
9
|
+
required: ['disallow'],
|
10
|
+
properties: {
|
11
|
+
disallow: {
|
12
|
+
...ARRAY_DEFAULT_OPTIONS,
|
13
|
+
items: {
|
14
|
+
enum: ['mutation', 'subscription'],
|
15
|
+
},
|
16
|
+
},
|
17
|
+
},
|
18
|
+
},
|
19
|
+
};
|
20
|
+
export const rule = {
|
21
|
+
meta: {
|
22
|
+
type: 'suggestion',
|
23
|
+
hasSuggestions: true,
|
24
|
+
docs: {
|
25
|
+
category: 'Schema',
|
26
|
+
description: 'Disallow using root types `mutation` and/or `subscription`.',
|
27
|
+
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-root-type.md',
|
28
|
+
requiresSchema: true,
|
29
|
+
isDisabledForAllConfig: true,
|
30
|
+
examples: [
|
31
|
+
{
|
32
|
+
title: 'Incorrect',
|
33
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
34
|
+
code: /* GraphQL */ `
|
35
|
+
type Mutation {
|
36
|
+
createUser(input: CreateUserInput!): User!
|
37
|
+
}
|
38
|
+
`,
|
39
|
+
},
|
40
|
+
{
|
41
|
+
title: 'Correct',
|
42
|
+
usage: [{ disallow: ['mutation', 'subscription'] }],
|
43
|
+
code: /* GraphQL */ `
|
44
|
+
type Query {
|
45
|
+
users: [User!]!
|
46
|
+
}
|
47
|
+
`,
|
48
|
+
},
|
49
|
+
],
|
50
|
+
},
|
51
|
+
schema,
|
52
|
+
},
|
53
|
+
create(context) {
|
54
|
+
const schema = requireGraphQLSchemaFromContext('no-root-type', context);
|
55
|
+
const disallow = new Set(context.options[0].disallow);
|
56
|
+
const rootTypeNames = [
|
57
|
+
disallow.has('mutation') && schema.getMutationType(),
|
58
|
+
disallow.has('subscription') && schema.getSubscriptionType(),
|
59
|
+
]
|
60
|
+
.filter(Boolean)
|
61
|
+
.map(type => type.name)
|
62
|
+
.join('|');
|
63
|
+
if (!rootTypeNames) {
|
64
|
+
return {};
|
65
|
+
}
|
66
|
+
const selector = `:matches(ObjectTypeDefinition, ObjectTypeExtension) > .name[value=/^(${rootTypeNames})$/]`;
|
67
|
+
return {
|
68
|
+
[selector](node) {
|
69
|
+
const typeName = node.value;
|
70
|
+
context.report({
|
71
|
+
node,
|
72
|
+
message: `Root type \`${typeName}\` is forbidden.`,
|
73
|
+
suggest: [
|
74
|
+
{
|
75
|
+
desc: `Remove \`${typeName}\` type`,
|
76
|
+
fix: fixer => fixer.remove(node.parent),
|
77
|
+
},
|
78
|
+
],
|
79
|
+
});
|
80
|
+
},
|
81
|
+
};
|
82
|
+
},
|
83
|
+
};
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { isScalarType } from 'graphql';
|
2
|
+
import { requireGraphQLSchemaFromContext } from '../utils.js';
|
3
|
+
const RULE_ID = 'no-scalar-result-type-on-mutation';
|
4
|
+
export const rule = {
|
5
|
+
meta: {
|
6
|
+
type: 'suggestion',
|
7
|
+
hasSuggestions: true,
|
8
|
+
docs: {
|
9
|
+
category: 'Schema',
|
10
|
+
description: 'Avoid scalar result type on mutation type to make sure to return a valid state.',
|
11
|
+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
12
|
+
requiresSchema: true,
|
13
|
+
examples: [
|
14
|
+
{
|
15
|
+
title: 'Incorrect',
|
16
|
+
code: /* GraphQL */ `
|
17
|
+
type Mutation {
|
18
|
+
createUser: Boolean
|
19
|
+
}
|
20
|
+
`,
|
21
|
+
},
|
22
|
+
{
|
23
|
+
title: 'Correct',
|
24
|
+
code: /* GraphQL */ `
|
25
|
+
type Mutation {
|
26
|
+
createUser: User!
|
27
|
+
}
|
28
|
+
`,
|
29
|
+
},
|
30
|
+
],
|
31
|
+
},
|
32
|
+
schema: [],
|
33
|
+
},
|
34
|
+
create(context) {
|
35
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
|
36
|
+
const mutationType = schema.getMutationType();
|
37
|
+
if (!mutationType) {
|
38
|
+
return {};
|
39
|
+
}
|
40
|
+
const selector = [
|
41
|
+
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=${mutationType.name}]`,
|
42
|
+
'> FieldDefinition > .gqlType Name',
|
43
|
+
].join(' ');
|
44
|
+
return {
|
45
|
+
[selector](node) {
|
46
|
+
const typeName = node.value;
|
47
|
+
const graphQLType = schema.getType(typeName);
|
48
|
+
if (isScalarType(graphQLType)) {
|
49
|
+
context.report({
|
50
|
+
node,
|
51
|
+
message: `Unexpected scalar result type \`${typeName}\`.`,
|
52
|
+
suggest: [
|
53
|
+
{
|
54
|
+
desc: `Remove \`${typeName}\``,
|
55
|
+
fix: fixer => fixer.remove(node),
|
56
|
+
},
|
57
|
+
],
|
58
|
+
});
|
59
|
+
}
|
60
|
+
},
|
61
|
+
};
|
62
|
+
},
|
63
|
+
};
|
@@ -0,0 +1,62 @@
|
|
1
|
+
const NO_TYPENAME_PREFIX = 'NO_TYPENAME_PREFIX';
|
2
|
+
export const rule = {
|
3
|
+
meta: {
|
4
|
+
type: 'suggestion',
|
5
|
+
hasSuggestions: true,
|
6
|
+
docs: {
|
7
|
+
category: 'Schema',
|
8
|
+
description: 'Enforces users to avoid using the type name in a field name while defining your schema.',
|
9
|
+
recommended: true,
|
10
|
+
url: 'https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/no-typename-prefix.md',
|
11
|
+
examples: [
|
12
|
+
{
|
13
|
+
title: 'Incorrect',
|
14
|
+
code: /* GraphQL */ `
|
15
|
+
type User {
|
16
|
+
userId: ID!
|
17
|
+
}
|
18
|
+
`,
|
19
|
+
},
|
20
|
+
{
|
21
|
+
title: 'Correct',
|
22
|
+
code: /* GraphQL */ `
|
23
|
+
type User {
|
24
|
+
id: ID!
|
25
|
+
}
|
26
|
+
`,
|
27
|
+
},
|
28
|
+
],
|
29
|
+
},
|
30
|
+
messages: {
|
31
|
+
[NO_TYPENAME_PREFIX]: 'Field "{{ fieldName }}" starts with the name of the parent type "{{ typeName }}"',
|
32
|
+
},
|
33
|
+
schema: [],
|
34
|
+
},
|
35
|
+
create(context) {
|
36
|
+
return {
|
37
|
+
'ObjectTypeDefinition, ObjectTypeExtension, InterfaceTypeDefinition, InterfaceTypeExtension'(node) {
|
38
|
+
const typeName = node.name.value;
|
39
|
+
const lowerTypeName = typeName.toLowerCase();
|
40
|
+
for (const field of node.fields) {
|
41
|
+
const fieldName = field.name.value;
|
42
|
+
if (fieldName.toLowerCase().startsWith(lowerTypeName)) {
|
43
|
+
context.report({
|
44
|
+
data: {
|
45
|
+
fieldName,
|
46
|
+
typeName,
|
47
|
+
},
|
48
|
+
messageId: NO_TYPENAME_PREFIX,
|
49
|
+
node: field.name,
|
50
|
+
suggest: [
|
51
|
+
{
|
52
|
+
desc: `Remove \`${fieldName.slice(0, typeName.length)}\` prefix`,
|
53
|
+
fix: fixer => fixer.replaceText(field.name, fieldName.replace(new RegExp(`^${typeName}`, 'i'), '')),
|
54
|
+
},
|
55
|
+
],
|
56
|
+
});
|
57
|
+
}
|
58
|
+
}
|
59
|
+
},
|
60
|
+
};
|
61
|
+
},
|
62
|
+
};
|
@@ -0,0 +1,154 @@
|
|
1
|
+
import { isInterfaceType, Kind, visit, DirectiveLocation, } from 'graphql';
|
2
|
+
import lowerCase from 'lodash.lowercase';
|
3
|
+
import { getTypeName, requireGraphQLSchemaFromContext } from '../utils.js';
|
4
|
+
const RULE_ID = 'no-unreachable-types';
|
5
|
+
const KINDS = [
|
6
|
+
Kind.DIRECTIVE_DEFINITION,
|
7
|
+
Kind.OBJECT_TYPE_DEFINITION,
|
8
|
+
Kind.OBJECT_TYPE_EXTENSION,
|
9
|
+
Kind.INTERFACE_TYPE_DEFINITION,
|
10
|
+
Kind.INTERFACE_TYPE_EXTENSION,
|
11
|
+
Kind.SCALAR_TYPE_DEFINITION,
|
12
|
+
Kind.SCALAR_TYPE_EXTENSION,
|
13
|
+
Kind.INPUT_OBJECT_TYPE_DEFINITION,
|
14
|
+
Kind.INPUT_OBJECT_TYPE_EXTENSION,
|
15
|
+
Kind.UNION_TYPE_DEFINITION,
|
16
|
+
Kind.UNION_TYPE_EXTENSION,
|
17
|
+
Kind.ENUM_TYPE_DEFINITION,
|
18
|
+
Kind.ENUM_TYPE_EXTENSION,
|
19
|
+
];
|
20
|
+
let reachableTypesCache;
|
21
|
+
const RequestDirectiveLocations = new Set([
|
22
|
+
DirectiveLocation.QUERY,
|
23
|
+
DirectiveLocation.MUTATION,
|
24
|
+
DirectiveLocation.SUBSCRIPTION,
|
25
|
+
DirectiveLocation.FIELD,
|
26
|
+
DirectiveLocation.FRAGMENT_DEFINITION,
|
27
|
+
DirectiveLocation.FRAGMENT_SPREAD,
|
28
|
+
DirectiveLocation.INLINE_FRAGMENT,
|
29
|
+
DirectiveLocation.VARIABLE_DEFINITION,
|
30
|
+
]);
|
31
|
+
function getReachableTypes(schema) {
|
32
|
+
// We don't want cache reachableTypes on test environment
|
33
|
+
// Otherwise reachableTypes will be same for all tests
|
34
|
+
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
|
35
|
+
return reachableTypesCache;
|
36
|
+
}
|
37
|
+
const reachableTypes = new Set();
|
38
|
+
const collect = (node) => {
|
39
|
+
const typeName = getTypeName(node);
|
40
|
+
if (reachableTypes.has(typeName)) {
|
41
|
+
return;
|
42
|
+
}
|
43
|
+
reachableTypes.add(typeName);
|
44
|
+
const type = schema.getType(typeName) || schema.getDirective(typeName);
|
45
|
+
if (isInterfaceType(type)) {
|
46
|
+
const { objects, interfaces } = schema.getImplementations(type);
|
47
|
+
for (const { astNode } of [...objects, ...interfaces]) {
|
48
|
+
visit(astNode, visitor);
|
49
|
+
}
|
50
|
+
}
|
51
|
+
else if (type.astNode) {
|
52
|
+
// astNode can be undefined for ID, String, Boolean
|
53
|
+
visit(type.astNode, visitor);
|
54
|
+
}
|
55
|
+
};
|
56
|
+
const visitor = {
|
57
|
+
InterfaceTypeDefinition: collect,
|
58
|
+
ObjectTypeDefinition: collect,
|
59
|
+
InputValueDefinition: collect,
|
60
|
+
UnionTypeDefinition: collect,
|
61
|
+
FieldDefinition: collect,
|
62
|
+
Directive: collect,
|
63
|
+
NamedType: collect,
|
64
|
+
};
|
65
|
+
for (const type of [
|
66
|
+
schema,
|
67
|
+
schema.getQueryType(),
|
68
|
+
schema.getMutationType(),
|
69
|
+
schema.getSubscriptionType(),
|
70
|
+
]) {
|
71
|
+
// if schema don't have Query type, schema.astNode will be undefined
|
72
|
+
if (type === null || type === void 0 ? void 0 : type.astNode) {
|
73
|
+
visit(type.astNode, visitor);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
for (const node of schema.getDirectives()) {
|
77
|
+
if (node.locations.some(location => RequestDirectiveLocations.has(location))) {
|
78
|
+
reachableTypes.add(node.name);
|
79
|
+
}
|
80
|
+
}
|
81
|
+
reachableTypesCache = reachableTypes;
|
82
|
+
return reachableTypesCache;
|
83
|
+
}
|
84
|
+
export const rule = {
|
85
|
+
meta: {
|
86
|
+
messages: {
|
87
|
+
[RULE_ID]: '{{ type }} `{{ typeName }}` is unreachable.',
|
88
|
+
},
|
89
|
+
docs: {
|
90
|
+
description: 'Requires all types to be reachable at some level by root level fields.',
|
91
|
+
category: 'Schema',
|
92
|
+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
93
|
+
requiresSchema: true,
|
94
|
+
examples: [
|
95
|
+
{
|
96
|
+
title: 'Incorrect',
|
97
|
+
code: /* GraphQL */ `
|
98
|
+
type User {
|
99
|
+
id: ID!
|
100
|
+
name: String
|
101
|
+
}
|
102
|
+
|
103
|
+
type Query {
|
104
|
+
me: String
|
105
|
+
}
|
106
|
+
`,
|
107
|
+
},
|
108
|
+
{
|
109
|
+
title: 'Correct',
|
110
|
+
code: /* GraphQL */ `
|
111
|
+
type User {
|
112
|
+
id: ID!
|
113
|
+
name: String
|
114
|
+
}
|
115
|
+
|
116
|
+
type Query {
|
117
|
+
me: User
|
118
|
+
}
|
119
|
+
`,
|
120
|
+
},
|
121
|
+
],
|
122
|
+
recommended: true,
|
123
|
+
},
|
124
|
+
type: 'suggestion',
|
125
|
+
schema: [],
|
126
|
+
hasSuggestions: true,
|
127
|
+
},
|
128
|
+
create(context) {
|
129
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
|
130
|
+
const reachableTypes = getReachableTypes(schema);
|
131
|
+
return {
|
132
|
+
[`:matches(${KINDS}) > .name`](node) {
|
133
|
+
const typeName = node.value;
|
134
|
+
if (!reachableTypes.has(typeName)) {
|
135
|
+
const type = lowerCase(node.parent.kind.replace(/(Extension|Definition)$/, ''));
|
136
|
+
context.report({
|
137
|
+
node,
|
138
|
+
messageId: RULE_ID,
|
139
|
+
data: {
|
140
|
+
type: type[0].toUpperCase() + type.slice(1),
|
141
|
+
typeName,
|
142
|
+
},
|
143
|
+
suggest: [
|
144
|
+
{
|
145
|
+
desc: `Remove \`${typeName}\``,
|
146
|
+
fix: fixer => fixer.remove(node.parent),
|
147
|
+
},
|
148
|
+
],
|
149
|
+
});
|
150
|
+
}
|
151
|
+
},
|
152
|
+
};
|
153
|
+
},
|
154
|
+
};
|
@@ -0,0 +1,127 @@
|
|
1
|
+
import { TypeInfo, visit, visitWithTypeInfo } from 'graphql';
|
2
|
+
import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils.js';
|
3
|
+
const RULE_ID = 'no-unused-fields';
|
4
|
+
let usedFieldsCache;
|
5
|
+
function getUsedFields(schema, operations) {
|
6
|
+
// We don't want cache usedFields on test environment
|
7
|
+
// Otherwise usedFields will be same for all tests
|
8
|
+
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
|
9
|
+
return usedFieldsCache;
|
10
|
+
}
|
11
|
+
const usedFields = Object.create(null);
|
12
|
+
const typeInfo = new TypeInfo(schema);
|
13
|
+
const visitor = visitWithTypeInfo(typeInfo, {
|
14
|
+
Field(node) {
|
15
|
+
var _a;
|
16
|
+
const fieldDef = typeInfo.getFieldDef();
|
17
|
+
if (!fieldDef) {
|
18
|
+
// skip visiting this node if field is not defined in schema
|
19
|
+
return false;
|
20
|
+
}
|
21
|
+
const parentTypeName = typeInfo.getParentType().name;
|
22
|
+
const fieldName = node.name.value;
|
23
|
+
(_a = usedFields[parentTypeName]) !== null && _a !== void 0 ? _a : (usedFields[parentTypeName] = new Set());
|
24
|
+
usedFields[parentTypeName].add(fieldName);
|
25
|
+
},
|
26
|
+
});
|
27
|
+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
|
28
|
+
for (const { document } of allDocuments) {
|
29
|
+
visit(document, visitor);
|
30
|
+
}
|
31
|
+
usedFieldsCache = usedFields;
|
32
|
+
return usedFieldsCache;
|
33
|
+
}
|
34
|
+
export const rule = {
|
35
|
+
meta: {
|
36
|
+
messages: {
|
37
|
+
[RULE_ID]: 'Field "{{fieldName}}" is unused',
|
38
|
+
},
|
39
|
+
docs: {
|
40
|
+
description: 'Requires all fields to be used at some level by siblings operations.',
|
41
|
+
category: 'Schema',
|
42
|
+
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
|
43
|
+
requiresSiblings: true,
|
44
|
+
requiresSchema: true,
|
45
|
+
isDisabledForAllConfig: true,
|
46
|
+
examples: [
|
47
|
+
{
|
48
|
+
title: 'Incorrect',
|
49
|
+
code: /* GraphQL */ `
|
50
|
+
type User {
|
51
|
+
id: ID!
|
52
|
+
name: String
|
53
|
+
someUnusedField: String
|
54
|
+
}
|
55
|
+
|
56
|
+
type Query {
|
57
|
+
me: User
|
58
|
+
}
|
59
|
+
|
60
|
+
query {
|
61
|
+
me {
|
62
|
+
id
|
63
|
+
name
|
64
|
+
}
|
65
|
+
}
|
66
|
+
`,
|
67
|
+
},
|
68
|
+
{
|
69
|
+
title: 'Correct',
|
70
|
+
code: /* GraphQL */ `
|
71
|
+
type User {
|
72
|
+
id: ID!
|
73
|
+
name: String
|
74
|
+
}
|
75
|
+
|
76
|
+
type Query {
|
77
|
+
me: User
|
78
|
+
}
|
79
|
+
|
80
|
+
query {
|
81
|
+
me {
|
82
|
+
id
|
83
|
+
name
|
84
|
+
}
|
85
|
+
}
|
86
|
+
`,
|
87
|
+
},
|
88
|
+
],
|
89
|
+
},
|
90
|
+
type: 'suggestion',
|
91
|
+
schema: [],
|
92
|
+
hasSuggestions: true,
|
93
|
+
},
|
94
|
+
create(context) {
|
95
|
+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
|
96
|
+
const siblingsOperations = requireSiblingsOperations(RULE_ID, context);
|
97
|
+
const usedFields = getUsedFields(schema, siblingsOperations);
|
98
|
+
return {
|
99
|
+
FieldDefinition(node) {
|
100
|
+
var _a;
|
101
|
+
const fieldName = node.name.value;
|
102
|
+
const parentTypeName = node.parent.name.value;
|
103
|
+
const isUsed = (_a = usedFields[parentTypeName]) === null || _a === void 0 ? void 0 : _a.has(fieldName);
|
104
|
+
if (isUsed) {
|
105
|
+
return;
|
106
|
+
}
|
107
|
+
context.report({
|
108
|
+
node: node.name,
|
109
|
+
messageId: RULE_ID,
|
110
|
+
data: { fieldName },
|
111
|
+
suggest: [
|
112
|
+
{
|
113
|
+
desc: `Remove \`${fieldName}\` field`,
|
114
|
+
fix(fixer) {
|
115
|
+
const sourceCode = context.getSourceCode();
|
116
|
+
const tokenBefore = sourceCode.getTokenBefore(node);
|
117
|
+
const tokenAfter = sourceCode.getTokenAfter(node);
|
118
|
+
const isEmptyType = tokenBefore.type === '{' && tokenAfter.type === '}';
|
119
|
+
return fixer.remove((isEmptyType ? node.parent : node));
|
120
|
+
},
|
121
|
+
},
|
122
|
+
],
|
123
|
+
});
|
124
|
+
},
|
125
|
+
};
|
126
|
+
},
|
127
|
+
};
|