Skip to content

Commit

Permalink
fix #1237 remove unreachable types from generated code
Browse files Browse the repository at this point in the history
  • Loading branch information
vikstrous committed May 2, 2022
1 parent 33fe0b9 commit ae8064c
Show file tree
Hide file tree
Showing 29 changed files with 2,913 additions and 183 deletions.
4 changes: 2 additions & 2 deletions api/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func Generate(cfg *config.Config, option ...Option) error {
}
}

if err := cfg.LoadSchema(); err != nil {
if err := cfg.LoadSchema(false); err != nil {
return fmt.Errorf("failed to load schema: %w", err)
}

Expand All @@ -66,7 +66,7 @@ func Generate(cfg *config.Config, option ...Option) error {
}

// LoadSchema again now we have everything
if err := cfg.LoadSchema(); err != nil {
if err := cfg.LoadSchema(true); err != nil {
return fmt.Errorf("failed to load schema: %w", err)
}

Expand Down
8 changes: 6 additions & 2 deletions codegen/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (c *Config) Init() error {
}

if c.Schema == nil {
if err := c.LoadSchema(); err != nil {
if err := c.LoadSchema(true); err != nil {
return err
}
}
Expand Down Expand Up @@ -613,7 +613,7 @@ func (c *Config) injectBuiltins() {
}
}

func (c *Config) LoadSchema() error {
func (c *Config) LoadSchema(removeUnreachable bool) error {
if c.Packages != nil {
c.Packages = &code.Packages{}
}
Expand All @@ -635,6 +635,10 @@ func (c *Config) LoadSchema() error {
schema.Types["Query"] = schema.Query
}

if removeUnreachable {
removeUnreachableTypes(schema)
}

c.Schema = schema
return nil
}
Expand Down
131 changes: 131 additions & 0 deletions codegen/config/remove_unreachable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package config

import (
"fmt"
"sort"
"strings"

"github.com/vektah/gqlparser/v2/ast"
)

type reachabilityCleaner struct {
schema *ast.Schema
reachable map[string]*ast.Definition
queue []*ast.Definition
}

func (r *reachabilityCleaner) addToQueue(def *ast.Definition) {
// simplify error handling by acceping nil and ignoring it
if def == nil {
return
}
if _, ok := r.reachable[def.Name]; !ok {
r.queue = append(r.queue, def)
}
r.reachable[def.Name] = r.schema.Types[def.Name]
}

func (r *reachabilityCleaner) findReachableTypes() map[string]*ast.Definition {
// Mark all types reachable from the current queue of definitions as reachable
for len(r.queue) > 0 {
// pop from stack
currentType := r.queue[0]
r.queue = r.queue[1:]

referencedFromCurrent := []*ast.Definition{}
referencedFromCurrent = append(referencedFromCurrent, r.schema.GetPossibleTypes(currentType)...)
referencedFromCurrent = append(referencedFromCurrent, r.schema.GetImplements(currentType)...)
for _, t := range currentType.Types {
referencedFromCurrent = append(referencedFromCurrent, r.schema.Types[t])
}
for _, i := range currentType.Interfaces {
referencedFromCurrent = append(referencedFromCurrent, r.schema.Types[i])
}
for _, f := range currentType.Fields {
referencedFromCurrent = append(referencedFromCurrent, r.schema.Types[f.Type.Name()])
for _, arg := range f.Arguments {
referencedFromCurrent = append(referencedFromCurrent, r.schema.Types[arg.Type.Name()])
if arg.DefaultValue != nil {
referencedFromCurrent = append(referencedFromCurrent, arg.DefaultValue.Definition)
}
}
if f.DefaultValue != nil {
referencedFromCurrent = append(referencedFromCurrent, f.DefaultValue.Definition)
}
}
for _, def := range referencedFromCurrent {
if def == nil {
continue
}
// If this type hasn't been seen before, make sure we expand it by adding to the queue
r.addToQueue(def)
// When adding a union or enum to the queue, make sure we also add all its possible implementations
for _, pt := range r.schema.PossibleTypes[def.Name] {
r.addToQueue(pt)
}
// When adding an implementation of an interface to the queue, make sure we also add the interface it implements
for _, pt := range r.schema.Implements[def.Name] {
r.addToQueue(pt)
}
}
}

// Mark all double underscore types as reachable because they are used for introspection
for _, d := range r.schema.Types {
if strings.HasPrefix(d.Name, "__") {
r.reachable[d.Name] = d
for _, pt := range r.schema.PossibleTypes[d.Name] {
r.reachable[pt.Name] = pt
}
}
}
return r.reachable
}

func calculateAndWarnOnUnreachableTypes(reachableTypes, allTypes map[string]*ast.Definition) {
unreachableTypes := map[string]struct{}{}
for all := range allTypes {
unreachableTypes[all] = struct{}{}
}
for r := range reachableTypes {
delete(unreachableTypes, r)
}

if len(unreachableTypes) > 0 {
unreachableTypesList := []string{}
for t := range unreachableTypes {
unreachableTypesList = append(unreachableTypesList, t)
}
sort.Strings(unreachableTypesList)
for _, typeName := range unreachableTypesList {
if !allTypes[typeName].BuiltIn {
fmt.Printf("Warning: unreachable type: %s\n", typeName)
}
}
}
}

func removeUnreachableTypes(a *ast.Schema) {
rc := reachabilityCleaner{
reachable: map[string]*ast.Definition{},
schema: a,
}
rc.addToQueue(a.Query)
rc.addToQueue(a.Mutation)
rc.addToQueue(a.Subscription)
// Entity is a fake type used in the federation plugin to generate resolver interfaces to be implemented
// TODO: find a better way to make sure it's considered reachable
rc.addToQueue(a.Types["Entity"])
// All directive arguments are considered reachable for now. No need to clean these up as aggressively.
for _, dd := range a.Directives {
for _, arg := range dd.Arguments {
rc.addToQueue(a.Types[arg.Type.Name()])
}
}

reachable := rc.findReachableTypes()

calculateAndWarnOnUnreachableTypes(reachable, a.Types)

a.Types = reachable
}
14 changes: 14 additions & 0 deletions codegen/testserver/followschema/builtinscalar.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions codegen/testserver/followschema/builtinscalar.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
extend type Query {
Map: Map!
}

"""
Since gqlgen defines default implementation for a Map scalar, this tests that the builtin is _not_
Expand Down
7 changes: 7 additions & 0 deletions codegen/testserver/followschema/loops.generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions codegen/testserver/followschema/loops.graphql
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
extend type Query {
LoopA: LoopA
}

type LoopA {
b: LoopB!
}
Expand Down
52 changes: 52 additions & 0 deletions codegen/testserver/followschema/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,22 @@ func (r *queryResolver) DeprecatedField(ctx context.Context) (string, error) {
panic("not implemented")
}

func (r *queryResolver) EmbeddedPointer(ctx context.Context) (*EmbeddedPointerModel, error) {
panic("not implemented")
}

func (r *queryResolver) ForcedResolver(ctx context.Context) (*ForcedResolver, error) {
panic("not implemented")
}

func (r *queryResolver) Status(ctx context.Context) (Status, error) {
panic("not implemented")
}

func (r *queryResolver) Map(ctx context.Context) (*Map, error) {
panic("not implemented")
}

func (r *queryResolver) Overlapping(ctx context.Context) (*OverlappingFields, error) {
panic("not implemented")
}
Expand Down Expand Up @@ -236,6 +252,10 @@ func (r *queryResolver) Issue896a(ctx context.Context) ([]*CheckIssue896, error)
panic("not implemented")
}

func (r *queryResolver) LoopA(ctx context.Context) (*LoopA, error) {
panic("not implemented")
}

func (r *queryResolver) MapStringInterface(ctx context.Context, in map[string]interface{}) (map[string]interface{}, error) {
panic("not implemented")
}
Expand Down Expand Up @@ -296,6 +316,10 @@ func (r *queryResolver) DefaultScalar(ctx context.Context, arg string) (string,
panic("not implemented")
}

func (r *queryResolver) EmbeddedDefaultScalar(ctx context.Context) (*EmbeddedDefaultScalar, error) {
panic("not implemented")
}

func (r *queryResolver) Slices(ctx context.Context) (*Slices, error) {
panic("not implemented")
}
Expand Down Expand Up @@ -324,10 +348,38 @@ func (r *queryResolver) ValidType(ctx context.Context) (*ValidType, error) {
panic("not implemented")
}

func (r *queryResolver) ContentChild(ctx context.Context) (ContentChild, error) {
panic("not implemented")
}

func (r *queryResolver) VariadicModel(ctx context.Context) (*VariadicModel, error) {
panic("not implemented")
}

func (r *queryResolver) AsdfIt(ctx context.Context) (*AsdfIt, error) {
panic("not implemented")
}

func (r *queryResolver) AIt(ctx context.Context) (*AIt, error) {
panic("not implemented")
}

func (r *queryResolver) IIt(ctx context.Context) (*IIt, error) {
panic("not implemented")
}

func (r *queryResolver) XXIt(ctx context.Context) (*XXIt, error) {
panic("not implemented")
}

func (r *queryResolver) AbIt(ctx context.Context) (*AbIt, error) {
panic("not implemented")
}

func (r *queryResolver) XxIt(ctx context.Context) (*XxIt, error) {
panic("not implemented")
}

func (r *queryResolver) WrappedStruct(ctx context.Context) (*WrappedStruct, error) {
panic("not implemented")
}
Expand Down

0 comments on commit ae8064c

Please sign in to comment.