-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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
proposal: cmd/fix: automate migrations for simple deprecations #32816
Comments
How would this deal with Go versions? For example, Perhaps it can grab the oldest supported Go version from the optional line in |
@mvdan, good question. I'd say that it should not attempt to do anything special with Go versions. For stuff in the standard library in particular, perhaps that implies that we shouldn't add |
I personally think this is a losing battle. Certain libraries support older Go versions, for their own reasons. You can say "just don't use An alternative is to use "two Go releases ago" as the fallback version, instead of 1.0. Then a module can specify an older or newer version as needed. |
The "Automatic API Updates" section of the initial vgo blog series sketched out an alternative. It might be interesting to contrast this proposal with that earlier sketch. One difference between that earlier sketch and the proposal here is that the earlier sketch I think had fewer comment variations, but I think relied on the v1 code being implemented as a shim around v2 (and perhaps it could in theory also support the reverse if v2 is implemented as a wrapper around v1). Example snippet from that older sketch:
In contrast, this newer proposal also addresses deprecation use cases (without a need for a major version change). |
@mvdan, I suppose we could use the annotations in |
@thepudds, note that @rsc's sketch there is substantially more powerful than what I'm proposing here. In particular, I'm not assuming that One of the examples there is:
but under this proposal that would be expressed as:
Of course, once |
Ah, noted. |
A few questions:
Trying to get a scope of what you're proposing.
|
I would generally expect so, yes: However, I think we still want both comments in general.
Sure: you could envision using it for package url
type URL struct {
[…]
// encoded path hint
RawPath string //go:fix-to .EscapedPath()
[…]
}
Agreed. It's the most magical, but also arguably the most useful. 🙂 I think it will require particular care around how we map
Correct. I could envision some ways to use example-based refactoring to convert uses of the
Sadly, no: I could imagine a package net
type Dialer {
[…]
DualStack bool //go:fix-write-to .setDualStack(_)
[…]
}
//go:forward
func (d *Dialer) setDualStack(dualStack bool) {
if !dualStack {
d.FallbackDelay = -1
}
}
Yes: package net
//go:forward
func (d *Dialer) Dial(network, address string) (Conn, error) {
return d.DialContext(context.TODO(), network, address)
}
Yep. Note that it's easy to introduce |
Is go-fix intended for changes that aren't strictly backwards compatible? |
I think that's an open question. This proposal leaves that decision up to package authors: nothing stops them from applying a |
(And do note that a |
|
Maybe, but since Note that the proposed But that's an interesting idea, and I would not object to adding a variant of
That seems like a reasonable thing for |
In general I'm working on the assumption that folks don't go around monkey-patching variables in other packages. (For example, you should never swap out I don't have a solution to that in general, but there are complementary proposals (such as #21130) that could help to detect those sorts of errors. Note that you can also use the race detector to detect these sorts of modifications: var ZR = Rectangle{}
func init() {
go func() { _ = ZR }() // Data race if anybody mutates ZR.
}() |
I would like to see this simpified down to a single comment that means "this is 100% guaranteed semantically OK, because it is literally a definitional substitution", and because the tool is For the io constants, os can do
const SEEK_SET = int(io.SeekSet) Also the //go:fix comments should not be in the doc comment; they should be above them instead. You want to be able to tell people "it is always OK to run go fix". A suggestion mechanism is fine as part of Deprecated comments but we should consider that in a separate proposal and separate tool. |
Is there any worry about name squatting on |
@cep21, did you have anything else in mind for it?
Yes.
The intent of this proposal is specifically to mark that the mapping is 1:1, and furthermore that the mapping is straightforward enough that a human doesn't need to think about how to apply it. Without such an annotation, I don't think it's feasible to build a tool that can decide when a |
Can you give an example that fits with rcs's simplified proposal? He mentions
In those cases, I can't see how it wouldn't be expected and safe for a |
More concretely, if the proposal is limited to just type aliases (as an example), in which situation would I not want to fix this? // deprecated: blah blah
type Circuit = newpackage.Circuit |
Are there any surprising consequences of the following proposal:
|
@cep21, the interesting use-cases for |
Note that one of the examples — |
That's not allowed by rcs's simplification since it's not guaranteed to be backwards compatible: It's possible to reassign var types. I may be misunderstanding what "definitional substitution" and "suggestion mechanism" means. Can those terms be defined? Either way, it's not a big concern and happy this issue is getting noticed! |
@cep21, I've been thinking some more about the interaction with deprecation. From discussions with @ianthehat, it seems that he would prefer something similar. At the very least, I think a heuristic based solely on // Deprecated: use T2.
type T1 = T2
// Deprecated: use T3
type T2 = T3 The arguably “correct” behavior is to inline uses of |
The interaction with literal values is interesting, though. In the declaration // Deprecated: Use a literal image.Rectangle{} instead.
var ZR = Rectangle{} it would certainly appropriate to replace the value with a literal. However, sometimes a value with an existing // Deprecated: Use io.SeekStart, io.SeekCurrent, and io.SeekEnd.
const (
SEEK_SET int = 0 // seek relative to the origin of the file
SEEK_CUR int = 1 // seek relative to the current offset
SEEK_END int = 2 // seek relative to the end
) Moreover, the problem is even worse if the initializer is not idempotent: // Deprecated: use os.ErrTemporary directly.
var ErrFlaky = fmt.Errorf("%w: operation flaky", os.ErrTemporary) // Deprecated: use a caller-allocated slice.
var Buffer = make([]byte, 1<<10) The non-idempotent case, at least, seems easier to detect, although I'm not sure whether it's easy enough to be effective in general. |
Change https://go.dev/cl/523995 mentions this issue: |
This change creates at new (internal) package that implements an inlining algorithm for Go functions, and an analyzer in the go/analysis framework that uses it to perform automatic inlining of calls to specially annotated ("inlineme") functions. Run this command to invoke the analyzer and apply any suggested fixes to the source tree: $ go run ./internal/refactor/inline/analyzer/main.go -fix packages... The package is intended for use both in interactive tools such as gopls and batch tools such as the analyzer just mentioned and the tool proposed in the attached issue. As noted in the code comments, correct inlining is a surprisingly tricky problem, so for now we primarily address the most general case in which a call f(args...) has the function name f replaced by a literal copy of the called function (func (...) {...})(args...). Only in the simplest of special cases is the call itself eliminated by replacing it with the function body. There is much further work to do by careful analysis of cases. The processing of the callee function occurs first, and results in a serializable summary of the callee that can be used for a later call to Inline, possibly from a different process, thus enabling "separate analysis" pipelines using the analysis.Fact mechanism. Recommended reviewing order: - callee.go, inline.go - inline_test.go, testdata/*txtar - analyzer/... Updates golang/go#32816 Change-Id: If28e43a6ba9ab92639276c5b50b5a89a3b0c54c4 Reviewed-on: https://go-review.googlesource.com/c/tools/+/519715 TryBot-Result: Gopher Robot <gobot@golang.org> Run-TryBot: Alan Donovan <adonovan@google.com> Auto-Submit: Alan Donovan <adonovan@google.com> Reviewed-by: Robert Findley <rfindley@google.com>
The function inliner, which was the major technical obstacle to building a prototype of the proposed tool, is now ready for serious use. "Inlining" references to annotated constants and vars could be squeezed into the existing logic by modeling them as calls to a zero-argument function:
|
This proposal has been added to the active column of the proposals project |
This proposal has been added to the active column of the proposals project |
The basic idea here is to allow a //go:fix annotation on a definition or perhaps at some point also inside a function (for selective inlining) to mean that go fix should inline the definition at the use site. @bcmills wrote a more detailed description at #32816 (comment). I expect there will be minor adjustments as we implement it, and that's fine for the purpose of this proposal. Are there any concerns or objections with the idea, or any aspects we may be overlooking that need discussion? |
I have two questions, one about the implementation and one about annotation syntax. The desired behavior could be implemented entirely within the go/analysis framework: for each function with a //go:fix annotation, the analyzer would export a fact consisting of the inliner's summary of the callee. For every call, the analyzer would report an "inlinable call" diagnostic with an associated SuggestedFix that does the inlining. Running a singlechecker for this analyzer with -fix would apply all the edits to a set of packages. (We have an implementation already.) One advantage of this approach is that we can drop the analyzer into other analysis drivers such as gopls (for real-time feedback) and nogo (for Bazel, and Google's Go code base), and it will just work. Q1. What are the pros and cons of adding this functionality within cmd/fix? Any good reasons not to develop only the analyzer approach? The second question is the particular spelling of the annotation: this issue proposes Another possibility is to use whatever general analysis annotation syntax we end up agreeing upon (if any), though we don't even have a proposal for that yet, so it will be a while before any decision is made. I imagine it might look something like a no-op call Q2. How should we spell it? |
+1 to using the analyzer framework for this job. We use that framework internally at Google for large scale changes and it's been a very pleasant experience. Using analyzers for the fix command has the other advantage that you can extend it quite easily. To illustrate a potential future scope increase for the go fix command: One deprecation I ran across a number of times is adding a function variant that takes a context:
Inlining Another comment I have is about |
Perhaps |
I'm fine with other syntactic variations, but heuristics based on (For example, it may be that the deprecation is due to a common class of bug, and migrating from the function to the thing it wraps would perpetuate that bug instead of fixing it as intended.) |
Agreed. We (in a closed source setting) commonly mark things Deprecated to encourage switching to a completely new implementation with a different API and make no attempt to have the old code call the new code. |
Oh, for sure, but I was thinking of some syntax in addition to Deprecated like |
What I like about this idea, is that it uses the same mechanism we use for deprecation and that it's visible in godoc: It means we only need to look at one place to understand the deprecation story. Deprecation is also a good backstop to prevent accidental use. Furthermore, we always have a deprecation message that we can use for autogenerated commit messages. |
Thanks everyone. The direction seems clear enough that we can put this on hold for an implementation. |
Placed on hold. |
Change https://go.dev/cl/634919 mentions this issue: |
This CL changes the proof-of-concept "inliner" analyzer to use "//go:fix inline" as the marker of a function that should be inlined. This seems to be the syntax emerging from the proposal golang/go#32816. Also, skip inlinings that cause literalization of the callee. Users of batch tools only want to see reductions. Updates golang/go#32816 Updates golang/go#70717 Change-Id: I6d70118f69b7c35f51e1d51863a0312b5dcc3b63 Reviewed-on: https://go-review.googlesource.com/c/tools/+/634919 Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Change https://go.dev/cl/635680 mentions this issue: |
Can we narrow this proposal to the interface between source code and automated migration tools (for example, the precise syntax of the annotation comment, the rules governing where it may be used, and the expectations of tooling), and treat the embodiment of such tools (whether in cmd/fix, cmd/vet, gopls, or elsewhere) as out of scope? We are ready to start deploying an analyzer for automated inlining of annotated functions within gopls, for interactive use, and we are blocked mainly by the question of which syntax to use. |
This change replaces the body of almost every function in the package with a call to its corresponding std function of the same name and--assumedly--semantics. If proposal golang/go#32816 is accepted, we will annotate each function with a "//go:fix inline" comment so that automated tooling such as golang.org/x/tools/internal/refactor/inline/analyzer can automatically inline the wrappers. (This CL is deemed to fix golang/go#70717 because it reduces maps.Clear to the same status as all the other maps functions.) Updates golang/go#32816 Updates golang/go#70815 Fixes golang/go#70717 Change-Id: I85246df07f903af97673b80024acdcae057b9f63 Reviewed-on: https://go-review.googlesource.com/c/exp/+/635680 Auto-Submit: Alan Donovan <adonovan@google.com> Reviewed-by: Ian Lance Taylor <iant@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
(I've mentioned this in passing on #32014 and #27248, but I don't see an actual proposal filed yet — so here it is!)
Over time, the author of a package may notice certain patterns in the usage of their API that they want to make simpler or clearer for users. When that happens, they may suggest that users update their own code to use the new API.
In many cases, the process of updating is nearly trivial, so it would be nice if some tool in the standard distribution could apply those trivial updates mechanically. The
go fix
subcommand was used for similar updates to the language itself, so perhaps it could be adapted to work for at least the simple cases.The very simplest sort of update rewrites a call to some function or use of some variable or constant to instead call some other function or use some other variable, constant, or expression.
To draw some examples from the standard library:
archive/tar.TypeRegA
says to useTypeReg
instead.archive/zip.FileHeader.{Compressed,Uncompressed}Size
say to use the corresponding*64
fields instead.image.ZR
andimage.ZP
say to use zero struct literals instead.go/importer.For
says to useForCompiler
, and is implemented as a trivial call toForCompiler
with an additional argument.io.SEEK_*
constants says to use correspondingio.Seek*
constants instead.syscall.StringByte{Slice,Ptr}
say to use the correspondingByte*FromString
functions, and are implemented as short wrappers around those functions.Outside the standard library, this pattern can also occur when a package makes a breaking change: if the
v3
API can express all of the same capabilities asv2
, thenv2
can be implemented as a wrapper aroundv3
— and references to thev2
identifiers can be replaced with direct references to the underlyingv3
identifiers.See the current proposal details in #32816 (comment) below.
Previous draft (prior to #32816 (comment)):
I propose that we establish a consistent convention to mark these kinds of mechanical replacements, and improve
cmd/fix
to be able to apply them.I'm not particularly tied to any given convention, but in the spirit of having something concrete to discuss, I propose two tokens,
//go:fix-to
and//go:forward
, as follows://go:fix-to EXPR
on a constant or variable indicates thatgo fix
should replace occurrences of that constant or variable with the expressionEXPR
. Free variables in the expression are interpreted as package names, andgo fix
may rewrite them to avoid collisions with non-packages.//go:fix-to .EXPR
on struct field indicates thatgo fix
should replace references to that field with the given selector expression (which may be a field access OR a method call) on the same value. Free variables are again interpreted as package names. If the expression is a method call, it replaces only reads of the field.//go:forward
on an individual constant or variable indicates thatgo fix
should replace the constant or variable with the expression from which it is initialized. If a constant is declared with a type but initialized from an untyped expression, the replacement is wrapped in a conversion to that type.//go:forward
on aconst
orvar
block indicates that every identifier within the block should be replaced by the expression from which it is initialized.//go:forward
on a method or function indicates thatgo fix
should inline its body at the call site, renaming any local variables and labels as needed to avoid collisions. The function or method must have at most onereturn
statement and nodefer
statements.//go:forward
on a type alias indicates thatgo fix
should replace references to the alias name with the (immediate) type it aliases.All of these rewrites must be acyclic: the result of a (transitively applied) rewrite must not refer to the thing being rewritten.
Note that there is a prototype example-based refactoring tool for Go in
golang.org/x/tools/cmd/eg
that supported a broader set of refactoring rules. It might be useful as a starting point.For the above examples, the resulting declarations might look like:
This proposal does not address the
archive/tar
use-case: callers producing archives might be able to transparently migrate fromTypeRegA
toTypeReg
, but it isn't obvious that callers consuming archives can safely do so.Nor does this proposal does not address the
archive/zip.FileHeader
use-case: the deprecation there indicates a loss of precision, and the caller should probably be prompted to make related fixes in the surrounding code.Both of those cases might be addressable with a deeper example-based refactoring tool, but (I claim) are too subtle to handle directly in
go fix
.CC @marwan-at-work @thepudds @jayconrod @ianthehat @myitcv @cep21
The text was updated successfully, but these errors were encountered: