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: 3 additions & 2 deletions src/main/java/graphql/Directives.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package graphql;


import com.google.common.collect.ImmutableSet;
import graphql.language.Description;
import graphql.language.DirectiveDefinition;
import graphql.language.StringValue;
import graphql.schema.GraphQLDirective;

import java.util.Set;

import static graphql.Scalars.GraphQLBoolean;
import static graphql.Scalars.GraphQLString;
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
Expand Down Expand Up @@ -122,6 +125,4 @@ public class Directives {
private static Description createDescription(String s) {
return new Description(s, null, false);
}


}
21 changes: 21 additions & 0 deletions src/main/java/graphql/schema/SchemaTraverser.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ public SchemaTraverser() {
this(GraphQLSchemaElement::getChildren);
}

/**
* This will visit all of the schema elements in the specified schema and invokes the visitor.
*
* @param typeVisitor a list of visitors to use
* @param schema the schema to visit
*
* @return a traversal result
*/
public TraverserResult depthFirstFullSchema(GraphQLTypeVisitor typeVisitor, GraphQLSchema schema) {
return depthFirstFullSchema(Collections.singletonList(typeVisitor), schema, Collections.emptyMap());
}

/**
* This will visit all of the schema elements in the specified schema, invoking each visitor in turn.
*
* @param typeVisitors a list of visitors to use
* @param schema the schema to visit
* @param rootVars this sets up variables to be made available to the {@link TraverserContext}. This can be empty but not null
*
* @return a traversal result
*/
public TraverserResult depthFirstFullSchema(List<GraphQLTypeVisitor> typeVisitors, GraphQLSchema schema, Map<Class<?>, Object> rootVars) {
Set<GraphQLSchemaElement> roots = new LinkedHashSet<>();
roots.add(schema.getQueryType());
Expand Down
258 changes: 258 additions & 0 deletions src/main/java/graphql/schema/usage/SchemaUsage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
package graphql.schema.usage;

import com.google.common.collect.ImmutableMap;
import graphql.Internal;
import graphql.PublicApi;
import graphql.introspection.Introspection;
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLNamedOutputType;
import graphql.schema.GraphQLNamedSchemaElement;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.DirectiveInfo;
import graphql.schema.idl.ScalarInfo;

import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static java.util.Collections.emptySet;

/**
* This class shows schema usage information. There are two aspects to this. To be strongly referenced, a schema element
* (directive or type) must some how be connected back to the root query, mutation or subscription type. It's possible to define
* 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
* types then it is not considered strongly references.
*
* 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
* 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.
*
* 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
* is to find the types and directives in a schema that could in theory be removed because they are not useful.
*/
@PublicApi
public class SchemaUsage {
private final Map<String, Integer> fieldReferenceCounts;
private final Map<String, Integer> inputFieldReferenceCounts;
private final Map<String, Integer> outputFieldReferenceCounts;
private final Map<String, Integer> argReferenceCount;
private final Map<String, Integer> interfaceReferenceCount;
private final Map<String, Integer> unionReferenceCount;
private final Map<String, Integer> directiveReferenceCount;
private final Map<String, Set<String>> interfaceImplementors;
private final Map<String, Set<String>> elementBackReferences;

private SchemaUsage(Builder builder) {
this.fieldReferenceCounts = ImmutableMap.copyOf(builder.fieldReferenceCounts);
this.inputFieldReferenceCounts = ImmutableMap.copyOf(builder.inputFieldReferenceCounts);
this.outputFieldReferenceCounts = ImmutableMap.copyOf(builder.outputFieldReferenceCounts);
this.argReferenceCount = ImmutableMap.copyOf(builder.argReferenceCount);
this.interfaceReferenceCount = ImmutableMap.copyOf(builder.interfaceReferenceCount);
this.unionReferenceCount = ImmutableMap.copyOf(builder.unionReferenceCount);
this.directiveReferenceCount = ImmutableMap.copyOf(builder.directiveReferenceCount);
this.interfaceImplementors = ImmutableMap.copyOf(builder.interfaceImplementors);
this.elementBackReferences = ImmutableMap.copyOf(builder.elementBackReferences);
}

/**
* This shows how many times a type is referenced by either an input or output field.
*
* @return a map of type name to field reference counts
*/
public Map<String, Integer> getFieldReferenceCounts() {
return fieldReferenceCounts;
}

/**
* This shows how many times a type is referenced by an output field.
*
* @return a map of type name to output field reference counts
*/
public Map<String, Integer> getOutputFieldReferenceCounts() {
return outputFieldReferenceCounts;
}

/**
* This shows how many times a type is referenced by an input field.
*
* @return a map of type name to input field reference counts
*/
public Map<String, Integer> getInputFieldReferenceCounts() {
return inputFieldReferenceCounts;
}

/**
* This shows how many times a type is referenced by an argument.
*
* @return a map of type name to argument reference counts
*/
public Map<String, Integer> getArgumentReferenceCounts() {
return argReferenceCount;
}

/**
* This shows how many times an interface type is referenced as a member in some other
* object or interface type.
*
* @return a map of interface type name to object or interface type reference counts
*/
public Map<String, Integer> getInterfaceReferenceCounts() {
return interfaceReferenceCount;
}

/**
* This shows how many times an object type is referenced as a member in some other
* union type.
*
* @return a map of object type name to union membership reference counts
*/
public Map<String, Integer> getUnionReferenceCounts() {
return unionReferenceCount;
}

/**
* This shows how many times a directive is applied on some other schema element.
*
* @return a map of directive name to applied directive counts
*/
public Map<String, Integer> getDirectiveReferenceCounts() {
return directiveReferenceCount;
}

/**
* Returns true if the named element is strongly reference somewhere in the schema back to the root types such as the schema
* query, mutation or subscription types.
*
* Graphql specified scalar types, introspection types and directives are always counted as referenced, even if
* not used explicitly.
*
* Directives that are defined but never applied on any schema elements will not report as referenced.
*
* @param schema the schema that contains the name type
* @param elementName the element name to check
*
* @return true if the element could be referenced
*/
public boolean isStronglyReferenced(GraphQLSchema schema, String elementName) {
return isReferencedImpl(schema, elementName, new HashSet<>());
}

/**
* This returns all the unreferenced named elements in a schema.
*
* @param schema the schema to check
*
* @return a set of the named schema elements where {@link #isStronglyReferenced(GraphQLSchema, String)} returns false
*/
public Set<GraphQLNamedSchemaElement> getUnReferencedElements(GraphQLSchema schema) {
Set<GraphQLNamedSchemaElement> elements = new LinkedHashSet<>();
schema.getAllTypesAsList().forEach(type -> {
if (!isStronglyReferenced(schema, type.getName())) {
elements.add(type);
}
});
schema.getDirectives().forEach(directive -> {
if (!isStronglyReferenced(schema, directive.getName())) {
elements.add(directive);
}
});
return elements;
}

private boolean isReferencedImpl(GraphQLSchema schema, String elementName, Set<String> pathSoFar) {
if (pathSoFar.contains(elementName)) {
return false; // circular reference to that element
}
pathSoFar.add(elementName);

if (ScalarInfo.isGraphqlSpecifiedScalar(elementName)) {
return true;
}

List<GraphQLDirective> directives = schema.getDirectives(elementName);
if (!directives.isEmpty()) {
String directiveName = directives.get(0).getName();
if (DirectiveInfo.isGraphqlSpecifiedDirective(directiveName)) {
return true;
}
if (isNamedElementReferenced(schema, directiveName, pathSoFar)) {
return true;
}
}

GraphQLNamedType type = schema.getTypeAs(elementName);
if (type == null) {
return false;
}
if (Introspection.isIntrospectionTypes(type)) {
return true;
}

if (type == schema.getQueryType()) {
return true;
}
if (type == schema.getMutationType()) {
return true;
}
if (type == schema.getSubscriptionType()) {
return true;
}

if (isNamedElementReferenced(schema, elementName, pathSoFar)) {
return true;
}

if (type instanceof GraphQLInterfaceType) {
Set<String> implementors = interfaceImplementors.getOrDefault(type.getName(), emptySet());
for (String implementor : implementors) {
if (isReferencedImpl(schema, implementor, pathSoFar)) {
return true;
}
}
}
if (type instanceof GraphQLObjectType) {
List<GraphQLNamedOutputType> interfaces = ((GraphQLObjectType) type).getInterfaces();
for (GraphQLNamedOutputType memberInterface : interfaces) {
Set<String> implementors = interfaceImplementors.getOrDefault(memberInterface.getName(), emptySet());
for (String implementor : implementors) {
if (isReferencedImpl(schema, implementor, pathSoFar)) {
return true;
}
}
}
}
return false;
}

private boolean isNamedElementReferenced(GraphQLSchema schema, String elementName, Set<String> pathSoFar) {
Set<String> references = elementBackReferences.getOrDefault(elementName, emptySet());
for (String reference : references) {
if (isReferencedImpl(schema, reference, pathSoFar)) {
return true;
}
}
return false;
}

@Internal
static class Builder {
Map<String, Integer> fieldReferenceCounts = new LinkedHashMap<>();
Map<String, Integer> inputFieldReferenceCounts = new LinkedHashMap<>();
Map<String, Integer> outputFieldReferenceCounts = new LinkedHashMap<>();
Map<String, Integer> argReferenceCount = new LinkedHashMap<>();
Map<String, Integer> interfaceReferenceCount = new LinkedHashMap<>();
Map<String, Integer> unionReferenceCount = new LinkedHashMap<>();
Map<String, Integer> directiveReferenceCount = new LinkedHashMap<>();
Map<String, Set<String>> interfaceImplementors = new LinkedHashMap<>();
Map<String, Set<String>> elementBackReferences = new LinkedHashMap<>();

SchemaUsage build() {
return new SchemaUsage(this);
}
}
}
Loading