@graphql-eslint/eslint-plugin 3.14.0-alpha-20221222211346-788e7eb → 3.14.0-alpha-20221223011223-bd3e820

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. package/cjs/configs/index.js +10 -10
  2. package/cjs/documents.js +5 -5
  3. package/cjs/estree-converter/converter.js +2 -2
  4. package/cjs/estree-converter/index.js +3 -3
  5. package/cjs/estree-converter/utils.js +2 -2
  6. package/cjs/flat-configs.js +36 -0
  7. package/cjs/index.js +16 -14
  8. package/cjs/parser.js +13 -13
  9. package/cjs/processor.js +2 -2
  10. package/cjs/rules/alphabetize.js +7 -7
  11. package/cjs/rules/graphql-js-validation.js +9 -9
  12. package/cjs/rules/index.js +66 -66
  13. package/cjs/rules/lone-executable-definition.js +4 -4
  14. package/cjs/rules/match-document-filename.js +4 -4
  15. package/cjs/rules/naming-convention.js +6 -6
  16. package/cjs/rules/no-anonymous-operations.js +2 -2
  17. package/cjs/rules/no-deprecated.js +2 -2
  18. package/cjs/rules/no-one-place-fragments.js +3 -4
  19. package/cjs/rules/no-root-type.js +3 -3
  20. package/cjs/rules/no-scalar-result-type-on-mutation.js +2 -2
  21. package/cjs/rules/no-unreachable-types.js +3 -3
  22. package/cjs/rules/no-unused-fields.js +3 -3
  23. package/cjs/rules/relay-arguments.js +2 -2
  24. package/cjs/rules/relay-edge-types.js +6 -6
  25. package/cjs/rules/relay-page-info.js +5 -5
  26. package/cjs/rules/require-deprecation-date.js +2 -2
  27. package/cjs/rules/require-deprecation-reason.js +2 -2
  28. package/cjs/rules/require-description.js +8 -8
  29. package/cjs/rules/require-field-of-type-query-in-mutation-result.js +3 -3
  30. package/cjs/rules/require-id-when-available.js +8 -8
  31. package/cjs/rules/selection-set-depth.js +5 -5
  32. package/cjs/rules/strict-id-in-types.js +7 -7
  33. package/cjs/rules/unique-fragment-name.js +4 -4
  34. package/cjs/rules/unique-operation-name.js +2 -2
  35. package/cjs/schema.js +2 -2
  36. package/esm/cache.js +25 -0
  37. package/esm/configs/base.js +4 -0
  38. package/esm/configs/index.js +12 -0
  39. package/esm/configs/operations-all.js +29 -0
  40. package/esm/configs/operations-recommended.js +53 -0
  41. package/esm/configs/relay.js +9 -0
  42. package/esm/configs/schema-all.js +22 -0
  43. package/esm/configs/schema-recommended.js +49 -0
  44. package/esm/documents.js +144 -0
  45. package/esm/estree-converter/converter.js +58 -0
  46. package/esm/estree-converter/index.js +3 -0
  47. package/esm/estree-converter/types.js +1 -0
  48. package/esm/estree-converter/utils.js +102 -0
  49. package/esm/flat-configs.js +33 -0
  50. package/esm/graphql-config.js +49 -0
  51. package/esm/index.js +9 -0
  52. package/esm/package.json +1 -0
  53. package/esm/parser.js +56 -0
  54. package/esm/processor.js +75 -0
  55. package/esm/rules/alphabetize.js +344 -0
  56. package/esm/rules/description-style.js +75 -0
  57. package/esm/rules/graphql-js-validation.js +498 -0
  58. package/esm/rules/index.js +71 -0
  59. package/esm/rules/input-name.js +133 -0
  60. package/esm/rules/lone-executable-definition.js +85 -0
  61. package/esm/rules/match-document-filename.js +232 -0
  62. package/esm/rules/naming-convention.js +307 -0
  63. package/esm/rules/no-anonymous-operations.js +64 -0
  64. package/esm/rules/no-case-insensitive-enum-values-duplicates.js +58 -0
  65. package/esm/rules/no-deprecated.js +121 -0
  66. package/esm/rules/no-duplicate-fields.js +109 -0
  67. package/esm/rules/no-hashtag-description.js +86 -0
  68. package/esm/rules/no-one-place-fragments.js +80 -0
  69. package/esm/rules/no-root-type.js +83 -0
  70. package/esm/rules/no-scalar-result-type-on-mutation.js +63 -0
  71. package/esm/rules/no-typename-prefix.js +62 -0
  72. package/esm/rules/no-unreachable-types.js +154 -0
  73. package/esm/rules/no-unused-fields.js +127 -0
  74. package/esm/rules/relay-arguments.js +118 -0
  75. package/esm/rules/relay-connection-types.js +104 -0
  76. package/esm/rules/relay-edge-types.js +186 -0
  77. package/esm/rules/relay-page-info.js +97 -0
  78. package/esm/rules/require-deprecation-date.js +120 -0
  79. package/esm/rules/require-deprecation-reason.js +53 -0
  80. package/esm/rules/require-description.js +190 -0
  81. package/esm/rules/require-field-of-type-query-in-mutation-result.js +69 -0
  82. package/esm/rules/require-id-when-available.js +196 -0
  83. package/esm/rules/require-nullable-fields-with-oneof.js +58 -0
  84. package/esm/rules/require-type-pattern-with-oneof.js +57 -0
  85. package/esm/rules/selection-set-depth.js +131 -0
  86. package/esm/rules/strict-id-in-types.js +159 -0
  87. package/esm/rules/unique-fragment-name.js +86 -0
  88. package/esm/rules/unique-operation-name.js +62 -0
  89. package/esm/schema.js +37 -0
  90. package/esm/testkit.js +181 -0
  91. package/esm/types.js +1 -0
  92. package/esm/utils.js +83 -0
  93. package/package.json +10 -1
  94. package/typings/estree-converter/converter.d.cts +1 -1
  95. package/typings/estree-converter/converter.d.ts +1 -1
  96. package/typings/estree-converter/index.d.cts +3 -3
  97. package/typings/estree-converter/index.d.ts +3 -3
  98. package/typings/estree-converter/types.d.cts +3 -3
  99. package/typings/estree-converter/types.d.ts +3 -3
  100. package/typings/estree-converter/utils.d.cts +2 -2
  101. package/typings/estree-converter/utils.d.ts +2 -2
  102. package/typings/flat-configs.d.cts +248 -0
  103. package/typings/flat-configs.d.ts +248 -0
  104. package/typings/graphql-config.d.cts +1 -1
  105. package/typings/graphql-config.d.ts +1 -1
  106. package/typings/index.d.cts +8 -7
  107. package/typings/index.d.ts +8 -7
  108. package/typings/parser.d.cts +1 -1
  109. package/typings/parser.d.ts +1 -1
  110. package/typings/rules/alphabetize.d.cts +1 -1
  111. package/typings/rules/alphabetize.d.ts +1 -1
  112. package/typings/rules/description-style.d.cts +1 -1
  113. package/typings/rules/description-style.d.ts +1 -1
  114. package/typings/rules/graphql-js-validation.d.cts +1 -1
  115. package/typings/rules/graphql-js-validation.d.ts +1 -1
  116. package/typings/rules/index.d.cts +45 -45
  117. package/typings/rules/index.d.ts +45 -45
  118. package/typings/rules/input-name.d.cts +1 -1
  119. package/typings/rules/input-name.d.ts +1 -1
  120. package/typings/rules/lone-executable-definition.d.cts +1 -1
  121. package/typings/rules/lone-executable-definition.d.ts +1 -1
  122. package/typings/rules/match-document-filename.d.cts +2 -2
  123. package/typings/rules/match-document-filename.d.ts +2 -2
  124. package/typings/rules/naming-convention.d.cts +1 -1
  125. package/typings/rules/naming-convention.d.ts +1 -1
  126. package/typings/rules/no-anonymous-operations.d.cts +1 -1
  127. package/typings/rules/no-anonymous-operations.d.ts +1 -1
  128. package/typings/rules/no-case-insensitive-enum-values-duplicates.d.cts +1 -1
  129. package/typings/rules/no-case-insensitive-enum-values-duplicates.d.ts +1 -1
  130. package/typings/rules/no-deprecated.d.cts +1 -1
  131. package/typings/rules/no-deprecated.d.ts +1 -1
  132. package/typings/rules/no-duplicate-fields.d.cts +1 -1
  133. package/typings/rules/no-duplicate-fields.d.ts +1 -1
  134. package/typings/rules/no-hashtag-description.d.cts +1 -1
  135. package/typings/rules/no-hashtag-description.d.ts +1 -1
  136. package/typings/rules/no-one-place-fragments.d.cts +1 -1
  137. package/typings/rules/no-one-place-fragments.d.ts +1 -1
  138. package/typings/rules/no-root-type.d.cts +1 -1
  139. package/typings/rules/no-root-type.d.ts +1 -1
  140. package/typings/rules/no-scalar-result-type-on-mutation.d.cts +1 -1
  141. package/typings/rules/no-scalar-result-type-on-mutation.d.ts +1 -1
  142. package/typings/rules/no-typename-prefix.d.cts +1 -1
  143. package/typings/rules/no-typename-prefix.d.ts +1 -1
  144. package/typings/rules/no-unreachable-types.d.cts +1 -1
  145. package/typings/rules/no-unreachable-types.d.ts +1 -1
  146. package/typings/rules/no-unused-fields.d.cts +1 -1
  147. package/typings/rules/no-unused-fields.d.ts +1 -1
  148. package/typings/rules/relay-arguments.d.cts +1 -1
  149. package/typings/rules/relay-arguments.d.ts +1 -1
  150. package/typings/rules/relay-connection-types.d.cts +1 -1
  151. package/typings/rules/relay-connection-types.d.ts +1 -1
  152. package/typings/rules/relay-edge-types.d.cts +1 -1
  153. package/typings/rules/relay-edge-types.d.ts +1 -1
  154. package/typings/rules/relay-page-info.d.cts +1 -1
  155. package/typings/rules/relay-page-info.d.ts +1 -1
  156. package/typings/rules/require-deprecation-date.d.cts +1 -1
  157. package/typings/rules/require-deprecation-date.d.ts +1 -1
  158. package/typings/rules/require-deprecation-reason.d.cts +1 -1
  159. package/typings/rules/require-deprecation-reason.d.ts +1 -1
  160. package/typings/rules/require-description.d.cts +1 -1
  161. package/typings/rules/require-description.d.ts +1 -1
  162. package/typings/rules/require-field-of-type-query-in-mutation-result.d.cts +1 -1
  163. package/typings/rules/require-field-of-type-query-in-mutation-result.d.ts +1 -1
  164. package/typings/rules/require-id-when-available.d.cts +1 -1
  165. package/typings/rules/require-id-when-available.d.ts +1 -1
  166. package/typings/rules/require-nullable-fields-with-oneof.d.cts +1 -1
  167. package/typings/rules/require-nullable-fields-with-oneof.d.ts +1 -1
  168. package/typings/rules/require-type-pattern-with-oneof.d.cts +1 -1
  169. package/typings/rules/require-type-pattern-with-oneof.d.ts +1 -1
  170. package/typings/rules/selection-set-depth.d.cts +1 -1
  171. package/typings/rules/selection-set-depth.d.ts +1 -1
  172. package/typings/rules/strict-id-in-types.d.cts +1 -1
  173. package/typings/rules/strict-id-in-types.d.ts +1 -1
  174. package/typings/rules/unique-fragment-name.d.cts +2 -2
  175. package/typings/rules/unique-fragment-name.d.ts +2 -2
  176. package/typings/rules/unique-operation-name.d.cts +1 -1
  177. package/typings/rules/unique-operation-name.d.ts +1 -1
  178. package/typings/schema.d.cts +1 -1
  179. package/typings/schema.d.ts +1 -1
  180. package/typings/testkit.d.cts +3 -3
  181. package/typings/testkit.d.ts +3 -3
  182. package/typings/types.d.cts +2 -2
  183. package/typings/types.d.ts +2 -2
  184. package/typings/utils.d.cts +2 -2
  185. package/typings/utils.d.ts +2 -2
@@ -0,0 +1,131 @@
1
+ import depthLimit from 'graphql-depth-limit';
2
+ import { Kind } from 'graphql';
3
+ import { ARRAY_DEFAULT_OPTIONS, logger, requireSiblingsOperations } from '../utils.js';
4
+ const RULE_ID = 'selection-set-depth';
5
+ const schema = {
6
+ type: 'array',
7
+ minItems: 1,
8
+ maxItems: 1,
9
+ items: {
10
+ type: 'object',
11
+ additionalProperties: false,
12
+ required: ['maxDepth'],
13
+ properties: {
14
+ maxDepth: {
15
+ type: 'number',
16
+ },
17
+ ignore: ARRAY_DEFAULT_OPTIONS,
18
+ },
19
+ },
20
+ };
21
+ export const rule = {
22
+ meta: {
23
+ type: 'suggestion',
24
+ hasSuggestions: true,
25
+ docs: {
26
+ category: 'Operations',
27
+ description: 'Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://npmjs.com/package/graphql-depth-limit).',
28
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
29
+ requiresSiblings: true,
30
+ examples: [
31
+ {
32
+ title: 'Incorrect',
33
+ usage: [{ maxDepth: 1 }],
34
+ code: `
35
+ query deep2 {
36
+ viewer { # Level 0
37
+ albums { # Level 1
38
+ title # Level 2
39
+ }
40
+ }
41
+ }
42
+ `,
43
+ },
44
+ {
45
+ title: 'Correct',
46
+ usage: [{ maxDepth: 4 }],
47
+ code: `
48
+ query deep2 {
49
+ viewer { # Level 0
50
+ albums { # Level 1
51
+ title # Level 2
52
+ }
53
+ }
54
+ }
55
+ `,
56
+ },
57
+ {
58
+ title: 'Correct (ignored field)',
59
+ usage: [{ maxDepth: 1, ignore: ['albums'] }],
60
+ code: `
61
+ query deep2 {
62
+ viewer { # Level 0
63
+ albums { # Level 1
64
+ title # Level 2
65
+ }
66
+ }
67
+ }
68
+ `,
69
+ },
70
+ ],
71
+ recommended: true,
72
+ configOptions: [{ maxDepth: 7 }],
73
+ },
74
+ schema,
75
+ },
76
+ create(context) {
77
+ let siblings = null;
78
+ try {
79
+ siblings = requireSiblingsOperations(RULE_ID, context);
80
+ }
81
+ catch (_a) {
82
+ logger.warn(`Rule "${RULE_ID}" works best with siblings operations loaded. For more info: https://bit.ly/graphql-eslint-operations`);
83
+ }
84
+ const { maxDepth, ignore = [] } = context.options[0];
85
+ const checkFn = depthLimit(maxDepth, { ignore });
86
+ return {
87
+ 'OperationDefinition, FragmentDefinition'(node) {
88
+ try {
89
+ const rawNode = node.rawNode();
90
+ const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode) : [];
91
+ const document = {
92
+ kind: Kind.DOCUMENT,
93
+ definitions: [rawNode, ...fragmentsInUse],
94
+ };
95
+ checkFn({
96
+ getDocument: () => document,
97
+ reportError(error) {
98
+ const { line, column } = error.locations[0];
99
+ const ancestors = context.getAncestors();
100
+ const token = ancestors[0].tokens.find(token => token.loc.start.line === line && token.loc.start.column === column - 1);
101
+ context.report({
102
+ loc: {
103
+ line,
104
+ column: column - 1,
105
+ },
106
+ message: error.message,
107
+ // Don't provide suggestions for fragment that can be in a separate file
108
+ ...(token && {
109
+ suggest: [
110
+ {
111
+ desc: 'Remove selections',
112
+ fix(fixer) {
113
+ const sourceCode = context.getSourceCode();
114
+ const foundNode = sourceCode.getNodeByRangeIndex(token.range[0]);
115
+ const parentNode = foundNode.parent.parent;
116
+ return fixer.remove(foundNode.kind === 'Name' ? parentNode.parent : parentNode);
117
+ },
118
+ },
119
+ ],
120
+ }),
121
+ });
122
+ },
123
+ });
124
+ }
125
+ catch (e) {
126
+ logger.warn(`Rule "${RULE_ID}" check failed due to a missing siblings operations. For more info: https://bit.ly/graphql-eslint-operations`, e);
127
+ }
128
+ },
129
+ };
130
+ },
131
+ };
@@ -0,0 +1,159 @@
1
+ import { Kind } from 'graphql';
2
+ import { ARRAY_DEFAULT_OPTIONS, requireGraphQLSchemaFromContext, englishJoinWords, } from '../utils.js';
3
+ const RULE_ID = 'strict-id-in-types';
4
+ const schema = {
5
+ type: 'array',
6
+ maxItems: 1,
7
+ items: {
8
+ type: 'object',
9
+ additionalProperties: false,
10
+ properties: {
11
+ acceptedIdNames: {
12
+ ...ARRAY_DEFAULT_OPTIONS,
13
+ default: ['id'],
14
+ },
15
+ acceptedIdTypes: {
16
+ ...ARRAY_DEFAULT_OPTIONS,
17
+ default: ['ID'],
18
+ },
19
+ exceptions: {
20
+ type: 'object',
21
+ additionalProperties: false,
22
+ properties: {
23
+ types: {
24
+ ...ARRAY_DEFAULT_OPTIONS,
25
+ description: 'This is used to exclude types with names that match one of the specified values.',
26
+ },
27
+ suffixes: {
28
+ ...ARRAY_DEFAULT_OPTIONS,
29
+ description: 'This is used to exclude types with names with suffixes that match one of the specified values.',
30
+ },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ };
36
+ export const rule = {
37
+ meta: {
38
+ type: 'suggestion',
39
+ docs: {
40
+ 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.',
41
+ category: 'Schema',
42
+ recommended: true,
43
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
44
+ requiresSchema: true,
45
+ examples: [
46
+ {
47
+ title: 'Incorrect',
48
+ usage: [
49
+ {
50
+ acceptedIdNames: ['id', '_id'],
51
+ acceptedIdTypes: ['ID'],
52
+ exceptions: { suffixes: ['Payload'] },
53
+ },
54
+ ],
55
+ code: /* GraphQL */ `
56
+ # Incorrect field name
57
+ type InvalidFieldName {
58
+ key: ID!
59
+ }
60
+
61
+ # Incorrect field type
62
+ type InvalidFieldType {
63
+ id: String!
64
+ }
65
+
66
+ # Incorrect exception suffix
67
+ type InvalidSuffixResult {
68
+ data: String!
69
+ }
70
+
71
+ # Too many unique identifiers. Must only contain one.
72
+ type InvalidFieldName {
73
+ id: ID!
74
+ _id: ID!
75
+ }
76
+ `,
77
+ },
78
+ {
79
+ title: 'Correct',
80
+ usage: [
81
+ {
82
+ acceptedIdNames: ['id', '_id'],
83
+ acceptedIdTypes: ['ID'],
84
+ exceptions: { types: ['Error'], suffixes: ['Payload'] },
85
+ },
86
+ ],
87
+ code: /* GraphQL */ `
88
+ type User {
89
+ id: ID!
90
+ }
91
+
92
+ type Post {
93
+ _id: ID!
94
+ }
95
+
96
+ type CreateUserPayload {
97
+ data: String!
98
+ }
99
+
100
+ type Error {
101
+ message: String!
102
+ }
103
+ `,
104
+ },
105
+ ],
106
+ },
107
+ schema,
108
+ },
109
+ create(context) {
110
+ const options = {
111
+ acceptedIdNames: ['id'],
112
+ acceptedIdTypes: ['ID'],
113
+ exceptions: {},
114
+ ...context.options[0],
115
+ };
116
+ const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
117
+ const rootTypeNames = [
118
+ schema.getQueryType(),
119
+ schema.getMutationType(),
120
+ schema.getSubscriptionType(),
121
+ ]
122
+ .filter(Boolean)
123
+ .map(type => type.name);
124
+ const selector = `ObjectTypeDefinition[name.value!=/^(${rootTypeNames.join('|')})$/]`;
125
+ return {
126
+ [selector](node) {
127
+ var _a, _b;
128
+ const typeName = node.name.value;
129
+ const shouldIgnoreNode = ((_a = options.exceptions.types) === null || _a === void 0 ? void 0 : _a.includes(typeName)) ||
130
+ ((_b = options.exceptions.suffixes) === null || _b === void 0 ? void 0 : _b.some(suffix => typeName.endsWith(suffix)));
131
+ if (shouldIgnoreNode) {
132
+ return;
133
+ }
134
+ const validIds = node.fields.filter(field => {
135
+ const fieldNode = field.rawNode();
136
+ const isValidIdName = options.acceptedIdNames.includes(fieldNode.name.value);
137
+ // To be a valid type, it must be non-null and one of the accepted types.
138
+ let isValidIdType = false;
139
+ if (fieldNode.type.kind === Kind.NON_NULL_TYPE &&
140
+ fieldNode.type.type.kind === Kind.NAMED_TYPE) {
141
+ isValidIdType = options.acceptedIdTypes.includes(fieldNode.type.type.name.value);
142
+ }
143
+ return isValidIdName && isValidIdType;
144
+ });
145
+ // Usually, there should be only one unique identifier field per type.
146
+ // Some clients allow multiple fields to be used. If more people need this,
147
+ // we can extend this rule later.
148
+ if (validIds.length !== 1) {
149
+ const pluralNamesSuffix = options.acceptedIdNames.length > 1 ? 's' : '';
150
+ const pluralTypesSuffix = options.acceptedIdTypes.length > 1 ? 's' : '';
151
+ context.report({
152
+ node: node.name,
153
+ message: `${typeName} must have exactly one non-nullable unique identifier. Accepted name${pluralNamesSuffix}: ${englishJoinWords(options.acceptedIdNames)}. Accepted type${pluralTypesSuffix}: ${englishJoinWords(options.acceptedIdTypes)}.`,
154
+ });
155
+ }
156
+ },
157
+ };
158
+ },
159
+ };
@@ -0,0 +1,86 @@
1
+ import { relative } from 'path';
2
+ import { Kind } from 'graphql';
3
+ import { normalizePath, requireSiblingsOperations, VIRTUAL_DOCUMENT_REGEX, CWD } from '../utils.js';
4
+ const RULE_ID = 'unique-fragment-name';
5
+ export const checkNode = (context, node, ruleId) => {
6
+ const documentName = node.name.value;
7
+ const siblings = requireSiblingsOperations(ruleId, context);
8
+ const siblingDocuments = node.kind === Kind.FRAGMENT_DEFINITION
9
+ ? siblings.getFragment(documentName)
10
+ : siblings.getOperation(documentName);
11
+ const filepath = context.getFilename();
12
+ const conflictingDocuments = siblingDocuments.filter(f => {
13
+ var _a;
14
+ const isSameName = ((_a = f.document.name) === null || _a === void 0 ? void 0 : _a.value) === documentName;
15
+ const isSamePath = normalizePath(f.filePath) === normalizePath(filepath);
16
+ return isSameName && !isSamePath;
17
+ });
18
+ if (conflictingDocuments.length > 0) {
19
+ context.report({
20
+ messageId: ruleId,
21
+ data: {
22
+ documentName,
23
+ summary: conflictingDocuments
24
+ .map(f => `\t${relative(CWD, f.filePath.replace(VIRTUAL_DOCUMENT_REGEX, ''))}`)
25
+ .join('\n'),
26
+ },
27
+ node: node.name,
28
+ });
29
+ }
30
+ };
31
+ export const rule = {
32
+ meta: {
33
+ type: 'suggestion',
34
+ docs: {
35
+ category: 'Operations',
36
+ description: 'Enforce unique fragment names across your project.',
37
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
38
+ requiresSiblings: true,
39
+ examples: [
40
+ {
41
+ title: 'Incorrect',
42
+ code: /* GraphQL */ `
43
+ # user.fragment.graphql
44
+ fragment UserFields on User {
45
+ id
46
+ name
47
+ fullName
48
+ }
49
+
50
+ # user-fields.graphql
51
+ fragment UserFields on User {
52
+ id
53
+ }
54
+ `,
55
+ },
56
+ {
57
+ title: 'Correct',
58
+ code: /* GraphQL */ `
59
+ # user.fragment.graphql
60
+ fragment AllUserFields on User {
61
+ id
62
+ name
63
+ fullName
64
+ }
65
+
66
+ # user-fields.graphql
67
+ fragment UserFields on User {
68
+ id
69
+ }
70
+ `,
71
+ },
72
+ ],
73
+ },
74
+ messages: {
75
+ [RULE_ID]: 'Fragment named "{{ documentName }}" already defined in:\n{{ summary }}',
76
+ },
77
+ schema: [],
78
+ },
79
+ create(context) {
80
+ return {
81
+ FragmentDefinition(node) {
82
+ checkNode(context, node, RULE_ID);
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,62 @@
1
+ import { checkNode } from './unique-fragment-name.js';
2
+ const RULE_ID = 'unique-operation-name';
3
+ export const rule = {
4
+ meta: {
5
+ type: 'suggestion',
6
+ docs: {
7
+ category: 'Operations',
8
+ description: 'Enforce unique operation names across your project.',
9
+ url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.md`,
10
+ requiresSiblings: true,
11
+ examples: [
12
+ {
13
+ title: 'Incorrect',
14
+ code: /* GraphQL */ `
15
+ # foo.query.graphql
16
+ query user {
17
+ user {
18
+ id
19
+ }
20
+ }
21
+
22
+ # bar.query.graphql
23
+ query user {
24
+ me {
25
+ id
26
+ }
27
+ }
28
+ `,
29
+ },
30
+ {
31
+ title: 'Correct',
32
+ code: /* GraphQL */ `
33
+ # foo.query.graphql
34
+ query user {
35
+ user {
36
+ id
37
+ }
38
+ }
39
+
40
+ # bar.query.graphql
41
+ query me {
42
+ me {
43
+ id
44
+ }
45
+ }
46
+ `,
47
+ },
48
+ ],
49
+ },
50
+ messages: {
51
+ [RULE_ID]: 'Operation named "{{ documentName }}" already defined in:\n{{ summary }}',
52
+ },
53
+ schema: [],
54
+ },
55
+ create(context) {
56
+ return {
57
+ 'OperationDefinition[name!=undefined]'(node) {
58
+ checkNode(context, node, RULE_ID);
59
+ },
60
+ };
61
+ },
62
+ };
package/esm/schema.js ADDED
@@ -0,0 +1,37 @@
1
+ import { GraphQLSchema } from 'graphql';
2
+ import debugFactory from 'debug';
3
+ import fg from 'fast-glob';
4
+ import chalk from 'chalk';
5
+ import { ModuleCache } from './cache.js';
6
+ const schemaCache = new ModuleCache();
7
+ const debug = debugFactory('graphql-eslint:schema');
8
+ export function getSchema(project, schemaOptions) {
9
+ const schemaKey = project.schema;
10
+ if (!schemaKey) {
11
+ return null;
12
+ }
13
+ const cache = schemaCache.get(schemaKey);
14
+ if (cache) {
15
+ return cache;
16
+ }
17
+ let schema;
18
+ try {
19
+ debug('Loading schema from %o', project.schema);
20
+ schema = project.loadSchemaSync(project.schema, 'GraphQLSchema', {
21
+ ...schemaOptions,
22
+ pluckConfig: project.extensions.pluckConfig,
23
+ });
24
+ if (debug.enabled) {
25
+ debug('Schema loaded: %o', schema instanceof GraphQLSchema);
26
+ const schemaPaths = fg.sync(project.schema, { absolute: true });
27
+ debug('Schema pointers %O', schemaPaths);
28
+ }
29
+ // Do not set error to cache, since cache reload will be done after some `lifetime` seconds
30
+ schemaCache.set(schemaKey, schema);
31
+ }
32
+ catch (error) {
33
+ error.message = chalk.red(`Error while loading schema: ${error.message}`);
34
+ schema = error;
35
+ }
36
+ return schema;
37
+ }
package/esm/testkit.js ADDED
@@ -0,0 +1,181 @@
1
+ import { createRequire } from 'module';
2
+ const require = createRequire(import.meta.url);
3
+ /* eslint-env jest */
4
+ import { readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { RuleTester, Linter } from 'eslint';
7
+ import { codeFrameColumns } from '@babel/code-frame';
8
+ function indentCode(code, indent = 4) {
9
+ return code.replace(/^/gm, ' '.repeat(indent));
10
+ }
11
+ // A simple version of `SourceCodeFixer.applyFixes`
12
+ // https://github.com/eslint/eslint/issues/14936#issuecomment-906746754
13
+ function applyFix(code, { range, text }) {
14
+ return [code.slice(0, range[0]), text, code.slice(range[1])].join('');
15
+ }
16
+ export class GraphQLRuleTester extends RuleTester {
17
+ constructor(parserOptions = {}) {
18
+ const config = {
19
+ parser: require.resolve('@graphql-eslint/eslint-plugin'),
20
+ parserOptions: {
21
+ ...parserOptions,
22
+ skipGraphQLConfig: true,
23
+ },
24
+ };
25
+ super(config);
26
+ this.config = config;
27
+ }
28
+ fromMockFile(path) {
29
+ return readFileSync(resolve(__dirname, `../tests/mocks/${path}`), 'utf-8');
30
+ }
31
+ runGraphQLTests(ruleId, rule, tests) {
32
+ const ruleTests = Linter.version.startsWith('8')
33
+ ? tests
34
+ : {
35
+ valid: tests.valid.map(test => {
36
+ if (typeof test === 'string') {
37
+ return test;
38
+ }
39
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
40
+ const { name, ...testCaseOptions } = test;
41
+ return testCaseOptions;
42
+ }),
43
+ invalid: tests.invalid.map(test => {
44
+ // ESLint 7 throws an error on CI - Unexpected top-level property "name"
45
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
46
+ const { name, ...testCaseOptions } = test;
47
+ return testCaseOptions;
48
+ }),
49
+ };
50
+ super.run(ruleId, rule, ruleTests);
51
+ const linter = new Linter();
52
+ linter.defineRule(ruleId, rule);
53
+ const hasOnlyTest = [...tests.valid, ...tests.invalid].some(t => typeof t !== 'string' && t.only);
54
+ // for (const [index, testCase] of tests.valid.entries()) {
55
+ // const { name, code, filename, only }: RuleTester.ValidTestCase =
56
+ // typeof testCase === 'string' ? { code: testCase } : testCase;
57
+ //
58
+ // if (hasOnlyTest && !only) {
59
+ // continue;
60
+ // }
61
+ //
62
+ // const verifyConfig = getVerifyConfig(ruleId, this.config, testCase);
63
+ // defineParser(linter, verifyConfig.parser);
64
+ //
65
+ // const messages = linter.verify(code, verifyConfig, { filename });
66
+ // const codeFrame = printCode(code, { line: 0, column: 0 });
67
+ //
68
+ // it(name || `Valid #${index + 1}\n${codeFrame}`, () => {
69
+ // expect(messages).toEqual([]);
70
+ // });
71
+ // }
72
+ for (const [idx, testCase] of tests.invalid.entries()) {
73
+ const { only, filename, options, name } = testCase;
74
+ if (hasOnlyTest && !only) {
75
+ continue;
76
+ }
77
+ const code = removeTrailingBlankLines(testCase.code);
78
+ const verifyConfig = getVerifyConfig(ruleId, this.config, testCase);
79
+ defineParser(linter, verifyConfig.parser);
80
+ const messages = linter.verify(code, verifyConfig, filename);
81
+ if (messages.length === 0) {
82
+ throw new Error('Invalid case should have at least one error.');
83
+ }
84
+ const codeFrame = indentCode(printCode(code, { line: 0, column: 0 }));
85
+ const messageForSnapshot = ['#### ⌨️ Code', codeFrame];
86
+ if (options) {
87
+ const opts = JSON.stringify(options, null, 2).slice(1, -1);
88
+ messageForSnapshot.push('#### ⚙️ Options', indentCode(removeTrailingBlankLines(opts), 2));
89
+ }
90
+ for (const [index, message] of messages.entries()) {
91
+ if (message.fatal) {
92
+ throw new Error(message.message);
93
+ }
94
+ const codeWithMessage = printCode(code, message, 1);
95
+ messageForSnapshot.push(printWithIndex('#### ❌ Error', index, messages.length), indentCode(codeWithMessage));
96
+ const { suggestions } = message;
97
+ // Don't print suggestions in snapshots for too big codes
98
+ if (suggestions && (code.match(/\n/g) || '').length < 1000) {
99
+ for (const [i, suggestion] of message.suggestions.entries()) {
100
+ const title = printWithIndex('#### 💡 Suggestion', i, suggestions.length, suggestion.desc);
101
+ const output = applyFix(code, suggestion.fix);
102
+ const codeFrame = printCode(output, { line: 0, column: 0 });
103
+ messageForSnapshot.push(title, indentCode(codeFrame, 2));
104
+ }
105
+ }
106
+ }
107
+ if (rule.meta.fixable) {
108
+ const { fixed, output } = linter.verifyAndFix(code, verifyConfig, filename);
109
+ if (fixed) {
110
+ messageForSnapshot.push('#### 🔧 Autofix output', indentCode(printCode(output)));
111
+ }
112
+ }
113
+ // @ts-expect-error -- we should import `vitest` but somebody could use globals from `jest`
114
+ it(name || `Invalid #${idx + 1}`, () => {
115
+ // @ts-expect-error -- ^ same
116
+ expect(messageForSnapshot.join('\n\n')).toMatchSnapshot();
117
+ });
118
+ }
119
+ }
120
+ }
121
+ function removeTrailingBlankLines(text) {
122
+ return text.replace(/^\s*\n/, '').trimEnd();
123
+ }
124
+ function printWithIndex(title, index, total, description) {
125
+ if (total > 1) {
126
+ title += ` ${index + 1}/${total}`;
127
+ }
128
+ if (description) {
129
+ title += `: ${description}`;
130
+ }
131
+ return title;
132
+ }
133
+ function getVerifyConfig(ruleId, testerConfig, testCase) {
134
+ const { parser = testerConfig.parser, parserOptions, options } = testCase;
135
+ return {
136
+ ...testerConfig,
137
+ parser,
138
+ parserOptions: {
139
+ ...testerConfig.parserOptions,
140
+ ...parserOptions,
141
+ },
142
+ rules: {
143
+ [ruleId]: Array.isArray(options) ? ['error', ...options] : 'error',
144
+ },
145
+ };
146
+ }
147
+ const parsers = new WeakMap();
148
+ function defineParser(linter, parser) {
149
+ if (!parser) {
150
+ return;
151
+ }
152
+ if (!parsers.has(linter)) {
153
+ parsers.set(linter, new Set());
154
+ }
155
+ const defined = parsers.get(linter);
156
+ if (!defined.has(parser)) {
157
+ defined.add(parser);
158
+ linter.defineParser(parser, require(parser));
159
+ }
160
+ }
161
+ function printCode(code, result = {}, linesOffset = Number.POSITIVE_INFINITY) {
162
+ const { line, column, endLine, endColumn, message } = result;
163
+ const location = {};
164
+ if (typeof line === 'number' && typeof column === 'number') {
165
+ location.start = {
166
+ line,
167
+ column,
168
+ };
169
+ }
170
+ if (typeof endLine === 'number' && typeof endColumn === 'number') {
171
+ location.end = {
172
+ line: endLine,
173
+ column: endColumn,
174
+ };
175
+ }
176
+ return codeFrameColumns(code, location, {
177
+ linesAbove: linesOffset,
178
+ linesBelow: linesOffset,
179
+ message,
180
+ });
181
+ }
package/esm/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};