Skip to content

Commit 625f083

Browse files
authored
NEW RULE: no-unreachable-types rule (#243)
1 parent 10debb5 commit 625f083

File tree

9 files changed

+468
-0
lines changed

9 files changed

+468
-0
lines changed

.changeset/forty-llamas-exist.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': minor
3+
---
4+
5+
New rule: no-unreachable-types

docs/rules/no-unreachable-types.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# `no-unreachable-types`
2+
3+
- Category: `Best Practices`
4+
- Rule name: `@graphql-eslint/no-unreachable-types`
5+
- Requires GraphQL Schema: `true` [ℹ️](../../README.md#extended-linting-rules-with-graphql-schema)
6+
- Requires GraphQL Operations: `false` [ℹ️](../../README.md#extended-linting-rules-with-siblings-operations)
7+
8+
This rule allow you to enforce that all types have to reachable by root level fields (Query.*, Mutation.*, Subscription.*).
9+
10+
## Usage Examples
11+
12+
### Incorrect (field)
13+
14+
```graphql
15+
# eslint @graphql-eslint/no-unreachable-types: ["error"]
16+
17+
type Query {
18+
me: String
19+
}
20+
21+
type User { # This is not used, so you'll get an error
22+
id: ID!
23+
name: String!
24+
}
25+
```
26+
27+
28+
### Correct
29+
30+
```graphql
31+
# eslint @graphql-eslint/no-unreachable-types: ["error"]
32+
33+
type Query {
34+
me: User
35+
}
36+
37+
type User { # This is now used, so you won't get an error
38+
id: ID!
39+
name: String!
40+
}
41+
```

packages/plugin/src/graphql-ast.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
GraphQLSchema,
3+
GraphQLFieldMap,
4+
GraphQLInputFieldMap,
5+
GraphQLField,
6+
GraphQLInputField,
7+
GraphQLInputType,
8+
GraphQLOutputType,
9+
GraphQLNamedType,
10+
GraphQLInterfaceType,
11+
GraphQLArgument,
12+
isObjectType,
13+
isInterfaceType,
14+
isUnionType,
15+
isInputObjectType,
16+
isListType,
17+
isNonNullType,
18+
} from 'graphql';
19+
20+
export function createReachableTypesService(schema: GraphQLSchema): () => Set<string>;
21+
export function createReachableTypesService(schema?: GraphQLSchema): () => Set<string> | null {
22+
if (schema) {
23+
let cache: Set<string> = null;
24+
return () => {
25+
if (!cache) {
26+
cache = collectReachableTypes(schema);
27+
}
28+
29+
return cache;
30+
};
31+
}
32+
33+
return () => null;
34+
}
35+
36+
export function collectReachableTypes(schema: GraphQLSchema): Set<string> {
37+
const reachableTypes = new Set<string>();
38+
39+
collectFrom(schema.getQueryType());
40+
collectFrom(schema.getMutationType());
41+
collectFrom(schema.getSubscriptionType());
42+
43+
return reachableTypes;
44+
45+
function collectFrom(type?: GraphQLNamedType): void {
46+
if (type && shouldCollect(type.name)) {
47+
if (isObjectType(type) || isInterfaceType(type)) {
48+
collectFromFieldMap(type.getFields());
49+
collectFromInterfaces(type.getInterfaces());
50+
} else if (isUnionType(type)) {
51+
type.getTypes().forEach(collectFrom);
52+
} else if (isInputObjectType(type)) {
53+
collectFromInputFieldMap(type.getFields());
54+
}
55+
}
56+
}
57+
58+
function collectFromFieldMap(fieldMap: GraphQLFieldMap<any, any>): void {
59+
for (const fieldName in fieldMap) {
60+
collectFromField(fieldMap[fieldName]);
61+
}
62+
}
63+
64+
function collectFromField(field: GraphQLField<any, any>): void {
65+
collectFromOutputType(field.type);
66+
field.args.forEach(collectFromArgument);
67+
}
68+
69+
function collectFromArgument(arg: GraphQLArgument): void {
70+
collectFromInputType(arg.type);
71+
}
72+
73+
function collectFromInputFieldMap(fieldMap: GraphQLInputFieldMap): void {
74+
for (const fieldName in fieldMap) {
75+
collectFromInputField(fieldMap[fieldName]);
76+
}
77+
}
78+
79+
function collectFromInputField(field: GraphQLInputField): void {
80+
collectFromInputType(field.type);
81+
}
82+
83+
function collectFromInterfaces(interfaces: GraphQLInterfaceType[]): void {
84+
if (interfaces) {
85+
interfaces.forEach(interfaceType => {
86+
collectFromFieldMap(interfaceType.getFields());
87+
collectFromInterfaces(interfaceType.getInterfaces());
88+
});
89+
}
90+
}
91+
92+
function collectFromOutputType(output: GraphQLOutputType): void {
93+
collectFrom(schema.getType(resolveName(output)));
94+
}
95+
96+
function collectFromInputType(input: GraphQLInputType): void {
97+
collectFrom(schema.getType(resolveName(input)));
98+
}
99+
100+
function resolveName(type: GraphQLOutputType | GraphQLInputType) {
101+
if (isListType(type) || isNonNullType(type)) {
102+
return resolveName(type.ofType);
103+
}
104+
105+
return type.name;
106+
}
107+
108+
function shouldCollect(name: string): boolean {
109+
if (!reachableTypes.has(name)) {
110+
reachableTypes.add(name);
111+
return true;
112+
}
113+
114+
return false;
115+
}
116+
}

packages/plugin/src/parser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { extractTokens } from './utils';
77
import { getSchema } from './schema';
88
import { getSiblingOperations } from './sibling-operations';
99
import { loadGraphqlConfig } from './graphql-config';
10+
import { createReachableTypesService } from './graphql-ast';
1011

1112
export function parse(code: string, options?: ParserOptions): Linter.ESLintParseResult['ast'] {
1213
return parseForESLint(code, options).ast;
@@ -20,6 +21,7 @@ export function parseForESLint(code: string, options?: ParserOptions): GraphQLES
2021
hasTypeInfo: schema !== null,
2122
schema,
2223
siblingOperations,
24+
getReachableTypes: createReachableTypesService(schema),
2325
};
2426

2527
try {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { GraphQLESLintRule } from '../types';
2+
import { requireReachableTypesFromContext } from '../utils';
3+
4+
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
5+
6+
const rule: GraphQLESLintRule = {
7+
meta: {
8+
messages: {
9+
[UNREACHABLE_TYPE]: `Type "{{ typeName }}" is unreachable`,
10+
},
11+
docs: {
12+
description: `Requires all types to be reachable at some level by root level fields`,
13+
category: 'Best Practices',
14+
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-unreachable-types.md`,
15+
requiresSchema: true,
16+
examples: [
17+
{
18+
title: 'Incorrect',
19+
code: /* GraphQL */ `
20+
type User {
21+
id: ID!
22+
name: String
23+
}
24+
25+
type Query {
26+
me: String
27+
}
28+
`,
29+
},
30+
{
31+
title: 'Correct',
32+
code: /* GraphQL */ `
33+
type User {
34+
id: ID!
35+
name: String
36+
}
37+
38+
type Query {
39+
me: User
40+
}
41+
`,
42+
},
43+
],
44+
},
45+
fixable: 'code',
46+
type: 'suggestion',
47+
},
48+
create(context) {
49+
function ensureReachability(node) {
50+
const typeName = node.name.value;
51+
const reachableTypes = requireReachableTypesFromContext('no-unreachable-types', context);
52+
53+
if (!reachableTypes.has(typeName)) {
54+
context.report({
55+
node,
56+
messageId: UNREACHABLE_TYPE,
57+
data: {
58+
typeName,
59+
},
60+
fix: fixer => fixer.removeRange(node.range)
61+
});
62+
}
63+
}
64+
65+
return {
66+
ObjectTypeDefinition: ensureReachability,
67+
ObjectTypeExtension: ensureReachability,
68+
InterfaceTypeDefinition: ensureReachability,
69+
InterfaceTypeExtension: ensureReachability,
70+
ScalarTypeDefinition: ensureReachability,
71+
ScalarTypeExtension: ensureReachability,
72+
InputObjectTypeDefinition: ensureReachability,
73+
InputObjectTypeExtension: ensureReachability,
74+
UnionTypeDefinition: ensureReachability,
75+
UnionTypeExtension: ensureReachability,
76+
EnumTypeDefinition: ensureReachability,
77+
EnumTypeExtension: ensureReachability,
78+
};
79+
},
80+
};
81+
82+
export default rule;

packages/plugin/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type ParserServices = {
1818
siblingOperations: SiblingOperations;
1919
hasTypeInfo: boolean;
2020
schema: GraphQLSchema | null;
21+
getReachableTypes: () => Set<string> | null;
2122
};
2223

2324
export type GraphQLESLintParseResult = Linter.ESLintParseResult & {

packages/plugin/src/utils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ export function requireGraphQLSchemaFromContext(
3838
return context.parserServices.schema;
3939
}
4040

41+
export function requireReachableTypesFromContext(
42+
ruleName: string,
43+
context: GraphQLESlintRuleContext<any>
44+
): Set<string> {
45+
if (!context || !context.parserServices) {
46+
throw new Error(
47+
`Rule '${ruleName}' requires 'parserOptions.schema' to be set. See http://bit.ly/graphql-eslint-schema for more info`
48+
);
49+
}
50+
51+
if (!context.parserServices.schema) {
52+
throw new Error(
53+
`Rule '${ruleName}' requires 'parserOptions.schema' to be set and schema to be loaded. See http://bit.ly/graphql-eslint-schema for more info`
54+
);
55+
}
56+
57+
return context.parserServices.getReachableTypes();
58+
}
59+
4160
function getLexer(source: Source): Lexer {
4261
// GraphQL v14
4362
const gqlLanguage = require('graphql/language');

0 commit comments

Comments
 (0)