@graphql-eslint/eslint-plugin
Version:
GraphQL plugin for ESLint
248 lines (235 loc) • 6.87 kB
JavaScript
"use strict";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 _cachejs = require('../../cache.js');
var _utilsjs = require('../../utils.js');
const RULE_ID = "no-unused-fields";
const RELAY_SCHEMA = (
/* GraphQL */
`
# Root Query Type
type Query {
user: User
}
# User Type
type User {
id: ID!
name: String!
friends(first: Int, after: String): FriendConnection!
}
# FriendConnection Type (Relay Connection)
type FriendConnection {
edges: [FriendEdge]
pageInfo: PageInfo!
}
# FriendEdge Type
type FriendEdge {
cursor: String!
node: Friend!
}
# Friend Type
type Friend {
id: ID!
name: String!
}
# PageInfo Type (Relay Pagination)
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
`
);
const RELAY_QUERY = (
/* GraphQL */
`
query {
user {
id
name
friends(first: 10) {
edges {
node {
id
name
}
}
}
}
}
`
);
const RELAY_DEFAULT_IGNORED_FIELD_SELECTORS = [
"[parent.name.value=PageInfo][name.value=/(endCursor|startCursor|hasNextPage|hasPreviousPage)/]",
"[parent.name.value=/Edge$/][name.value=cursor]",
"[parent.name.value=/Connection$/][name.value=pageInfo]"
];
const schema = {
type: "array",
maxItems: 1,
items: {
type: "object",
additionalProperties: false,
properties: {
ignoredFieldSelectors: {
type: "array",
uniqueItems: true,
minItems: 1,
description: [
"Fields that will be ignored and are allowed to be unused.",
"",
"E.g. The following selector will ignore all the relay pagination fields for every connection exposed in the schema:",
"```json",
JSON.stringify(RELAY_DEFAULT_IGNORED_FIELD_SELECTORS, null, 2),
"```",
_utilsjs.eslintSelectorsTip
].join("\n"),
items: {
type: "string",
pattern: "^\\[(.+)]$"
}
}
}
}
};
const usedFieldsCache = new (0, _cachejs.ModuleCache)();
function getUsedFields(schema2, operations) {
const cachedValue = usedFieldsCache.get(schema2);
if (process.env.NODE_ENV !== "test" && cachedValue) {
return cachedValue;
}
const usedFields = /* @__PURE__ */ Object.create(null);
const typeInfo = new (0, _graphql.TypeInfo)(schema2);
const visitor = _graphql.visitWithTypeInfo.call(void 0, typeInfo, {
Field(node) {
const fieldDef = typeInfo.getFieldDef();
if (!fieldDef) {
return false;
}
const parentTypeName = typeInfo.getParentType().name;
const fieldName = node.name.value;
usedFields[parentTypeName] ??= /* @__PURE__ */ new Set();
usedFields[parentTypeName].add(fieldName);
}
});
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
for (const { document } of allDocuments) {
_graphql.visit.call(void 0, document, visitor);
}
usedFieldsCache.set(schema2, usedFields);
return usedFields;
}
const rule = {
meta: {
messages: {
[RULE_ID]: 'Field "{{fieldName}}" is unused'
},
docs: {
description: "Requires all fields to be used at some level by siblings operations.",
category: "Schema",
url: `https://the-guild.dev/graphql/eslint/rules/${RULE_ID}`,
requiresSiblings: true,
requiresSchema: true,
// Requires documents to be set
isDisabledForAllConfig: true,
examples: [
{
title: "Incorrect",
code: (
/* GraphQL */
`
type User {
id: ID!
name: String
someUnusedField: String
}
type Query {
me: User
}
query {
me {
id
name
}
}
`
)
},
{
title: "Correct",
code: (
/* GraphQL */
`
type User {
id: ID!
name: String
}
type Query {
me: User
}
query {
me {
id
name
}
}
`
)
},
{
title: "Correct (ignoring fields)",
usage: [{ ignoredFieldSelectors: RELAY_DEFAULT_IGNORED_FIELD_SELECTORS }],
code: (
/* GraphQL */
`
### 1\uFE0F\u20E3 YOUR SCHEMA
${RELAY_SCHEMA}
### 2\uFE0F\u20E3 YOUR QUERY
${RELAY_QUERY}
`
)
}
]
},
type: "suggestion",
schema,
hasSuggestions: true
},
create(context) {
const schema2 = _utilsjs.requireGraphQLSchema.call(void 0, RULE_ID, context);
const siblingsOperations = _utilsjs.requireGraphQLOperations.call(void 0, RULE_ID, context);
const usedFields = getUsedFields(schema2, siblingsOperations);
const { ignoredFieldSelectors } = context.options[0] || {};
const selector = (ignoredFieldSelectors || []).reduce(
(acc, selector2) => `${acc}:not(${selector2})`,
"FieldDefinition"
);
return {
[selector](node) {
const fieldName = node.name.value;
const parentTypeName = node.parent.name.value;
const isUsed = _optionalChain([usedFields, 'access', _ => _[parentTypeName], 'optionalAccess', _2 => _2.has, 'call', _3 => _3(fieldName)]);
if (isUsed) {
return;
}
context.report({
node: node.name,
messageId: RULE_ID,
data: { fieldName },
suggest: [
{
desc: `Remove \`${fieldName}\` field`,
fix(fixer) {
const sourceCode = context.getSourceCode();
const tokenBefore = sourceCode.getTokenBefore(node);
const tokenAfter = sourceCode.getTokenAfter(node);
const isEmptyType = tokenBefore.type === "{" && tokenAfter.type === "}";
return fixer.remove(isEmptyType ? node.parent : node);
}
}
]
});
}
};
}
};
exports.rule = rule;