@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
234 lines (222 loc) • 6.9 kB
JavaScript
Object.defineProperty(exports, "__esModule", {value: true}); function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }var _graphql = require('graphql');
var _utils = require('@graphql-tools/utils');
var _utilsjs = require('../../utils.js');
const RULE_ID = "require-description";
const ALLOWED_KINDS = [
..._utilsjs.TYPES_KINDS,
_graphql.Kind.DIRECTIVE_DEFINITION,
_graphql.Kind.FIELD_DEFINITION,
_graphql.Kind.INPUT_VALUE_DEFINITION,
_graphql.Kind.ENUM_VALUE_DEFINITION,
_graphql.Kind.OPERATION_DEFINITION
];
const entries = /* @__PURE__ */ Object.create(null);
for (const kind of [...ALLOWED_KINDS].sort()) {
let description = `> [!NOTE]
>
> Read more about this kind on [spec.graphql.org](https://spec.graphql.org/October2021/#${kind}).`;
if (kind === _graphql.Kind.OPERATION_DEFINITION) {
description += [
"",
"",
"> [!WARNING]",
">",
'> You must use only comment syntax `#` and not description syntax `"""` or `"`.'
].join("\n");
}
entries[kind] = { type: "boolean", description };
}
const schema = {
type: "array",
minItems: 1,
maxItems: 1,
items: {
type: "object",
additionalProperties: false,
minProperties: 1,
properties: {
types: {
type: "boolean",
enum: [true],
description: `Includes:
${_utilsjs.TYPES_KINDS.map((kind) => `- \`${kind}\``).join("\n")}`
},
rootField: {
type: "boolean",
enum: [true],
description: "Definitions within `Query`, `Mutation`, and `Subscription` root types."
},
ignoredSelectors: {
..._utilsjs.ARRAY_DEFAULT_OPTIONS,
description: ["Ignore specific selectors", _utilsjs.eslintSelectorsTip].join("\n")
},
...entries
}
}
};
const 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
}
`
)
},
{
title: "Correct",
usage: [
{
ignoredSelectors: [
"[type=ObjectTypeDefinition][name.value=PageInfo]",
"[type=ObjectTypeDefinition][name.value=/(Connection|Edge)$/]"
]
}
],
code: (
/* GraphQL */
`
type FriendConnection {
edges: [FriendEdge]
pageInfo: PageInfo!
}
type FriendEdge {
cursor: String!
node: Friend!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
`
)
}
],
configOptions: [
{
types: true,
[_graphql.Kind.DIRECTIVE_DEFINITION]: true,
rootField: true
}
],
recommended: true
},
type: "suggestion",
messages: {
[RULE_ID]: "Description is required for {{ nodeName }}"
},
schema
},
create(context) {
const { types, rootField, ignoredSelectors = [], ...restOptions } = context.options[0] || {};
const kinds = new Set(types ? _utilsjs.TYPES_KINDS : []);
for (const [kind, isEnabled] of Object.entries(restOptions)) {
if (isEnabled) {
kinds.add(kind);
} else {
kinds.delete(kind);
}
}
if (rootField) {
const schema2 = _utilsjs.requireGraphQLSchema.call(void 0, RULE_ID, context);
const rootTypeNames = _utils.getRootTypeNames.call(void 0, schema2);
kinds.add(
`:matches(ObjectTypeDefinition, ObjectTypeExtension)[name.value=/^(${[
...rootTypeNames
].join(",")})$/] > FieldDefinition`
);
}
let selector = `:matches(${[...kinds]})`;
for (const str of ignoredSelectors) {
selector += `:not(${str})`;
}
return {
[selector](node) {
let description = "";
const isOperation = node.kind === _graphql.Kind.OPERATION_DEFINITION;
if (isOperation) {
const rawNode = node.rawNode();
const { prev, line } = rawNode.loc.startToken;
if (_optionalChain([prev, 'optionalAccess', _ => _.kind]) === _graphql.TokenKind.COMMENT) {
const value = prev.value.trim();
const linesBefore = line - prev.line;
if (!value.startsWith("eslint") && linesBefore === 1) {
description = value;
}
}
} else {
description = _optionalChain([node, 'access', _2 => _2.description, 'optionalAccess', _3 => _3.value, 'access', _4 => _4.trim, 'call', _5 => _5()]) || "";
}
if (description.length === 0) {
context.report({
loc: isOperation ? _utilsjs.getLocation.call(void 0, node.loc.start, node.operation) : node.name.loc,
messageId: RULE_ID,
data: {
nodeName: _utilsjs.getNodeName.call(void 0, node)
}
});
}
}
};
}
};
exports.RULE_ID = RULE_ID; exports.rule = rule;
;