Skip to content

Commit

Permalink
NEW RULE: no-unreachable-types rule (#243)
Browse files Browse the repository at this point in the history
  • Loading branch information
kamilkisiela committed Dec 23, 2020
1 parent 10debb5 commit 625f083
Show file tree
Hide file tree
Showing 9 changed files with 468 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-llamas-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

New rule: no-unreachable-types
41 changes: 41 additions & 0 deletions docs/rules/no-unreachable-types.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# `no-unreachable-types`

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

This rule allow you to enforce that all types have to reachable by root level fields (Query.*, Mutation.*, Subscription.*).

## Usage Examples

### Incorrect (field)

```graphql
# eslint @graphql-eslint/no-unreachable-types: ["error"]

type Query {
me: String
}

type User { # This is not used, so you'll get an error
id: ID!
name: String!
}
```


### Correct

```graphql
# eslint @graphql-eslint/no-unreachable-types: ["error"]

type Query {
me: User
}

type User { # This is now used, so you won't get an error
id: ID!
name: String!
}
```
116 changes: 116 additions & 0 deletions packages/plugin/src/graphql-ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import {
GraphQLSchema,
GraphQLFieldMap,
GraphQLInputFieldMap,
GraphQLField,
GraphQLInputField,
GraphQLInputType,
GraphQLOutputType,
GraphQLNamedType,
GraphQLInterfaceType,
GraphQLArgument,
isObjectType,
isInterfaceType,
isUnionType,
isInputObjectType,
isListType,
isNonNullType,
} from 'graphql';

export function createReachableTypesService(schema: GraphQLSchema): () => Set<string>;
export function createReachableTypesService(schema?: GraphQLSchema): () => Set<string> | null {
if (schema) {
let cache: Set<string> = null;
return () => {
if (!cache) {
cache = collectReachableTypes(schema);
}

return cache;
};
}

return () => null;
}

export function collectReachableTypes(schema: GraphQLSchema): Set<string> {
const reachableTypes = new Set<string>();

collectFrom(schema.getQueryType());
collectFrom(schema.getMutationType());
collectFrom(schema.getSubscriptionType());

return reachableTypes;

function collectFrom(type?: GraphQLNamedType): void {
if (type && shouldCollect(type.name)) {
if (isObjectType(type) || isInterfaceType(type)) {
collectFromFieldMap(type.getFields());
collectFromInterfaces(type.getInterfaces());
} else if (isUnionType(type)) {
type.getTypes().forEach(collectFrom);
} else if (isInputObjectType(type)) {
collectFromInputFieldMap(type.getFields());
}
}
}

function collectFromFieldMap(fieldMap: GraphQLFieldMap<any, any>): void {
for (const fieldName in fieldMap) {
collectFromField(fieldMap[fieldName]);
}
}

function collectFromField(field: GraphQLField<any, any>): void {
collectFromOutputType(field.type);
field.args.forEach(collectFromArgument);
}

function collectFromArgument(arg: GraphQLArgument): void {
collectFromInputType(arg.type);
}

function collectFromInputFieldMap(fieldMap: GraphQLInputFieldMap): void {
for (const fieldName in fieldMap) {
collectFromInputField(fieldMap[fieldName]);
}
}

function collectFromInputField(field: GraphQLInputField): void {
collectFromInputType(field.type);
}

function collectFromInterfaces(interfaces: GraphQLInterfaceType[]): void {
if (interfaces) {
interfaces.forEach(interfaceType => {
collectFromFieldMap(interfaceType.getFields());
collectFromInterfaces(interfaceType.getInterfaces());
});
}
}

function collectFromOutputType(output: GraphQLOutputType): void {
collectFrom(schema.getType(resolveName(output)));
}

function collectFromInputType(input: GraphQLInputType): void {
collectFrom(schema.getType(resolveName(input)));
}

function resolveName(type: GraphQLOutputType | GraphQLInputType) {
if (isListType(type) || isNonNullType(type)) {
return resolveName(type.ofType);
}

return type.name;
}

function shouldCollect(name: string): boolean {
if (!reachableTypes.has(name)) {
reachableTypes.add(name);
return true;
}

return false;
}
}
2 changes: 2 additions & 0 deletions packages/plugin/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { extractTokens } from './utils';
import { getSchema } from './schema';
import { getSiblingOperations } from './sibling-operations';
import { loadGraphqlConfig } from './graphql-config';
import { createReachableTypesService } from './graphql-ast';

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

try {
Expand Down
82 changes: 82 additions & 0 deletions packages/plugin/src/rules/no-unreachable-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { GraphQLESLintRule } from '../types';
import { requireReachableTypesFromContext } from '../utils';

const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';

const rule: GraphQLESLintRule = {
meta: {
messages: {
[UNREACHABLE_TYPE]: `Type "{{ typeName }}" is unreachable`,
},
docs: {
description: `Requires all types to be reachable at some level by root level fields`,
category: 'Best Practices',
url: `https://github.com/dotansimha/graphql-eslint/blob/master/docs/rules/no-unreachable-types.md`,
requiresSchema: true,
examples: [
{
title: 'Incorrect',
code: /* GraphQL */ `
type User {
id: ID!
name: String
}
type Query {
me: String
}
`,
},
{
title: 'Correct',
code: /* GraphQL */ `
type User {
id: ID!
name: String
}
type Query {
me: User
}
`,
},
],
},
fixable: 'code',
type: 'suggestion',
},
create(context) {
function ensureReachability(node) {
const typeName = node.name.value;
const reachableTypes = requireReachableTypesFromContext('no-unreachable-types', context);

if (!reachableTypes.has(typeName)) {
context.report({
node,
messageId: UNREACHABLE_TYPE,
data: {
typeName,
},
fix: fixer => fixer.removeRange(node.range)
});
}
}

return {
ObjectTypeDefinition: ensureReachability,
ObjectTypeExtension: ensureReachability,
InterfaceTypeDefinition: ensureReachability,
InterfaceTypeExtension: ensureReachability,
ScalarTypeDefinition: ensureReachability,
ScalarTypeExtension: ensureReachability,
InputObjectTypeDefinition: ensureReachability,
InputObjectTypeExtension: ensureReachability,
UnionTypeDefinition: ensureReachability,
UnionTypeExtension: ensureReachability,
EnumTypeDefinition: ensureReachability,
EnumTypeExtension: ensureReachability,
};
},
};

export default rule;
1 change: 1 addition & 0 deletions packages/plugin/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ParserServices = {
siblingOperations: SiblingOperations;
hasTypeInfo: boolean;
schema: GraphQLSchema | null;
getReachableTypes: () => Set<string> | null;
};

export type GraphQLESLintParseResult = Linter.ESLintParseResult & {
Expand Down
19 changes: 19 additions & 0 deletions packages/plugin/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ export function requireGraphQLSchemaFromContext(
return context.parserServices.schema;
}

export function requireReachableTypesFromContext(
ruleName: string,
context: GraphQLESlintRuleContext<any>
): Set<string> {
if (!context || !context.parserServices) {
throw new Error(
`Rule '${ruleName}' requires 'parserOptions.schema' to be set. See http://bit.ly/graphql-eslint-schema for more info`
);
}

if (!context.parserServices.schema) {
throw new Error(
`Rule '${ruleName}' requires 'parserOptions.schema' to be set and schema to be loaded. See http://bit.ly/graphql-eslint-schema for more info`
);
}

return context.parserServices.getReachableTypes();
}

function getLexer(source: Source): Lexer {
// GraphQL v14
const gqlLanguage = require('graphql/language');
Expand Down

0 comments on commit 625f083

Please sign in to comment.