diff --git a/api/generate.go b/api/generate.go index 3256bdc3de1..4d29c3f3a92 100644 --- a/api/generate.go +++ b/api/generate.go @@ -1,12 +1,13 @@ package api import ( + "fmt" "syscall" + "time" "github.com/99designs/gqlgen/codegen" "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/plugin" - "github.com/99designs/gqlgen/plugin/modelgen" "github.com/99designs/gqlgen/plugin/resolvergen" "github.com/99designs/gqlgen/plugin/schemaconfig" "github.com/pkg/errors" @@ -14,12 +15,13 @@ import ( ) func Generate(cfg *config.Config, option ...Option) error { + timeStartTotal := time.Now() _ = syscall.Unlink(cfg.Exec.Filename) _ = syscall.Unlink(cfg.Model.Filename) plugins := []plugin.Plugin{ schemaconfig.New(), - modelgen.New(), + //modelgen.New(), resolvergen.New(), } @@ -35,15 +37,40 @@ func Generate(cfg *config.Config, option ...Option) error { } } } + + // load configs now to get packages needed for loading + schema, schemaStr, err := cfg.LoadSchema() + if err != nil { + return err + } + + err = cfg.Check() + if err != nil { + return err + } + + /// WARNING: now we inject builtins before autobinding because autobinding required package paths to be resolved and injecting builtins can add new paths + cfg.InjectBuiltins(schema) + + packageNames := append(cfg.AutoBind, cfg.Models.ReferencedPackages()...) + timeStart := time.Now() + pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadTypes | packages.LoadSyntax | packages.NeedName}, packageNames...) + if err != nil { + return errors.Wrap(err, "loading failed") + } + fmt.Println("loading time ", time.Now().Sub(timeStart)) + // Merge again now that the generated models have been injected into the typemap - data, err := codegen.BuildData(cfg) + data, err := codegen.BuildData(cfg, pkgs, schema, schemaStr) if err != nil { return errors.Wrap(err, "merging failed") } - if err = codegen.GenerateCode(data); err != nil { + timeStart = time.Now() + if err = codegen.GenerateCode(data, pkgs); err != nil { return errors.Wrap(err, "generating core failed") } + fmt.Println("generation time ", time.Now().Sub(timeStart)) for _, p := range plugins { if mut, ok := p.(plugin.CodeGenerator); ok { @@ -54,9 +81,14 @@ func Generate(cfg *config.Config, option ...Option) error { } } - if err := validate(cfg); err != nil { - return errors.Wrap(err, "validation failed") - } + /* + timeStart = time.Now() + if err := validate(cfg); err != nil { + return errors.Wrap(err, "validation failed") + } + fmt.Println("validation time ", time.Now().Sub(timeStart)) + */ + fmt.Println("total time ", time.Now().Sub(timeStartTotal)) return nil } diff --git a/codegen/config/binder.go b/codegen/config/binder.go index 1dfec6c19b0..90e2921520e 100644 --- a/codegen/config/binder.go +++ b/codegen/config/binder.go @@ -20,12 +20,7 @@ type Binder struct { References []*TypeReference } -func (c *Config) NewBinder(s *ast.Schema) (*Binder, error) { - pkgs, err := packages.Load(&packages.Config{Mode: packages.LoadTypes | packages.LoadSyntax}, c.Models.ReferencedPackages()...) - if err != nil { - return nil, err - } - +func (c *Config) NewBinder(s *ast.Schema, pkgs []*packages.Package) (*Binder, error) { mp := map[string]*packages.Package{} for _, p := range pkgs { populatePkg(mp, p) diff --git a/codegen/config/config.go b/codegen/config/config.go index 45f8a7bd81a..a629324c894 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -385,21 +385,29 @@ func (c *Config) normalize() error { return nil } -func (c *Config) Autobind(s *ast.Schema) error { +// TODO: this has to respect ./..., all, etc. +func (c *Config) isAutobind(pkg *packages.Package) bool { + for _, ab := range c.AutoBind { + if pkg.PkgPath == ab { + return true + } + } + return false +} + +func (c *Config) Autobind(s *ast.Schema, ps []*packages.Package) error { if len(c.AutoBind) == 0 { return nil } - ps, err := packages.Load(&packages.Config{Mode: packages.LoadTypes}, c.AutoBind...) - if err != nil { - return err - } - for _, t := range s.Types { if c.Models.UserDefined(t.Name) { continue } for _, p := range ps { + if !c.isAutobind(p) { + continue + } if t := p.Types.Scope().Lookup(t.Name); t != nil { c.Models.Add(t.Name(), t.Pkg().Path()+"."+t.Name()) break @@ -417,6 +425,9 @@ func (c *Config) Autobind(s *ast.Schema) error { } for _, p := range ps { + if !c.isAutobind(p) { + continue + } if p.Name != pkg { continue } diff --git a/codegen/data.go b/codegen/data.go index f743dee3624..5f1f3419412 100644 --- a/codegen/data.go +++ b/codegen/data.go @@ -7,6 +7,7 @@ import ( "github.com/99designs/gqlgen/codegen/config" "github.com/pkg/errors" "github.com/vektah/gqlparser/ast" + "golang.org/x/tools/go/packages" ) // Data is a unified model of the code to be generated. Plugins may modify this structure to do things like implement @@ -35,30 +36,21 @@ type builder struct { Directives map[string]*Directive } -func BuildData(cfg *config.Config) (*Data, error) { +func BuildData(cfg *config.Config, pkgs []*packages.Package, schema *ast.Schema, schemaStr map[string]string) (*Data, error) { b := builder{ Config: cfg, } + b.Schema = schema + b.SchemaStr = schemaStr var err error - b.Schema, b.SchemaStr, err = cfg.LoadSchema() - if err != nil { - return nil, err - } - - err = cfg.Check() - if err != nil { - return nil, err - } - err = cfg.Autobind(b.Schema) + err = cfg.Autobind(b.Schema, pkgs) if err != nil { return nil, err } - cfg.InjectBuiltins(b.Schema) - - b.Binder, err = b.Config.NewBinder(b.Schema) + b.Binder, err = b.Config.NewBinder(b.Schema, pkgs) if err != nil { return nil, err } diff --git a/codegen/generate.go b/codegen/generate.go index eafa3f87434..4ead6950edb 100644 --- a/codegen/generate.go +++ b/codegen/generate.go @@ -2,14 +2,17 @@ package codegen import ( "github.com/99designs/gqlgen/codegen/templates" + "github.com/99designs/gqlgen/internal/code" + "golang.org/x/tools/go/packages" ) -func GenerateCode(data *Data) error { +func GenerateCode(data *Data, packages []*packages.Package) error { return templates.Render(templates.Options{ PackageName: data.Config.Exec.Package, Filename: data.Config.Exec.Filename, Data: data, RegionTags: true, GeneratedHeader: true, + NameForPackage: code.NewNameForPackage(packages), }) } diff --git a/codegen/templates/import.go b/codegen/templates/import.go index d5bd16a6a1e..ab84e1e279d 100644 --- a/codegen/templates/import.go +++ b/codegen/templates/import.go @@ -10,14 +10,16 @@ import ( ) type Import struct { - Name string - Path string - Alias string + NameForPackage code.NameForPackage + Name string + Path string + Alias string } type Imports struct { - imports []*Import - destDir string + NameForPackage code.NameForPackage + imports []*Import + destDir string } func (i *Import) String() string { @@ -49,7 +51,7 @@ func (s *Imports) Reserve(path string, aliases ...string) (string, error) { return "", nil } - name := code.NameForPackage(path) + name := s.NameForPackage.Get(path) var alias string if len(aliases) != 1 { alias = name @@ -69,9 +71,10 @@ func (s *Imports) Reserve(path string, aliases ...string) (string, error) { } s.imports = append(s.imports, &Import{ - Name: name, - Path: path, - Alias: alias, + NameForPackage: s.NameForPackage, + Name: name, + Path: path, + Alias: alias, }) return "", nil @@ -94,7 +97,7 @@ func (s *Imports) Lookup(path string) string { } imp := &Import{ - Name: code.NameForPackage(path), + Name: s.NameForPackage.Get(path), Path: path, } s.imports = append(s.imports, imp) diff --git a/codegen/templates/templates.go b/codegen/templates/templates.go index 5d5f69bf88d..55b9ce99399 100644 --- a/codegen/templates/templates.go +++ b/codegen/templates/templates.go @@ -15,6 +15,7 @@ import ( "text/template" "unicode" + "github.com/99designs/gqlgen/internal/code" "github.com/99designs/gqlgen/internal/imports" "github.com/pkg/errors" ) @@ -43,6 +44,8 @@ type Options struct { // Data will be passed to the template execution. Data interface{} Funcs template.FuncMap + // Lookups for pre-cached package names + NameForPackage code.NameForPackage } // Render renders a gql plugin template from the given Options. Render is an @@ -53,7 +56,7 @@ func Render(cfg Options) error { if CurrentImports != nil { panic(fmt.Errorf("recursive or concurrent call to RenderToFile detected")) } - CurrentImports = &Imports{destDir: filepath.Dir(cfg.Filename)} + CurrentImports = &Imports{NameForPackage: cfg.NameForPackage, destDir: filepath.Dir(cfg.Filename)} // load path relative to calling source file _, callerFile, _, _ := runtime.Caller(1) @@ -143,7 +146,7 @@ func Render(cfg Options) error { } CurrentImports = nil - return write(cfg.Filename, result.Bytes()) + return write(cfg.Filename, result.Bytes(), cfg.NameForPackage) } func center(width int, pad string, s string) string { @@ -551,13 +554,13 @@ func render(filename string, tpldata interface{}) (*bytes.Buffer, error) { return buf, t.Execute(buf, tpldata) } -func write(filename string, b []byte) error { +func write(filename string, b []byte, nameForPackage code.NameForPackage) error { err := os.MkdirAll(filepath.Dir(filename), 0755) if err != nil { return errors.Wrap(err, "failed to create directory") } - formatted, err := imports.Prune(filename, b) + formatted, err := imports.Prune(filename, b, nameForPackage) if err != nil { fmt.Fprintf(os.Stderr, "gofmt failed on %s: %s\n", filepath.Base(filename), err.Error()) formatted = b diff --git a/internal/code/imports.go b/internal/code/imports.go index ad62f7c56b1..828b3828238 100644 --- a/internal/code/imports.go +++ b/internal/code/imports.go @@ -14,8 +14,6 @@ import ( "golang.org/x/tools/go/packages" ) -var nameForPackageCache = sync.Map{} - var gopaths []string func init() { @@ -92,24 +90,39 @@ func ImportPathForDir(dir string) (res string) { var modregex = regexp.MustCompile("module (.*)\n") +type NameForPackage struct { + cache *sync.Map + packages []*packages.Package +} + +func NewNameForPackage(packages []*packages.Package) NameForPackage { + return NameForPackage{ + cache: &sync.Map{}, + packages: packages, + } +} + // NameForPackage returns the package name for a given import path. This can be really slow. -func NameForPackage(importPath string) string { +func (n NameForPackage) Get(importPath string) string { if importPath == "" { panic(errors.New("import path can not be empty")) } - if v, ok := nameForPackageCache.Load(importPath); ok { + if v, ok := n.cache.Load(importPath); ok { return v.(string) } importPath = QualifyPackagePath(importPath) - p, _ := packages.Load(&packages.Config{ - Mode: packages.NeedName, - }, importPath) + var p *packages.Package + for _, pkg := range n.packages { + if pkg.PkgPath == importPath { + p = pkg + } + } - if len(p) != 1 || p[0].Name == "" { + if p == nil || p.Name == "" { return SanitizePackageName(filepath.Base(importPath)) } - nameForPackageCache.Store(importPath, p[0].Name) + n.cache.Store(importPath, p.Name) - return p[0].Name + return p.Name } diff --git a/internal/code/util.go b/internal/code/util.go index 2be83a23cea..626154331b4 100644 --- a/internal/code/util.go +++ b/internal/code/util.go @@ -39,6 +39,11 @@ func NormalizeVendor(pkg string) string { // x/tools/packages only supports 'qualified package paths' so this will need to be done prior to calling it // See https://github.com/golang/go/issues/30289 func QualifyPackagePath(importPath string) string { + // TODO: check if we are in go module mode or not + goModuleMode := true + if goModuleMode { + return importPath + } wd, _ := os.Getwd() pkg, err := build.Import(importPath, wd, 0) diff --git a/internal/imports/prune.go b/internal/imports/prune.go index 27ac94ac0f0..62b1a4f2f50 100644 --- a/internal/imports/prune.go +++ b/internal/imports/prune.go @@ -24,7 +24,7 @@ func (fn visitFn) Visit(node ast.Node) ast.Visitor { } // Prune removes any unused imports -func Prune(filename string, src []byte) ([]byte, error) { +func Prune(filename string, src []byte, nameForPackage code.NameForPackage) ([]byte, error) { fset := token.NewFileSet() file, err := parser.ParseFile(fset, filename, src, parser.ParseComments|parser.AllErrors) @@ -32,7 +32,7 @@ func Prune(filename string, src []byte) ([]byte, error) { return nil, err } - unused := getUnusedImports(file) + unused := getUnusedImports(file, nameForPackage) for ipath, name := range unused { astutil.DeleteNamedImport(fset, file, name, ipath) } @@ -46,7 +46,7 @@ func Prune(filename string, src []byte) ([]byte, error) { return imports.Process(filename, buf.Bytes(), &imports.Options{FormatOnly: true, Comments: true, TabIndent: true, TabWidth: 8}) } -func getUnusedImports(file ast.Node) map[string]string { +func getUnusedImports(file ast.Node, nameForPackage code.NameForPackage) map[string]string { imported := map[string]*ast.ImportSpec{} used := map[string]bool{} @@ -65,7 +65,7 @@ func getUnusedImports(file ast.Node) map[string]string { break } - local := code.NameForPackage(ipath) + local := nameForPackage.Get(ipath) imported[local] = v case *ast.SelectorExpr: