Skip to content

Commit 2032a66

Browse files
author
Dimitri POSTOLOV
authored
fix no-unreachable-types rule when interface implementing other interface (#584)
* fix `no-unreachable-types` rule when interface implementing other interface * fix cache for reachableTypesCache and usedFieldsCache * fix visiting nodes in getReachableTypes()
1 parent 3948a14 commit 2032a66

File tree

5 files changed

+166
-102
lines changed

5 files changed

+166
-102
lines changed

.changeset/weak-keys-fold.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': minor
3+
---
4+
5+
fix `no-unreachable-types` rule when interface implementing other interface

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"json-schema-to-markdown": "1.1.1",
4949
"lint-staged": "11.1.2",
5050
"patch-package": "6.4.7",
51+
"prettier": "2.3.2",
5152
"rimraf": "3.0.2",
5253
"ts-jest": "27.0.5",
5354
"ts-node": "10.2.1",

packages/plugin/src/graphql-ast.ts

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import { GraphQLSchema, TypeInfo, ASTKindToNode, Visitor, visit, visitWithTypeInfo } from 'graphql';
2-
import { getDocumentNodeFromSchema, getRootTypeNames } from '@graphql-tools/utils';
1+
import {
2+
ASTNode,
3+
Visitor,
4+
TypeInfo,
5+
GraphQLSchema,
6+
ASTKindToNode,
7+
visit,
8+
isInterfaceType,
9+
visitWithTypeInfo,
10+
} from 'graphql';
311
import { SiblingOperations } from './sibling-operations';
412

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

16-
const astNode = getDocumentNodeFromSchema(schema); // Transforms the schema into ASTNode
17-
const cache: Record<string, number> = Object.create(null);
26+
const collect = (node: ASTNode): false | void => {
27+
const typeName = getTypeName(node);
28+
if (reachableTypes.has(typeName)) {
29+
return;
30+
}
31+
reachableTypes.add(typeName);
32+
const type = schema.getType(typeName) || schema.getDirective(typeName);
1833

19-
const collect = (nodeType: any): void => {
20-
let node = nodeType;
21-
while (node.type) {
22-
node = node.type;
34+
if (isInterfaceType(type)) {
35+
const { objects, interfaces } = schema.getImplementations(type);
36+
for (const { astNode } of [...objects, ...interfaces]) {
37+
visit(astNode, visitor);
38+
}
39+
} else {
40+
visit(type.astNode, visitor);
2341
}
24-
const typeName = node.name.value;
25-
cache[typeName] ??= 0;
26-
cache[typeName] += 1;
2742
};
2843

2944
const visitor: Visitor<ASTKindToNode> = {
30-
SchemaDefinition(node) {
31-
node.operationTypes.forEach(collect);
32-
},
33-
ObjectTypeDefinition(node) {
34-
collect(node);
35-
node.interfaces?.forEach(collect);
36-
},
37-
UnionTypeDefinition(node) {
38-
collect(node);
39-
node.types?.forEach(collect);
40-
},
41-
InputObjectTypeDefinition: collect,
4245
InterfaceTypeDefinition: collect,
43-
ScalarTypeDefinition: collect,
46+
ObjectTypeDefinition: collect,
4447
InputValueDefinition: collect,
45-
DirectiveDefinition: collect,
46-
EnumTypeDefinition: collect,
48+
UnionTypeDefinition: collect,
4749
FieldDefinition: collect,
4850
Directive: collect,
51+
NamedType: collect,
4952
};
5053

51-
visit(astNode, visitor);
52-
53-
const operationTypeNames = getRootTypeNames(schema);
54-
55-
const usedTypes = Object.entries(cache)
56-
.filter(([typeName, usedCount]) => usedCount > 1 || operationTypeNames.has(typeName))
57-
.map(([typeName]) => typeName);
58-
59-
reachableTypesCache = new Set(usedTypes);
54+
for (const type of [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()]) {
55+
if (type) {
56+
visit(type.astNode, visitor);
57+
}
58+
}
59+
reachableTypesCache = reachableTypes;
6060
return reachableTypesCache;
6161
}
6262

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

7776
const visitor = visitWithTypeInfo(typeInfo, {
78-
Field: {
79-
enter(node): false | void {
80-
const fieldDef = typeInfo.getFieldDef();
81-
if (!fieldDef) {
82-
// skip visiting this node if field is not defined in schema
83-
return false;
84-
}
85-
const parentTypeName = typeInfo.getParentType().name;
86-
const fieldName = node.name.value;
77+
Field(node): false | void {
78+
const fieldDef = typeInfo.getFieldDef();
79+
if (!fieldDef) {
80+
// skip visiting this node if field is not defined in schema
81+
return false;
82+
}
83+
const parentTypeName = typeInfo.getParentType().name;
84+
const fieldName = node.name.value;
8785

88-
usedFields[parentTypeName] ??= new Set();
89-
usedFields[parentTypeName].add(fieldName);
90-
},
86+
usedFields[parentTypeName] ??= new Set();
87+
usedFields[parentTypeName].add(fieldName);
9188
},
9289
});
9390

91+
const allDocuments = [...operations.getOperations(), ...operations.getFragments()];
9492
for (const { document } of allDocuments) {
9593
visit(document, visitor);
9694
}

0 commit comments

Comments
 (0)