Skip to content

Go codegenerators, unpredictable direct dependencies and the strict dependencies check #2571

@robbertvanginkel

Description

@robbertvanginkel

While attempting to develop gazelle extensions for go codegenerators that take go code as input and generate go code as output, we're running into trouble determining the dependencies to add to a rule. Specifically in the presence of a typealias, some codegenerators generate code with imports from the source's transitive dependencies. With rules_go's "strict dependencies" check, this is currently hard to model in bazel/gazelle.

For example, consider the following toy project for which we want to generate mocks with golang/mock:

-- ./foo/foo.go --
package foo

import "github.com/example/project/bar"

type Foo interface {
	DoFoo(bar.Bar)
}
-- ./bar/bar.go --
package bar

import "github.com/example/project/baz"

type Bar = baz.Baz
-- ./baz/baz.go --
package baz

type Baz struct{}

Using https://github.com/jmhodges/bazel_gomock, its straightforward to add a rule that would generate go file with a mock:

gomock(
    name = "foo_mock",
    out = "foo_mock.go",
    interfaces = ["Foo"],
    library = "//foo:go_default_library",
)

Our goal was to have a gazelle extension that could detect that the foo_mock.go file was generated based off of //foo:go_default_library, so that we could generate a go_library (or a similar go_mock_library) rule for foo_mock.go that would include all deps of //foo:go_default_library in its deps. Something like:

go_library(
	name = "foo_mock_library",
	srcs = ["foo_mock.go"],
	deps = ["//bar:go_default_library"], # filled by the deps from //foo:go_default_library
)

However, with that you'll run into the following compile error:

compilepkg: missing strict dependencies:
	foo_mock.go: import of "github.com/example/project/baz"

Looking at the generated file, this is because mockgen "follows the typealias" (golang/mock#244) and generates code with possible imports from the set of transitive dependencies of the library being mocked, not just the direct dependencies:

$ bazel build //:foo_mock.go && cat bazel-bin/foo_mock.go
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/example/project/foo (interfaces: Foo)

// Package mock_foo is a generated GoMock package.
package mock_foo

import (
	baz "github.com/example/project/baz"
	gomock "github.com/golang/mock/gomock"
	reflect "reflect"
)

...

// DoFoo mocks base method
func (m *MockFoo) DoFoo(arg0 baz.Baz) {

Gazelle doesn't seem to be a very good fit for creating rules where the dependencies can't directly be inferred by reading a local source file. We'd like to find a way to help auto generate some of these rules as writing them manually can be tedious and error prone.

We've thought of a few ways to tackle this:

  • Don't (only) use gazelle. We can have an external tool that: creates a mock rule, builds mock rule, reads generated imports, writes a gazelle directive with generated importpaths, runs gazelle to resolve those imports to labels. This works but is slow, fragile and the resulting directives are very duplicative to the rules they generate.

  • Use an aspect to collect all transitive GoSource providers from dependencies of the library to mock in a custom go_mock_library rule and supply them as directs when using go.new_library(). A custom rule like this could avoid the need for specifying deps, but it seems like this could lead to overspecifying dependencies and result in unnecessary rebuilds.

  • Investigate if the "missing strict dependencies" check can be relaxed. The value of this check for handwritten code is clear, but it came to mind that this might be less necessary for generated code in which the generated dependencies are always a subset of the original library's transitive dependencies: the problematic case of breakage when an implicit dependency on something transitive that gets refactored away wouldn't be possible, as when the refactoring removes the implicit dependency it could not be part of the codegenerator's result. Not sure if this is possible at all.

  • Avoid / fix the codegenerators that generate code for which deps can't be easily inferred. Unfortunately, it seems like codegen that is not solely based on go/ast exhibits the typealias problem. We've observed it in a few internal ones that do codegen based on go/types and x/tools/go/packages too.

Hoping there might be some advice/insight on how this could be best handled.

cc @linzhp

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions