@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
195 lines (193 loc) • 7.41 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.rule = void 0;
const utils_1 = require("@graphql-tools/utils");
const graphql_1 = require("graphql");
const utils_js_1 = require("../utils.js");
const RULE_ID = 'require-description';
const ALLOWED_KINDS = [
...utils_js_1.TYPES_KINDS,
graphql_1.Kind.DIRECTIVE_DEFINITION,
graphql_1.Kind.FIELD_DEFINITION,
graphql_1.Kind.INPUT_VALUE_DEFINITION,
graphql_1.Kind.ENUM_VALUE_DEFINITION,
graphql_1.Kind.OPERATION_DEFINITION,
];
function getNodeName(node) {
const DisplayNodeNameMap = {
[graphql_1.Kind.OBJECT_TYPE_DEFINITION]: 'type',
[graphql_1.Kind.INTERFACE_TYPE_DEFINITION]: 'interface',
[graphql_1.Kind.ENUM_TYPE_DEFINITION]: 'enum',
[graphql_1.Kind.SCALAR_TYPE_DEFINITION]: 'scalar',
[graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION]: 'input',
[graphql_1.Kind.UNION_TYPE_DEFINITION]: 'union',
[graphql_1.Kind.DIRECTIVE_DEFINITION]: 'directive',
};
switch (node.kind) {
case graphql_1.Kind.OBJECT_TYPE_DEFINITION:
case graphql_1.Kind.INTERFACE_TYPE_DEFINITION:
case graphql_1.Kind.ENUM_TYPE_DEFINITION:
case graphql_1.Kind.SCALAR_TYPE_DEFINITION:
case graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION:
case graphql_1.Kind.UNION_TYPE_DEFINITION:
return `${DisplayNodeNameMap[node.kind]} ${node.name.value}`;
case graphql_1.Kind.DIRECTIVE_DEFINITION:
return `${DisplayNodeNameMap[node.kind]} @${node.name.value}`;
case graphql_1.Kind.FIELD_DEFINITION:
case graphql_1.Kind.INPUT_VALUE_DEFINITION:
case graphql_1.Kind.ENUM_VALUE_DEFINITION:
return `${node.parent.name.value}.${node.name.value}`;
case graphql_1.Kind.OPERATION_DEFINITION:
return node.name ? `${node.operation} ${node.name.value}` : node.operation;
}
}
const schema = {
type: 'array',
minItems: 1,
maxItems: 1,
items: {
type: 'object',
additionalProperties: false,
minProperties: 1,
properties: {
types: {
type: 'boolean',
description: `Includes:\n${utils_js_1.TYPES_KINDS.map(kind => `- \`${kind}\``).join('\n')}`,
},
rootField: {
type: 'boolean',
description: 'Definitions within `Query`, `Mutation`, and `Subscription` root types.',
},
...Object.fromEntries([...ALLOWED_KINDS].sort().map(kind => {
let description = `Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
if (kind === graphql_1.Kind.OPERATION_DEFINITION) {
description +=
'\n> You must use only comment syntax `#` and not description syntax `"""` or `"`.';
}
return [kind, { type: 'boolean', description }];
})),
},
},
};
exports.rule = {
meta: {
docs: {
category: 'Schema',
description: 'Enforce descriptions in type definitions and operations.',
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
examples: [
{
title: 'Incorrect',
usage: [{ types: true, FieldDefinition: true }],
code: /* GraphQL */ `
type someTypeName {
name: String
}
`,
},
{
title: 'Correct',
usage: [{ types: true, FieldDefinition: true }],
code: /* GraphQL */ `
"""
Some type description
"""
type someTypeName {
"""
Name description
"""
name: String
}
`,
},
{
title: 'Correct',
usage: [{ OperationDefinition: true }],
code: /* GraphQL */ `
# Create a new user
mutation createUser {
# ...
}
`,
},
{
title: 'Correct',
usage: [{ rootField: true }],
code: /* GraphQL */ `
type Mutation {
"Create a new user"
createUser: User
}
type User {
name: String
}
`,
},
],
configOptions: [
{
types: true,
[graphql_1.Kind.DIRECTIVE_DEFINITION]: true,
// rootField: true TODO enable in graphql-eslint v4
},
],
recommended: true,
},
type: 'suggestion',
messages: {
[RULE_ID]: 'Description is required for `{{ nodeName }}`.',
},
schema,
},
create(context) {
const { types, rootField, ...restOptions } = context.options[0] || {};
const kinds = new Set(types ? utils_js_1.TYPES_KINDS : []);
for (const [kind, isEnabled] of Object.entries(restOptions)) {
if (isEnabled) {
kinds.add(kind);
}
else {
kinds.delete(kind);
}
}
if (rootField) {
const schema = (0, utils_js_1.requireGraphQLSchemaFromContext)(RULE_ID, context);
const rootTypeNames = (0, utils_1.getRootTypeNames)(schema);
kinds.add(`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/^(${[
...rootTypeNames,
].join(',')})$/] > FieldDefinition`);
}
const selector = [...kinds].join(',');
return {
[selector](node) {
var _a;
let description = '';
const isOperation = node.kind === graphql_1.Kind.OPERATION_DEFINITION;
if (isOperation) {
const rawNode = node.rawNode();
const { prev, line } = rawNode.loc.startToken;
if ((prev === null || prev === void 0 ? void 0 : prev.kind) === graphql_1.TokenKind.COMMENT) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TODO: remove `!` when drop support of graphql@15
const value = prev.value.trim();
const linesBefore = line - prev.line;
if (!value.startsWith('eslint') && linesBefore === 1) {
description = value;
}
}
}
else {
description = ((_a = node.description) === null || _a === void 0 ? void 0 : _a.value.trim()) || '';
}
if (description.length === 0) {
context.report({
loc: isOperation ? (0, utils_js_1.getLocation)(node.loc.start, node.operation) : node.name.loc,
messageId: RULE_ID,
data: {
nodeName: getNodeName(node),
},
});
}
},
};
},
};