Skip to content

Commit 35b8dcd

Browse files
authored
Schema Usage support (#2548)
* Schema Usage support * More work on schema usage to handle circular references * Immutable maps, and new method names * removed miss cache - never used * Feedback on Andi on linked hashmap
1 parent 10eb985 commit 35b8dcd

5 files changed

Lines changed: 681 additions & 2 deletions

File tree

src/main/java/graphql/Directives.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package graphql;
22

33

4+
import com.google.common.collect.ImmutableSet;
45
import graphql.language.Description;
56
import graphql.language.DirectiveDefinition;
67
import graphql.language.StringValue;
78
import graphql.schema.GraphQLDirective;
89

10+
import java.util.Set;
11+
912
import static graphql.Scalars.GraphQLBoolean;
1013
import static graphql.Scalars.GraphQLString;
1114
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
@@ -122,6 +125,4 @@ public class Directives {
122125
private static Description createDescription(String s) {
123126
return new Description(s, null, false);
124127
}
125-
126-
127128
}

src/main/java/graphql/schema/SchemaTraverser.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,27 @@ public SchemaTraverser() {
3232
this(GraphQLSchemaElement::getChildren);
3333
}
3434

35+
/**
36+
* This will visit all of the schema elements in the specified schema and invokes the visitor.
37+
*
38+
* @param typeVisitor a list of visitors to use
39+
* @param schema the schema to visit
40+
*
41+
* @return a traversal result
42+
*/
43+
public TraverserResult depthFirstFullSchema(GraphQLTypeVisitor typeVisitor, GraphQLSchema schema) {
44+
return depthFirstFullSchema(Collections.singletonList(typeVisitor), schema, Collections.emptyMap());
45+
}
46+
47+
/**
48+
* This will visit all of the schema elements in the specified schema, invoking each visitor in turn.
49+
*
50+
* @param typeVisitors a list of visitors to use
51+
* @param schema the schema to visit
52+
* @param rootVars this sets up variables to be made available to the {@link TraverserContext}. This can be empty but not null
53+
*
54+
* @return a traversal result
55+
*/
3556
public TraverserResult depthFirstFullSchema(List<GraphQLTypeVisitor> typeVisitors, GraphQLSchema schema, Map<Class<?>, Object> rootVars) {
3657
Set<GraphQLSchemaElement> roots = new LinkedHashSet<>();
3758
roots.add(schema.getQueryType());
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
package graphql.schema.usage;
2+
3+
import com.google.common.collect.ImmutableMap;
4+
import graphql.Internal;
5+
import graphql.PublicApi;
6+
import graphql.introspection.Introspection;
7+
import graphql.schema.GraphQLDirective;
8+
import graphql.schema.GraphQLInterfaceType;
9+
import graphql.schema.GraphQLNamedOutputType;
10+
import graphql.schema.GraphQLNamedSchemaElement;
11+
import graphql.schema.GraphQLNamedType;
12+
import graphql.schema.GraphQLObjectType;
13+
import graphql.schema.GraphQLSchema;
14+
import graphql.schema.idl.DirectiveInfo;
15+
import graphql.schema.idl.ScalarInfo;
16+
17+
import java.util.HashSet;
18+
import java.util.LinkedHashMap;
19+
import java.util.LinkedHashSet;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Set;
23+
24+
import static java.util.Collections.emptySet;
25+
26+
/**
27+
* This class shows schema usage information. There are two aspects to this. To be strongly referenced, a schema element
28+
* (directive or type) must some how be connected back to the root query, mutation or subscription type. It's possible to define
29+
* types that reference other types but if they do not get used by a field / interface or union that itself leads back to the root
30+
* types then it is not considered strongly references.
31+
*
32+
* Its reference counts however might be non-zero yet not be strongly referenced. For example given `type A { f : B } type B { f : A }` both types A and B
33+
* will have non-zero counts but if no other type points to A or B that leads back to the root types then they are not strongly referenced types.
34+
*
35+
* Such types could be removed from the schema because there is no way to ever consume these types in a query. A common use case for this class
36+
* is to find the types and directives in a schema that could in theory be removed because they are not useful.
37+
*/
38+
@PublicApi
39+
public class SchemaUsage {
40+
private final Map<String, Integer> fieldReferenceCounts;
41+
private final Map<String, Integer> inputFieldReferenceCounts;
42+
private final Map<String, Integer> outputFieldReferenceCounts;
43+
private final Map<String, Integer> argReferenceCount;
44+
private final Map<String, Integer> interfaceReferenceCount;
45+
private final Map<String, Integer> unionReferenceCount;
46+
private final Map<String, Integer> directiveReferenceCount;
47+
private final Map<String, Set<String>> interfaceImplementors;
48+
private final Map<String, Set<String>> elementBackReferences;
49+
50+
private SchemaUsage(Builder builder) {
51+
this.fieldReferenceCounts = ImmutableMap.copyOf(builder.fieldReferenceCounts);
52+
this.inputFieldReferenceCounts = ImmutableMap.copyOf(builder.inputFieldReferenceCounts);
53+
this.outputFieldReferenceCounts = ImmutableMap.copyOf(builder.outputFieldReferenceCounts);
54+
this.argReferenceCount = ImmutableMap.copyOf(builder.argReferenceCount);
55+
this.interfaceReferenceCount = ImmutableMap.copyOf(builder.interfaceReferenceCount);
56+
this.unionReferenceCount = ImmutableMap.copyOf(builder.unionReferenceCount);
57+
this.directiveReferenceCount = ImmutableMap.copyOf(builder.directiveReferenceCount);
58+
this.interfaceImplementors = ImmutableMap.copyOf(builder.interfaceImplementors);
59+
this.elementBackReferences = ImmutableMap.copyOf(builder.elementBackReferences);
60+
}
61+
62+
/**
63+
* This shows how many times a type is referenced by either an input or output field.
64+
*
65+
* @return a map of type name to field reference counts
66+
*/
67+
public Map<String, Integer> getFieldReferenceCounts() {
68+
return fieldReferenceCounts;
69+
}
70+
71+
/**
72+
* This shows how many times a type is referenced by an output field.
73+
*
74+
* @return a map of type name to output field reference counts
75+
*/
76+
public Map<String, Integer> getOutputFieldReferenceCounts() {
77+
return outputFieldReferenceCounts;
78+
}
79+
80+
/**
81+
* This shows how many times a type is referenced by an input field.
82+
*
83+
* @return a map of type name to input field reference counts
84+
*/
85+
public Map<String, Integer> getInputFieldReferenceCounts() {
86+
return inputFieldReferenceCounts;
87+
}
88+
89+
/**
90+
* This shows how many times a type is referenced by an argument.
91+
*
92+
* @return a map of type name to argument reference counts
93+
*/
94+
public Map<String, Integer> getArgumentReferenceCounts() {
95+
return argReferenceCount;
96+
}
97+
98+
/**
99+
* This shows how many times an interface type is referenced as a member in some other
100+
* object or interface type.
101+
*
102+
* @return a map of interface type name to object or interface type reference counts
103+
*/
104+
public Map<String, Integer> getInterfaceReferenceCounts() {
105+
return interfaceReferenceCount;
106+
}
107+
108+
/**
109+
* This shows how many times an object type is referenced as a member in some other
110+
* union type.
111+
*
112+
* @return a map of object type name to union membership reference counts
113+
*/
114+
public Map<String, Integer> getUnionReferenceCounts() {
115+
return unionReferenceCount;
116+
}
117+
118+
/**
119+
* This shows how many times a directive is applied on some other schema element.
120+
*
121+
* @return a map of directive name to applied directive counts
122+
*/
123+
public Map<String, Integer> getDirectiveReferenceCounts() {
124+
return directiveReferenceCount;
125+
}
126+
127+
/**
128+
* Returns true if the named element is strongly reference somewhere in the schema back to the root types such as the schema
129+
* query, mutation or subscription types.
130+
*
131+
* Graphql specified scalar types, introspection types and directives are always counted as referenced, even if
132+
* not used explicitly.
133+
*
134+
* Directives that are defined but never applied on any schema elements will not report as referenced.
135+
*
136+
* @param schema the schema that contains the name type
137+
* @param elementName the element name to check
138+
*
139+
* @return true if the element could be referenced
140+
*/
141+
public boolean isStronglyReferenced(GraphQLSchema schema, String elementName) {
142+
return isReferencedImpl(schema, elementName, new HashSet<>());
143+
}
144+
145+
/**
146+
* This returns all the unreferenced named elements in a schema.
147+
*
148+
* @param schema the schema to check
149+
*
150+
* @return a set of the named schema elements where {@link #isStronglyReferenced(GraphQLSchema, String)} returns false
151+
*/
152+
public Set<GraphQLNamedSchemaElement> getUnReferencedElements(GraphQLSchema schema) {
153+
Set<GraphQLNamedSchemaElement> elements = new LinkedHashSet<>();
154+
schema.getAllTypesAsList().forEach(type -> {
155+
if (!isStronglyReferenced(schema, type.getName())) {
156+
elements.add(type);
157+
}
158+
});
159+
schema.getDirectives().forEach(directive -> {
160+
if (!isStronglyReferenced(schema, directive.getName())) {
161+
elements.add(directive);
162+
}
163+
});
164+
return elements;
165+
}
166+
167+
private boolean isReferencedImpl(GraphQLSchema schema, String elementName, Set<String> pathSoFar) {
168+
if (pathSoFar.contains(elementName)) {
169+
return false; // circular reference to that element
170+
}
171+
pathSoFar.add(elementName);
172+
173+
if (ScalarInfo.isGraphqlSpecifiedScalar(elementName)) {
174+
return true;
175+
}
176+
177+
List<GraphQLDirective> directives = schema.getDirectives(elementName);
178+
if (!directives.isEmpty()) {
179+
String directiveName = directives.get(0).getName();
180+
if (DirectiveInfo.isGraphqlSpecifiedDirective(directiveName)) {
181+
return true;
182+
}
183+
if (isNamedElementReferenced(schema, directiveName, pathSoFar)) {
184+
return true;
185+
}
186+
}
187+
188+
GraphQLNamedType type = schema.getTypeAs(elementName);
189+
if (type == null) {
190+
return false;
191+
}
192+
if (Introspection.isIntrospectionTypes(type)) {
193+
return true;
194+
}
195+
196+
if (type == schema.getQueryType()) {
197+
return true;
198+
}
199+
if (type == schema.getMutationType()) {
200+
return true;
201+
}
202+
if (type == schema.getSubscriptionType()) {
203+
return true;
204+
}
205+
206+
if (isNamedElementReferenced(schema, elementName, pathSoFar)) {
207+
return true;
208+
}
209+
210+
if (type instanceof GraphQLInterfaceType) {
211+
Set<String> implementors = interfaceImplementors.getOrDefault(type.getName(), emptySet());
212+
for (String implementor : implementors) {
213+
if (isReferencedImpl(schema, implementor, pathSoFar)) {
214+
return true;
215+
}
216+
}
217+
}
218+
if (type instanceof GraphQLObjectType) {
219+
List<GraphQLNamedOutputType> interfaces = ((GraphQLObjectType) type).getInterfaces();
220+
for (GraphQLNamedOutputType memberInterface : interfaces) {
221+
Set<String> implementors = interfaceImplementors.getOrDefault(memberInterface.getName(), emptySet());
222+
for (String implementor : implementors) {
223+
if (isReferencedImpl(schema, implementor, pathSoFar)) {
224+
return true;
225+
}
226+
}
227+
}
228+
}
229+
return false;
230+
}
231+
232+
private boolean isNamedElementReferenced(GraphQLSchema schema, String elementName, Set<String> pathSoFar) {
233+
Set<String> references = elementBackReferences.getOrDefault(elementName, emptySet());
234+
for (String reference : references) {
235+
if (isReferencedImpl(schema, reference, pathSoFar)) {
236+
return true;
237+
}
238+
}
239+
return false;
240+
}
241+
242+
@Internal
243+
static class Builder {
244+
Map<String, Integer> fieldReferenceCounts = new LinkedHashMap<>();
245+
Map<String, Integer> inputFieldReferenceCounts = new LinkedHashMap<>();
246+
Map<String, Integer> outputFieldReferenceCounts = new LinkedHashMap<>();
247+
Map<String, Integer> argReferenceCount = new LinkedHashMap<>();
248+
Map<String, Integer> interfaceReferenceCount = new LinkedHashMap<>();
249+
Map<String, Integer> unionReferenceCount = new LinkedHashMap<>();
250+
Map<String, Integer> directiveReferenceCount = new LinkedHashMap<>();
251+
Map<String, Set<String>> interfaceImplementors = new LinkedHashMap<>();
252+
Map<String, Set<String>> elementBackReferences = new LinkedHashMap<>();
253+
254+
SchemaUsage build() {
255+
return new SchemaUsage(this);
256+
}
257+
}
258+
}

0 commit comments

Comments
 (0)