@graphql-eslint/eslint-plugin 3.3.0 → 3.4.0-alpha-c01d913.0

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/README.md CHANGED
@@ -42,7 +42,7 @@ npm install --save-dev @graphql-eslint/eslint-plugin
42
42
 
43
43
  > Make sure you have `graphql` dependency in your project.
44
44
 
45
- ### Configuration
45
+ ## Configuration
46
46
 
47
47
  To get started, define an override in your ESLint config to apply this plugin to `.graphql` files. Add the [rules](docs/README.md) you want applied.
48
48
 
@@ -65,7 +65,7 @@ To get started, define an override in your ESLint config to apply this plugin to
65
65
 
66
66
  If your GraphQL definitions are defined only in `.graphql` files, and you're only using rules that apply to individual files, you should be good to go 👍. If you would like use a remote schema or use rules that apply across the entire collection of definitions at once, see [here](#using-a-remote-schema-or-rules-with-constraints-that-span-the-entire-schema).
67
67
 
68
- #### Tell ESLint to apply this plugin to GraphQL definitions defined in code files
68
+ ### Apply this plugin to GraphQL definitions defined in code files
69
69
 
70
70
  If you are defining GraphQL schema or GraphQL operations in code files, you'll want to define an additional override to extend the functionality of this plugin to the schema and operations in those files.
71
71
 
@@ -90,7 +90,7 @@ If you are defining GraphQL schema or GraphQL operations in code files, you'll w
90
90
 
91
91
  Under the hood, specifying the `@graphql-eslint/graphql` processor for code files will cause `graphql-eslint/graphql` to extract the schema and operation definitions from these files into virtual GraphQL documents with `.graphql` extensions. This will allow the overrides you've defined for `.graphql` files, via `"files": ["*.graphql"]`, to get applied to the definitions defined in your code files.
92
92
 
93
- #### Using a remote schema or rules with constraints that span the entire schema
93
+ ### Extended linting rules with GraphQL Schema
94
94
 
95
95
  Some rules require an understanding of the entire schema at once. For example, [no-unreachable-types](https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-unreachable-types.md#no-unreachable-types) checks that all types are reachable by root-level fields.
96
96
 
@@ -120,7 +120,7 @@ The parser allows you to specify a json file / graphql files(s) / url / raw stri
120
120
 
121
121
  > Some rules require type information to operate, it's marked in the docs for each rule!
122
122
 
123
- #### Extended linting rules with siblings operations
123
+ ### Extended linting rules with siblings operations
124
124
 
125
125
  While implementing this tool, we had to find solutions for a better integration of the GraphQL ecosystem and ESLint core.
126
126
 
@@ -180,7 +180,7 @@ You can find a list of [ESLint directives here](https://eslint.org/docs/2.13.1/u
180
180
 
181
181
  You can find a complete list of [all available rules here](docs/README.md).
182
182
 
183
- ## Deprecated Rules
183
+ ### Deprecated Rules
184
184
 
185
185
  See [docs/deprecated-rules.md](docs/deprecated-rules.md).
186
186
 
@@ -201,7 +201,7 @@ See [docs/deprecated-rules.md](docs/deprecated-rules.md).
201
201
 
202
202
  > If you are in a monorepo project, you probably need both sets of rules.
203
203
 
204
- ## Config usage
204
+ ### Config usage
205
205
 
206
206
  For example, to enable the `schema-recommended` config, enable it in your `.eslintrc` file with the `extends` option:
207
207
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Category: `Schema`
4
4
  - Rule name: `@graphql-eslint/possible-type-extension`
5
- - Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
5
+ - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6
6
  - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7
7
 
8
8
  A type extension is only valid if the type is defined and has the same kind.
@@ -2,7 +2,7 @@
2
2
 
3
3
  - Category: `Schema`
4
4
  - Rule name: `@graphql-eslint/unique-enum-value-names`
5
- - Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
5
+ - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6
6
  - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7
7
 
8
8
  A GraphQL enum type is only valid if all its values are uniquely named.
@@ -4,7 +4,7 @@
4
4
 
5
5
  - Category: `Schema`
6
6
  - Rule name: `@graphql-eslint/unique-field-definition-names`
7
- - Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
7
+ - Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
8
8
  - Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
9
9
 
10
10
  A GraphQL complex type is only valid if all its fields are uniquely named.
@@ -1,3 +1,4 @@
1
1
  import { GraphQLConfig } from 'graphql-config';
2
2
  import { ParserOptions } from './types';
3
- export declare function loadGraphqlConfig(options: ParserOptions): GraphQLConfig;
3
+ export declare function loadCachedGraphQLConfig(options: ParserOptions): GraphQLConfig;
4
+ export declare function loadGraphQLConfig(options: ParserOptions): GraphQLConfig;
package/index.js CHANGED
@@ -10,10 +10,11 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const utils = require('@graphql-tools/utils');
12
12
  const lowerCase = _interopDefault(require('lodash.lowercase'));
13
+ const chalk = _interopDefault(require('chalk'));
14
+ const graphqlConfig = require('graphql-config');
15
+ const codeFileLoader = require('@graphql-tools/code-file-loader');
13
16
  const depthLimit = _interopDefault(require('graphql-depth-limit'));
14
17
  const graphqlTagPluck = require('@graphql-tools/graphql-tag-pluck');
15
- const graphqlConfig$1 = require('graphql-config');
16
- const codeFileLoader = require('@graphql-tools/code-file-loader');
17
18
  const eslint = require('eslint');
18
19
  const codeFrame = require('@babel/code-frame');
19
20
 
@@ -174,6 +175,47 @@ const configs = {
174
175
  'operations-all': operationsAllConfig,
175
176
  };
176
177
 
178
+ let graphQLConfig;
179
+ function loadCachedGraphQLConfig(options) {
180
+ // We don't want cache config on test environment
181
+ // Otherwise schema and documents will be same for all tests
182
+ if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
183
+ return graphQLConfig;
184
+ }
185
+ graphQLConfig = loadGraphQLConfig(options);
186
+ return graphQLConfig;
187
+ }
188
+ function loadGraphQLConfig(options) {
189
+ const onDiskConfig = options.skipGraphQLConfig
190
+ ? null
191
+ : graphqlConfig.loadConfigSync({
192
+ throwOnEmpty: false,
193
+ throwOnMissing: false,
194
+ extensions: [addCodeFileLoaderExtension],
195
+ });
196
+ const configOptions = options.projects
197
+ ? { projects: options.projects }
198
+ : {
199
+ schema: (options.schema || ''),
200
+ documents: options.documents || options.operations,
201
+ extensions: options.extensions,
202
+ include: options.include,
203
+ exclude: options.exclude,
204
+ };
205
+ graphQLConfig =
206
+ onDiskConfig ||
207
+ new graphqlConfig.GraphQLConfig({
208
+ config: configOptions,
209
+ filepath: 'virtual-config',
210
+ }, [addCodeFileLoaderExtension]);
211
+ return graphQLConfig;
212
+ }
213
+ const addCodeFileLoaderExtension = api => {
214
+ api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
215
+ api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
216
+ return { name: 'graphql-eslint-loaders' };
217
+ };
218
+
177
219
  function requireSiblingsOperations(ruleName, context) {
178
220
  if (!context.parserServices) {
179
221
  throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
@@ -192,6 +234,43 @@ function requireGraphQLSchemaFromContext(ruleName, context) {
192
234
  }
193
235
  return context.parserServices.schema;
194
236
  }
237
+ const logger = {
238
+ // eslint-disable-next-line no-console
239
+ error: (...args) => console.error(chalk.red('error'), '[graphql-eslint]', chalk(...args)),
240
+ // eslint-disable-next-line no-console
241
+ warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
242
+ };
243
+ const schemaToExtendCache = new Map();
244
+ const getGraphQLSchemaToExtend = (ruleId, context) => {
245
+ // If schema is not loaded, there is no reason to make partial schema aka schemaToExtend
246
+ if (!context.parserServices.hasTypeInfo) {
247
+ if (!getGraphQLSchemaToExtend.warningPrintedMap[ruleId]) {
248
+ logger.warn(`Rule "${ruleId}" works best with schema loaded. See http://bit.ly/graphql-eslint-schema for more info`);
249
+ getGraphQLSchemaToExtend.warningPrintedMap[ruleId] = true;
250
+ }
251
+ return null;
252
+ }
253
+ const filename = context.getPhysicalFilename();
254
+ if (!schemaToExtendCache.has(filename)) {
255
+ const { schema, schemaOptions } = context.parserOptions;
256
+ const gqlConfig = loadGraphQLConfig({ schema });
257
+ const projectForFile = gqlConfig.getProjectForFile(filename);
258
+ let schemaToExtend;
259
+ try {
260
+ schemaToExtend = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
261
+ ...schemaOptions,
262
+ ignore: filename,
263
+ });
264
+ }
265
+ catch (_a) {
266
+ // If error throws just ignore it because maybe schema is located in 1 file
267
+ schemaToExtend = null;
268
+ }
269
+ schemaToExtendCache.set(filename, schemaToExtend);
270
+ }
271
+ return schemaToExtendCache.get(filename);
272
+ };
273
+ getGraphQLSchemaToExtend.warningPrintedMap = Object.create(null);
195
274
  function requireReachableTypesFromContext(ruleName, context) {
196
275
  const schema = requireGraphQLSchemaFromContext(ruleName, context);
197
276
  return context.parserServices.reachableTypes(schema);
@@ -326,14 +405,14 @@ function getLocation(loc, fieldName = '', offset) {
326
405
  };
327
406
  }
328
407
 
329
- function validateDocument(sourceNode, context, schema, documentNode, rule) {
408
+ function validateDocument(context, schema = null, documentNode, rule, isSchemaToExtend = false) {
330
409
  if (documentNode.definitions.length === 0) {
331
410
  return;
332
411
  }
333
412
  try {
334
- const validationErrors = schema
413
+ const validationErrors = schema && !isSchemaToExtend
335
414
  ? graphql.validate(schema, documentNode, [rule])
336
- : validate.validateSDL(documentNode, null, [rule]);
415
+ : validate.validateSDL(documentNode, schema, [rule]);
337
416
  for (const error of validationErrors) {
338
417
  /*
339
418
  * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
@@ -358,7 +437,8 @@ function validateDocument(sourceNode, context, schema, documentNode, rule) {
358
437
  }
359
438
  catch (e) {
360
439
  context.report({
361
- node: sourceNode,
440
+ // Report on first character
441
+ loc: { column: 0, line: 1 },
362
442
  message: e.message,
363
443
  });
364
444
  }
@@ -440,18 +520,23 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
440
520
  },
441
521
  },
442
522
  create(context) {
523
+ if (!ruleFn) {
524
+ logger.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
525
+ return {};
526
+ }
527
+ let schema;
528
+ if (docs.requiresSchemaToExtend) {
529
+ schema = getGraphQLSchemaToExtend(ruleId, context);
530
+ }
531
+ else if (docs.requiresSchema) {
532
+ schema = requireGraphQLSchemaFromContext(ruleId, context);
533
+ }
443
534
  return {
444
535
  Document(node) {
445
- if (!ruleFn) {
446
- // eslint-disable-next-line no-console
447
- console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
448
- return;
449
- }
450
- const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
451
536
  const documentNode = getDocumentNode
452
537
  ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
453
538
  : node.rawNode();
454
- validateDocument(node, context, schema, documentNode, ruleFn);
539
+ validateDocument(context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
455
540
  },
456
541
  };
457
542
  },
@@ -601,7 +686,9 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
601
686
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
602
687
  category: 'Schema',
603
688
  description: `A type extension is only valid if the type is defined and has the same kind.`,
604
- recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
689
+ recommended: false,
690
+ requiresSchema: true,
691
+ requiresSchemaToExtend: true,
605
692
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
606
693
  category: ['Schema', 'Operations'],
607
694
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
@@ -629,9 +716,13 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
629
716
  category: 'Schema',
630
717
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
631
718
  recommended: false,
719
+ requiresSchema: true,
720
+ requiresSchemaToExtend: true,
632
721
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
633
722
  category: 'Schema',
634
723
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
724
+ requiresSchema: true,
725
+ requiresSchemaToExtend: true,
635
726
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
636
727
  category: 'Operations',
637
728
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
@@ -3034,12 +3125,13 @@ const rule$j = {
3034
3125
  },
3035
3126
  };
3036
3127
 
3128
+ const RULE_ID$2 = 'selection-set-depth';
3037
3129
  const rule$k = {
3038
3130
  meta: {
3039
3131
  docs: {
3040
3132
  category: 'Operations',
3041
3133
  description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
3042
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/selection-set-depth.md',
3134
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
3043
3135
  requiresSiblings: true,
3044
3136
  examples: [
3045
3137
  {
@@ -3113,11 +3205,10 @@ const rule$k = {
3113
3205
  create(context) {
3114
3206
  let siblings = null;
3115
3207
  try {
3116
- siblings = requireSiblingsOperations('selection-set-depth', context);
3208
+ siblings = requireSiblingsOperations(RULE_ID$2, context);
3117
3209
  }
3118
3210
  catch (e) {
3119
- // eslint-disable-next-line no-console
3120
- console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3211
+ logger.warn(`Rule "${RULE_ID$2}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3121
3212
  }
3122
3213
  const { maxDepth } = context.options[0];
3123
3214
  const ignore = context.options[0].ignore || [];
@@ -3142,8 +3233,7 @@ const rule$k = {
3142
3233
  });
3143
3234
  }
3144
3235
  catch (e) {
3145
- // eslint-disable-next-line no-console
3146
- console.warn(`Rule "selection-set-depth" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3236
+ logger.warn(`Rule "${RULE_ID$2}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3147
3237
  }
3148
3238
  },
3149
3239
  };
@@ -3525,8 +3615,7 @@ function createGraphqlProcessor() {
3525
3615
  }
3526
3616
  }
3527
3617
  catch (e) {
3528
- // eslint-disable-next-line no-console
3529
- console.warn(`[graphql-eslint/processor]: Unable to process file "${filename}": `, e);
3618
+ logger.warn(`Unable to process file "${filename}"`, e);
3530
3619
  }
3531
3620
  }
3532
3621
  return [text];
@@ -3566,20 +3655,25 @@ const schemaCache = new Map();
3566
3655
  function getSchema(options = {}, gqlConfig) {
3567
3656
  const realFilepath = options.filePath ? getOnDiskFilepath(options.filePath) : null;
3568
3657
  const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
3569
- const schemaKey = utils.asArray(projectForFile.schema)
3570
- .sort()
3571
- .join(',');
3658
+ const schemaKey = utils.asArray(projectForFile.schema).sort().join(',');
3572
3659
  if (!schemaKey) {
3573
3660
  return null;
3574
3661
  }
3575
- let schema = schemaCache.get(schemaKey);
3576
- if (!schema) {
3662
+ if (schemaCache.has(schemaKey)) {
3663
+ return schemaCache.get(schemaKey);
3664
+ }
3665
+ let schema;
3666
+ try {
3577
3667
  schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
3578
3668
  cache: loaderCache,
3579
- ...options.schemaOptions
3669
+ ...options.schemaOptions,
3580
3670
  });
3581
- schemaCache.set(schemaKey, schema);
3582
3671
  }
3672
+ catch (e) {
3673
+ schema = null;
3674
+ logger.error('Error while loading schema\n', e);
3675
+ }
3676
+ schemaCache.set(schemaKey, schema);
3583
3677
  return schema;
3584
3678
  }
3585
3679
 
@@ -3592,10 +3686,10 @@ const handleVirtualPath = (documents) => {
3592
3686
  return source;
3593
3687
  }
3594
3688
  (_a = filepathMap[location]) !== null && _a !== void 0 ? _a : (filepathMap[location] = -1);
3595
- const index = filepathMap[location] += 1;
3689
+ const index = (filepathMap[location] += 1);
3596
3690
  return {
3597
3691
  ...source,
3598
- location: path.resolve(location, `${index}_document.graphql`)
3692
+ location: path.resolve(location, `${index}_document.graphql`),
3599
3693
  };
3600
3694
  });
3601
3695
  };
@@ -3604,9 +3698,7 @@ const siblingOperationsCache = new Map();
3604
3698
  const getSiblings = (filePath, gqlConfig) => {
3605
3699
  const realFilepath = filePath ? getOnDiskFilepath(filePath) : null;
3606
3700
  const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
3607
- const documentsKey = utils.asArray(projectForFile.documents)
3608
- .sort()
3609
- .join(',');
3701
+ const documentsKey = utils.asArray(projectForFile.documents).sort().join(',');
3610
3702
  if (!documentsKey) {
3611
3703
  return [];
3612
3704
  }
@@ -3614,7 +3706,7 @@ const getSiblings = (filePath, gqlConfig) => {
3614
3706
  if (!siblings) {
3615
3707
  const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
3616
3708
  skipGraphQLImport: true,
3617
- cache: loaderCache
3709
+ cache: loaderCache,
3618
3710
  });
3619
3711
  siblings = handleVirtualPath(documents);
3620
3712
  operationsCache.set(documentsKey, siblings);
@@ -3627,8 +3719,7 @@ function getSiblingOperations(options, gqlConfig) {
3627
3719
  let printed = false;
3628
3720
  const noopWarn = () => {
3629
3721
  if (!printed) {
3630
- // eslint-disable-next-line no-console
3631
- console.warn(`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`);
3722
+ logger.warn(`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`);
3632
3723
  printed = true;
3633
3724
  }
3634
3725
  return [];
@@ -3692,8 +3783,7 @@ function getSiblingOperations(options, gqlConfig) {
3692
3783
  const name = spread.name.value;
3693
3784
  const fragmentInfo = getFragment(name);
3694
3785
  if (fragmentInfo.length === 0) {
3695
- // eslint-disable-next-line no-console
3696
- console.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3786
+ logger.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3697
3787
  return;
3698
3788
  }
3699
3789
  const fragment = fragmentInfo[0];
@@ -3723,42 +3813,6 @@ function getSiblingOperations(options, gqlConfig) {
3723
3813
  return siblingOperations;
3724
3814
  }
3725
3815
 
3726
- let graphqlConfig;
3727
- function loadGraphqlConfig(options) {
3728
- // We don't want cache config on test environment
3729
- // Otherwise schema and documents will be same for all tests
3730
- if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
3731
- return graphqlConfig;
3732
- }
3733
- const onDiskConfig = options.skipGraphQLConfig
3734
- ? null
3735
- : graphqlConfig$1.loadConfigSync({
3736
- throwOnEmpty: false,
3737
- throwOnMissing: false,
3738
- extensions: [addCodeFileLoaderExtension],
3739
- });
3740
- graphqlConfig =
3741
- onDiskConfig ||
3742
- new graphqlConfig$1.GraphQLConfig({
3743
- config: options.projects
3744
- ? { projects: options.projects }
3745
- : {
3746
- schema: (options.schema || ''),
3747
- documents: options.documents || options.operations,
3748
- extensions: options.extensions,
3749
- include: options.include,
3750
- exclude: options.exclude,
3751
- },
3752
- filepath: 'virtual-config',
3753
- }, [addCodeFileLoaderExtension]);
3754
- return graphqlConfig;
3755
- }
3756
- const addCodeFileLoaderExtension = api => {
3757
- api.loaders.schema.register(new codeFileLoader.CodeFileLoader());
3758
- api.loaders.documents.register(new codeFileLoader.CodeFileLoader());
3759
- return { name: 'graphql-eslint-loaders' };
3760
- };
3761
-
3762
3816
  let reachableTypesCache;
3763
3817
  function getReachableTypes(schema) {
3764
3818
  // We don't want cache reachableTypes on test environment
@@ -3841,7 +3895,7 @@ function parse(code, options) {
3841
3895
  return parseForESLint(code, options).ast;
3842
3896
  }
3843
3897
  function parseForESLint(code, options = {}) {
3844
- const gqlConfig = loadGraphqlConfig(options);
3898
+ const gqlConfig = loadCachedGraphQLConfig(options);
3845
3899
  const schema = getSchema(options, gqlConfig);
3846
3900
  const parserServices = {
3847
3901
  hasTypeInfo: schema !== null,
@@ -3873,6 +3927,7 @@ function parseForESLint(code, options = {}) {
3873
3927
  };
3874
3928
  }
3875
3929
  catch (e) {
3930
+ e.message = `[graphql-eslint] ${e.message}`;
3876
3931
  // In case of GraphQL parser error, we report it to ESLint as a parser error that matches the requirements
3877
3932
  // of ESLint. This will make sure to display it correctly in IDEs and lint results.
3878
3933
  if (e instanceof graphql.GraphQLError) {
@@ -3880,11 +3935,10 @@ function parseForESLint(code, options = {}) {
3880
3935
  index: e.positions[0],
3881
3936
  lineNumber: e.locations[0].line,
3882
3937
  column: e.locations[0].column,
3883
- message: `[graphql-eslint]: ${e.message}`,
3938
+ message: e.message,
3884
3939
  };
3885
3940
  throw eslintError;
3886
3941
  }
3887
- e.message = `[graphql-eslint]: ${e.message}`;
3888
3942
  throw e;
3889
3943
  }
3890
3944
  }
package/index.mjs CHANGED
@@ -4,10 +4,11 @@ import { statSync, existsSync, readFileSync } from 'fs';
4
4
  import { dirname, extname, basename, relative, resolve } from 'path';
5
5
  import { asArray, parseGraphQLSDL } from '@graphql-tools/utils';
6
6
  import lowerCase from 'lodash.lowercase';
7
- import depthLimit from 'graphql-depth-limit';
8
- import { parseCode } from '@graphql-tools/graphql-tag-pluck';
7
+ import chalk from 'chalk';
9
8
  import { loadConfigSync, GraphQLConfig } from 'graphql-config';
10
9
  import { CodeFileLoader } from '@graphql-tools/code-file-loader';
10
+ import depthLimit from 'graphql-depth-limit';
11
+ import { parseCode } from '@graphql-tools/graphql-tag-pluck';
11
12
  import { RuleTester, Linter } from 'eslint';
12
13
  import { codeFrameColumns } from '@babel/code-frame';
13
14
 
@@ -168,6 +169,47 @@ const configs = {
168
169
  'operations-all': operationsAllConfig,
169
170
  };
170
171
 
172
+ let graphQLConfig;
173
+ function loadCachedGraphQLConfig(options) {
174
+ // We don't want cache config on test environment
175
+ // Otherwise schema and documents will be same for all tests
176
+ if (process.env.NODE_ENV !== 'test' && graphQLConfig) {
177
+ return graphQLConfig;
178
+ }
179
+ graphQLConfig = loadGraphQLConfig(options);
180
+ return graphQLConfig;
181
+ }
182
+ function loadGraphQLConfig(options) {
183
+ const onDiskConfig = options.skipGraphQLConfig
184
+ ? null
185
+ : loadConfigSync({
186
+ throwOnEmpty: false,
187
+ throwOnMissing: false,
188
+ extensions: [addCodeFileLoaderExtension],
189
+ });
190
+ const configOptions = options.projects
191
+ ? { projects: options.projects }
192
+ : {
193
+ schema: (options.schema || ''),
194
+ documents: options.documents || options.operations,
195
+ extensions: options.extensions,
196
+ include: options.include,
197
+ exclude: options.exclude,
198
+ };
199
+ graphQLConfig =
200
+ onDiskConfig ||
201
+ new GraphQLConfig({
202
+ config: configOptions,
203
+ filepath: 'virtual-config',
204
+ }, [addCodeFileLoaderExtension]);
205
+ return graphQLConfig;
206
+ }
207
+ const addCodeFileLoaderExtension = api => {
208
+ api.loaders.schema.register(new CodeFileLoader());
209
+ api.loaders.documents.register(new CodeFileLoader());
210
+ return { name: 'graphql-eslint-loaders' };
211
+ };
212
+
171
213
  function requireSiblingsOperations(ruleName, context) {
172
214
  if (!context.parserServices) {
173
215
  throw new Error(`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`);
@@ -186,6 +228,43 @@ function requireGraphQLSchemaFromContext(ruleName, context) {
186
228
  }
187
229
  return context.parserServices.schema;
188
230
  }
231
+ const logger = {
232
+ // eslint-disable-next-line no-console
233
+ error: (...args) => console.error(chalk.red('error'), '[graphql-eslint]', chalk(...args)),
234
+ // eslint-disable-next-line no-console
235
+ warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
236
+ };
237
+ const schemaToExtendCache = new Map();
238
+ const getGraphQLSchemaToExtend = (ruleId, context) => {
239
+ // If schema is not loaded, there is no reason to make partial schema aka schemaToExtend
240
+ if (!context.parserServices.hasTypeInfo) {
241
+ if (!getGraphQLSchemaToExtend.warningPrintedMap[ruleId]) {
242
+ logger.warn(`Rule "${ruleId}" works best with schema loaded. See http://bit.ly/graphql-eslint-schema for more info`);
243
+ getGraphQLSchemaToExtend.warningPrintedMap[ruleId] = true;
244
+ }
245
+ return null;
246
+ }
247
+ const filename = context.getPhysicalFilename();
248
+ if (!schemaToExtendCache.has(filename)) {
249
+ const { schema, schemaOptions } = context.parserOptions;
250
+ const gqlConfig = loadGraphQLConfig({ schema });
251
+ const projectForFile = gqlConfig.getProjectForFile(filename);
252
+ let schemaToExtend;
253
+ try {
254
+ schemaToExtend = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
255
+ ...schemaOptions,
256
+ ignore: filename,
257
+ });
258
+ }
259
+ catch (_a) {
260
+ // If error throws just ignore it because maybe schema is located in 1 file
261
+ schemaToExtend = null;
262
+ }
263
+ schemaToExtendCache.set(filename, schemaToExtend);
264
+ }
265
+ return schemaToExtendCache.get(filename);
266
+ };
267
+ getGraphQLSchemaToExtend.warningPrintedMap = Object.create(null);
189
268
  function requireReachableTypesFromContext(ruleName, context) {
190
269
  const schema = requireGraphQLSchemaFromContext(ruleName, context);
191
270
  return context.parserServices.reachableTypes(schema);
@@ -320,14 +399,14 @@ function getLocation(loc, fieldName = '', offset) {
320
399
  };
321
400
  }
322
401
 
323
- function validateDocument(sourceNode, context, schema, documentNode, rule) {
402
+ function validateDocument(context, schema = null, documentNode, rule, isSchemaToExtend = false) {
324
403
  if (documentNode.definitions.length === 0) {
325
404
  return;
326
405
  }
327
406
  try {
328
- const validationErrors = schema
407
+ const validationErrors = schema && !isSchemaToExtend
329
408
  ? validate(schema, documentNode, [rule])
330
- : validateSDL(documentNode, null, [rule]);
409
+ : validateSDL(documentNode, schema, [rule]);
331
410
  for (const error of validationErrors) {
332
411
  /*
333
412
  * TODO: Fix ESTree-AST converter because currently it's incorrectly convert loc.end
@@ -352,7 +431,8 @@ function validateDocument(sourceNode, context, schema, documentNode, rule) {
352
431
  }
353
432
  catch (e) {
354
433
  context.report({
355
- node: sourceNode,
434
+ // Report on first character
435
+ loc: { column: 0, line: 1 },
356
436
  message: e.message,
357
437
  });
358
438
  }
@@ -434,18 +514,23 @@ const validationToRule = (ruleId, ruleName, docs, getDocumentNode) => {
434
514
  },
435
515
  },
436
516
  create(context) {
517
+ if (!ruleFn) {
518
+ logger.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
519
+ return {};
520
+ }
521
+ let schema;
522
+ if (docs.requiresSchemaToExtend) {
523
+ schema = getGraphQLSchemaToExtend(ruleId, context);
524
+ }
525
+ else if (docs.requiresSchema) {
526
+ schema = requireGraphQLSchemaFromContext(ruleId, context);
527
+ }
437
528
  return {
438
529
  Document(node) {
439
- if (!ruleFn) {
440
- // eslint-disable-next-line no-console
441
- console.warn(`You rule "${ruleId}" depends on a GraphQL validation rule "${ruleName}" but it's not available in the "graphql-js" version you are using. Skipping...`);
442
- return;
443
- }
444
- const schema = docs.requiresSchema ? requireGraphQLSchemaFromContext(ruleId, context) : null;
445
530
  const documentNode = getDocumentNode
446
531
  ? getDocumentNode({ ruleId, context, schema, node: node.rawNode() })
447
532
  : node.rawNode();
448
- validateDocument(node, context, schema, documentNode, ruleFn);
533
+ validateDocument(context, schema, documentNode, ruleFn, docs.requiresSchemaToExtend);
449
534
  },
450
535
  };
451
536
  },
@@ -595,7 +680,9 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
595
680
  }), validationToRule('possible-type-extension', 'PossibleTypeExtensions', {
596
681
  category: 'Schema',
597
682
  description: `A type extension is only valid if the type is defined and has the same kind.`,
598
- recommended: false, // TODO: enable after https://github.com/dotansimha/graphql-eslint/issues/787 will be fixed
683
+ recommended: false,
684
+ requiresSchema: true,
685
+ requiresSchemaToExtend: true,
599
686
  }), validationToRule('provided-required-arguments', 'ProvidedRequiredArguments', {
600
687
  category: ['Schema', 'Operations'],
601
688
  description: `A field or directive is only valid if all required (non-null without a default value) field arguments have been provided.`,
@@ -623,9 +710,13 @@ const GRAPHQL_JS_VALIDATIONS = Object.assign({}, validationToRule('executable-de
623
710
  category: 'Schema',
624
711
  description: `A GraphQL enum type is only valid if all its values are uniquely named.`,
625
712
  recommended: false,
713
+ requiresSchema: true,
714
+ requiresSchemaToExtend: true,
626
715
  }), validationToRule('unique-field-definition-names', 'UniqueFieldDefinitionNames', {
627
716
  category: 'Schema',
628
717
  description: `A GraphQL complex type is only valid if all its fields are uniquely named.`,
718
+ requiresSchema: true,
719
+ requiresSchemaToExtend: true,
629
720
  }), validationToRule('unique-input-field-names', 'UniqueInputFieldNames', {
630
721
  category: 'Operations',
631
722
  description: `A GraphQL input object value is only valid if all supplied fields are uniquely named.`,
@@ -3028,12 +3119,13 @@ const rule$j = {
3028
3119
  },
3029
3120
  };
3030
3121
 
3122
+ const RULE_ID$2 = 'selection-set-depth';
3031
3123
  const rule$k = {
3032
3124
  meta: {
3033
3125
  docs: {
3034
3126
  category: 'Operations',
3035
3127
  description: `Limit the complexity of the GraphQL operations solely by their depth. Based on [graphql-depth-limit](https://github.com/stems/graphql-depth-limit).`,
3036
- url: 'https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/selection-set-depth.md',
3128
+ url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/${RULE_ID$2}.md`,
3037
3129
  requiresSiblings: true,
3038
3130
  examples: [
3039
3131
  {
@@ -3107,11 +3199,10 @@ const rule$k = {
3107
3199
  create(context) {
3108
3200
  let siblings = null;
3109
3201
  try {
3110
- siblings = requireSiblingsOperations('selection-set-depth', context);
3202
+ siblings = requireSiblingsOperations(RULE_ID$2, context);
3111
3203
  }
3112
3204
  catch (e) {
3113
- // eslint-disable-next-line no-console
3114
- console.warn(`Rule "selection-set-depth" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3205
+ logger.warn(`Rule "${RULE_ID$2}" works best with siblings operations loaded. For more info: http://bit.ly/graphql-eslint-operations`);
3115
3206
  }
3116
3207
  const { maxDepth } = context.options[0];
3117
3208
  const ignore = context.options[0].ignore || [];
@@ -3136,8 +3227,7 @@ const rule$k = {
3136
3227
  });
3137
3228
  }
3138
3229
  catch (e) {
3139
- // eslint-disable-next-line no-console
3140
- console.warn(`Rule "selection-set-depth" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3230
+ logger.warn(`Rule "${RULE_ID$2}" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`, e);
3141
3231
  }
3142
3232
  },
3143
3233
  };
@@ -3519,8 +3609,7 @@ function createGraphqlProcessor() {
3519
3609
  }
3520
3610
  }
3521
3611
  catch (e) {
3522
- // eslint-disable-next-line no-console
3523
- console.warn(`[graphql-eslint/processor]: Unable to process file "${filename}": `, e);
3612
+ logger.warn(`Unable to process file "${filename}"`, e);
3524
3613
  }
3525
3614
  }
3526
3615
  return [text];
@@ -3560,20 +3649,25 @@ const schemaCache = new Map();
3560
3649
  function getSchema(options = {}, gqlConfig) {
3561
3650
  const realFilepath = options.filePath ? getOnDiskFilepath(options.filePath) : null;
3562
3651
  const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
3563
- const schemaKey = asArray(projectForFile.schema)
3564
- .sort()
3565
- .join(',');
3652
+ const schemaKey = asArray(projectForFile.schema).sort().join(',');
3566
3653
  if (!schemaKey) {
3567
3654
  return null;
3568
3655
  }
3569
- let schema = schemaCache.get(schemaKey);
3570
- if (!schema) {
3656
+ if (schemaCache.has(schemaKey)) {
3657
+ return schemaCache.get(schemaKey);
3658
+ }
3659
+ let schema;
3660
+ try {
3571
3661
  schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
3572
3662
  cache: loaderCache,
3573
- ...options.schemaOptions
3663
+ ...options.schemaOptions,
3574
3664
  });
3575
- schemaCache.set(schemaKey, schema);
3576
3665
  }
3666
+ catch (e) {
3667
+ schema = null;
3668
+ logger.error('Error while loading schema\n', e);
3669
+ }
3670
+ schemaCache.set(schemaKey, schema);
3577
3671
  return schema;
3578
3672
  }
3579
3673
 
@@ -3586,10 +3680,10 @@ const handleVirtualPath = (documents) => {
3586
3680
  return source;
3587
3681
  }
3588
3682
  (_a = filepathMap[location]) !== null && _a !== void 0 ? _a : (filepathMap[location] = -1);
3589
- const index = filepathMap[location] += 1;
3683
+ const index = (filepathMap[location] += 1);
3590
3684
  return {
3591
3685
  ...source,
3592
- location: resolve(location, `${index}_document.graphql`)
3686
+ location: resolve(location, `${index}_document.graphql`),
3593
3687
  };
3594
3688
  });
3595
3689
  };
@@ -3598,9 +3692,7 @@ const siblingOperationsCache = new Map();
3598
3692
  const getSiblings = (filePath, gqlConfig) => {
3599
3693
  const realFilepath = filePath ? getOnDiskFilepath(filePath) : null;
3600
3694
  const projectForFile = realFilepath ? gqlConfig.getProjectForFile(realFilepath) : gqlConfig.getDefault();
3601
- const documentsKey = asArray(projectForFile.documents)
3602
- .sort()
3603
- .join(',');
3695
+ const documentsKey = asArray(projectForFile.documents).sort().join(',');
3604
3696
  if (!documentsKey) {
3605
3697
  return [];
3606
3698
  }
@@ -3608,7 +3700,7 @@ const getSiblings = (filePath, gqlConfig) => {
3608
3700
  if (!siblings) {
3609
3701
  const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
3610
3702
  skipGraphQLImport: true,
3611
- cache: loaderCache
3703
+ cache: loaderCache,
3612
3704
  });
3613
3705
  siblings = handleVirtualPath(documents);
3614
3706
  operationsCache.set(documentsKey, siblings);
@@ -3621,8 +3713,7 @@ function getSiblingOperations(options, gqlConfig) {
3621
3713
  let printed = false;
3622
3714
  const noopWarn = () => {
3623
3715
  if (!printed) {
3624
- // eslint-disable-next-line no-console
3625
- console.warn(`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`);
3716
+ logger.warn(`getSiblingOperations was called without any operations. Make sure to set "parserOptions.operations" to make this feature available!`);
3626
3717
  printed = true;
3627
3718
  }
3628
3719
  return [];
@@ -3686,8 +3777,7 @@ function getSiblingOperations(options, gqlConfig) {
3686
3777
  const name = spread.name.value;
3687
3778
  const fragmentInfo = getFragment(name);
3688
3779
  if (fragmentInfo.length === 0) {
3689
- // eslint-disable-next-line no-console
3690
- console.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3780
+ logger.warn(`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`);
3691
3781
  return;
3692
3782
  }
3693
3783
  const fragment = fragmentInfo[0];
@@ -3717,42 +3807,6 @@ function getSiblingOperations(options, gqlConfig) {
3717
3807
  return siblingOperations;
3718
3808
  }
3719
3809
 
3720
- let graphqlConfig;
3721
- function loadGraphqlConfig(options) {
3722
- // We don't want cache config on test environment
3723
- // Otherwise schema and documents will be same for all tests
3724
- if (process.env.NODE_ENV !== 'test' && graphqlConfig) {
3725
- return graphqlConfig;
3726
- }
3727
- const onDiskConfig = options.skipGraphQLConfig
3728
- ? null
3729
- : loadConfigSync({
3730
- throwOnEmpty: false,
3731
- throwOnMissing: false,
3732
- extensions: [addCodeFileLoaderExtension],
3733
- });
3734
- graphqlConfig =
3735
- onDiskConfig ||
3736
- new GraphQLConfig({
3737
- config: options.projects
3738
- ? { projects: options.projects }
3739
- : {
3740
- schema: (options.schema || ''),
3741
- documents: options.documents || options.operations,
3742
- extensions: options.extensions,
3743
- include: options.include,
3744
- exclude: options.exclude,
3745
- },
3746
- filepath: 'virtual-config',
3747
- }, [addCodeFileLoaderExtension]);
3748
- return graphqlConfig;
3749
- }
3750
- const addCodeFileLoaderExtension = api => {
3751
- api.loaders.schema.register(new CodeFileLoader());
3752
- api.loaders.documents.register(new CodeFileLoader());
3753
- return { name: 'graphql-eslint-loaders' };
3754
- };
3755
-
3756
3810
  let reachableTypesCache;
3757
3811
  function getReachableTypes(schema) {
3758
3812
  // We don't want cache reachableTypes on test environment
@@ -3835,7 +3889,7 @@ function parse(code, options) {
3835
3889
  return parseForESLint(code, options).ast;
3836
3890
  }
3837
3891
  function parseForESLint(code, options = {}) {
3838
- const gqlConfig = loadGraphqlConfig(options);
3892
+ const gqlConfig = loadCachedGraphQLConfig(options);
3839
3893
  const schema = getSchema(options, gqlConfig);
3840
3894
  const parserServices = {
3841
3895
  hasTypeInfo: schema !== null,
@@ -3867,6 +3921,7 @@ function parseForESLint(code, options = {}) {
3867
3921
  };
3868
3922
  }
3869
3923
  catch (e) {
3924
+ e.message = `[graphql-eslint] ${e.message}`;
3870
3925
  // In case of GraphQL parser error, we report it to ESLint as a parser error that matches the requirements
3871
3926
  // of ESLint. This will make sure to display it correctly in IDEs and lint results.
3872
3927
  if (e instanceof GraphQLError) {
@@ -3874,11 +3929,10 @@ function parseForESLint(code, options = {}) {
3874
3929
  index: e.positions[0],
3875
3930
  lineNumber: e.locations[0].line,
3876
3931
  column: e.locations[0].column,
3877
- message: `[graphql-eslint]: ${e.message}`,
3932
+ message: e.message,
3878
3933
  };
3879
3934
  throw eslintError;
3880
3935
  }
3881
- e.message = `[graphql-eslint]: ${e.message}`;
3882
3936
  throw e;
3883
3937
  }
3884
3938
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graphql-eslint/eslint-plugin",
3
- "version": "3.3.0",
3
+ "version": "3.4.0-alpha-c01d913.0",
4
4
  "description": "GraphQL plugin for ESLint",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {
@@ -11,6 +11,7 @@
11
11
  "@graphql-tools/code-file-loader": "7.2.3",
12
12
  "@graphql-tools/graphql-tag-pluck": "7.1.4",
13
13
  "@graphql-tools/utils": "8.5.5",
14
+ "chalk": "4.1.2",
14
15
  "graphql-config": "4.1.0",
15
16
  "graphql-depth-limit": "1.1.0",
16
17
  "lodash.lowercase": "4.3.0"
package/types.d.ts CHANGED
@@ -51,6 +51,7 @@ export declare type RuleDocsInfo<T> = {
51
51
  category: CategoryType | CategoryType[];
52
52
  requiresSchema?: true;
53
53
  requiresSiblings?: true;
54
+ requiresSchemaToExtend?: true;
54
55
  examples?: {
55
56
  title: string;
56
57
  code: string;
package/utils.d.ts CHANGED
@@ -6,6 +6,15 @@ import { SiblingOperations } from './sibling-operations';
6
6
  import { UsedFields, ReachableTypes } from './graphql-ast';
7
7
  export declare function requireSiblingsOperations(ruleName: string, context: GraphQLESLintRuleContext): SiblingOperations | never;
8
8
  export declare function requireGraphQLSchemaFromContext(ruleName: string, context: GraphQLESLintRuleContext): GraphQLSchema | never;
9
+ export declare const logger: {
10
+ error: (...args: any[]) => void;
11
+ warn: (...args: any[]) => void;
12
+ };
13
+ declare type GetGraphQLSchemaToExtend = {
14
+ (ruleId: string, ctx: GraphQLESLintRuleContext): GraphQLSchema | null;
15
+ warningPrintedMap: Record<string, boolean>;
16
+ };
17
+ export declare const getGraphQLSchemaToExtend: GetGraphQLSchemaToExtend;
9
18
  export declare function requireReachableTypesFromContext(ruleName: string, context: GraphQLESLintRuleContext): ReachableTypes | never;
10
19
  export declare function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext): UsedFields | never;
11
20
  export declare function extractTokens(source: Source): AST.Token[];
@@ -27,3 +36,4 @@ export declare function getLocation(loc: Partial<AST.SourceLocation>, fieldName?
27
36
  offsetStart?: number;
28
37
  offsetEnd?: number;
29
38
  }): AST.SourceLocation;
39
+ export {};