Skip to content

Commit 403b946

Browse files
authored
enhance(eslint-plugin): refactor the parts using tools (#564)
* enhance(eslint-plugin): refactor the parts using tools * Fix loader cacher
1 parent 844ac70 commit 403b946

File tree

8 files changed

+1087
-1131
lines changed

8 files changed

+1087
-1131
lines changed

.changeset/strong-mails-rest.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+
enhance(eslint-plugin): refactor the parts using tools

packages/plugin/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@
2626
"prepack": "bob prepack"
2727
},
2828
"dependencies": {
29-
"@graphql-tools/code-file-loader": "^7.0.1",
30-
"@graphql-tools/graphql-tag-pluck": "^7.0.1",
29+
"@graphql-tools/code-file-loader": "^7.0.2",
30+
"@graphql-tools/graphql-tag-pluck": "^7.0.2",
3131
"@graphql-tools/import": "^6.3.1",
32-
"@graphql-tools/utils": "^8.0.1",
32+
"@graphql-tools/utils": "^8.0.2",
3333
"graphql-config": "^4.0.1",
3434
"graphql-depth-limit": "1.1.0"
3535
},

packages/plugin/src/graphql-ast.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo, parse } from 'graphql';
2-
import { printSchemaWithDirectives } from '@graphql-tools/utils';
1+
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo } from 'graphql';
2+
import { getDocumentNodeFromSchema, getRootTypeNames } from '@graphql-tools/utils';
33
import { SiblingOperations } from './sibling-operations';
44

55
export type ReachableTypes = Set<string>;
@@ -12,10 +12,9 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
1212
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
1313
return reachableTypesCache;
1414
}
15-
// 👀 `printSchemaWithDirectives` keep all custom directives and `printSchema` from `graphql` not
16-
const printedSchema = printSchemaWithDirectives(schema); // Returns a string representation of the schema
17-
const astNode = parse(printedSchema); // Transforms the string into ASTNode
18-
const cache = Object.create(null);
15+
16+
const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
17+
const cache: Record<string, number> = Object.create(null);
1918

2019
const collect = (nodeType: any): void => {
2120
let node = nodeType;
@@ -32,10 +31,12 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
3231
node.operationTypes.forEach(collect);
3332
},
3433
ObjectTypeDefinition(node) {
35-
[node, ...node.interfaces].forEach(collect);
34+
collect(node);
35+
node.interfaces?.forEach(collect);
3636
},
3737
UnionTypeDefinition(node) {
38-
[node, ...node.types].forEach(collect);
38+
collect(node);
39+
node.types?.forEach(collect);
3940
},
4041
InputObjectTypeDefinition: collect,
4142
InterfaceTypeDefinition: collect,
@@ -49,7 +50,8 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
4950

5051
visit(astNode, visitor);
5152

52-
const operationTypeNames = new Set(['Query', 'Mutation', 'Subscription']);
53+
const operationTypeNames = getRootTypeNames(schema);
54+
5355
const usedTypes = Object.entries(cache)
5456
.filter(([typeName, usedCount]) => usedCount > 1 || operationTypeNames.has(typeName))
5557
.map(([typeName]) => typeName);
@@ -68,7 +70,7 @@ export function getUsedFields(schema: GraphQLSchema, operations: SiblingOperatio
6870
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
6971
return usedFieldsCache;
7072
}
71-
const usedFields: UsedFields = {};
73+
const usedFields: UsedFields = Object.create(null);
7274
const typeInfo = new TypeInfo(schema);
7375
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
7476

packages/plugin/src/schema.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { GraphQLSchema } from 'graphql';
22
import { GraphQLConfig } from 'graphql-config';
33
import { asArray } from '@graphql-tools/utils';
44
import { ParserOptions } from './types';
5-
import { getOnDiskFilepath } from './utils';
5+
import { getOnDiskFilepath, loaderCache } from './utils';
66

77
const schemaCache: Map<string, GraphQLSchema> = new Map();
88

@@ -17,12 +17,15 @@ export function getSchema(options: ParserOptions = {}, gqlConfig: GraphQLConfig)
1717
return null;
1818
}
1919

20-
if (schemaCache.has(schemaKey)) {
21-
return schemaCache.get(schemaKey);
22-
}
20+
let schema = schemaCache.get(schemaKey);
2321

24-
const schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', options.schemaOptions);
25-
schemaCache.set(schemaKey, schema);
22+
if (!schema) {
23+
schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
24+
cache: loaderCache,
25+
...options.schemaOptions
26+
});
27+
schemaCache.set(schemaKey, schema);
28+
}
2629

2730
return schema;
2831
}

packages/plugin/src/sibling-operations.ts

Lines changed: 92 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import { Source, asArray } from '@graphql-tools/utils';
1212
import { GraphQLConfig } from 'graphql-config';
1313
import { ParserOptions } from './types';
14-
import { getOnDiskFilepath } from './utils';
14+
import { getOnDiskFilepath, loaderCache } from './utils';
1515

1616
export type FragmentSource = { filePath: string; document: FragmentDefinitionNode };
1717
export type OperationSource = { filePath: string; document: OperationDefinitionNode };
@@ -61,15 +61,16 @@ const getSiblings = (filePath: string, gqlConfig: GraphQLConfig): Source[] => {
6161
return [];
6262
}
6363

64-
if (operationsCache.has(documentsKey)) {
65-
return operationsCache.get(documentsKey);
66-
}
64+
let siblings = operationsCache.get(documentsKey);
6765

68-
const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
69-
skipGraphQLImport: true,
70-
});
71-
const siblings = handleVirtualPath(documents)
72-
operationsCache.set(documentsKey, siblings);
66+
if (!siblings) {
67+
const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
68+
skipGraphQLImport: true,
69+
cache: loaderCache
70+
});
71+
siblings = handleVirtualPath(documents)
72+
operationsCache.set(documentsKey, siblings);
73+
}
7374

7475
return siblings;
7576
};
@@ -106,95 +107,95 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
106107
// Since the siblings array is cached, we can use it as cache key.
107108
// We should get the same array reference each time we get
108109
// to this point for the same graphql project
109-
if (siblingOperationsCache.has(siblings)) {
110-
return siblingOperationsCache.get(siblings);
111-
}
112-
113-
let fragmentsCache: FragmentSource[] | null = null;
114-
115-
const getFragments = (): FragmentSource[] => {
116-
if (fragmentsCache === null) {
117-
const result: FragmentSource[] = [];
118-
119-
for (const source of siblings) {
120-
for (const definition of source.document.definitions || []) {
121-
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
122-
result.push({
123-
filePath: source.location,
124-
document: definition,
125-
});
110+
let siblingOperations = siblingOperationsCache.get(siblings);
111+
if (!siblingOperations) {
112+
let fragmentsCache: FragmentSource[] | null = null;
113+
114+
const getFragments = (): FragmentSource[] => {
115+
if (fragmentsCache === null) {
116+
const result: FragmentSource[] = [];
117+
118+
for (const source of siblings) {
119+
for (const definition of source.document.definitions || []) {
120+
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
121+
result.push({
122+
filePath: source.location,
123+
document: definition,
124+
});
125+
}
126126
}
127127
}
128+
fragmentsCache = result;
128129
}
129-
fragmentsCache = result;
130-
}
131-
return fragmentsCache;
132-
};
133-
134-
let cachedOperations: OperationSource[] | null = null;
135-
136-
const getOperations = (): OperationSource[] => {
137-
if (cachedOperations === null) {
138-
const result: OperationSource[] = [];
139-
140-
for (const source of siblings) {
141-
for (const definition of source.document.definitions || []) {
142-
if (definition.kind === Kind.OPERATION_DEFINITION) {
143-
result.push({
144-
filePath: source.location,
145-
document: definition,
146-
});
130+
return fragmentsCache;
131+
};
132+
133+
let cachedOperations: OperationSource[] | null = null;
134+
135+
const getOperations = (): OperationSource[] => {
136+
if (cachedOperations === null) {
137+
const result: OperationSource[] = [];
138+
139+
for (const source of siblings) {
140+
for (const definition of source.document.definitions || []) {
141+
if (definition.kind === Kind.OPERATION_DEFINITION) {
142+
result.push({
143+
filePath: source.location,
144+
document: definition,
145+
});
146+
}
147147
}
148148
}
149+
cachedOperations = result;
149150
}
150-
cachedOperations = result;
151-
}
152-
return cachedOperations;
153-
};
154-
155-
const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);
156-
157-
const collectFragments = (
158-
selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
159-
recursive = true,
160-
collected: Map<string, FragmentDefinitionNode> = new Map()
161-
) => {
162-
visit(selectable, {
163-
FragmentSpread(spread: FragmentSpreadNode) {
164-
const name = spread.name.value;
165-
const fragmentInfo = getFragment(name);
166-
167-
if (fragmentInfo.length === 0) {
168-
// eslint-disable-next-line no-console
169-
console.warn(
170-
`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`
171-
);
172-
return;
173-
}
174-
const fragment = fragmentInfo[0];
175-
const alreadyVisited = collected.has(name);
151+
return cachedOperations;
152+
};
176153

177-
if (!alreadyVisited) {
178-
collected.set(name, fragment.document);
179-
if (recursive) {
180-
collectFragments(fragment.document, recursive, collected);
154+
const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);
155+
156+
const collectFragments = (
157+
selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
158+
recursive = true,
159+
collected: Map<string, FragmentDefinitionNode> = new Map()
160+
) => {
161+
visit(selectable, {
162+
FragmentSpread(spread: FragmentSpreadNode) {
163+
const name = spread.name.value;
164+
const fragmentInfo = getFragment(name);
165+
166+
if (fragmentInfo.length === 0) {
167+
// eslint-disable-next-line no-console
168+
console.warn(
169+
`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`
170+
);
171+
return;
181172
}
182-
}
183-
},
184-
});
185-
return collected;
186-
};
187-
188-
const siblingOperations: SiblingOperations = {
189-
available: true,
190-
getFragments,
191-
getOperations,
192-
getFragment,
193-
getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
194-
getOperation: name => getOperations().filter(o => o.document.name?.value === name),
195-
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
196-
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
197-
};
198-
siblingOperationsCache.set(siblings, siblingOperations);
173+
const fragment = fragmentInfo[0];
174+
const alreadyVisited = collected.has(name);
175+
176+
if (!alreadyVisited) {
177+
collected.set(name, fragment.document);
178+
if (recursive) {
179+
collectFragments(fragment.document, recursive, collected);
180+
}
181+
}
182+
},
183+
});
184+
return collected;
185+
};
186+
187+
siblingOperations = {
188+
available: true,
189+
getFragments,
190+
getOperations,
191+
getFragment,
192+
getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
193+
getOperation: name => getOperations().filter(o => o.document.name?.value === name),
194+
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
195+
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
196+
};
197+
198+
siblingOperationsCache.set(siblings, siblingOperations);
199+
}
199200
return siblingOperations;
200201
}

packages/plugin/src/utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { statSync } from 'fs';
22
import { dirname } from 'path';
3-
import { Source, Lexer, GraphQLSchema, Token, DocumentNode } from 'graphql';
3+
import { Lexer, GraphQLSchema, Token, DocumentNode, Source } from 'graphql';
44
import { GraphQLESLintRuleContext } from './types';
55
import { AST } from 'eslint';
66
import { SiblingOperations } from './sibling-operations';
77
import { UsedFields, ReachableTypes } from './graphql-ast';
8+
import { asArray, Source as LoaderSource } from '@graphql-tools/utils';
89

910
export function requireSiblingsOperations(
1011
ruleName: string,
@@ -134,3 +135,21 @@ export const getOnDiskFilepath = (filepath: string): string => {
134135

135136
return filepath;
136137
};
138+
139+
// Small workaround for the bug in older versions of @graphql-tools/load
140+
// Can be removed after graphql-config bumps to a new version
141+
export const loaderCache: Record<string, LoaderSource[]> = new Proxy(Object.create(null), {
142+
get(cache, key) {
143+
const value = cache[key];
144+
if (value) {
145+
return asArray(value);
146+
}
147+
return undefined;
148+
},
149+
set(cache, key, value) {
150+
if (value) {
151+
cache[key] = asArray(value);
152+
}
153+
return true;
154+
}
155+
});

patches/eslint+7.31.0.patch renamed to patches/eslint+7.32.0.patch

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
diff --git a/node_modules/eslint/lib/rule-tester/rule-tester.js b/node_modules/eslint/lib/rule-tester/rule-tester.js
2-
index cac81bc..3a3dbbe 100644
2+
index 2b55249..08547f3 100644
33
--- a/node_modules/eslint/lib/rule-tester/rule-tester.js
44
+++ b/node_modules/eslint/lib/rule-tester/rule-tester.js
5-
@@ -905,7 +905,17 @@ class RuleTester {
5+
@@ -911,7 +911,17 @@ class RuleTester {
66
"Expected no autofixes to be suggested"
77
);
88
} else {

0 commit comments

Comments
 (0)