Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve package-sniffing and bind correctly to types in the same package #316

Merged
merged 6 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ Note that genqlient is now tested through Go 1.22.
- The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details.
- For schemas with enum values that differ only in casing, it's now possible to disable smart-casing in genqlient.yaml; see the [documentation](genqlient.yaml) for `casing` for details.
- Support .graphqls and .gql file extensions
- More accurately guess the package name for generated code (and warn if the config option -- now almost never needed -- looks wrong).

### Bug fixes:
- The presence of negative pointer directives, i.e., `# @genqlient(pointer: false)` are now respected even in the when `optional: pointer` is set in the configuration file.
- Made name collisions between query/mutation arguments and local function variables less likely.
- Fix generation issue related to golang type implementation of complex graphql union fragments
- Bind correctly to types in the same package as the generated code.

## v0.6.0

Expand Down
14 changes: 12 additions & 2 deletions docs/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ operations:
# genqlient.yaml. Default: generated.go.
generated: generated/genqlient.go

# The package name for the output code; defaults to the directory name of
# the generated-code file.
# The package name for the output code; defaults to the package-name
# corresponding to the setting of `generated`, above.
#
# This is rarely needed: only if you want the package-name to differ from the
# suffix of the package-path, and there are no other Go files in the package
# already.
package: mygenerated

# If set, a file at this path (relative to genqlient.yaml) will be generated
Expand Down Expand Up @@ -139,6 +143,9 @@ optional_generic_type: github.com/organisation/repository/example.Type
# guarantees that the fields requested in the query match those present in
# the Go type.
#
# Note: if binding to types in the same package as the generated code, make
# sure you don't bind to generated types! Otherwise, things get very circular.
#
# To get equivalent behavior in just one query, use @genqlient(bind: ...);
# see genqlient_directive.graphql for more details.
bindings:
Expand Down Expand Up @@ -224,6 +231,9 @@ bindings:
# to the bindings map, above, for each exported type in the package. Multiple
# packages may be specified, and later ones take precedence over earlier ones.
# Explicit entries in bindings take precedence over all package bindings.
#
# Note: make sure this isn't the package with your generated code, or things
# will get circular very fast.
package_bindings:
- package: github.com/you/yourpkg/models

Expand Down
2 changes: 0 additions & 2 deletions example/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ schema: schema.graphql
operations:
- genqlient.graphql
generated: generated.go
# needed since it doesn't match the directory name:
package: main

# We bind github's DateTime scalar type to Go's time.Time (which conveniently
# already defines MarshalJSON and UnmarshalJSON). This means genqlient will
Expand Down
100 changes: 80 additions & 20 deletions generate/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type Config struct {
// The directory of the config-file (relative to which all the other paths
// are resolved). Set by ValidateAndFillDefaults.
baseDir string
// The package-path into which we are generating.
pkgPath string
}

// A TypeBinding represents a Go type to which genqlient will bind a particular
Expand Down Expand Up @@ -132,6 +134,46 @@ func pathJoin(a, b string) string {
return filepath.Join(a, b)
}

func (c *Config) getPackageNameAndPath() (pkgName, pkgPath string, err error) {
abs, err := filepath.Abs(c.Generated)
if err != nil {
return "", "", err
}

dir := filepath.Dir(abs)
pkgNameGuess := filepath.Base(dir)
if !token.IsIdentifier(pkgNameGuess) {
pkgNameGuess = ""
}
Comment on lines +149 to +152
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this could profitably be later down in the function, maybe lines 166- could be something like:

        packageNameGuess := ""
	if token.IsIdentifier(filepath.Base(pkg.PkgPath)) {
		pkgNameGuess = filepath.Base(pkg.PkgPath)
	} else if token.IsIdentifier(filepath.Base(dir)) {
                pkgNameGuess = filepath.Base(dir)
        }

But I don't really understand if/how that else case could fire. Maybe add some documentation here.


pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName}, dir)
if err != nil {
return pkgNameGuess, "", err
} else if len(pkgs) != 1 {
return pkgNameGuess, "", fmt.Errorf("found %v packages in %v, expected 1", len(pkgs), dir)
}

pkg := pkgs[0]
// TODO(benkraft): Can PkgPath ever be empty without error? If so, we could
// warn.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I don't know the semantics of packages.Load, but it seems like Name is empty if the package doesn't exist, but PkgPath is still set in that case?

if pkg.Name != "" {
return pkg.Name, pkg.PkgPath, nil
}

// e.g. empty package yet to be created, see if we can just guess a
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this comment is very helpful!

// reasonable name.
pathSuffix := filepath.Base(pkg.PkgPath)
if token.IsIdentifier(pathSuffix) {
pkgNameGuess = pathSuffix
}

if pkgNameGuess != "" {
return pkgNameGuess, pkg.PkgPath, nil
} else {
return "", "", fmt.Errorf("no package found in %v", dir)
}
}

// ValidateAndFillDefaults ensures that the configuration is valid, and fills
// in any options that were unspecified.
//
Expand Down Expand Up @@ -167,29 +209,42 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error {
"\nExample: \"github.com/Org/Repo/optional.Value\"")
}

if c.Package != "" {
if !token.IsIdentifier(c.Package) {
// No need for link here -- if you're already setting the package
// you know where to set the package.
return errorf(nil, "invalid package in genqlient.yaml: '%v' is not a valid identifier", c.Package)
}
} else {
abs, err := filepath.Abs(c.Generated)
if err != nil {
return errorf(nil, "unable to guess package-name: %v"+
"\nSet package name in genqlient.yaml"+
"\nExample: https://github.com/Khan/genqlient/blob/main/example/genqlient.yaml#L6", err)
}
if c.Package != "" && !token.IsIdentifier(c.Package) {
// No need for link here -- if you're already setting the package
// you know where to set the package.
return errorf(nil, "invalid package in genqlient.yaml: '%v' is not a valid identifier", c.Package)
}

base := filepath.Base(filepath.Dir(abs))
if !token.IsIdentifier(base) {
return errorf(nil, "unable to guess package-name: '%v' is not a valid identifier"+
"\nSet package name in genqlient.yaml"+
"\nExample: https://github.com/Khan/genqlient/blob/main/example/genqlient.yaml#L6", base)
pkgName, pkgPath, err := c.getPackageNameAndPath()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to comment that this ignores c.Package, and that below you'll check to make sure they're in agreement (or, at least, not in disagreement.)

if err == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to figure out what was going on here, because I'm so used to the Go convention of checking for the error case in the if's, not the success case.

I wonder if there's a way to reorganize this code to keep the current semantics but perhaps adhere more closely to Go convention. Maybe not.

if c.Package == pkgName || c.Package == "" {
c.Package = pkgName
} else {
warn(errorf(nil, "warning: package setting in genqlient.yaml '%v' looks wrong "+
"('%v' is in package '%v') but proceeding with '%v' anyway\n",
c.Package, c.Generated, pkgName, c.Package))
}

c.Package = base
} else if c.Package != "" {
// If you specified a valid package, at least try to use that.
// But we can't set pkgPath, which means you'll run into trouble
// binding against the generated package, so at least warn.
warn(errorf(nil, "warning: unable to identify current package-path "+
"(using 'package' config '%v'): %v\n", c.Package, err))
} else if pkgName != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's unusual, though I guess not unheard of, for the value of the other return-values to matter when err is not nil. It may be worth emphasizing this in the relevant parts of getPackageNameAndPath, though.

// If the directory-name is valid, use that. This is useful if you
// somehow can't build, and especially for tests.
warn(errorf(nil, "warning: unable to identify current package-path "+
"(using directory name '%v': %v\n", pkgName, err))
c.Package = pkgName
} else {
return errorf(nil, "unable to guess package-name: %v"+
"\nSet package name in genqlient.yaml"+
"\nExample: https://github.com/Khan/genqlient/blob/main/example/genqlient.yaml#L6", err)
}
// This is not likely to work if we got an error, especially if we did the
// c.Package fallback. But it's more likely to work than nothing, so we may
// as well.
c.pkgPath = pkgPath

if len(c.PackageBindings) > 0 {
for _, binding := range c.PackageBindings {
Expand All @@ -201,6 +256,11 @@ func (c *Config) ValidateAndFillDefaults(baseDir string) error {
binding.Package)
}

if binding.Package == c.pkgPath {
warn(errorf(nil, "warning: package_bindings set to the same package as your generated "+
"code ('%v'); this will probably cause circularity issues", c.pkgPath))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"probably cause circular-import issues"?

}

mode := packages.NeedDeps | packages.NeedTypes
pkgs, err := packages.Load(&packages.Config{
Mode: mode,
Expand Down
6 changes: 3 additions & 3 deletions generate/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
)

const (
findConfigDir = "testdata/find-config"
validConfigDir = "testdata/valid-config"
invalidConfigDir = "testdata/invalid-config"
findConfigDir = "testdata/findConfig"
validConfigDir = "testdata/validConfig"
invalidConfigDir = "testdata/invalidConfig"
)

func TestFindCfg(t *testing.T) {
Expand Down
3 changes: 3 additions & 0 deletions generate/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ func (g *generator) ref(fullyQualifiedName string) (qualifiedName string, err er

pkgPath := nameToImport[:i]
localName := nameToImport[i+1:]
if pkgPath == g.Config.pkgPath {
return prefix + localName, nil
}
alias, ok := g.imports[pkgPath]
if !ok {
if g.importsLocked {
Expand Down
5 changes: 5 additions & 0 deletions generate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ import (
"github.com/alexflint/go-arg"
)

// TODO(benkraft): Make this mockable for tests?
func warn(err error) {
fmt.Println(err)
}

func readConfigGenerateAndWrite(configFilename string) error {
var config *Config
var err error
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
invalid config file testdata/invalid-config/InvalidCasing.yaml: unknown casing algorithm: bogus
invalid config file testdata/invalidConfig/InvalidCasing.yaml: unknown casing algorithm: bogus
Original file line number Diff line number Diff line change
@@ -1 +1 @@
invalid config file testdata/invalid-config/InvalidOptional.yaml: optional must be one of: 'value' (default), 'pointer', or 'generic'
invalid config file testdata/invalidConfig/InvalidOptional.yaml: optional must be one of: 'value' (default), 'pointer', or 'generic'
Original file line number Diff line number Diff line change
@@ -1 +1 @@
invalid config file testdata/invalid-config/InvalidPackage.yaml: invalid package in genqlient.yaml: 'bogus-package-name' is not a valid identifier
invalid config file testdata/invalidConfig/InvalidPackage.yaml: invalid package in genqlient.yaml: 'bogus-package-name' is not a valid identifier
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
(*generate.Config)({
Schema: (generate.StringList) <nil>,
Operations: (generate.StringList) <nil>,
Generated: (string) (len=34) "testdata/valid-config/generated.go",
Generated: (string) (len=33) "testdata/validConfig/generated.go",
Package: (string) (len=11) "validConfig",
ExportOperations: (string) "",
ContextType: (string) (len=15) "context.Context",
Expand All @@ -17,5 +17,6 @@
StructReferences: (bool) false,
Extensions: (bool) false,
AllowBrokenFeatures: (bool) false,
baseDir: (string) (len=21) "testdata/valid-config"
baseDir: (string) (len=20) "testdata/validConfig",
pkgPath: (string) (len=55) "github.com/Khan/genqlient/generate/testdata/validConfig"
})
13 changes: 7 additions & 6 deletions generate/testdata/snapshots/TestValidConfigs-Lists.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
(*generate.Config)({
Schema: (generate.StringList) (len=2) {
(string) (len=42) "testdata/valid-config/first_schema.graphql",
(string) (len=43) "testdata/valid-config/second_schema.graphql"
(string) (len=41) "testdata/validConfig/first_schema.graphql",
(string) (len=42) "testdata/validConfig/second_schema.graphql"
},
Operations: (generate.StringList) (len=2) {
(string) (len=46) "testdata/valid-config/first_operations.graphql",
(string) (len=47) "testdata/valid-config/second_operations.graphql"
(string) (len=45) "testdata/validConfig/first_operations.graphql",
(string) (len=46) "testdata/validConfig/second_operations.graphql"
},
Generated: (string) (len=34) "testdata/valid-config/generated.go",
Generated: (string) (len=33) "testdata/validConfig/generated.go",
Package: (string) (len=11) "validConfig",
ExportOperations: (string) "",
ContextType: (string) (len=15) "context.Context",
Expand All @@ -23,5 +23,6 @@
StructReferences: (bool) false,
Extensions: (bool) false,
AllowBrokenFeatures: (bool) false,
baseDir: (string) (len=21) "testdata/valid-config"
baseDir: (string) (len=20) "testdata/validConfig",
pkgPath: (string) (len=55) "github.com/Khan/genqlient/generate/testdata/validConfig"
})
9 changes: 5 additions & 4 deletions generate/testdata/snapshots/TestValidConfigs-Strings.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
(*generate.Config)({
Schema: (generate.StringList) (len=1) {
(string) (len=36) "testdata/valid-config/schema.graphql"
(string) (len=35) "testdata/validConfig/schema.graphql"
},
Operations: (generate.StringList) (len=1) {
(string) (len=40) "testdata/valid-config/operations.graphql"
(string) (len=39) "testdata/validConfig/operations.graphql"
},
Generated: (string) (len=34) "testdata/valid-config/generated.go",
Generated: (string) (len=33) "testdata/validConfig/generated.go",
Package: (string) (len=11) "validConfig",
ExportOperations: (string) "",
ContextType: (string) (len=15) "context.Context",
Expand All @@ -21,5 +21,6 @@
StructReferences: (bool) false,
Extensions: (bool) false,
AllowBrokenFeatures: (bool) false,
baseDir: (string) (len=21) "testdata/valid-config"
baseDir: (string) (len=20) "testdata/validConfig",
pkgPath: (string) (len=55) "github.com/Khan/genqlient/generate/testdata/validConfig"
})
1 change: 0 additions & 1 deletion generate/testdata/valid-config/Simple.yaml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,3 @@ schema:
operations:
- first_operations.graphql
- second_operations.graphql

package: validConfig
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
schema: schema.graphql
operations: operations.graphql
package: validConfig
11 changes: 8 additions & 3 deletions internal/integration/generated.go

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

2 changes: 2 additions & 0 deletions internal/integration/genqlient.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ bindings:
type: time.Time
marshaler: "github.com/Khan/genqlient/internal/testutil.MarshalDate"
unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate"
MyGreatScalar:
type: github.com/Khan/genqlient/internal/integration.MyGreatScalar
2 changes: 1 addition & 1 deletion internal/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (

func TestSimpleQuery(t *testing.T) {
_ = `# @genqlient
query simpleQuery { me { id name luckyNumber } }`
query simpleQuery { me { id name luckyNumber greatScalar } }`

ctx := context.Background()
server := server.RunServer()
Expand Down
2 changes: 2 additions & 0 deletions internal/integration/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
scalar Date
scalar MyGreatScalar

type Query {
me: User
Expand All @@ -23,6 +24,7 @@ type User implements Being & Lucky {
hair: Hair
birthdate: Date
friends: [User!]!
greatScalar: MyGreatScalar
}

input NewUser {
Expand Down
Loading
Loading