Skip to content

Commit

Permalink
enhance(eslint-plugin): refactor the parts using tools (#564)
Browse files Browse the repository at this point in the history
* enhance(eslint-plugin): refactor the parts using tools

* Fix loader cacher
  • Loading branch information
ardatan committed Aug 11, 2021
1 parent 844ac70 commit 403b946
Show file tree
Hide file tree
Showing 8 changed files with 1,087 additions and 1,131 deletions.
5 changes: 5 additions & 0 deletions .changeset/strong-mails-rest.md
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': patch
---

enhance(eslint-plugin): refactor the parts using tools
6 changes: 3 additions & 3 deletions packages/plugin/package.json
Expand Up @@ -26,10 +26,10 @@
"prepack": "bob prepack"
},
"dependencies": {
"@graphql-tools/code-file-loader": "^7.0.1",
"@graphql-tools/graphql-tag-pluck": "^7.0.1",
"@graphql-tools/code-file-loader": "^7.0.2",
"@graphql-tools/graphql-tag-pluck": "^7.0.2",
"@graphql-tools/import": "^6.3.1",
"@graphql-tools/utils": "^8.0.1",
"@graphql-tools/utils": "^8.0.2",
"graphql-config": "^4.0.1",
"graphql-depth-limit": "1.1.0"
},
Expand Down
22 changes: 12 additions & 10 deletions packages/plugin/src/graphql-ast.ts
@@ -1,5 +1,5 @@
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo, parse } from 'graphql';
import { printSchemaWithDirectives } from '@graphql-tools/utils';
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo } from 'graphql';
import { getDocumentNodeFromSchema, getRootTypeNames } from '@graphql-tools/utils';
import { SiblingOperations } from './sibling-operations';

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

const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
const cache: Record<string, number> = Object.create(null);

const collect = (nodeType: any): void => {
let node = nodeType;
Expand All @@ -32,10 +31,12 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
node.operationTypes.forEach(collect);
},
ObjectTypeDefinition(node) {
[node, ...node.interfaces].forEach(collect);
collect(node);
node.interfaces?.forEach(collect);
},
UnionTypeDefinition(node) {
[node, ...node.types].forEach(collect);
collect(node);
node.types?.forEach(collect);
},
InputObjectTypeDefinition: collect,
InterfaceTypeDefinition: collect,
Expand All @@ -49,7 +50,8 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {

visit(astNode, visitor);

const operationTypeNames = new Set(['Query', 'Mutation', 'Subscription']);
const operationTypeNames = getRootTypeNames(schema);

const usedTypes = Object.entries(cache)
.filter(([typeName, usedCount]) => usedCount > 1 || operationTypeNames.has(typeName))
.map(([typeName]) => typeName);
Expand All @@ -68,7 +70,7 @@ export function getUsedFields(schema: GraphQLSchema, operations: SiblingOperatio
if (process.env.NODE_ENV !== 'test' && usedFieldsCache) {
return usedFieldsCache;
}
const usedFields: UsedFields = {};
const usedFields: UsedFields = Object.create(null);
const typeInfo = new TypeInfo(schema);
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];

Expand Down
15 changes: 9 additions & 6 deletions packages/plugin/src/schema.ts
Expand Up @@ -2,7 +2,7 @@ import { GraphQLSchema } from 'graphql';
import { GraphQLConfig } from 'graphql-config';
import { asArray } from '@graphql-tools/utils';
import { ParserOptions } from './types';
import { getOnDiskFilepath } from './utils';
import { getOnDiskFilepath, loaderCache } from './utils';

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

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

if (schemaCache.has(schemaKey)) {
return schemaCache.get(schemaKey);
}
let schema = schemaCache.get(schemaKey);

const schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', options.schemaOptions);
schemaCache.set(schemaKey, schema);
if (!schema) {
schema = projectForFile.loadSchemaSync(projectForFile.schema, 'GraphQLSchema', {
cache: loaderCache,
...options.schemaOptions
});
schemaCache.set(schemaKey, schema);
}

return schema;
}
183 changes: 92 additions & 91 deletions packages/plugin/src/sibling-operations.ts
Expand Up @@ -11,7 +11,7 @@ import {
import { Source, asArray } from '@graphql-tools/utils';
import { GraphQLConfig } from 'graphql-config';
import { ParserOptions } from './types';
import { getOnDiskFilepath } from './utils';
import { getOnDiskFilepath, loaderCache } from './utils';

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

if (operationsCache.has(documentsKey)) {
return operationsCache.get(documentsKey);
}
let siblings = operationsCache.get(documentsKey);

const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
skipGraphQLImport: true,
});
const siblings = handleVirtualPath(documents)
operationsCache.set(documentsKey, siblings);
if (!siblings) {
const documents = projectForFile.loadDocumentsSync(projectForFile.documents, {
skipGraphQLImport: true,
cache: loaderCache
});
siblings = handleVirtualPath(documents)
operationsCache.set(documentsKey, siblings);
}

return siblings;
};
Expand Down Expand Up @@ -106,95 +107,95 @@ export function getSiblingOperations(options: ParserOptions, gqlConfig: GraphQLC
// Since the siblings array is cached, we can use it as cache key.
// We should get the same array reference each time we get
// to this point for the same graphql project
if (siblingOperationsCache.has(siblings)) {
return siblingOperationsCache.get(siblings);
}

let fragmentsCache: FragmentSource[] | null = null;

const getFragments = (): FragmentSource[] => {
if (fragmentsCache === null) {
const result: FragmentSource[] = [];

for (const source of siblings) {
for (const definition of source.document.definitions || []) {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
result.push({
filePath: source.location,
document: definition,
});
let siblingOperations = siblingOperationsCache.get(siblings);
if (!siblingOperations) {
let fragmentsCache: FragmentSource[] | null = null;

const getFragments = (): FragmentSource[] => {
if (fragmentsCache === null) {
const result: FragmentSource[] = [];

for (const source of siblings) {
for (const definition of source.document.definitions || []) {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
result.push({
filePath: source.location,
document: definition,
});
}
}
}
fragmentsCache = result;
}
fragmentsCache = result;
}
return fragmentsCache;
};

let cachedOperations: OperationSource[] | null = null;

const getOperations = (): OperationSource[] => {
if (cachedOperations === null) {
const result: OperationSource[] = [];

for (const source of siblings) {
for (const definition of source.document.definitions || []) {
if (definition.kind === Kind.OPERATION_DEFINITION) {
result.push({
filePath: source.location,
document: definition,
});
return fragmentsCache;
};

let cachedOperations: OperationSource[] | null = null;

const getOperations = (): OperationSource[] => {
if (cachedOperations === null) {
const result: OperationSource[] = [];

for (const source of siblings) {
for (const definition of source.document.definitions || []) {
if (definition.kind === Kind.OPERATION_DEFINITION) {
result.push({
filePath: source.location,
document: definition,
});
}
}
}
cachedOperations = result;
}
cachedOperations = result;
}
return cachedOperations;
};

const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);

const collectFragments = (
selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
recursive = true,
collected: Map<string, FragmentDefinitionNode> = new Map()
) => {
visit(selectable, {
FragmentSpread(spread: FragmentSpreadNode) {
const name = spread.name.value;
const fragmentInfo = getFragment(name);

if (fragmentInfo.length === 0) {
// eslint-disable-next-line no-console
console.warn(
`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`
);
return;
}
const fragment = fragmentInfo[0];
const alreadyVisited = collected.has(name);
return cachedOperations;
};

if (!alreadyVisited) {
collected.set(name, fragment.document);
if (recursive) {
collectFragments(fragment.document, recursive, collected);
const getFragment = (name: string) => getFragments().filter(f => f.document.name?.value === name);

const collectFragments = (
selectable: SelectionSetNode | OperationDefinitionNode | FragmentDefinitionNode,
recursive = true,
collected: Map<string, FragmentDefinitionNode> = new Map()
) => {
visit(selectable, {
FragmentSpread(spread: FragmentSpreadNode) {
const name = spread.name.value;
const fragmentInfo = getFragment(name);

if (fragmentInfo.length === 0) {
// eslint-disable-next-line no-console
console.warn(
`Unable to locate fragment named "${name}", please make sure it's loaded using "parserOptions.operations"`
);
return;
}
}
},
});
return collected;
};

const siblingOperations: SiblingOperations = {
available: true,
getFragments,
getOperations,
getFragment,
getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
getOperation: name => getOperations().filter(o => o.document.name?.value === name),
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
};
siblingOperationsCache.set(siblings, siblingOperations);
const fragment = fragmentInfo[0];
const alreadyVisited = collected.has(name);

if (!alreadyVisited) {
collected.set(name, fragment.document);
if (recursive) {
collectFragments(fragment.document, recursive, collected);
}
}
},
});
return collected;
};

siblingOperations = {
available: true,
getFragments,
getOperations,
getFragment,
getFragmentByType: typeName => getFragments().filter(f => f.document.typeCondition?.name?.value === typeName),
getOperation: name => getOperations().filter(o => o.document.name?.value === name),
getOperationByType: type => getOperations().filter(o => o.document.operation === type),
getFragmentsInUse: (selectable, recursive = true) => Array.from(collectFragments(selectable, recursive).values()),
};

siblingOperationsCache.set(siblings, siblingOperations);
}
return siblingOperations;
}
21 changes: 20 additions & 1 deletion packages/plugin/src/utils.ts
@@ -1,10 +1,11 @@
import { statSync } from 'fs';
import { dirname } from 'path';
import { Source, Lexer, GraphQLSchema, Token, DocumentNode } from 'graphql';
import { Lexer, GraphQLSchema, Token, DocumentNode, Source } from 'graphql';
import { GraphQLESLintRuleContext } from './types';
import { AST } from 'eslint';
import { SiblingOperations } from './sibling-operations';
import { UsedFields, ReachableTypes } from './graphql-ast';
import { asArray, Source as LoaderSource } from '@graphql-tools/utils';

export function requireSiblingsOperations(
ruleName: string,
Expand Down Expand Up @@ -134,3 +135,21 @@ export const getOnDiskFilepath = (filepath: string): string => {

return filepath;
};

// Small workaround for the bug in older versions of @graphql-tools/load
// Can be removed after graphql-config bumps to a new version
export const loaderCache: Record<string, LoaderSource[]> = new Proxy(Object.create(null), {
get(cache, key) {
const value = cache[key];
if (value) {
return asArray(value);
}
return undefined;
},
set(cache, key, value) {
if (value) {
cache[key] = asArray(value);
}
return true;
}
});
4 changes: 2 additions & 2 deletions patches/eslint+7.31.0.patch → patches/eslint+7.32.0.patch
@@ -1,8 +1,8 @@
diff --git a/node_modules/eslint/lib/rule-tester/rule-tester.js b/node_modules/eslint/lib/rule-tester/rule-tester.js
index cac81bc..3a3dbbe 100644
index 2b55249..08547f3 100644
--- a/node_modules/eslint/lib/rule-tester/rule-tester.js
+++ b/node_modules/eslint/lib/rule-tester/rule-tester.js
@@ -905,7 +905,17 @@ class RuleTester {
@@ -911,7 +911,17 @@ class RuleTester {
"Expected no autofixes to be suggested"
);
} else {
Expand Down

0 comments on commit 403b946

Please sign in to comment.