Skip to content

Commit

Permalink
NEW RULE: selection-set-depth
Browse files Browse the repository at this point in the history
  • Loading branch information
dotansimha committed Dec 22, 2020
1 parent e26a5d4 commit b093f88
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/curvy-baboons-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

NEW RULE: selection-set-depth
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
- [`no-operation-name-suffix`](./rules/no-operation-name-suffix.md)
- [`require-deprecation-reason`](./rules/require-deprecation-reason.md)
- [`avoid-operation-name-prefix`](./rules/avoid-operation-name-prefix.md)
- [`selection-set-depth`](./rules/selection-set-depth.md)
- [`no-case-insensitive-enum-values-duplicates`](./rules/no-case-insensitive-enum-values-duplicates.md)
- [`require-description`](./rules/require-description.md)
- [`require-id-when-available`](./rules/require-id-when-available.md)
Expand Down
56 changes: 56 additions & 0 deletions docs/rules/selection-set-depth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# `selection-set-depth`

- Category: `Best Practices`
- Rule name: `@graphql-eslint/selection-set-depth`
- Requires GraphQL Schema: `false` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
- Requires GraphQL Operations: `true` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)

Limit the complexity of the GraphQL operations solely by their depth. Based on https://github.com/stems/graphql-depth-limit .

## Usage Examples

### Incorrect

```graphql
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":1}]]

query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
```

### Correct

```graphql
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":4}]]

query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
```

### Correct (ignored field)

```graphql
# eslint @graphql-eslint/selection-set-depth: ["error", [{"maxDepth":1,"ignore":["albums"]}]]

query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
```

## Config Schema

The schema defines the following properties:
4 changes: 3 additions & 1 deletion packages/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
"@graphql-tools/code-file-loader": "~6.2.6",
"@graphql-tools/url-loader": "~6.7.0",
"@graphql-tools/graphql-tag-pluck": "~6.3.0",
"graphql-config": "^3.2.0"
"graphql-config": "^3.2.0",
"graphql-depth-limit": "1.1.0"
},
"devDependencies": {
"@types/graphql-depth-limit": "1.1.2",
"@types/eslint": "7.2.6",
"bob-the-bundler": "1.1.0",
"graphql-config": "3.2.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/plugin/src/rules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import uniqueFragmentName from './unique-fragment-name';
import uniqueOperationName from './unique-operation-name';
import noDeprecated from './no-deprecated';
import noHashtagDescription from './no-hashtag-description';
import selectionSetDepth from './selection-set-depth';
import { GraphQLESLintRule } from '../types';
import { GRAPHQL_JS_VALIDATIONS } from './graphql-js-validation';

Expand All @@ -27,6 +28,7 @@ export const rules: Record<string, GraphQLESLintRule> = {
'no-operation-name-suffix': noOperationNameSuffix,
'require-deprecation-reason': requireDeprecationReason,
'avoid-operation-name-prefix': avoidOperationNamePrefix,
'selection-set-depth': selectionSetDepth,
'no-case-insensitive-enum-values-duplicates': noCaseInsensitiveEnumValuesDuplicates,
'require-description': requireDescription,
'require-id-when-available': requireIdWhenAvailable,
Expand Down
133 changes: 133 additions & 0 deletions packages/plugin/src/rules/selection-set-depth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { GraphQLESLintRule } from '../types';
import depthLimit from 'graphql-depth-limit';
import { DocumentNode, FragmentDefinitionNode, GraphQLError, Kind, OperationDefinitionNode } from 'graphql';
import { GraphQLESTreeNode } from '../estree-parser';
import { requireSiblingsOperations } from '../utils';
import { SiblingOperations } from '../sibling-operations';

type SelectionSetDepthRuleConfig = [{ maxDepth: number; ignore?: string[] }];

const rule: GraphQLESLintRule<SelectionSetDepthRuleConfig> = {
meta: {
docs: {
category: 'Best Practices',
description: `Limit the complexity of the GraphQL operations solely by their depth. Based on https://github.com/stems/graphql-depth-limit .`,
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/selection-set-depth.md`,
requiresSchema: false,
requiresSiblings: true,
examples: [
{
title: 'Incorrect',
usage: [{ maxDepth: 1 }],
code: `
query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
`,
},
{
title: 'Correct',
usage: [{ maxDepth: 4 }],
code: `
query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
`,
},
{
title: 'Correct (ignored field)',
usage: [{ maxDepth: 1, ignore: ['albums'] }],
code: `
query deep2 {
viewer { # Level 0
albums { # Level 1
title # Level 2
}
}
}
`,
},
],
},
type: 'suggestion',
schema: {
type: 'array',
additionalItems: false,
minItems: 1,
maxItems: 1,
items: {
type: 'object',
require: ['maxDepth'],
properties: {
maxDepth: {
type: 'number',
},
ignore: {
type: 'array',
items: {
type: 'string',
},
},
},
},
},
},
create(context) {
let siblings: SiblingOperations | null = null;

try {
siblings = requireSiblingsOperations('selection-set-depth', context);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(
`Rule "selection-set-depth" works best with sibligns operations loaded. For more info: http://bit.ly/graphql-eslint-operations`
);
}

const maxDepth = context.options[0].maxDepth;
const ignore = context.options[0].ignore || [];
const checkFn = depthLimit(maxDepth, { ignore });

return ['OperationDefinition', 'FragmentDefinition'].reduce((prev, nodeType) => {
return {
...prev,
[nodeType]: (node: GraphQLESTreeNode<OperationDefinitionNode> | GraphQLESTreeNode<FragmentDefinitionNode>) => {
try {
const rawNode = node.rawNode();
const fragmentsInUse = siblings ? siblings.getFragmentsInUse(rawNode, true) : [];
const document: DocumentNode = {
kind: Kind.DOCUMENT,
definitions: [rawNode, ...fragmentsInUse],
};

checkFn({
getDocument: () => document,
reportError: (error: GraphQLError) => {
context.report({
loc: error.locations[0],
message: error.message,
});
},
});
} catch (e) {
// eslint-disable-next-line no-console
console.warn(
`Rule "selection-set-depth" check failed due to a missing siblings operations. For more info: http://bit.ly/graphql-eslint-operations`,
e
);
}
},
};
}, {});
},
};

export default rule;
89 changes: 89 additions & 0 deletions packages/plugin/tests/selection-set-depth.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { GraphQLRuleTester } from '../src/testkit';
import rule from '../src/rules/selection-set-depth';
import { ParserOptions } from '../src/types';

const WITH_SIBLINGS = {
parserOptions: <ParserOptions>{
operations: [
`fragment AlbumFields on Album {
id
}`,
],
},
};

const ruleTester = new GraphQLRuleTester();

ruleTester.runGraphQLTests('selection-set-depth', rule, {
valid: [
{
options: [{ maxDepth: 2 }],
code: /* GraphQL */ `
query deep2 {
viewer {
# Level 0
albums {
# Level 1
title # Level 2
}
}
}
`,
},
{
...WITH_SIBLINGS,
options: [{ maxDepth: 2 }],
code: /* GraphQL */ `
query deep2 {
viewer {
albums {
...AlbumFields
}
}
}
`,
},
{
...WITH_SIBLINGS,
options: [{ maxDepth: 1, ignore: ['albums'] }],
code: /* GraphQL */ `
query deep2 {
viewer {
albums {
...AlbumFields
}
}
}
`,
},
],
invalid: [
{
options: [{ maxDepth: 1 }],
errors: [{ message: `'deep2' exceeds maximum operation depth of 1` }],
code: /* GraphQL */ `
query deep2 {
viewer {
albums {
title
}
}
}
`,
},
{
...WITH_SIBLINGS,
options: [{ maxDepth: 1 }],
errors: [{ message: `'deep2' exceeds maximum operation depth of 1` }],
code: /* GraphQL */ `
query deep2 {
viewer {
albums {
...AlbumFields
}
}
}
`,
},
],
});
23 changes: 22 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,13 @@
dependencies:
"@types/node" "*"

"@types/graphql-depth-limit@1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@types/graphql-depth-limit/-/graphql-depth-limit-1.1.2.tgz#8e8a7b68548d703a3c3bd7f0531f314b3051f556"
integrity sha512-CJoghYUfE5/IKrqexgSsTECP0RcP2Ii+ulv/BjjFniABNAMgfwCTKFnCkkRskg9Sr3WZeJrSLjwBhNFetP5Tyw==
dependencies:
graphql "^14.5.3"

"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
version "2.0.3"
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
Expand Down Expand Up @@ -3234,6 +3241,13 @@ graphql-config@3.2.0, graphql-config@^3.2.0:
string-env-interpolation "1.0.1"
tslib "^2.0.0"

graphql-depth-limit@1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/graphql-depth-limit/-/graphql-depth-limit-1.1.0.tgz#59fe6b2acea0ab30ee7344f4c75df39cc18244e8"
integrity sha512-+3B2BaG8qQ8E18kzk9yiSdAa75i/hnnOwgSeAxVJctGQPvmeiLtqKOYF6HETCyRjiF7Xfsyal0HbLlxCQkgkrw==
dependencies:
arrify "^1.0.1"

graphql-tag@^2.11.0:
version "2.11.0"
resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd"
Expand All @@ -3260,6 +3274,13 @@ graphql@15.4.0:
resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347"
integrity sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA==

graphql@^14.5.3:
version "14.7.0"
resolved "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"
integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==
dependencies:
iterall "^1.2.2"

growly@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
Expand Down Expand Up @@ -3788,7 +3809,7 @@ istanbul-reports@^3.0.2:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"

iterall@^1.2.1:
iterall@^1.2.1, iterall@^1.2.2:
version "1.3.0"
resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
Expand Down

0 comments on commit b093f88

Please sign in to comment.