Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/strong-mails-rest.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

printSchemaWithDirectives also uses this function and no need to do print and parse again.

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);
Copy link
Member Author

@ardatan ardatan Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will reduce the number of iterations

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
Original file line number Diff line number Diff line change
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)) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using get once will be better for performance

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
Original file line number Diff line number Diff line change
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
Copy link
Member Author

@ardatan ardatan Aug 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might improve the performance a bit as well and remove the need of schemaCache and operationsCache

});
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading