big overhaul of error-formatting, to get positions more consistently

fixes #2
This commit is contained in:
Ben Kraft
2021-04-15 17:01:46 -07:00
parent ade01c9280
commit 2c087c7821
18 changed files with 233 additions and 74 deletions
+13 -13
View File
@@ -40,6 +40,8 @@ import (
// the directive "a" is ignored, "b" and "c" apply to all relevant nodes in the // the directive "a" is ignored, "b" and "c" apply to all relevant nodes in the
// query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2. // query, "d" applies to arg2 and arg3, and "e" applies to field1 and field2.
type GenqlientDirective struct { type GenqlientDirective struct {
pos *ast.Position
// If set, this argument will be omitted if it's equal to its Go zero // If set, this argument will be omitted if it's equal to its Go zero
// value. For example, given the following query: // value. For example, given the following query:
// # @genqlient(omitempty: true) // # @genqlient(omitempty: true)
@@ -68,23 +70,25 @@ func setBool(dst **bool, v *ast.Value) error {
ei, err := v.Value(nil) // no vars allowed ei, err := v.Value(nil) // no vars allowed
// TODO: here and below, put positions on these errors // TODO: here and below, put positions on these errors
if err != nil { if err != nil {
return fmt.Errorf("invalid boolean value %v: %w", v, err) return errorf(v.Position, "invalid boolean value %v: %v", v, err)
} }
if b, ok := ei.(bool); ok { if b, ok := ei.(bool); ok {
*dst = &b *dst = &b
return nil return nil
} }
return fmt.Errorf("expected boolean, got non-boolean value %T(%v)", ei, ei) return errorf(v.Position, "expected boolean, got non-boolean value %T(%v)", ei, ei)
} }
func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) { func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
if dir.Name != "genqlient" { if dir.Name != "genqlient" {
// Actually we just won't get here; we only get here if the line starts // Actually we just won't get here; we only get here if the line starts
// with "# @genqlient", unless there's some sort of bug. // with "# @genqlient", unless there's some sort of bug.
return nil, fmt.Errorf("the only valid comment-directive is @genqlient, got %v", dir.Name) return nil, errorf(dir.Position, "the only valid comment-directive is @genqlient, got %v", dir.Name)
} }
var retval GenqlientDirective var retval GenqlientDirective
retval.pos = dir.Position
var err error var err error
for _, arg := range dir.Arguments { for _, arg := range dir.Arguments {
switch arg.Name { switch arg.Name {
@@ -94,7 +98,7 @@ func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) {
case "pointer": case "pointer":
err = setBool(&retval.Pointer, arg.Value) err = setBool(&retval.Pointer, arg.Value)
default: default:
return nil, fmt.Errorf("unknown argument %v for @genqlient", arg.Name) return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -111,16 +115,16 @@ func (dir *GenqlientDirective) validate(node interface{}) error {
return nil return nil
case *ast.VariableDefinition: case *ast.VariableDefinition:
if dir.Omitempty != nil && node.Type.NonNull { if dir.Omitempty != nil && node.Type.NonNull {
return fmt.Errorf("omitempty may only be used on optional arguments") return errorf(dir.pos, "omitempty may only be used on optional arguments")
} }
return nil return nil
case *ast.Field: case *ast.Field:
if dir.Omitempty != nil { if dir.Omitempty != nil {
return fmt.Errorf("omitempty is not appilcable to fields") return errorf(dir.pos, "omitempty is not appilcable to fields")
} }
return nil return nil
default: default:
return fmt.Errorf("invalid directive location: %T", node) return errorf(dir.pos, "invalid directive location: %T", node)
} }
} }
@@ -174,13 +178,9 @@ func (g *generator) parsePrecedingComment(
func parseDirective(line string, pos *ast.Position) (*ast.Directive, error) { func parseDirective(line string, pos *ast.Position) (*ast.Directive, error) {
// HACK: parse the "directive" by making a fake query containing it. // HACK: parse the "directive" by making a fake query containing it.
fakeQuery := fmt.Sprintf("query %v { field }", line) fakeQuery := fmt.Sprintf("query %v { field }", line)
doc, err := parser.ParseQuery(&ast.Source{ doc, err := parser.ParseQuery(&ast.Source{Input: fakeQuery})
Name: fmt.Sprintf("@genqlient directive at %v:%v:%v",
pos.Src.Name, pos.Line, pos.Column),
Input: fakeQuery,
})
if err != nil { if err != nil {
return nil, err return nil, errorf(pos, "invalid genqlient directive: %v", err)
} }
return doc.Operations[0].Directives[0], nil return doc.Operations[0].Directives[0], nil
} }
+5 -6
View File
@@ -1,7 +1,6 @@
package generate package generate
import ( import (
"fmt"
"go/token" "go/token"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
@@ -103,12 +102,12 @@ func (c *Config) ValidateAndFillDefaults(configFilename string) error {
if c.Package == "" { if c.Package == "" {
abs, err := filepath.Abs(c.Generated) abs, err := filepath.Abs(c.Generated)
if err != nil { if err != nil {
return fmt.Errorf("unable to guess package-name: %v", err) return errorf(nil, "unable to guess package-name: %v", err)
} }
base := filepath.Base(filepath.Dir(abs)) base := filepath.Base(filepath.Dir(abs))
if !token.IsIdentifier(base) { if !token.IsIdentifier(base) {
return fmt.Errorf("unable to guess package-name: %v is not a valid identifier", base) return errorf(nil, "unable to guess package-name: %v is not a valid identifier", base)
} }
c.Package = base c.Package = base
@@ -122,18 +121,18 @@ func ReadAndValidateConfig(filename string) (*Config, error) {
if filename != "" { if filename != "" {
text, err := ioutil.ReadFile(filename) text, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("unreadable config file %v: %v", filename, err) return nil, errorf(nil, "unreadable config file %v: %v", filename, err)
} }
err = yaml.Unmarshal(text, &config) err = yaml.Unmarshal(text, &config)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid config file %v: %v", filename, err) return nil, errorf(nil, "invalid config file %v: %v", filename, err)
} }
} }
err := config.ValidateAndFillDefaults(filename) err := config.ValidateAndFillDefaults(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid config file %v: %v", filename, err) return nil, errorf(nil, "invalid config file %v: %v", filename, err)
} }
return &config, nil return &config, nil
+132
View File
@@ -0,0 +1,132 @@
package generate
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
)
type errorPos struct {
filename string
line, col int
}
func (pos *errorPos) String() string {
filename, lineOffset := splitFilename(pos.filename)
line := lineOffset + pos.line
if line != 0 {
return fmt.Sprintf("%v:%v", filename, line)
} else {
return filename
}
}
type genqlientError struct {
pos *errorPos
msg string
wrapped error
}
func splitFilename(filename string) (name string, lineOffset int) {
split := strings.Split(filename, ":")
if len(split) != 2 {
return filename, 0
}
offset, err := strconv.Atoi(split[1])
if err != nil {
return split[0], 0
}
return split[0], offset - 1
}
func (err *genqlientError) Error() string {
if err.pos != nil {
return err.pos.String() + ": " + err.msg
} else {
return err.msg
}
}
func (err *genqlientError) Unwrap() error {
return err.wrapped
}
func errorf(pos *ast.Position, msg string, args ...interface{}) error {
// TODO: alternately accept a filename only, or maybe even a go-parser pos
// We do all our own wrapping, because if the wrapped error already has a
// pos, we want to extract it out and put it at the front, not in the
// middle.
var wrapped error
var wrapIndex int
for i, arg := range args {
if wrapped == nil {
var ok bool
wrapped, ok = arg.(error)
if ok {
wrapIndex = i
}
}
}
var wrappedGenqlient *genqlientError
isGenqlient := errors.As(wrapped, &wrappedGenqlient)
var wrappedGraphQL *gqlerror.Error
isGraphQL := errors.As(wrapped, &wrappedGraphQL)
if !isGraphQL {
var wrappedGraphQLList gqlerror.List
isGraphQLList := errors.As(wrapped, &wrappedGraphQLList)
if isGraphQLList && len(wrappedGraphQLList) > 0 {
isGraphQL = true
wrappedGraphQL = wrappedGraphQLList[0]
}
}
var errPos *errorPos
if pos != nil {
errPos = &errorPos{
filename: pos.Src.Name,
line: pos.Line,
col: pos.Column,
}
} else if isGenqlient {
errPos = wrappedGenqlient.pos
} else if isGraphQL {
filename, _ := wrappedGraphQL.Extensions["file"].(string)
if filename != "" {
var loc gqlerror.Location
if len(wrappedGraphQL.Locations) > 0 {
loc = wrappedGraphQL.Locations[0]
}
errPos = &errorPos{
filename: filename,
line: loc.Line,
col: loc.Column,
}
}
}
if wrapped != nil {
errText := wrapped.Error()
if isGenqlient {
errText = wrappedGenqlient.msg
} else if isGraphQL {
errText = wrappedGraphQL.Message
}
args[wrapIndex] = errText
}
msg = fmt.Sprintf(msg, args...)
return &genqlientError{
msg: msg,
pos: errPos,
wrapped: wrapped,
}
}
+10 -8
View File
@@ -3,7 +3,6 @@ package generate
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"go/format" "go/format"
"sort" "sort"
"strings" "strings"
@@ -134,7 +133,7 @@ func (g *generator) getArgument(
func (g *generator) addOperation(op *ast.OperationDefinition) error { func (g *generator) addOperation(op *ast.OperationDefinition) error {
if op.Name == "" { if op.Name == "" {
return fmt.Errorf("operations must have operation-names") return errorf(op.Position, "operations must have operation-names")
} }
var builder strings.Builder var builder strings.Builder
@@ -199,11 +198,14 @@ func Generate(config *Config) (map[string][]byte, error) {
// package-name, if it turns out to be more convenient that way. (As-is, // package-name, if it turns out to be more convenient that way. (As-is,
// we generate a broken file, with just (unused) imports.) // we generate a broken file, with just (unused) imports.)
if len(document.Operations) == 0 { if len(document.Operations) == 0 {
return nil, fmt.Errorf("no queries found in %v", config.Operations) // Hard to have a position when there are no operations :(
return nil, errorf(nil, "no queries found, looked in: %v",
strings.Join(config.Operations, ", "))
} }
if len(document.Fragments) > 0 && !allowBrokenFeatures { if len(document.Fragments) > 0 && !allowBrokenFeatures {
return nil, fmt.Errorf("genqlient does not yet support fragments") return nil, errorf(document.Fragments[0].Position,
"genqlient does not yet support fragments")
} }
g := newGenerator(config, schema) g := newGenerator(config, schema)
@@ -216,18 +218,18 @@ func Generate(config *Config) (map[string][]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
err = g.execute("operation.go.tmpl", &buf, g) err = g.execute("operation.go.tmpl", &buf, g)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not render template: %v", err) return nil, errorf(nil, "could not render template: %v", err)
} }
unformatted := buf.Bytes() unformatted := buf.Bytes()
formatted, err := format.Source(unformatted) formatted, err := format.Source(unformatted)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not gofmt code: %v\n---unformatted code---\n%v", return nil, errorf(nil, "could not gofmt code: %v\n---unformatted code---\n%v",
err, string(unformatted)) err, string(unformatted))
} }
importsed, err := imports.Process(config.Generated, formatted, nil) importsed, err := imports.Process(config.Generated, formatted, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("could not goimports code: %v\n---unimportsed code---\n%v", return nil, errorf(nil, "could not goimports code: %v\n---unimportsed code---\n%v",
err, string(formatted)) err, string(formatted))
} }
@@ -243,7 +245,7 @@ func Generate(config *Config) (map[string][]byte, error) {
retval[config.ExportOperations], err = json.MarshalIndent( retval[config.ExportOperations], err = json.MarshalIndent(
exportedOperations{Operations: g.Operations}, "", " ") exportedOperations{Operations: g.Operations}, "", " ")
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to export queries: %v", err) return nil, errorf(nil, "unable to export queries: %v", err)
} }
} }
+1 -2
View File
@@ -2,7 +2,6 @@ package generate
import ( import (
"errors" "errors"
"fmt"
"go/format" "go/format"
"io/ioutil" "io/ioutil"
"os" "os"
@@ -20,7 +19,7 @@ func gofmt(filename, src string) (string, error) {
src = strings.TrimSpace(src) src = strings.TrimSpace(src)
formatted, err := format.Source([]byte(src)) formatted, err := format.Source([]byte(src))
if err != nil { if err != nil {
return src, fmt.Errorf("go parse error in %v: %w", filename, err) return src, errorf(nil, "go parse error in %v: %v", filename, err)
} }
return string(formatted), nil return string(formatted), nil
} }
+3 -3
View File
@@ -1,7 +1,6 @@
package generate package generate
import ( import (
"fmt"
"go/types" "go/types"
"strconv" "strconv"
"strings" "strings"
@@ -45,7 +44,8 @@ func (g *generator) getRef(fullyQualifiedName string, addImport bool) (qualified
// confusing, why would you want it. But the empty interface, // confusing, why would you want it. But the empty interface,
// specifically, is useful. // specifically, is useful.
if fullyQualifiedName != "interface{}" && types.Universe.Lookup(fullyQualifiedName) == nil { if fullyQualifiedName != "interface{}" && types.Universe.Lookup(fullyQualifiedName) == nil {
return "", fmt.Errorf( // TODO: pass in pos here
return "", errorf(nil,
`unknown name "%v"; expected a builtin or path/to/package.Name`, fullyQualifiedName) `unknown name "%v"; expected a builtin or path/to/package.Name`, fullyQualifiedName)
} }
return fullyQualifiedName, nil return fullyQualifiedName, nil
@@ -60,7 +60,7 @@ func (g *generator) getRef(fullyQualifiedName string, addImport bool) (qualified
var ok bool var ok bool
alias, ok = g.imports[pkgPath] alias, ok = g.imports[pkgPath]
if !ok { if !ok {
return "", fmt.Errorf(`no alias defined for package "%v"`, pkgPath) return "", errorf(nil, `no alias defined for package "%v"`, pkgPath)
} }
} }
return alias + "." + localName, nil return alias + "." + localName, nil
+8 -3
View File
@@ -5,6 +5,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
func readConfigGenerateAndWrite(configFilename string) error { func readConfigGenerateAndWrite(configFilename string) error {
@@ -21,14 +22,14 @@ func readConfigGenerateAndWrite(configFilename string) error {
for filename, content := range generated { for filename, content := range generated {
err = os.MkdirAll(filepath.Dir(filename), 0o755) err = os.MkdirAll(filepath.Dir(filename), 0o755)
if err != nil { if err != nil {
return fmt.Errorf( return errorf(nil,
"could not create parent directory for generated file %v: %v", "could not create parent directory for generated file %v: %v",
filename, err) filename, err)
} }
err = ioutil.WriteFile(filename, content, 0o644) err = ioutil.WriteFile(filename, content, 0o644)
if err != nil { if err != nil {
return fmt.Errorf("could not write generated file %v: %v", return errorf(nil, "could not write generated file %v: %v",
filename, err) filename, err)
} }
} }
@@ -50,6 +51,10 @@ func Main() {
case 1: case 1:
err = readConfigGenerateAndWrite("") err = readConfigGenerateAndWrite("")
default: default:
err = fmt.Errorf("usage: %s [config]", os.Args[0]) argv0 := os.Args[0]
if strings.Contains(argv0, string(filepath.Separator)+"go-build") {
argv0 = "go run github.com/Khan/genqlient"
}
err = errorf(nil, "usage: %s [config]", argv0)
} }
} }
+14 -10
View File
@@ -20,13 +20,13 @@ import (
func getSchema(filename string) (*ast.Schema, error) { func getSchema(filename string) (*ast.Schema, error) {
text, err := ioutil.ReadFile(filename) text, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("unreadable schema file %v: %v", filename, err) return nil, errorf(nil, "unreadable schema file %v: %v", filename, err)
} }
schema, graphqlError := gqlparser.LoadSchema( schema, graphqlError := gqlparser.LoadSchema(
&ast.Source{Name: filename, Input: string(text)}) &ast.Source{Name: filename, Input: string(text)})
if graphqlError != nil { if graphqlError != nil {
return nil, fmt.Errorf("invalid schema file %v: %v", return nil, errorf(nil, "invalid schema file %v: %v",
filename, graphqlError) filename, graphqlError)
} }
@@ -42,7 +42,7 @@ func getAndValidateQueries(basedir string, filenames []string, schema *ast.Schem
// Cf. gqlparser.LoadQuery // Cf. gqlparser.LoadQuery
graphqlErrors := validator.Validate(schema, queryDoc) graphqlErrors := validator.Validate(schema, queryDoc)
if graphqlErrors != nil { if graphqlErrors != nil {
return nil, fmt.Errorf("query-spec does not match schema: %v", graphqlErrors) return nil, errorf(nil, "query-spec does not match schema: %v", graphqlErrors)
} }
return queryDoc, nil return queryDoc, nil
@@ -64,7 +64,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error)
for _, filename := range filenames { for _, filename := range filenames {
matches, err := filepath.Glob(filename) matches, err := filepath.Glob(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("can't expand file-glob %v: %v", filename, err) return nil, errorf(nil, "can't expand file-glob %v: %v", filename, err)
} }
expandedFilenames = append(expandedFilenames, matches...) expandedFilenames = append(expandedFilenames, matches...)
} }
@@ -72,7 +72,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error)
for _, filename := range expandedFilenames { for _, filename := range expandedFilenames {
text, err := ioutil.ReadFile(filename) text, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return nil, fmt.Errorf("unreadable query-spec file %v: %v", filename, err) return nil, errorf(nil, "unreadable query-spec file %v: %v", filename, err)
} }
switch filepath.Ext(filename) { switch filepath.Ext(filename) {
@@ -95,7 +95,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error)
} }
default: default:
return nil, fmt.Errorf("unknown file type: %v", filename) return nil, errorf(nil, "unknown file type: %v", filename)
} }
} }
@@ -113,7 +113,7 @@ func getQueriesFromString(text string, basedir, filename string) (*ast.QueryDocu
document, graphqlError := parser.ParseQuery( document, graphqlError := parser.ParseQuery(
&ast.Source{Name: filename, Input: text}) &ast.Source{Name: filename, Input: text})
if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck if graphqlError != nil { // ParseQuery returns type *graphql.Error, yuck
return nil, fmt.Errorf("invalid query-spec file %v: %v", filename, graphqlError) return nil, errorf(nil, "invalid query-spec file %v: %v", filename, graphqlError)
} }
return document, nil return document, nil
@@ -123,7 +123,7 @@ func getQueriesFromGo(text string, basedir, filename string) ([]*ast.QueryDocume
fset := goToken.NewFileSet() fset := goToken.NewFileSet()
f, err := goParser.ParseFile(fset, filename, text, 0) f, err := goParser.ParseFile(fset, filename, text, 0)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid Go file %v: %v", filename, err) return nil, errorf(nil, "invalid Go file %v: %v", filename, err)
} }
var retval []*ast.QueryDocument var retval []*ast.QueryDocument
@@ -147,9 +147,13 @@ func getQueriesFromGo(text string, basedir, filename string) ([]*ast.QueryDocume
return true return true
} }
filename := fset.Position(basicLit.Pos()).Filename // We put the filename as <real filename>:<line>, which errors.go knows
// how to parse back out (since it's what gqlparser will give to us in
// our errors).
pos := fset.Position(basicLit.Pos())
fakeFilename := fmt.Sprintf("%v:%v", pos.Filename, pos.Line)
var query *ast.QueryDocument var query *ast.QueryDocument
query, err = getQueriesFromString(value, basedir, filename) query, err = getQueriesFromString(value, basedir, fakeFilename)
if err != nil { if err != nil {
return false return false
} }
+2 -3
View File
@@ -1,7 +1,6 @@
package generate package generate
import ( import (
"fmt"
"io" "io"
"path/filepath" "path/filepath"
"runtime" "runtime"
@@ -26,13 +25,13 @@ func (g *generator) execute(tmplRelFilename string, w io.Writer, data interface{
var err error var err error
tmpl, err = template.New(tmplRelFilename).Funcs(funcMap).ParseFiles(absFilename) tmpl, err = template.New(tmplRelFilename).Funcs(funcMap).ParseFiles(absFilename)
if err != nil { if err != nil {
return fmt.Errorf("could not load template %v: %v", absFilename, err) return errorf(nil, "could not load template %v: %v", absFilename, err)
} }
g.templateCache[tmplRelFilename] = tmpl g.templateCache[tmplRelFilename] = tmpl
} }
err := tmpl.Execute(w, data) err := tmpl.Execute(w, data)
if err != nil { if err != nil {
return fmt.Errorf("could not render template: %v", err) return errorf(nil, "could not render template: %v", err)
} }
return nil return nil
} }
+1 -1
View File
@@ -1 +1 @@
query-spec does not match schema: testdata/errors/InvalidQuery.go:2: Cannot query field "g" on type "Query". Did you mean "f"? testdata/errors/InvalidQuery.go:4: query-spec does not match schema: Cannot query field "g" on type "Query". Did you mean "f"?
+1 -1
View File
@@ -1 +1 @@
query-spec does not match schema: testdata/errors/InvalidQuery.graphql:1: Cannot query field "g" on type "Query". Did you mean "f"? testdata/errors/InvalidQuery.graphql:1: query-spec does not match schema: Cannot query field "g" on type "Query". Did you mean "f"?
+1 -1
View File
@@ -1 +1 @@
invalid schema file testdata/errors/InvalidSchema.schema.graphql: testdata/errors/InvalidSchema.schema.graphql:4: Expected :, found } testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found }
+1 -1
View File
@@ -1 +1 @@
invalid schema file testdata/errors/InvalidSchema.schema.graphql: testdata/errors/InvalidSchema.schema.graphql:4: Expected :, found } testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found }
+1 -1
View File
@@ -1 +1 @@
no queries found in [testdata/errors/NoQuery.go] no queries found, looked in: testdata/errors/NoQuery.go
+1 -1
View File
@@ -1 +1 @@
no queries found in [testdata/errors/NoQuery.graphql] no queries found, looked in: testdata/errors/NoQuery.graphql
+1 -1
View File
@@ -1 +1 @@
unknown scalar UnknownScalar: please add it to genqlient.yaml testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml
+1 -1
View File
@@ -1 +1 @@
unknown scalar UnknownScalar: please add it to genqlient.yaml testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml
+37 -18
View File
@@ -22,11 +22,11 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definiti
return g.schema.Mutation, nil return g.schema.Mutation, nil
case ast.Subscription: case ast.Subscription:
if !allowBrokenFeatures { if !allowBrokenFeatures {
return nil, fmt.Errorf("genqlient does not yet support subscriptions") return nil, errorf(nil, "genqlient does not yet support subscriptions")
} }
return g.schema.Subscription, nil return g.schema.Subscription, nil
default: default:
return nil, fmt.Errorf("unexpected operation: %v", operation) return nil, errorf(nil, "unexpected operation: %v", operation)
} }
} }
@@ -36,7 +36,7 @@ func (g *generator) getTypeForOperation(operation *ast.OperationDefinition, quer
if def, ok := g.typeMap[name]; ok { if def, ok := g.typeMap[name]; ok {
// TODO: check for and handle conflicts a better way // TODO: check for and handle conflicts a better way
return "", fmt.Errorf("%s defined twice:\n%s", name, def) return "", errorf(operation.Position, "%s defined twice:\n%s", name, def)
} }
fields, err := selections(g, operation.SelectionSet, queryOptions) fields, err := selections(g, operation.SelectionSet, queryOptions)
@@ -46,10 +46,10 @@ func (g *generator) getTypeForOperation(operation *ast.OperationDefinition, quer
baseType, err := g.baseTypeForOperation(operation.Operation) baseType, err := g.baseTypeForOperation(operation.Operation)
if err != nil { if err != nil {
return "", err return "", errorf(operation.Position, "%v", err)
} }
return g.addTypeForDefinition(operation.Name, name, baseType, fields, queryOptions) return g.addTypeForDefinition(operation.Name, name, baseType, operation.Position, fields, queryOptions)
} }
var builtinTypes = map[string]string{ var builtinTypes = map[string]string{
@@ -61,7 +61,13 @@ var builtinTypes = map[string]string{
"ID": "string", "ID": "string",
} }
func (g *generator) addTypeForDefinition(namePrefix, nameOverride string, typ *ast.Definition, fields []field, options *GenqlientDirective) (name string, err error) { func (g *generator) addTypeForDefinition(
namePrefix, nameOverride string,
typ *ast.Definition,
pos *ast.Position,
fields []field,
options *GenqlientDirective,
) (name string, err error) {
// If this is a builtin type or custom scalar, just refer to it. // If this is a builtin type or custom scalar, just refer to it.
goName, ok := g.Config.Scalars[typ.Name] goName, ok := g.Config.Scalars[typ.Name]
if ok { if ok {
@@ -114,7 +120,7 @@ func (g *generator) addTypeForDefinition(namePrefix, nameOverride string, typ *a
// name. // name.
builder := &typeBuilder{typeName: name, typeNamePrefix: namePrefix, generator: g} builder := &typeBuilder{typeName: name, typeNamePrefix: namePrefix, generator: g}
fmt.Fprintf(builder, "type %s ", name) fmt.Fprintf(builder, "type %s ", name)
err = builder.writeTypedef(typ, fields, options) err = builder.writeTypedef(typ, pos, fields, options)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -146,6 +152,7 @@ type field interface {
Alias() string Alias() string
Options() (*GenqlientDirective, error) Options() (*GenqlientDirective, error)
Type() *ast.Type Type() *ast.Type
Pos() *ast.Position
SubFields() ([]field, error) SubFields() ([]field, error)
} }
@@ -176,6 +183,10 @@ func (s outputField) Type() *ast.Type {
return s.field.Definition.Type return s.field.Definition.Type
} }
func (s outputField) Pos() *ast.Position {
return s.field.Position
}
func (s outputField) SubFields() ([]field, error) { func (s outputField) SubFields() ([]field, error) {
return selections(s.generator, s.field.SelectionSet, s.queryOptions) return selections(s.generator, s.field.SelectionSet, s.queryOptions)
} }
@@ -186,10 +197,12 @@ func selections(g *generator, selectionSet ast.SelectionSet, options *GenqlientD
switch selection := selection.(type) { switch selection := selection.(type) {
case *ast.Field: case *ast.Field:
retval[i] = outputField{g, options, selection} retval[i] = outputField{g, options, selection}
case *ast.FragmentSpread, *ast.InlineFragment: case *ast.FragmentSpread:
return nil, fmt.Errorf("not implemented: %T", selection) return nil, errorf(selection.Position, "not implemented: %T", selection)
case *ast.InlineFragment:
return nil, errorf(selection.Position, "not implemented: %T", selection)
default: default:
return nil, fmt.Errorf("invalid selection type: %v", selection) return nil, errorf(nil, "invalid selection type: %T", selection)
} }
} }
return retval, nil return retval, nil
@@ -209,7 +222,8 @@ func (s inputField) Options() (*GenqlientDirective, error) {
} }
return s.queryOptions.merge(directive), nil return s.queryOptions.merge(directive), nil
} }
func (s inputField) Type() *ast.Type { return s.field.Type } func (s inputField) Type() *ast.Type { return s.field.Type }
func (s inputField) Pos() *ast.Position { return s.field.Position }
func (s inputField) SubFields() ([]field, error) { func (s inputField) SubFields() ([]field, error) {
return selectionsForType(s.generator, s.field.Type, s.queryOptions), nil return selectionsForType(s.generator, s.field.Type, s.queryOptions), nil
@@ -236,7 +250,7 @@ func (builder *typeBuilder) writeField(field field) error {
if typ == nil { if typ == nil {
// Unclear why gqlparser hasn't already rejected this, // Unclear why gqlparser hasn't already rejected this,
// but empirically it might not. // but empirically it might not.
return fmt.Errorf("undefined field %v", field.Alias()) return errorf(field.Pos(), "undefined field %v", field.Alias())
} }
fields, err := field.SubFields() fields, err := field.SubFields()
@@ -291,7 +305,7 @@ func (builder *typeBuilder) writeType(namePrefix, nameOverride string, typ *ast.
def := builder.schema.Types[typ.Name()] def := builder.schema.Types[typ.Name()]
// Writes a typedef elsewhere (if not already defined) // Writes a typedef elsewhere (if not already defined)
name, err := builder.addTypeForDefinition(namePrefix, nameOverride, def, fields, options) name, err := builder.addTypeForDefinition(namePrefix, nameOverride, def, typ.Position, fields, options)
if err != nil { if err != nil {
return err return err
} }
@@ -300,7 +314,12 @@ func (builder *typeBuilder) writeType(namePrefix, nameOverride string, typ *ast.
return nil return nil
} }
func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field, options *GenqlientDirective) error { func (builder *typeBuilder) writeTypedef(
typedef *ast.Definition,
pos *ast.Position,
fields []field,
options *GenqlientDirective,
) error {
switch typedef.Kind { switch typedef.Kind {
case ast.Object, ast.InputObject: case ast.Object, ast.InputObject:
builder.WriteString("struct {\n") builder.WriteString("struct {\n")
@@ -318,7 +337,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field
case ast.Interface, ast.Union: case ast.Interface, ast.Union:
if !allowBrokenFeatures { if !allowBrokenFeatures {
return fmt.Errorf("not implemented: %v", typedef.Kind) return errorf(pos, "not implemented: %v", typedef.Kind)
} }
// First, write the interface type. // First, write the interface type.
@@ -332,7 +351,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field
// Then, write the implementations. // Then, write the implementations.
// TODO(benkraft): Put a doc-comment somewhere with the list. // TODO(benkraft): Put a doc-comment somewhere with the list.
for _, impldef := range builder.schema.GetPossibleTypes(typedef) { for _, impldef := range builder.schema.GetPossibleTypes(typedef) {
name, err := builder.addTypeForDefinition(builder.typeNamePrefix, "", impldef, fields, options) name, err := builder.addTypeForDefinition(builder.typeNamePrefix, "", impldef, pos, fields, options)
if err != nil { if err != nil {
return err return err
} }
@@ -355,8 +374,8 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field
builder.WriteString(")\n") builder.WriteString(")\n")
return nil return nil
case ast.Scalar: case ast.Scalar:
return fmt.Errorf("unknown scalar %v: please add it to genqlient.yaml", typedef.Name) return errorf(pos, "unknown scalar %v: please add it to genqlient.yaml", typedef.Name)
default: default:
return fmt.Errorf("unexpected kind: %v", typedef.Kind) return errorf(pos, "unexpected kind: %v", typedef.Kind)
} }
} }