Skip to content

Commit 5065482

Browse files
author
Dimitri POSTOLOV
authored
2️⃣ Fix caching for no-unreachable-types and no-unused-fields rules (#548)
1 parent 3701b2a commit 5065482

File tree

7 files changed

+86
-122
lines changed

7 files changed

+86
-122
lines changed

.changeset/lemon-donkeys-hammer.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-eslint/eslint-plugin': patch
3+
---
4+
5+
fix caching for `no-unreachable-types` and `no-unused-fields` rules

packages/plugin/src/graphql-ast.ts

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,41 @@ import {
99
GraphQLNamedType,
1010
GraphQLInterfaceType,
1111
GraphQLArgument,
12+
GraphQLDirective,
1213
isObjectType,
1314
isInterfaceType,
1415
isUnionType,
1516
isInputObjectType,
1617
isListType,
1718
isNonNullType,
18-
GraphQLDirective,
1919
TypeInfo,
2020
visit,
2121
visitWithTypeInfo,
2222
} from 'graphql';
2323
import { SiblingOperations } from './sibling-operations';
2424

25-
export function createReachableTypesService(schema: GraphQLSchema): () => Set<string>;
26-
export function createReachableTypesService(schema?: GraphQLSchema): () => Set<string> | null {
27-
if (schema) {
28-
let cache: Set<string> = null;
29-
return () => {
30-
if (!cache) {
31-
cache = collectReachableTypes(schema);
32-
}
25+
export type ReachableTypes = Set<string>;
3326

34-
return cache;
35-
};
36-
}
27+
let reachableTypesCache: ReachableTypes;
3728

38-
return () => null;
39-
}
29+
export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
30+
// We don't want cache reachableTypes on test environment
31+
// Otherwise reachableTypes will be same for all tests
32+
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
33+
return reachableTypesCache
34+
}
4035

41-
export function collectReachableTypes(schema: GraphQLSchema): Set<string> {
42-
const reachableTypes = new Set<string>();
36+
const reachableTypes: ReachableTypes = new Set();
4337

4438
collectFromDirectives(schema.getDirectives());
4539
collectFrom(schema.getQueryType());
4640
collectFrom(schema.getMutationType());
4741
collectFrom(schema.getSubscriptionType());
4842

49-
return reachableTypes;
43+
reachableTypesCache = reachableTypes;
44+
return reachableTypesCache;
5045

51-
function collectFromDirectives(directives: readonly GraphQLDirective[]) {
46+
function collectFromDirectives(directives: readonly GraphQLDirective[]): void {
5247
for (const directive of directives || []) {
5348
reachableTypes.add(directive.name);
5449
directive.args.forEach(collectFromArgument);
@@ -119,43 +114,33 @@ export function collectReachableTypes(schema: GraphQLSchema): Set<string> {
119114
if (isListType(type) || isNonNullType(type)) {
120115
return resolveName(type.ofType);
121116
}
122-
123117
return type.name;
124118
}
125119

126120
function shouldCollect(name: string): boolean {
127-
if (!reachableTypes.has(name)) {
128-
reachableTypes.add(name);
129-
return true;
121+
if (reachableTypes.has(name)) {
122+
return false;
130123
}
131-
132-
return false;
124+
reachableTypes.add(name);
125+
return true;
133126
}
134127
}
135128

136-
export type FieldsCache = Record<string, Set<string>>;
129+
export type UsedFields = Record<string, Set<string>>;
137130

138-
export function createUsedFieldsService(schema: GraphQLSchema, operations: SiblingOperations): () => FieldsCache | null {
139-
if (!schema || !operations) {
140-
return () => null;
141-
}
142-
143-
let cache: FieldsCache = null;
144-
145-
return () => {
146-
if (!cache) {
147-
cache = collectUsedFields(schema, operations);
148-
}
131+
let usedFieldsCache: UsedFields;
149132

150-
return cache;
151-
};
152-
}
133+
export function getUsedFields(schema: GraphQLSchema, operations: SiblingOperations): UsedFields {
134+
// We don't want cache usedFields on test environment
135+
// Otherwise usedFields will be same for all tests
136+
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
137+
return usedFieldsCache;
138+
}
153139

154-
export function collectUsedFields(schema: GraphQLSchema, operations: SiblingOperations): FieldsCache {
155-
const cache: FieldsCache = {};
140+
const usedFields: UsedFields = {};
156141

157142
const addField = (typeName, fieldName) => {
158-
const fieldType = cache[typeName] ?? (cache[typeName] = new Set());
143+
const fieldType = usedFields[typeName] ?? (usedFields[typeName] = new Set());
159144
fieldType.add(fieldName);
160145
};
161146

@@ -180,14 +165,12 @@ export function collectUsedFields(schema: GraphQLSchema, operations: SiblingOper
180165
},
181166
});
182167

183-
const allDocuments = [
184-
...operations.getOperations(),
185-
...operations.getFragments(),
186-
];
168+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
187169

188170
for (const { document } of allDocuments) {
189171
visit(document, visitor);
190172
}
191173

192-
return cache;
174+
usedFieldsCache = usedFields;
175+
return usedFieldsCache;
193176
}

packages/plugin/src/graphql-config.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,15 @@ export function loadGraphqlConfig(options: ParserOptions): GraphQLConfig {
2323
onDiskConfig ||
2424
new GraphQLConfig(
2525
{
26-
config: {
27-
schema: options.schema || '', // if undefined will throw error `Project 'default' not found`
28-
documents: options.documents || options.operations,
29-
extensions: options.extensions,
30-
include: options.include,
31-
exclude: options.exclude,
32-
projects: options.projects,
33-
},
26+
config: options.projects
27+
? { projects: options.projects }
28+
: {
29+
schema: options.schema || '', // if `options.schema` is `undefined` will throw error `Project 'default' not found`
30+
documents: options.documents || options.operations,
31+
extensions: options.extensions,
32+
include: options.include,
33+
exclude: options.exclude,
34+
},
3435
filepath: 'virtual-config',
3536
},
3637
[addCodeFileLoaderExtension]

packages/plugin/src/parser.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,26 @@ import { parseGraphQLSDL } from '@graphql-tools/utils';
22
import { GraphQLError, TypeInfo } from 'graphql';
33
import { Linter } from 'eslint';
44
import { convertToESTree } from './estree-parser';
5-
import { GraphQLESLintParseResult, ParserOptions } from './types';
5+
import { GraphQLESLintParseResult, ParserOptions, ParserServices } from './types';
66
import { extractTokens } from './utils';
77
import { getSchema } from './schema';
88
import { getSiblingOperations } from './sibling-operations';
99
import { loadGraphqlConfig } from './graphql-config';
10-
import { createReachableTypesService, createUsedFieldsService } from './graphql-ast';
10+
import { getReachableTypes, getUsedFields } from './graphql-ast';
1111

1212
export function parse(code: string, options?: ParserOptions): Linter.ESLintParseResult['ast'] {
1313
return parseForESLint(code, options).ast;
1414
}
1515

16-
export function parseForESLint(code: string, options?: ParserOptions): GraphQLESLintParseResult {
16+
export function parseForESLint(code: string, options: ParserOptions = {}): GraphQLESLintParseResult {
1717
const gqlConfig = loadGraphqlConfig(options);
1818
const schema = getSchema(options, gqlConfig);
19-
const siblingOperations = getSiblingOperations(options, gqlConfig);
20-
const parserServices = {
19+
const parserServices: ParserServices = {
2120
hasTypeInfo: schema !== null,
2221
schema,
23-
siblingOperations,
24-
getReachableTypes: createReachableTypesService(schema),
25-
getUsedFields: createUsedFieldsService(schema, siblingOperations),
22+
siblingOperations: getSiblingOperations(options, gqlConfig),
23+
reachableTypes: getReachableTypes,
24+
usedFields: getUsedFields,
2625
};
2726

2827
try {

packages/plugin/src/sibling-operations.ts

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
OperationDefinitionNode,
66
SelectionSetNode,
77
visit,
8+
OperationTypeNode,
89
} from 'graphql';
910
import { Source, asArray } from '@graphql-tools/utils';
1011
import { GraphQLConfig } from 'graphql-config';
@@ -25,7 +26,7 @@ export type SiblingOperations = {
2526
recursive: boolean
2627
): FragmentDefinitionNode[];
2728
getOperation(operationName: string): OperationSource[];
28-
getOperationByType(operationType: 'query' | 'mutation' | 'subscription'): OperationSource[];
29+
getOperationByType(operationType: OperationTypeNode): OperationSource[];
2930
};
3031

3132
const operationsCache: Map<string, Source[]> = new Map();
@@ -106,10 +107,8 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
106107
}
107108
}
108109
}
109-
110110
fragmentsCache = result;
111111
}
112-
113112
return fragmentsCache;
114113
};
115114

@@ -129,10 +128,8 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
129128
}
130129
}
131130
}
132-
133131
cachedOperations = result;
134132
}
135-
136133
return cachedOperations;
137134
};
138135

@@ -144,7 +141,7 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
144141
collected: Map<string, FragmentDefinitionNode> = new Map()
145142
) => {
146143
visit(selectable, {
147-
FragmentSpread: (spread: FragmentSpreadNode) => {
144+
FragmentSpread(spread: FragmentSpreadNode) {
148145
const name = spread.name.value;
149146
const fragmentInfo = getFragment(name);
150147

@@ -153,32 +150,29 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
153150
console.warn(
154151
`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`
155152
);
156-
} else {
157-
const fragment = fragmentInfo[0];
158-
const alreadyVisited = collected.has(name);
159-
160-
if (!alreadyVisited) {
161-
collected.set(spread.name.value, fragment.document);
153+
return;
154+
}
155+
const fragment = fragmentInfo[0];
156+
const alreadyVisited = collected.has(name);
162157

163-
if (recursive) {
164-
collectFragments(fragment.document, recursive, collected);
165-
}
158+
if (!alreadyVisited) {
159+
collected.set(name, fragment.document);
160+
if (recursive) {
161+
collectFragments(fragment.document, recursive, collected);
166162
}
167163
}
168164
},
169165
});
170-
171166
return collected;
172167
};
173168

174-
const siblingOperations = {
169+
const siblingOperations: SiblingOperations = {
175170
available: true,
176171
getFragments,
177172
getOperations,
178173
getFragment,
179-
getFragmentByType: (typeName: string) =>
180-
getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
181-
getOperation: (name: string) => getOperations().filter(o => o.document.name?.value === name),
174+
getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
175+
getOperation: name => getOperations().filter(o => o.document.name?.value === name),
182176
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
183177
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
184178
};

packages/plugin/src/types.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ 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 { FieldsCache } from './graphql-ast';
8+
import { getReachableTypes, getUsedFields } from './graphql-ast';
99

1010
export interface ParserOptions {
1111
schema?: SchemaPointer;
@@ -22,11 +22,11 @@ export interface ParserOptions {
2222
}
2323

2424
export type ParserServices = {
25-
siblingOperations: SiblingOperations;
2625
hasTypeInfo: boolean;
2726
schema: GraphQLSchema | null;
28-
getReachableTypes: () => Set<string> | null;
29-
getUsedFields: () => FieldsCache | null;
27+
siblingOperations: SiblingOperations;
28+
reachableTypes: typeof getReachableTypes;
29+
usedFields: typeof getUsedFields;
3030
};
3131

3232
export type GraphQLESLintParseResult = Linter.ESLintParseResult & {

packages/plugin/src/utils.ts

Lines changed: 17 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { Source, Lexer, GraphQLSchema, Token, DocumentNode } from 'graphql';
44
import { GraphQLESLintRuleContext } from './types';
55
import { AST } from 'eslint';
66
import { SiblingOperations } from './sibling-operations';
7-
import { FieldsCache } from './graphql-ast';
7+
import { UsedFields, ReachableTypes } from './graphql-ast';
88

9-
export function requireSiblingsOperations(ruleName: string, context: GraphQLESLintRuleContext<any>): SiblingOperations {
10-
if (!context || !context.parserServices) {
9+
export function requireSiblingsOperations(
10+
ruleName: string,
11+
context: GraphQLESLintRuleContext
12+
): SiblingOperations | never {
13+
if (!context.parserServices) {
1114
throw new Error(
1215
`Rule '${ruleName}' requires 'parserOptions.operations' to be set and loaded. See http://bit.ly/graphql-eslint-operations for more info`
1316
);
@@ -24,9 +27,9 @@ export function requireSiblingsOperations(ruleName: string, context: GraphQLESLi
2427

2528
export function requireGraphQLSchemaFromContext(
2629
ruleName: string,
27-
context: GraphQLESLintRuleContext<any>
28-
): GraphQLSchema {
29-
if (!context || !context.parserServices) {
30+
context: GraphQLESLintRuleContext
31+
): GraphQLSchema | never {
32+
if (!context.parserServices) {
3033
throw new Error(
3134
`Rule '${ruleName}' requires 'parserOptions.schema' to be set. See http://bit.ly/graphql-eslint-schema for more info`
3235
);
@@ -43,37 +46,16 @@ export function requireGraphQLSchemaFromContext(
4346

4447
export function requireReachableTypesFromContext(
4548
ruleName: string,
46-
context: GraphQLESLintRuleContext<any>
47-
): Set<string> {
48-
if (!context || !context.parserServices) {
49-
throw new Error(
50-
`Rule '${ruleName}' requires 'parserOptions.schema' to be set. See http://bit.ly/graphql-eslint-schema for more info`
51-
);
52-
}
53-
54-
if (!context.parserServices.schema) {
55-
throw new Error(
56-
`Rule '${ruleName}' requires 'parserOptions.schema' to be set and schema to be loaded. See http://bit.ly/graphql-eslint-schema for more info`
57-
);
58-
}
59-
60-
return context.parserServices.getReachableTypes();
49+
context: GraphQLESLintRuleContext
50+
): ReachableTypes | never {
51+
const schema = requireGraphQLSchemaFromContext(ruleName, context);
52+
return context.parserServices.reachableTypes(schema);
6153
}
6254

63-
export function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext<any>): FieldsCache {
64-
if (!context || !context.parserServices) {
65-
throw new Error(
66-
`Rule '${ruleName}' requires 'parserOptions.schema' to be set. See http://bit.ly/graphql-eslint-schema for more info`
67-
);
68-
}
69-
70-
if (!context.parserServices.schema) {
71-
throw new Error(
72-
`Rule '${ruleName}' requires 'parserOptions.schema' to be set and schema to be loaded. See http://bit.ly/graphql-eslint-schema for more info`
73-
);
74-
}
75-
76-
return context.parserServices.getUsedFields();
55+
export function requireUsedFieldsFromContext(ruleName: string, context: GraphQLESLintRuleContext): UsedFields | never {
56+
const schema = requireGraphQLSchemaFromContext(ruleName, context);
57+
const siblings = requireSiblingsOperations(ruleName, context);
58+
return context.parserServices.usedFields(schema, siblings);
7759
}
7860

7961
function getLexer(source: Source): Lexer {

0 commit comments

Comments
 (0)