diff --git a/generate/comments.go b/generate/comments.go index 9932ae2..f64071f 100644 --- a/generate/comments.go +++ b/generate/comments.go @@ -40,6 +40,8 @@ import ( // 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. type GenqlientDirective struct { + pos *ast.Position + // If set, this argument will be omitted if it's equal to its Go zero // value. For example, given the following query: // # @genqlient(omitempty: true) @@ -68,23 +70,25 @@ func setBool(dst **bool, v *ast.Value) error { ei, err := v.Value(nil) // no vars allowed // TODO: here and below, put positions on these errors 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 { *dst = &b 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) { if dir.Name != "genqlient" { // Actually we just won't get here; we only get here if the line starts // 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 + retval.pos = dir.Position + var err error for _, arg := range dir.Arguments { switch arg.Name { @@ -94,7 +98,7 @@ func fromGraphQL(dir *ast.Directive) (*GenqlientDirective, error) { case "pointer": err = setBool(&retval.Pointer, arg.Value) 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 { return nil, err @@ -111,16 +115,16 @@ func (dir *GenqlientDirective) validate(node interface{}) error { return nil case *ast.VariableDefinition: 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 case *ast.Field: 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 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) { // HACK: parse the "directive" by making a fake query containing it. fakeQuery := fmt.Sprintf("query %v { field }", line) - doc, err := parser.ParseQuery(&ast.Source{ - Name: fmt.Sprintf("@genqlient directive at %v:%v:%v", - pos.Src.Name, pos.Line, pos.Column), - Input: fakeQuery, - }) + doc, err := parser.ParseQuery(&ast.Source{Input: fakeQuery}) if err != nil { - return nil, err + return nil, errorf(pos, "invalid genqlient directive: %v", err) } return doc.Operations[0].Directives[0], nil } diff --git a/generate/config.go b/generate/config.go index 0b84f34..876be53 100644 --- a/generate/config.go +++ b/generate/config.go @@ -1,7 +1,6 @@ package generate import ( - "fmt" "go/token" "io/ioutil" "path/filepath" @@ -103,12 +102,12 @@ func (c *Config) ValidateAndFillDefaults(configFilename string) error { if c.Package == "" { abs, err := filepath.Abs(c.Generated) 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)) 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 @@ -122,18 +121,18 @@ func ReadAndValidateConfig(filename string) (*Config, error) { if filename != "" { text, err := ioutil.ReadFile(filename) 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) 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) 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 diff --git a/generate/errors.go b/generate/errors.go new file mode 100644 index 0000000..d20975e --- /dev/null +++ b/generate/errors.go @@ -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, + } +} diff --git a/generate/generate.go b/generate/generate.go index 69f559a..f4e5790 100644 --- a/generate/generate.go +++ b/generate/generate.go @@ -3,7 +3,6 @@ package generate import ( "bytes" "encoding/json" - "fmt" "go/format" "sort" "strings" @@ -134,7 +133,7 @@ func (g *generator) getArgument( func (g *generator) addOperation(op *ast.OperationDefinition) error { if op.Name == "" { - return fmt.Errorf("operations must have operation-names") + return errorf(op.Position, "operations must have operation-names") } 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, // we generate a broken file, with just (unused) imports.) 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 { - 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) @@ -216,18 +218,18 @@ func Generate(config *Config) (map[string][]byte, error) { var buf bytes.Buffer err = g.execute("operation.go.tmpl", &buf, g) 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() formatted, err := format.Source(unformatted) 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)) } importsed, err := imports.Process(config.Generated, formatted, 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)) } @@ -243,7 +245,7 @@ func Generate(config *Config) (map[string][]byte, error) { retval[config.ExportOperations], err = json.MarshalIndent( exportedOperations{Operations: g.Operations}, "", " ") if err != nil { - return nil, fmt.Errorf("unable to export queries: %v", err) + return nil, errorf(nil, "unable to export queries: %v", err) } } diff --git a/generate/generate_test.go b/generate/generate_test.go index 08a4eb7..2c0a0db 100644 --- a/generate/generate_test.go +++ b/generate/generate_test.go @@ -2,7 +2,6 @@ package generate import ( "errors" - "fmt" "go/format" "io/ioutil" "os" @@ -20,7 +19,7 @@ func gofmt(filename, src string) (string, error) { src = strings.TrimSpace(src) formatted, err := format.Source([]byte(src)) 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 } diff --git a/generate/imports.go b/generate/imports.go index 50f64dc..73288a0 100644 --- a/generate/imports.go +++ b/generate/imports.go @@ -1,7 +1,6 @@ package generate import ( - "fmt" "go/types" "strconv" "strings" @@ -45,7 +44,8 @@ func (g *generator) getRef(fullyQualifiedName string, addImport bool) (qualified // confusing, why would you want it. But the empty interface, // specifically, is useful. 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) } return fullyQualifiedName, nil @@ -60,7 +60,7 @@ func (g *generator) getRef(fullyQualifiedName string, addImport bool) (qualified var ok bool alias, ok = g.imports[pkgPath] 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 diff --git a/generate/main.go b/generate/main.go index d0cf898..e3ffdd3 100644 --- a/generate/main.go +++ b/generate/main.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" ) func readConfigGenerateAndWrite(configFilename string) error { @@ -21,14 +22,14 @@ func readConfigGenerateAndWrite(configFilename string) error { for filename, content := range generated { err = os.MkdirAll(filepath.Dir(filename), 0o755) if err != nil { - return fmt.Errorf( + return errorf(nil, "could not create parent directory for generated file %v: %v", filename, err) } err = ioutil.WriteFile(filename, content, 0o644) 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) } } @@ -50,6 +51,10 @@ func Main() { case 1: err = readConfigGenerateAndWrite("") 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) } } diff --git a/generate/parse.go b/generate/parse.go index 327ef3c..a5f259a 100644 --- a/generate/parse.go +++ b/generate/parse.go @@ -20,13 +20,13 @@ import ( func getSchema(filename string) (*ast.Schema, error) { text, err := ioutil.ReadFile(filename) 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( &ast.Source{Name: filename, Input: string(text)}) if graphqlError != nil { - return nil, fmt.Errorf("invalid schema file %v: %v", + return nil, errorf(nil, "invalid schema file %v: %v", filename, graphqlError) } @@ -42,7 +42,7 @@ func getAndValidateQueries(basedir string, filenames []string, schema *ast.Schem // Cf. gqlparser.LoadQuery graphqlErrors := validator.Validate(schema, queryDoc) 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 @@ -64,7 +64,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error) for _, filename := range filenames { matches, err := filepath.Glob(filename) 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...) } @@ -72,7 +72,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error) for _, filename := range expandedFilenames { text, err := ioutil.ReadFile(filename) 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) { @@ -95,7 +95,7 @@ func getQueries(basedir string, filenames []string) (*ast.QueryDocument, error) } 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( &ast.Source{Name: filename, Input: text}) 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 @@ -123,7 +123,7 @@ func getQueriesFromGo(text string, basedir, filename string) ([]*ast.QueryDocume fset := goToken.NewFileSet() f, err := goParser.ParseFile(fset, filename, text, 0) 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 @@ -147,9 +147,13 @@ func getQueriesFromGo(text string, basedir, filename string) ([]*ast.QueryDocume return true } - filename := fset.Position(basicLit.Pos()).Filename + // We put the filename as :, 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 - query, err = getQueriesFromString(value, basedir, filename) + query, err = getQueriesFromString(value, basedir, fakeFilename) if err != nil { return false } diff --git a/generate/template.go b/generate/template.go index 11f4e26..8f61e33 100644 --- a/generate/template.go +++ b/generate/template.go @@ -1,7 +1,6 @@ package generate import ( - "fmt" "io" "path/filepath" "runtime" @@ -26,13 +25,13 @@ func (g *generator) execute(tmplRelFilename string, w io.Writer, data interface{ var err error tmpl, err = template.New(tmplRelFilename).Funcs(funcMap).ParseFiles(absFilename) 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 } err := tmpl.Execute(w, data) if err != nil { - return fmt.Errorf("could not render template: %v", err) + return errorf(nil, "could not render template: %v", err) } return nil } diff --git a/generate/testdata/errors/InvalidQuery.go.error b/generate/testdata/errors/InvalidQuery.go.error index 19694bb..2545e3a 100644 --- a/generate/testdata/errors/InvalidQuery.go.error +++ b/generate/testdata/errors/InvalidQuery.go.error @@ -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"? \ No newline at end of file diff --git a/generate/testdata/errors/InvalidQuery.graphql.error b/generate/testdata/errors/InvalidQuery.graphql.error index 10a329c..99c6326 100644 --- a/generate/testdata/errors/InvalidQuery.graphql.error +++ b/generate/testdata/errors/InvalidQuery.graphql.error @@ -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"? \ No newline at end of file diff --git a/generate/testdata/errors/InvalidSchema.go.error b/generate/testdata/errors/InvalidSchema.go.error index 3d7c49d..9f41e23 100644 --- a/generate/testdata/errors/InvalidSchema.go.error +++ b/generate/testdata/errors/InvalidSchema.go.error @@ -1 +1 @@ -invalid schema file testdata/errors/InvalidSchema.schema.graphql: testdata/errors/InvalidSchema.schema.graphql:4: Expected :, found } \ No newline at end of file +testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found } \ No newline at end of file diff --git a/generate/testdata/errors/InvalidSchema.graphql.error b/generate/testdata/errors/InvalidSchema.graphql.error index 3d7c49d..9f41e23 100644 --- a/generate/testdata/errors/InvalidSchema.graphql.error +++ b/generate/testdata/errors/InvalidSchema.graphql.error @@ -1 +1 @@ -invalid schema file testdata/errors/InvalidSchema.schema.graphql: testdata/errors/InvalidSchema.schema.graphql:4: Expected :, found } \ No newline at end of file +testdata/errors/InvalidSchema.schema.graphql:4: invalid schema file testdata/errors/InvalidSchema.schema.graphql: Expected :, found } \ No newline at end of file diff --git a/generate/testdata/errors/NoQuery.go.error b/generate/testdata/errors/NoQuery.go.error index 21f742d..77fa55d 100644 --- a/generate/testdata/errors/NoQuery.go.error +++ b/generate/testdata/errors/NoQuery.go.error @@ -1 +1 @@ -no queries found in [testdata/errors/NoQuery.go] \ No newline at end of file +no queries found, looked in: testdata/errors/NoQuery.go \ No newline at end of file diff --git a/generate/testdata/errors/NoQuery.graphql.error b/generate/testdata/errors/NoQuery.graphql.error index e8b70ee..7968485 100644 --- a/generate/testdata/errors/NoQuery.graphql.error +++ b/generate/testdata/errors/NoQuery.graphql.error @@ -1 +1 @@ -no queries found in [testdata/errors/NoQuery.graphql] \ No newline at end of file +no queries found, looked in: testdata/errors/NoQuery.graphql \ No newline at end of file diff --git a/generate/testdata/errors/UnknownScalar.go.error b/generate/testdata/errors/UnknownScalar.go.error index 21c5c29..04be9c2 100644 --- a/generate/testdata/errors/UnknownScalar.go.error +++ b/generate/testdata/errors/UnknownScalar.go.error @@ -1 +1 @@ -unknown scalar UnknownScalar: please add it to genqlient.yaml \ No newline at end of file +testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml \ No newline at end of file diff --git a/generate/testdata/errors/UnknownScalar.graphql.error b/generate/testdata/errors/UnknownScalar.graphql.error index 21c5c29..04be9c2 100644 --- a/generate/testdata/errors/UnknownScalar.graphql.error +++ b/generate/testdata/errors/UnknownScalar.graphql.error @@ -1 +1 @@ -unknown scalar UnknownScalar: please add it to genqlient.yaml \ No newline at end of file +testdata/errors/UnknownScalar.schema.graphql:3: unknown scalar UnknownScalar: please add it to genqlient.yaml \ No newline at end of file diff --git a/generate/types.go b/generate/types.go index 6b0767d..26785a7 100644 --- a/generate/types.go +++ b/generate/types.go @@ -22,11 +22,11 @@ func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definiti return g.schema.Mutation, nil case ast.Subscription: 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 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 { // 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) @@ -46,10 +46,10 @@ func (g *generator) getTypeForOperation(operation *ast.OperationDefinition, quer baseType, err := g.baseTypeForOperation(operation.Operation) 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{ @@ -61,7 +61,13 @@ var builtinTypes = map[string]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. goName, ok := g.Config.Scalars[typ.Name] if ok { @@ -114,7 +120,7 @@ func (g *generator) addTypeForDefinition(namePrefix, nameOverride string, typ *a // name. builder := &typeBuilder{typeName: name, typeNamePrefix: namePrefix, generator: g} fmt.Fprintf(builder, "type %s ", name) - err = builder.writeTypedef(typ, fields, options) + err = builder.writeTypedef(typ, pos, fields, options) if err != nil { return "", err } @@ -146,6 +152,7 @@ type field interface { Alias() string Options() (*GenqlientDirective, error) Type() *ast.Type + Pos() *ast.Position SubFields() ([]field, error) } @@ -176,6 +183,10 @@ func (s outputField) Type() *ast.Type { return s.field.Definition.Type } +func (s outputField) Pos() *ast.Position { + return s.field.Position +} + func (s outputField) SubFields() ([]field, error) { 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) { case *ast.Field: retval[i] = outputField{g, options, selection} - case *ast.FragmentSpread, *ast.InlineFragment: - return nil, fmt.Errorf("not implemented: %T", selection) + case *ast.FragmentSpread: + return nil, errorf(selection.Position, "not implemented: %T", selection) + case *ast.InlineFragment: + return nil, errorf(selection.Position, "not implemented: %T", selection) default: - return nil, fmt.Errorf("invalid selection type: %v", selection) + return nil, errorf(nil, "invalid selection type: %T", selection) } } return retval, nil @@ -209,7 +222,8 @@ func (s inputField) Options() (*GenqlientDirective, error) { } 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) { return selectionsForType(s.generator, s.field.Type, s.queryOptions), nil @@ -236,7 +250,7 @@ func (builder *typeBuilder) writeField(field field) error { if typ == nil { // Unclear why gqlparser hasn't already rejected this, // 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() @@ -291,7 +305,7 @@ func (builder *typeBuilder) writeType(namePrefix, nameOverride string, typ *ast. def := builder.schema.Types[typ.Name()] // 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 { return err } @@ -300,7 +314,12 @@ func (builder *typeBuilder) writeType(namePrefix, nameOverride string, typ *ast. 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 { case ast.Object, ast.InputObject: builder.WriteString("struct {\n") @@ -318,7 +337,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field case ast.Interface, ast.Union: if !allowBrokenFeatures { - return fmt.Errorf("not implemented: %v", typedef.Kind) + return errorf(pos, "not implemented: %v", typedef.Kind) } // First, write the interface type. @@ -332,7 +351,7 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field // Then, write the implementations. // TODO(benkraft): Put a doc-comment somewhere with the list. 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 { return err } @@ -355,8 +374,8 @@ func (builder *typeBuilder) writeTypedef(typedef *ast.Definition, fields []field builder.WriteString(")\n") return nil 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: - return fmt.Errorf("unexpected kind: %v", typedef.Kind) + return errorf(pos, "unexpected kind: %v", typedef.Kind) } }