@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
159 lines (156 loc) • 5.56 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const tslib_1 = require("tslib");
const graphql_1 = require("graphql");
const lodash_lowercase_1 = tslib_1.__importDefault(require("lodash.lowercase"));
const utils_js_1 = require("../utils.js");
const RULE_ID = 'no-unreachable-types';
const KINDS = [
graphql_1.Kind.DIRECTIVE_DEFINITION,
graphql_1.Kind.OBJECT_TYPE_DEFINITION,
graphql_1.Kind.OBJECT_TYPE_EXTENSION,
graphql_1.Kind.INTERFACE_TYPE_DEFINITION,
graphql_1.Kind.INTERFACE_TYPE_EXTENSION,
graphql_1.Kind.SCALAR_TYPE_DEFINITION,
graphql_1.Kind.SCALAR_TYPE_EXTENSION,
graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION,
graphql_1.Kind.INPUT_OBJECT_TYPE_EXTENSION,
graphql_1.Kind.UNION_TYPE_DEFINITION,
graphql_1.Kind.UNION_TYPE_EXTENSION,
graphql_1.Kind.ENUM_TYPE_DEFINITION,
graphql_1.Kind.ENUM_TYPE_EXTENSION,
];
let reachableTypesCache;
const RequestDirectiveLocations = new Set([
graphql_1.DirectiveLocation.QUERY,
graphql_1.DirectiveLocation.MUTATION,
graphql_1.DirectiveLocation.SUBSCRIPTION,
graphql_1.DirectiveLocation.FIELD,
graphql_1.DirectiveLocation.FRAGMENT_DEFINITION,
graphql_1.DirectiveLocation.FRAGMENT_SPREAD,
graphql_1.DirectiveLocation.INLINE_FRAGMENT,
graphql_1.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 = (0, utils_js_1.getTypeName)(node);
if (reachableTypes.has(typeName)) {
return;
}
reachableTypes.add(typeName);
const type = schema.getType(typeName) || schema.getDirective(typeName);
if ((0, graphql_1.isInterfaceType)(type)) {
const { objects, interfaces } = schema.getImplementations(type);
for (const { astNode } of [...objects, ...interfaces]) {
(0, graphql_1.visit)(astNode, visitor);
}
}
else if (type === null || type === void 0 ? void 0 : type.astNode) {
// astNode can be undefined for ID, String, Boolean
(0, graphql_1.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) {
(0, graphql_1.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;
}
exports.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://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
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 = (0, utils_js_1.requireGraphQLSchemaFromContext)(RULE_ID, context);
const reachableTypes = getReachableTypes(schema);
return {
[`:matches(${KINDS}) > .name`](node) {
const typeName = node.value;
if (!reachableTypes.has(typeName)) {
const type = (0, lodash_lowercase_1.default)(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),
},
],
});
}
},
};
},
};