internal/refactor/inline: check caller/callee file Go versions
Inlining a function body declared in a file using a newer Go
release into a caller using an older Go release is liable to
introducing build errors. (This is particularly a risk for
go:fix inline directives introduced by modernizers for newer
language features.)
Ideally the type checker would record, for each piece of syntax,
what minimum Go version it requires, so that we could compute
the dependencies precisely for a given callee function body.
In the meantime, we use the version of the callee file as a whole,
which is quite conservative, and check that the caller file's
version is not older.
Updates golang/go#75726
+ test
diff --git a/internal/refactor/inline/callee.go b/internal/refactor/inline/callee.go
index b46340c..ab2979d 100644
--- a/internal/refactor/inline/callee.go
+++ b/internal/refactor/inline/callee.go
@@ -36,6 +36,7 @@
// results of type analysis (does not reach go/types data structures)
PkgPath string // package path of declaring package
Name string // user-friendly name for error messages
+ GoVersion string // version of Go effective in callee file
Unexported []string // names of free objects that are unexported
FreeRefs []freeRef // locations of references to free objects
FreeObjs []object // descriptions of free objects
@@ -114,6 +115,24 @@
return nil, fmt.Errorf("cannot inline function %s as it has no body", name)
}
+ // Record the file's Go goVersion so that we don't
+ // inline newer code into file using an older dialect.
+ //
+ // Using the file version is overly conservative.
+ // A more precise solution would be for the type checker to
+ // record which language features the callee actually needs;
+ // see https://go.dev/issue/75726.
+ //
+ // We don't have the ast.File handy, so instead of a
+ // lookup we must scan the entire FileVersions map.
+ var goVersion string
+ for file, v := range info.FileVersions {
+ if file.Pos() < decl.Pos() && decl.Pos() < file.End() {
+ goVersion = v
+ break
+ }
+ }
+
// Record the location of all free references in the FuncDecl.
// (Parameters are not free by this definition.)
var (
@@ -342,6 +361,7 @@
Content: content,
PkgPath: pkg.Path(),
Name: name,
+ GoVersion: goVersion,
Unexported: unexported,
FreeObjs: freeObjs,
FreeRefs: freeRefs,
diff --git a/internal/refactor/inline/calleefx_test.go b/internal/refactor/inline/calleefx_test.go
index b643c7a..55a869c 100644
--- a/internal/refactor/inline/calleefx_test.go
+++ b/internal/refactor/inline/calleefx_test.go
@@ -132,12 +132,13 @@
}
info := &types.Info{
- Defs: make(map[*ast.Ident]types.Object),
- Uses: make(map[*ast.Ident]types.Object),
- Types: make(map[ast.Expr]types.TypeAndValue),
- Implicits: make(map[ast.Node]types.Object),
- Selections: make(map[*ast.SelectorExpr]*types.Selection),
- Scopes: make(map[ast.Node]*types.Scope),
+ Defs: make(map[*ast.Ident]types.Object),
+ Uses: make(map[*ast.Ident]types.Object),
+ Types: make(map[ast.Expr]types.TypeAndValue),
+ Implicits: make(map[ast.Node]types.Object),
+ Selections: make(map[*ast.SelectorExpr]*types.Selection),
+ Scopes: make(map[ast.Node]*types.Scope),
+ FileVersions: make(map[*ast.File]string),
}
conf := &types.Config{Error: func(err error) { t.Error(err) }}
pkg, err := conf.Check("p", fset, []*ast.File{calleeFile}, info)
diff --git a/internal/refactor/inline/inline.go b/internal/refactor/inline/inline.go
index 2443504..6d2f6b4 100644
--- a/internal/refactor/inline/inline.go
+++ b/internal/refactor/inline/inline.go
@@ -27,6 +27,7 @@
"golang.org/x/tools/internal/packagepath"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
+ "golang.org/x/tools/internal/versions"
)
// A Caller describes the function call and its enclosing context.
@@ -677,6 +678,13 @@
callee.Name, callee.Unexported[0])
}
+ // Reject cross-file inlining if callee requires a newer dialect of Go (#75726).
+ callerGoVersion := caller.Info.FileVersions[caller.File]
+ if versions.Before(callerGoVersion, callee.GoVersion) {
+ return nil, fmt.Errorf("cannot inline call to %s (declared using %s) into a file using %s",
+ callee.Name, callee.GoVersion, callerGoVersion)
+ }
+
// -- analyze callee's free references in caller context --
// Compute syntax path enclosing Call, innermost first (Path[0]=Call),
diff --git a/internal/refactor/inline/inline_test.go b/internal/refactor/inline/inline_test.go
index 93dd262..6cafd69 100644
--- a/internal/refactor/inline/inline_test.go
+++ b/internal/refactor/inline/inline_test.go
@@ -384,6 +384,12 @@
`var _ = G[int]{}.f(0)`,
`error: generic methods not yet supported`,
},
+ {
+ "Can't inline a callee using newer Go to a caller using older Go (#75726).",
+ "//go:build go1.23\n\npackage p\nfunc f() int { return 0 }",
+ "//go:build go1.22\n\npackage p\nvar _ = f()",
+ `error: cannot inline call to p.f \(declared using go1.23\) into a file using go1.22`,
+ },
})
}
@@ -1807,6 +1813,14 @@
}
func runTests(t *testing.T, tests []testcase) {
+ addPackageClause := func(src string) string {
+ // Most test cases omit it for brevity.
+ if !strings.Contains(src, "package p") {
+ src = "package p\n" + src
+ }
+ return src
+ }
+
for _, test := range tests {
t.Run(test.descr, func(t *testing.T) {
fset := token.NewFileSet()
@@ -1819,7 +1833,7 @@
}
// Parse callee file and find first func decl named f.
- calleeContent := "package p\n" + test.callee
+ calleeContent := addPackageClause(test.callee)
calleeFile := mustParse("callee.go", calleeContent)
var decl *ast.FuncDecl
for _, d := range calleeFile.Decls {
@@ -1833,7 +1847,7 @@
}
// Parse caller file and find first call to f().
- callerContent := "package p\n" + test.caller
+ callerContent := addPackageClause(test.caller)
callerFile := mustParse("caller.go", callerContent)
var call *ast.CallExpr
ast.Inspect(callerFile, func(n ast.Node) bool {
@@ -1865,12 +1879,13 @@
// Type check both files as one package.
info := &types.Info{
- Defs: make(map[*ast.Ident]types.Object),
- Uses: make(map[*ast.Ident]types.Object),
- Types: make(map[ast.Expr]types.TypeAndValue),
- Implicits: make(map[ast.Node]types.Object),
- Selections: make(map[*ast.SelectorExpr]*types.Selection),
- Scopes: make(map[ast.Node]*types.Scope),
+ Defs: make(map[*ast.Ident]types.Object),
+ Uses: make(map[*ast.Ident]types.Object),
+ Types: make(map[ast.Expr]types.TypeAndValue),
+ Implicits: make(map[ast.Node]types.Object),
+ Selections: make(map[*ast.SelectorExpr]*types.Selection),
+ Scopes: make(map[ast.Node]*types.Scope),
+ FileVersions: make(map[*ast.File]string),
}
conf := &types.Config{Error: func(err error) { t.Error(err) }}
pkg, err := conf.Check("p", fset, []*ast.File{callerFile, calleeFile}, info)
diff --git a/internal/refactor/inline/util.go b/internal/refactor/inline/util.go
index 205e5b6..5f895cc 100644
--- a/internal/refactor/inline/util.go
+++ b/internal/refactor/inline/util.go
@@ -97,6 +97,7 @@
assert(info.Selections != nil, "types.Info.Selections is nil")
assert(info.Types != nil, "types.Info.Types is nil")
assert(info.Uses != nil, "types.Info.Uses is nil")
+ assert(info.FileVersions != nil, "types.Info.FileVersions is nil")
}
// intersects reports whether the maps' key sets intersect.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Help me understand the consequences of this.
This will usually be OK because if my dependency is in another module, my current module must have go version at least that of its dependency? So the only functions that would not be inlinable are those that have a file build constraint?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Help me understand the consequences of this.
This will usually be OK because if my dependency is in another module, my current module must have go version at least that of its dependency? So the only functions that would not be inlinable are those that have a file build constraint?
The go command will allow a lower-Go-version module to depend on a higher-versioned one, and so this check could deny users the ability to inline, but such a configuration is untidy and the next go mod tidy operation will update the lower module to the minimum of its dependencies; so I don't expect this denial to be very common.
I added an integration test of this scenario in gopls; there are no apparent problems from the untidiness, and the check fires as it should.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
if versions.Before(callerGoVersion, callee.GoVersion) {Sorry, I cannot reason about the handling for "", and even if I figured out that it was OK (which, after 1.2 minutes of reading I'm still unclear on), I'd forget again in the future.
Can we just change this to:
```
if callerGoVersion != "" && callee.GoVersion != "" && versions.Before(callerGoVersion, calleeGoVersion) {
```
So that it's obvious that it fails open?
if !strings.Contains(src, "package p") {
src = "package p\n" + src
}| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
if versions.Before(callerGoVersion, callee.GoVersion) {Sorry, I cannot reason about the handling for "", and even if I figured out that it was OK (which, after 1.2 minutes of reading I'm still unclear on), I'd forget again in the future.
Can we just change this to:
```
if callerGoVersion != "" && callee.GoVersion != "" && versions.Before(callerGoVersion, calleeGoVersion) {
```So that it's obvious that it fails open?
Yeah, that's fair. It should always be populated in a production analysis driver, but many tests forget to set it. Done.
if !strings.Contains(src, "package p") {
src = "package p\n" + src
}Yuck. Do, or do not. There is no try.
Not sure what you are suggesting. Surely not making hundreds of test cases uglier just because one of them needs to put a comment before the package declaration?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Code-Review | +2 |
if !strings.Contains(src, "package p") {
src = "package p\n" + src
}Alan DonovanYuck. Do, or do not. There is no try.
Not sure what you are suggesting. Surely not making hundreds of test cases uglier just because one of them needs to put a comment before the package declaration?
Sorry for being flippant 😊
No, I wasn't suggesting that: just than this type of convenient action at a distance should be avoided.
Alternatives:
1. `func runTests(t *testing.T, tests []testcase, opts *testOpts)` (and have a field to control the prepending of package clause).
2. `func testOne(t *testing.T, caller, callee string, want)`, where caller and callee are full source, and call this from a new test function that passes the full source.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Auto-Submit | +1 |
| Commit-Queue | +1 |
if !strings.Contains(src, "package p") {
src = "package p\n" + src
}Alan DonovanYuck. Do, or do not. There is no try.
Robert FindleyNot sure what you are suggesting. Surely not making hundreds of test cases uglier just because one of them needs to put a comment before the package declaration?
Sorry for being flippant 😊
No, I wasn't suggesting that: just than this type of convenient action at a distance should be avoided.
Alternatives:
1. `func runTests(t *testing.T, tests []testcase, opts *testOpts)` (and have a field to control the prepending of package clause).
2. `func testOne(t *testing.T, caller, callee string, want)`, where caller and callee are full source, and call this from a new test function that passes the full source.
OK, I've used the options mechanism of test.descr.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
3 is the latest approved patch-set.
The change was submitted with unreviewed changes in the following files:
```
The name of the file: internal/refactor/inline/inline_test.go
Insertions: 9, Deletions: 10.
@@ -365,7 +365,7 @@
// strategy with the checklist of concerns enumerated in the package
// doc comment.
type testcase struct {
- descr string // description; substrings enable options (e.g. "IgnoreEffects")
+ descr string // description; substrings enable test or inliner options (e.g. "IgnoreEffects", "NoPackageClause")
callee, caller string // Go source files (sans package decl) of caller, callee
want string // expected new portion of caller file, or "error: regexp"
}
@@ -385,7 +385,7 @@
`error: generic methods not yet supported`,
},
{
- "Can't inline a callee using newer Go to a caller using older Go (#75726).",
+ "[NoPackageClause] Can't inline a callee using newer Go to a caller using older Go (#75726).",
"//go:build go1.23\n\npackage p\nfunc f() int { return 0 }",
"//go:build go1.22\n\npackage p\nvar _ = f()",
`error: cannot inline call to p.f \(declared using go1.23\) into a file using go1.22`,
@@ -1813,14 +1813,6 @@
}
func runTests(t *testing.T, tests []testcase) {
- addPackageClause := func(src string) string {
- // Most test cases omit it for brevity.
- if !strings.Contains(src, "package p") {
- src = "package p\n" + src
- }
- return src
- }
-
for _, test := range tests {
t.Run(test.descr, func(t *testing.T) {
fset := token.NewFileSet()
@@ -1832,6 +1824,13 @@
return f
}
+ addPackageClause := func(src string) string {
+ if !strings.Contains(test.descr, "NoPackageClause") {
+ src = "package p\n" + src
+ }
+ return src
+ }
+
// Parse callee file and find first func decl named f.
calleeContent := addPackageClause(test.callee)
calleeFile := mustParse("callee.go", calleeContent)
```
internal/refactor/inline: check caller/callee file Go versions
Inlining a function body declared in a file using a newer Go
release into a caller using an older Go release is liable to
introducing build errors. (This is particularly a risk for
go:fix inline directives introduced by modernizers for newer
language features.)
Ideally the type checker would record, for each piece of syntax,
what minimum Go version it requires, so that we could compute
the dependencies precisely for a given callee function body.
In the meantime, we use the version of the callee file as a whole,
which is quite conservative, and check that the caller file's
version is not older.
Updates golang/go#75726
+ unit test, gopls integration test
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |