@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
155 lines (152 loc) • 5.12 kB
JavaScript
import { isInterfaceType, Kind, visit, DirectiveLocation, } from 'graphql';
import lowerCase from 'lodash.lowercase';
import { getTypeName, requireGraphQLSchemaFromContext } from '../utils.js';
const RULE_ID = 'no-unreachable-types';
const KINDS = [
Kind.DIRECTIVE_DEFINITION,
Kind.OBJECT_TYPE_DEFINITION,
Kind.OBJECT_TYPE_EXTENSION,
Kind.INTERFACE_TYPE_DEFINITION,
Kind.INTERFACE_TYPE_EXTENSION,
Kind.SCALAR_TYPE_DEFINITION,
Kind.SCALAR_TYPE_EXTENSION,
Kind.INPUT_OBJECT_TYPE_DEFINITION,
Kind.INPUT_OBJECT_TYPE_EXTENSION,
Kind.UNION_TYPE_DEFINITION,
Kind.UNION_TYPE_EXTENSION,
Kind.ENUM_TYPE_DEFINITION,
Kind.ENUM_TYPE_EXTENSION,
];
let reachableTypesCache;
const RequestDirectiveLocations = new Set([
DirectiveLocation.QUERY,
DirectiveLocation.MUTATION,
DirectiveLocation.SUBSCRIPTION,
DirectiveLocation.FIELD,
DirectiveLocation.FRAGMENT_DEFINITION,
DirectiveLocation.FRAGMENT_SPREAD,
DirectiveLocation.INLINE_FRAGMENT,
DirectiveLocation.VARIABLE_DEFINITION,
]);
function getReachableTypes(schema) {
// We don't want cache reachableTypes on test environment
// Otherwise reachableTypes will be same for all tests
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
return reachableTypesCache;
}
const reachableTypes = new Set();
const collect = (node) => {
const typeName = getTypeName(node);
if (reachableTypes.has(typeName)) {
return;
}
reachableTypes.add(typeName);
const type = schema.getType(typeName) || schema.getDirective(typeName);
if (isInterfaceType(type)) {
const { objects, interfaces } = schema.getImplementations(type);
for (const { astNode } of [...objects, ...interfaces]) {
visit(astNode, visitor);
}
}
else if (type.astNode) {
// astNode can be undefined for ID, String, Boolean
visit(type.astNode, visitor);
}
};
const visitor = {
InterfaceTypeDefinition: collect,
ObjectTypeDefinition: collect,
InputValueDefinition: collect,
UnionTypeDefinition: collect,
FieldDefinition: collect,
Directive: collect,
NamedType: collect,
};
for (const type of [
schema,
schema.getQueryType(),
schema.getMutationType(),
schema.getSubscriptionType(),
]) {
// if schema don't have Query type, schema.astNode will be undefined
if (type === null || type === void 0 ? void 0 : type.astNode) {
visit(type.astNode, visitor);
}
}
for (const node of schema.getDirectives()) {
if (node.locations.some(location => RequestDirectiveLocations.has(location))) {
reachableTypes.add(node.name);
}
}
reachableTypesCache = reachableTypes;
return reachableTypesCache;
}
export const rule = {
meta: {
messages: {
[RULE_ID]: '{{ type }} `{{ typeName }}` is unreachable.',
},
docs: {
description: 'Requires all types to be reachable at some level by root level fields.',
category: 'Schema',
url: `https://github.com/B2o5T/graphql-eslint/blob/master/docs/rules/${RULE_ID}.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
}
`,
},
],
recommended: true,
},
type: 'suggestion',
schema: [],
hasSuggestions: true,
},
create(context) {
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
const reachableTypes = getReachableTypes(schema);
return {
[`:matches(${KINDS}) > .name`](node) {
const typeName = node.value;
if (!reachableTypes.has(typeName)) {
const type = lowerCase(node.parent.kind.replace(/(Extension|Definition)$/, ''));
context.report({
node,
messageId: RULE_ID,
data: {
type: type[0].toUpperCase() + type.slice(1),
typeName,
},
suggest: [
{
desc: `Remove \`${typeName}\``,
fix: fixer => fixer.remove(node.parent),
},
],
});
}
},
};
},
};