Skip to content

Commit 17425dd

Browse files
authored
refactor: no-unused-fields and no-unreachable-types, move visitor functions inside rule files (#973)
* move visitor functions inside rule files * improve error message in `no-unreachable-types`
1 parent 1f697f0 commit 17425dd

File tree

9 files changed

+146
-160
lines changed

9 files changed

+146
-160
lines changed

packages/plugin/src/graphql-ast.ts

Lines changed: 0 additions & 95 deletions
This file was deleted.

packages/plugin/src/parser.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { GraphQLESLintParseResult, ParserOptions, ParserServices } from './types
66
import { getSchema } from './schema';
77
import { getSiblingOperations } from './sibling-operations';
88
import { loadGraphQLConfig } from './graphql-config';
9-
import { getReachableTypes, getUsedFields } from './graphql-ast';
109

1110
export function parse(code: string, options?: ParserOptions): Linter.ESLintParseResult['ast'] {
1211
return parseForESLint(code, options).ast;
@@ -19,8 +18,6 @@ export function parseForESLint(code: string, options: ParserOptions = {}): Graph
1918
hasTypeInfo: schema !== null,
2019
schema,
2120
siblingOperations: getSiblingOperations(options, gqlConfig),
22-
reachableTypes: getReachableTypes,
23-
usedFields: getUsedFields,
2421
};
2522

2623
try {
@@ -31,7 +28,10 @@ export function parseForESLint(code: string, options: ParserOptions = {}): Graph
3128
noLocation: false,
3229
});
3330

34-
const { rootTree, comments } = convertToESTree(graphqlAst.document as ASTNode, schema ? new TypeInfo(schema) : null);
31+
const { rootTree, comments } = convertToESTree(
32+
graphqlAst.document as ASTNode,
33+
schema ? new TypeInfo(schema) : null
34+
);
3535
const tokens = extractTokens(new Source(code, filePath));
3636

3737
return {

packages/plugin/src/rules/no-unreachable-types.ts

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { ASTKindToNode, Kind } from 'graphql';
1+
import { ASTKindToNode, ASTNode, ASTVisitor, GraphQLSchema, isInterfaceType, Kind, visit } from 'graphql';
2+
import lowerCase from 'lodash.lowercase';
23
import { GraphQLESLintRule, ValueOf } from '../types';
3-
import { requireReachableTypesFromContext } from '../utils';
4+
import { getTypeName, requireGraphQLSchemaFromContext } from '../utils';
45
import { GraphQLESTreeNode } from '../estree-parser';
56

6-
const UNREACHABLE_TYPE = 'UNREACHABLE_TYPE';
77
const RULE_ID = 'no-unreachable-types';
88

99
const KINDS = [
@@ -25,10 +25,66 @@ const KINDS = [
2525
type AllowedKind = typeof KINDS[number];
2626
type AllowedKindToNode = Pick<ASTKindToNode, AllowedKind>;
2727

28+
type ReachableTypes = Set<string>;
29+
30+
let reachableTypesCache: ReachableTypes;
31+
32+
function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
33+
// We don't want cache reachableTypes on test environment
34+
// Otherwise reachableTypes will be same for all tests
35+
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
36+
return reachableTypesCache;
37+
}
38+
const reachableTypes: ReachableTypes = new Set();
39+
40+
const collect = (node: ASTNode): false | void => {
41+
const typeName = getTypeName(node);
42+
if (reachableTypes.has(typeName)) {
43+
return;
44+
}
45+
reachableTypes.add(typeName);
46+
const type = schema.getType(typeName) || schema.getDirective(typeName);
47+
48+
if (isInterfaceType(type)) {
49+
const { objects, interfaces } = schema.getImplementations(type);
50+
for (const { astNode } of [...objects, ...interfaces]) {
51+
visit(astNode, visitor);
52+
}
53+
} else if (type.astNode) {
54+
// astNode can be undefined for ID, String, Boolean
55+
visit(type.astNode, visitor);
56+
}
57+
};
58+
59+
const visitor: ASTVisitor = {
60+
InterfaceTypeDefinition: collect,
61+
ObjectTypeDefinition: collect,
62+
InputValueDefinition: collect,
63+
UnionTypeDefinition: collect,
64+
FieldDefinition: collect,
65+
Directive: collect,
66+
NamedType: collect,
67+
};
68+
69+
for (const type of [
70+
schema, // visiting SchemaDefinition node
71+
schema.getQueryType(),
72+
schema.getMutationType(),
73+
schema.getSubscriptionType(),
74+
]) {
75+
// if schema don't have Query type, schema.astNode will be undefined
76+
if (type?.astNode) {
77+
visit(type.astNode, visitor);
78+
}
79+
}
80+
reachableTypesCache = reachableTypes;
81+
return reachableTypesCache;
82+
}
83+
2884
const rule: GraphQLESLintRule = {
2985
meta: {
3086
messages: {
31-
[UNREACHABLE_TYPE]: 'Type "{{ typeName }}" is unreachable',
87+
[RULE_ID]: '{{ type }} `{{ typeName }}` is unreachable.',
3288
},
3389
docs: {
3490
description: `Requires all types to be reachable at some level by root level fields.`,
@@ -70,18 +126,23 @@ const rule: GraphQLESLintRule = {
70126
hasSuggestions: true,
71127
},
72128
create(context) {
73-
const reachableTypes = requireReachableTypesFromContext(RULE_ID, context);
129+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
130+
const reachableTypes = getReachableTypes(schema);
74131
const selector = KINDS.join(',');
75132

76133
return {
77134
[selector](node: GraphQLESTreeNode<ValueOf<AllowedKindToNode>>) {
78135
const typeName = node.name.value;
79136

80137
if (!reachableTypes.has(typeName)) {
138+
const type = lowerCase(node.kind.replace(/(Extension|Definition)$/, ''))
81139
context.report({
82140
node: node.name,
83-
messageId: UNREACHABLE_TYPE,
84-
data: { typeName },
141+
messageId: RULE_ID,
142+
data: {
143+
type: type[0].toUpperCase() + type.slice(1),
144+
typeName
145+
},
85146
suggest: [
86147
{
87148
desc: `Remove \`${typeName}\``,

packages/plugin/src/rules/no-unused-fields.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,50 @@
1+
import { GraphQLSchema, TypeInfo, visit, visitWithTypeInfo } from 'graphql';
12
import { GraphQLESLintRule } from '../types';
2-
import { requireUsedFieldsFromContext } from '../utils';
3+
import { requireGraphQLSchemaFromContext, requireSiblingsOperations } from '../utils';
4+
import { SiblingOperations } from '../sibling-operations';
35

4-
const UNUSED_FIELD = 'UNUSED_FIELD';
56
const RULE_ID = 'no-unused-fields';
67

8+
type UsedFields = Record<string, Set<string>>;
9+
10+
let usedFieldsCache: UsedFields;
11+
12+
function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): UsedFields {
13+
// We don't want cache usedFields on test environment
14+
// Otherwise usedFields will be same for all tests
15+
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
16+
return usedFieldsCache;
17+
}
18+
const usedFields: UsedFields = Object.create(null);
19+
const typeInfo = new TypeInfo(schema);
20+
21+
const visitor = visitWithTypeInfo(typeInfo, {
22+
Field(node): false | void {
23+
const fieldDef = typeInfo.getFieldDef();
24+
if (!fieldDef) {
25+
// skip visiting this node if field is not defined in schema
26+
return false;
27+
}
28+
const parentTypeName = typeInfo.getParentType().name;
29+
const fieldName = node.name.value;
30+
31+
usedFields[parentTypeName] ??= new Set();
32+
usedFields[parentTypeName].add(fieldName);
33+
},
34+
});
35+
36+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
37+
for (const { document } of allDocuments) {
38+
visit(document, visitor);
39+
}
40+
usedFieldsCache = usedFields;
41+
return usedFieldsCache;
42+
}
43+
744
const rule: GraphQLESLintRule = {
845
meta: {
946
messages: {
10-
[UNUSED_FIELD]: `Field "{{fieldName}}" is unused`,
47+
[RULE_ID]: `Field "{{fieldName}}" is unused`,
1148
},
1249
docs: {
1350
description: `Requires all fields to be used at some level by siblings operations.`,
@@ -65,7 +102,9 @@ const rule: GraphQLESLintRule = {
65102
hasSuggestions: true,
66103
},
67104
create(context) {
68-
const usedFields = requireUsedFieldsFromContext(RULE_ID, context);
105+
const schema = requireGraphQLSchemaFromContext(RULE_ID, context);
106+
const siblingsOperations = requireSiblingsOperations(RULE_ID, context);
107+
const usedFields = getUsedFields(schema, siblingsOperations);
69108

70109
return {
71110
FieldDefinition(node) {
@@ -79,7 +118,7 @@ const rule: GraphQLESLintRule = {
79118

80119
context.report({
81120
node: node.name,
82-
messageId: UNUSED_FIELD,
121+
messageId: RULE_ID,
83122
data: { fieldName },
84123
suggest: [
85124
{

packages/plugin/src/rules/selection-set-depth.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ const rule: GraphQLESLintRule<[SelectionSetDepthRuleConfig]> = {
128128
{
129129
desc: 'Remove selections',
130130
fix(fixer) {
131-
const { line, column } = error.locations[0];
132131
const ancestors = context.getAncestors();
133132
const token = (ancestors[0] as AST.Program).tokens.find(
134133
token => token.loc.start.line === line && token.loc.start.column === column - 1

packages/plugin/src/types.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { IExtensions, IGraphQLProject, DocumentPointer, SchemaPointer } from 'gr
55
import { GraphQLESLintRuleListener } from './testkit';
66
import { GraphQLESTreeNode } from './estree-parser';
77
import { SiblingOperations } from './sibling-operations';
8-
import { getReachableTypes, getUsedFields } from './graphql-ast';
98

109
export interface ParserOptions {
1110
schema?: SchemaPointer | Record<string, { headers: Record<string, string> }>;
@@ -27,8 +26,6 @@ export type ParserServices = {
2726
hasTypeInfo: boolean;
2827
schema: GraphQLSchema | null;
2928
siblingOperations: SiblingOperations;
30-
reachableTypes: typeof getReachableTypes;
31-
usedFields: typeof getUsedFields;
3229
};
3330

3431
export type GraphQLESLintParseResult = Linter.ESLintParseResult & {

packages/plugin/src/utils.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import lowerCase from 'lodash.lowercase';
77
import chalk from 'chalk';
88
import { GraphQLESLintRuleContext } from './types';
99
import { SiblingOperations } from './sibling-operations';
10-
import { ReachableTypes, UsedFields } from './graphql-ast';
1110
import type * as ESTree from 'estree';
1211

1312
export function requireSiblingsOperations(
@@ -55,20 +54,6 @@ export const logger = {
5554
warn: (...args) => console.warn(chalk.yellow('warning'), '[graphql-eslint]', chalk(...args)),
5655
};
5756

58-
export function requireReachableTypesFromContext(
59-
ruleName: string,
60-
context: GraphQLESLintRuleContext
61-
): ReachableTypes | never {
62-
const schema = requireGraphQLSchemaFromContext(ruleName, context);
63-
return context.parserServices.reachableTypes(schema);
64-
}
65-
66-
export function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext): UsedFields | never {
67-
const schema = requireGraphQLSchemaFromContext(ruleName, context);
68-
const siblings = requireSiblingsOperations(ruleName, context);
69-
return context.parserServices.usedFields(schema, siblings);
70-
}
71-
7257
export const normalizePath = (path: string): string => (path || '').replace(/\\/g, '/');
7358

7459
/**

0 commit comments

Comments
 (0)