|
| 1 | +# Writing Custom Rules |
| 2 | + |
| 3 | +To get started with your own rules, start by understanding how [ESLint custom rules works](https://eslint.org/docs/developer-guide/working-with-rules). |
| 4 | + |
| 5 | +`graphql-eslint` converts the [GraphQL AST](https://graphql.org/graphql-js/language/) into [ESTree structure](https://github.com/estree/estree), so it allows you to easily travel the GraphQL AST tree easily. |
| 6 | + |
| 7 | +You can visit any GraphQL AST node in your custom rules, and report this as error. You don't need to have special handlers for code-files, since `graphql-eslint` extracts usages of `gql` and magic `/* GraphQL */` comments automatically, and runs it through the parser, and eventually it knows to adjust errors location to fit in your code files original location. |
| 8 | + |
| 9 | +## Getting Started |
| 10 | + |
| 11 | +Start by creating a [simple ESLint rule file](https://eslint.org/docs/developer-guide/working-with-rules), and choose the AST nodes you wish to visit. It can either be a [simple AST node `Kind`](https://github.com/graphql/graphql-js/blob/master/src/language/kinds.d.ts) or a complex [ESLint selector](https://eslint.org/docs/developer-guide/selectors) that allows you to travel and filter AST nodes. |
| 12 | + |
| 13 | +We recommend you to read the [graphql-eslint parser documentation](./parser.md) before getting started, to understand the differences between the AST structures. |
| 14 | + |
| 15 | +The `graphql-eslint` comes with a TypeScript wrapper for ESLint rules, and provides a testkit to simplify testing process with GraphQL schemas, so you can use that by importing `GraphQLESLintRule` type. But if you wish to use JavaScript - that's fine :) |
| 16 | + |
| 17 | +Here's an example for a simple rule that reports on anonymous GraphQL operations: |
| 18 | + |
| 19 | +```ts |
| 20 | +import { GraphQLESLintRule } from '@graphql-eslint/eslint-plugin'; |
| 21 | + |
| 22 | +const rule: GraphQLESLintRule = { |
| 23 | + create(context) { |
| 24 | + return { |
| 25 | + OperationDefinition(node) { |
| 26 | + if (node && (!node.name || node.name.value === '')) { |
| 27 | + context.report({ |
| 28 | + node: node, |
| 29 | + message: `Oops, name is required!`, |
| 30 | + }); |
| 31 | + } |
| 32 | + }, |
| 33 | + }; |
| 34 | + }, |
| 35 | +}; |
| 36 | +``` |
| 37 | + |
| 38 | +So what happens here? |
| 39 | + |
| 40 | +1. `@graphql-eslint/eslint-plugin` handles the parsing process for your GraphQL content. It will load the GraphQL files (either from code files or from `.graphql` files with SDL), parse it using GraphQL parser, converts it to ESTree structure and let ESLint do the rest. |
| 41 | +1. You rule is being loaded by ESLint, and executes just like any other ESLint rule. |
| 42 | +1. Our custom rule asks ESLint to run our function for every `OperationDefinition` found. |
| 43 | +1. If the `OperationDefinition` node doesn't have a valid `name` - we report an error to ESLint. |
| 44 | + |
| 45 | +#### More Examples |
| 46 | + |
| 47 | +You can scan the `packages/plugin/src/rules` directory in this repo for references for implementing rules. It coverts most of the use-cases and concepts of rules. |
| 48 | + |
| 49 | +## Accessing original GraphQL AST nodes |
| 50 | + |
| 51 | +Since our parser converts GraphQL AST to ESTree structure, there are some minor differences in the structure of the objects. |
| 52 | +If you are using TypeScript, and you typed your rule with `GraphQLESLintRule` - you'll see that each `node` is a bit different from from the AST nodes of GraphQL (you can read more about that in [graphql-eslint parser documentation](./parser.md)). |
| 53 | + |
| 54 | +If you need access to the original GraphQL AST `node`, you can use `.rawNode()` method on each node you get from the AST structure of ESLint. |
| 55 | + |
| 56 | +This is useful if you wish to use other GraphQL tools that works with the original GraphQL AST objects. |
| 57 | + |
| 58 | +Here's an example for using original `graphql-js` validate method to validate `OperationDefinition`: |
| 59 | + |
| 60 | +```ts |
| 61 | +import { validate } from 'graphql-js'; |
| 62 | +import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'; |
| 63 | + |
| 64 | +export const rule = { |
| 65 | + create(context) { |
| 66 | + return { |
| 67 | + OperationDefinition(node) { |
| 68 | + const schema = requireGraphQLSchemaFromContext(context); |
| 69 | + |
| 70 | + validate(context, schema, { |
| 71 | + kind: Kind.DOCUMENT, |
| 72 | + definitions: [node.rawNode()], |
| 73 | + }); |
| 74 | + }, |
| 75 | + }; |
| 76 | + }, |
| 77 | +}; |
| 78 | +``` |
| 79 | + |
| 80 | +## `TypeInfo` / `GraphQLSchema` |
| 81 | + |
| 82 | +If you provide GraphQL schema in your ESLint configuration, it will get loaded automatically, and become available in your rules in two ways: |
| 83 | + |
| 84 | +1. You'll be able to access the loaded `GraphQLSchema` object. |
| 85 | +2. In every visited node, you'll be able to use `.typeInfo()` method to get an object with complete type information on your visited node (see [TypeInfo documentation](https://graphql.org/graphql-js/utilities/#typeinfo)). |
| 86 | + |
| 87 | +#### Getting `GraphQLSchema` |
| 88 | + |
| 89 | +To mark your ESLint rules as a rule that needs access to GraphQL schema, start by running `requireGraphQLSchemaFromContext` from the plugin package, it will make sure to return a schema, or throw an error for the user about the missing schema. |
| 90 | + |
| 91 | +```ts |
| 92 | +const schema = requireGraphQLSchemaFromContext(context); |
| 93 | +``` |
| 94 | + |
| 95 | +#### Accessing TypeInfo |
| 96 | + |
| 97 | +If schema is provided and loaded successfully, the `typeInfo` will be available to use. Otherwise - it will be `undefined`. |
| 98 | +If your plugin requires `typeInfo` in order to operate and run, make sure to call `requireGraphQLSchemaFromContext` - it will validate that the schema is loaded. |
| 99 | + |
| 100 | +`typeInfo` is provided on every node, based on the type of that node, for example, to access the `GraphQLOutputType` while you are visiting a `SelectionSet` node, you can do: |
| 101 | + |
| 102 | +```ts |
| 103 | +import { requireGraphQLSchemaFromContext } from '@graphql-eslint/eslint-plugin'; |
| 104 | + |
| 105 | +export const rule = { |
| 106 | + create(context) { |
| 107 | + requireGraphQLSchemaFromContext(context); |
| 108 | + |
| 109 | + return { |
| 110 | + SelectionSet(node) { |
| 111 | + const typeInfo = node.typeInfo(); |
| 112 | + |
| 113 | + if (typeInfo && typeInfo.gqlType) { |
| 114 | + console.log(`The GraphQLOutputType is: ${typeInfo.gqlType}`); |
| 115 | + } |
| 116 | + }, |
| 117 | + }; |
| 118 | + }, |
| 119 | +}; |
| 120 | +``` |
| 121 | + |
| 122 | +The structure of the return value of `.typeInfo()` is [defined here](https://github.com/dotansimha/graphql-eslint/blob/master/packages/plugin/src/estree-parser/converter.ts#L38-L46). So based on the `node` you are using, you'll get a different values on `.typeInfo()` result. |
| 123 | + |
| 124 | +## Testing your rules |
| 125 | + |
| 126 | +To test your rules, you can either use the wrapped `GraphQLRuleTester` from this library, or use the built-it [`RuleTester`](https://eslint.org/docs/developer-guide/working-with-rules#rule-unit-tests) of ESLint. |
| 127 | + |
| 128 | +The wrapped `GraphQLRuleTester` provides built-in configured parser, and a schema loader, if you need to test your rule with a loaded schema. |
| 129 | + |
| 130 | +```ts |
| 131 | +import { GraphQLRuleTester } from '@graphql-eslint/eslint-plugin'; |
| 132 | +import { rule } from './my-rule'; |
| 133 | + |
| 134 | +const ruleTester = new GraphQLRuleTester(); |
| 135 | + |
| 136 | +ruleTester.runGraphQLTests('my-rule', rule, { |
| 137 | + valid: [ |
| 138 | + { |
| 139 | + code: `query something { foo }`, |
| 140 | + }, |
| 141 | + ], |
| 142 | + invalid: [ |
| 143 | + { |
| 144 | + code: `query invalid { foo }`, |
| 145 | + }, |
| 146 | + ], |
| 147 | +}); |
| 148 | +``` |
0 commit comments