Skip to content

Commit

Permalink
fix no-unreachable-types rule when interface implementing other int…
Browse files Browse the repository at this point in the history
…erface (#584)

* fix `no-unreachable-types` rule when interface implementing other interface

* fix cache for reachableTypesCache and usedFieldsCache

* fix visiting nodes in getReachableTypes()
  • Loading branch information
Dimitri POSTOLOV committed Sep 6, 2021
1 parent 3948a14 commit 2032a66
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 102 deletions.
5 changes: 5 additions & 0 deletions .changeset/weak-keys-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-eslint/eslint-plugin': minor
---

fix `no-unreachable-types` rule when interface implementing other interface
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"json-schema-to-markdown": "1.1.1",
"lint-staged": "11.1.2",
"patch-package": "6.4.7",
"prettier": "2.3.2",
"rimraf": "3.0.2",
"ts-jest": "27.0.5",
"ts-node": "10.2.1",
Expand Down
94 changes: 46 additions & 48 deletions packages/plugin/src/graphql-ast.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo } from 'graphql';
import { getDocumentNodeFromSchema, getRootTypeNames } from '@graphql-tools/utils';
import {
ASTNode,
Visitor,
TypeInfo,
GraphQLSchema,
ASTKindToNode,
visit,
isInterfaceType,
visitWithTypeInfo,
} from 'graphql';
import { SiblingOperations } from './sibling-operations';

export type ReachableTypes = Set<string>;
Expand All @@ -12,51 +20,43 @@ export function getReachableTypes(schema: GraphQLSchema): ReachableTypes {
if (process.env.NODE_ENV !== 'test' && reachableTypesCache) {
return reachableTypesCache;
}
const reachableTypes: ReachableTypes = new Set();
const getTypeName = node => ('type' in node ? getTypeName(node.type) : node.name.value);

const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
const cache: Record<string, number> = Object.create(null);
const collect = (node: ASTNode): false | void => {
const typeName = getTypeName(node);
if (reachableTypes.has(typeName)) {
return;
}
reachableTypes.add(typeName);
const type = schema.getType(typeName) || schema.getDirective(typeName);

const collect = (nodeType: any): void => {
let node = nodeType;
while (node.type) {
node = node.type;
if (isInterfaceType(type)) {
const { objects, interfaces } = schema.getImplementations(type);
for (const { astNode } of [...objects, ...interfaces]) {
visit(astNode, visitor);
}
} else {
visit(type.astNode, visitor);
}
const typeName = node.name.value;
cache[typeName] ??= 0;
cache[typeName] += 1;
};

const visitor: Visitor<ASTKindToNode> = {
SchemaDefinition(node) {
node.operationTypes.forEach(collect);
},
ObjectTypeDefinition(node) {
collect(node);
node.interfaces?.forEach(collect);
},
UnionTypeDefinition(node) {
collect(node);
node.types?.forEach(collect);
},
InputObjectTypeDefinition: collect,
InterfaceTypeDefinition: collect,
ScalarTypeDefinition: collect,
ObjectTypeDefinition: collect,
InputValueDefinition: collect,
DirectiveDefinition: collect,
EnumTypeDefinition: collect,
UnionTypeDefinition: collect,
FieldDefinition: collect,
Directive: collect,
NamedType: collect,
};

visit(astNode, visitor);

const operationTypeNames = getRootTypeNames(schema);

const usedTypes = Object.entries(cache)
.filter(([typeName, usedCount]) => usedCount > 1 || operationTypeNames.has(typeName))
.map(([typeName]) => typeName);

reachableTypesCache = new Set(usedTypes);
for (const type of [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]) {
if (type) {
visit(type.astNode, visitor);
}
}
reachableTypesCache = reachableTypes;
return reachableTypesCache;
}

Expand All @@ -72,25 +72,23 @@ export function getUsedFields(schema: GraphQLSchema, operations: SiblingOperatio
}
const usedFields: UsedFields = Object.create(null);
const typeInfo = new TypeInfo(schema);
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];

const visitor = visitWithTypeInfo(typeInfo, {
Field: {
enter(node): false | void {
const fieldDef = typeInfo.getFieldDef();
if (!fieldDef) {
// skip visiting this node if field is not defined in schema
return false;
}
const parentTypeName = typeInfo.getParentType().name;
const fieldName = node.name.value;
Field(node): false | void {
const fieldDef = typeInfo.getFieldDef();
if (!fieldDef) {
// skip visiting this node if field is not defined in schema
return false;
}
const parentTypeName = typeInfo.getParentType().name;
const fieldName = node.name.value;

usedFields[parentTypeName] ??= new Set();
usedFields[parentTypeName].add(fieldName);
},
usedFields[parentTypeName] ??= new Set();
usedFields[parentTypeName].add(fieldName);
},
});

const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
for (const { document } of allDocuments) {
visit(document, visitor);
}
Expand Down

0 comments on commit 2032a66

Please sign in to comment.