go icon indicating copy to clipboard operation
go copied to clipboard

x/tools/cmd/stringer: support generating types defined in test files

Open rogpeppe opened this issue 1 year ago • 9 comments

Go version

go version devel go1.23-5e1e3a0025 Thu Mar 21 17:25:54 2024 +0000 linux/amd64

What did you do?

Run this testscript script:

exec go generate ./...
-- go.mod --
module test
-- x_test.go --
package test

//go:generate stringer -type foo

type foo int

const (
	fooA foo = iota
	fooB
	fooC
)
-- x.go --
package test

What did you see happen?

> exec go generate ./...
[stderr]
stringer: no values defined for type foo
x_test.go:3: running "stringer": exit status 1

What did you expect to see?

I'd expect it to succeed and generate a file named foo_string_test.go containing a String method for the type foo.

It's sometimes useful to have the convenience of stringer to generate a String method even if the type is only defined for a test.

rogpeppe avatar Mar 27 '24 10:03 rogpeppe

Hello @thanm & @rogpeppe Is this something I could help fix?

There is a comment in stringer.go

// TODO: Need to think about constants in test files. Maybe write type_string_test.go
// in a separate pass? For later.

which seems straightforward enough. Not sure if a separate pass is necessary, as long as its possible to tell where each relevant type is located.

vikblom avatar May 01 '24 12:05 vikblom

@vikblom sure, I think you could pick this up. Seems straightforward and useful.

findleyr avatar Jun 28 '24 13:06 findleyr

Hi @findleyr , I finally had some time to sit down and have a closer look at this. I have an approach in mind but would like your input before writing up a CR. I would guess that 1 type in 1 package is the most common usage, but handling N types (already supported) in 3 potential packages, opens up for some edge cases.

The main loop could be: loop over the relevant packages, find the values of matching types (if any), and generate the code for them in the given package.

Issue 1: As I understand it, there may be 3 packages in play:

  • package foo in foo.go, called foo.
  • package foo in foo_test.go, called foo.test.
  • package foo_test in foo2_test.go called foo_test. So foo_string_test.go would clash if a type exists in both foo.test and foo_test. Maybe foo_string_pkg_test.go can be a tiebreaker?

Issue 2: Stringer will warn if a type is not found. Today the code exits immediately if values are not found, but if there are more packages to check it would need to keep going. We could track which types have been found at least once, and print the warning at the end, if a type has never been found.

This could be a sizeable diff so I'm open to splitting this across CRs, or hearing suggestions for a simpler approach. I'm not sure how strongly this change needs to stick to existing output/warnings/exits.

vikblom avatar Jul 15 '24 07:07 vikblom

Hi @vikblom, sorry for the slow response.

The package foo_test will essentially always import package foo, and when it does so, it will import the test variant of foo (what you're calling foo.test), which includes all test files. Therefore, it would be an error for a type to exist in both foo_test and foo.test.

Of course, constant values could be defined in foo.test or foo_test, for a type defined in foo.

I think it makes sense to implement the following heuristic: run stringer on the narrowest package containing the type declaration (in gopls, we say 'narrowest' to mean the package with the fewest files, which is the same as saying: foo > foo.test).

findleyr avatar Jul 26 '24 22:07 findleyr

Hello again, pardon the summer slowness on my end.

That heuristic sounds like a good approach. Then any types "left over" at the end can be errored as not found, matching the behaviour today :+1:

I still think there is a need to generate different files. Since stringer can accept many types at once, they might be declared in different packages (foo, foo.test or foo_test). So the generated code should match that, else methods can't be defined.

vikblom avatar Aug 07 '24 16:08 vikblom

Change https://go.dev/cl/604535 mentions this issue: cmd/stringer: support types declared in test files

gopherbot avatar Aug 09 '24 17:08 gopherbot

Therefore, it would be an error for a type to exist in both foo_test and foo.test

I'm sorry, but this was a total hallucination or bad miscommunication on my part -- I suppose I must have been thinking of dot-importing the package under test.

It is of course possible to redefine a type in the x_test package. Sorry! Let's sort this out on the CL.

findleyr avatar Aug 16 '24 18:08 findleyr

It might make sense to put this functionality behind a new flag.

vikblom avatar Aug 18 '24 15:08 vikblom

@vikblom I think we want to avoid new flags whenever possible. The way the functionality is implemented in your CL is such that existing use-cases should continue to function as they did before, and new use-cases are now supported. I don't think we need a flag to gate a new, purely additive feature.

findleyr avatar Aug 26 '24 17:08 findleyr