Skip to content

Commit e6ae4e8

Browse files
QuasarSEJay Conrod
authored and
Jay Conrod
committed
cmd/go/internal/generate: stop premature variable substitution in commands
go:generate commands passed no arguments are currently subject to premature variable substitution due to mistakenly assuming append guarantees a copy. The change fixes this by forcing a slice copy at each invocation of a command. The previous code assumed that append would always generate a copy of its inputs. However, append wouldn't create a copy if there was no need to increase capacity and it would just return the original input slice. This resulted in premature variable substitutions in the "master word list" of generate commands, thus yielding incorrect results across multiple invocations of the same command when the body contained substitutions e.g. environment variables, moreover these can change during the lifetime of go:generate processing a file. Note that this behavior would not manifest itself if any arguments were passed to the command, because append would make a copy of the slice as it needed to increase its capacity. The "hacky" work-around was to always pass at least one argument to any command, even if the command ignores it. e.g., //go:generate MyNoArgsCmd ' ' This CL fixes that issue and removes the need for the hack mentioned above. Fixes #31608 Change-Id: I782ac2234bd7035a37f61c101ee4aee38ed8d29f GitHub-Last-Rev: 796d343 GitHub-Pull-Request: #31527 Reviewed-on: https://go-review.googlesource.com/c/go/+/172580 Run-TryBot: Jay Conrod <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 43001a0 commit e6ae4e8

File tree

2 files changed

+204
-1
lines changed

2 files changed

+204
-1
lines changed

src/cmd/go/internal/generate/generate.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,12 @@ Words:
374374
// Substitute command if required.
375375
if len(words) > 0 && g.commands[words[0]] != nil {
376376
// Replace 0th word by command substitution.
377-
words = append(g.commands[words[0]], words[1:]...)
377+
//
378+
// Force a copy of the command definition to
379+
// ensure words doesn't end up as a reference
380+
// to the g.commands content.
381+
tmpCmdWords := append([]string(nil), (g.commands[words[0]])...)
382+
words = append(tmpCmdWords, words[1:]...)
378383
}
379384
// Substitute environment variables.
380385
for i, word := range words {

src/cmd/go/internal/generate/generate_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package generate
66

77
import (
8+
"os"
89
"reflect"
910
"runtime"
1011
"testing"
@@ -15,6 +16,15 @@ type splitTest struct {
1516
out []string
1617
}
1718

19+
// Same as above, except including source line number to set
20+
type splitTestWithLine struct {
21+
in string
22+
out []string
23+
lineNumber int
24+
}
25+
26+
const anyLineNo = 0
27+
1828
var splitTests = []splitTest{
1929
{"", nil},
2030
{"x", []string{"x"}},
@@ -54,3 +64,191 @@ func TestGenerateCommandParse(t *testing.T) {
5464
}
5565
}
5666
}
67+
68+
// These environment variables will be undefined before the splitTestWithLine tests
69+
var undefEnvList = []string{
70+
"_XYZZY_",
71+
}
72+
73+
// These environment variables will be defined before the splitTestWithLine tests
74+
var defEnvMap = map[string]string{
75+
"_PLUGH_": "SomeVal",
76+
"_X": "Y",
77+
}
78+
79+
// TestGenerateCommandShortHand - similar to TestGenerateCommandParse,
80+
// except:
81+
// 1. if the result starts with -command, record that shorthand
82+
// before moving on to the next test.
83+
// 2. If a source line number is specified, set that in the parser
84+
// before executing the test. i.e., execute the split as if it
85+
// processing that source line.
86+
func TestGenerateCommandShorthand(t *testing.T) {
87+
g := &Generator{
88+
r: nil, // Unused here.
89+
path: "/usr/ken/sys/proc.go",
90+
dir: "/usr/ken/sys",
91+
file: "proc.go",
92+
pkg: "sys",
93+
commands: make(map[string][]string),
94+
}
95+
96+
var inLine string
97+
var expected, got []string
98+
99+
g.setEnv()
100+
101+
// Set up the system environment variables
102+
for i := range undefEnvList {
103+
os.Unsetenv(undefEnvList[i])
104+
}
105+
for k := range defEnvMap {
106+
os.Setenv(k, defEnvMap[k])
107+
}
108+
109+
// simple command from environment variable
110+
inLine = "//go:generate -command CMD0 \"ab${_X}cd\""
111+
expected = []string{"-command", "CMD0", "abYcd"}
112+
got = g.split(inLine + "\n")
113+
114+
if !reflect.DeepEqual(got, expected) {
115+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
116+
}
117+
118+
// try again, with an extra level of indirection (should leave variable in command)
119+
inLine = "//go:generate -command CMD0 \"ab${DOLLAR}{_X}cd\""
120+
expected = []string{"-command", "CMD0", "ab${_X}cd"}
121+
got = g.split(inLine + "\n")
122+
123+
if !reflect.DeepEqual(got, expected) {
124+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
125+
}
126+
127+
// Now the interesting part, record that output as a command
128+
g.setShorthand(got)
129+
130+
// see that the command still substitutes correctly from env. variable
131+
inLine = "//go:generate CMD0"
132+
expected = []string{"abYcd"}
133+
got = g.split(inLine + "\n")
134+
135+
if !reflect.DeepEqual(got, expected) {
136+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
137+
}
138+
139+
// Now change the value of $X and see if the recorded definition is
140+
// still intact (vs. having the $_X already substituted out)
141+
142+
os.Setenv("_X", "Z")
143+
inLine = "//go:generate CMD0"
144+
expected = []string{"abZcd"}
145+
got = g.split(inLine + "\n")
146+
147+
if !reflect.DeepEqual(got, expected) {
148+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
149+
}
150+
151+
// What if the variable is now undefined? Should be empty substitution.
152+
153+
os.Unsetenv("_X")
154+
inLine = "//go:generate CMD0"
155+
expected = []string{"abcd"}
156+
got = g.split(inLine + "\n")
157+
158+
if !reflect.DeepEqual(got, expected) {
159+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
160+
}
161+
162+
// Try another undefined variable as an extra check
163+
os.Unsetenv("_Z")
164+
inLine = "//go:generate -command CMD1 \"ab${_Z}cd\""
165+
expected = []string{"-command", "CMD1", "abcd"}
166+
got = g.split(inLine + "\n")
167+
168+
if !reflect.DeepEqual(got, expected) {
169+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
170+
}
171+
172+
g.setShorthand(got)
173+
174+
inLine = "//go:generate CMD1"
175+
expected = []string{"abcd"}
176+
got = g.split(inLine + "\n")
177+
178+
if !reflect.DeepEqual(got, expected) {
179+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
180+
}
181+
182+
const val = "someNewValue"
183+
os.Setenv("_Z", val)
184+
185+
// try again with the properly-escaped variable.
186+
187+
inLine = "//go:generate -command CMD2 \"ab${DOLLAR}{_Z}cd\""
188+
expected = []string{"-command", "CMD2", "ab${_Z}cd"}
189+
got = g.split(inLine + "\n")
190+
191+
if !reflect.DeepEqual(got, expected) {
192+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
193+
}
194+
195+
g.setShorthand(got)
196+
197+
inLine = "//go:generate CMD2"
198+
expected = []string{"ab" + val + "cd"}
199+
got = g.split(inLine + "\n")
200+
201+
if !reflect.DeepEqual(got, expected) {
202+
t.Errorf("split(%q): got %q expected %q", inLine, got, expected)
203+
}
204+
}
205+
206+
// Command-related tests for TestGenerateCommandShortHand2
207+
// -- Note line numbers included to check substitutions from "build-in" variable - $GOLINE
208+
var splitTestsLines = []splitTestWithLine{
209+
{"-command TEST1 $GOLINE", []string{"-command", "TEST1", "22"}, 22},
210+
{"-command TEST2 ${DOLLAR}GOLINE", []string{"-command", "TEST2", "$GOLINE"}, 26},
211+
{"TEST1", []string{"22"}, 33},
212+
{"TEST2", []string{"66"}, 66},
213+
{"TEST1 ''", []string{"22", "''"}, 99},
214+
{"TEST2 ''", []string{"44", "''"}, 44},
215+
}
216+
217+
// TestGenerateCommandShortHand - similar to TestGenerateCommandParse,
218+
// except:
219+
// 1. if the result starts with -command, record that shorthand
220+
// before moving on to the next test.
221+
// 2. If a source line number is specified, set that in the parser
222+
// before executing the test. i.e., execute the split as if it
223+
// processing that source line.
224+
func TestGenerateCommandShortHand2(t *testing.T) {
225+
g := &Generator{
226+
r: nil, // Unused here.
227+
path: "/usr/ken/sys/proc.go",
228+
dir: "/usr/ken/sys",
229+
file: "proc.go",
230+
pkg: "sys",
231+
commands: make(map[string][]string),
232+
}
233+
g.setEnv()
234+
for _, test := range splitTestsLines {
235+
// if the test specified a line number, reflect that
236+
if test.lineNumber != anyLineNo {
237+
g.lineNum = test.lineNumber
238+
g.setEnv()
239+
}
240+
// First with newlines.
241+
got := g.split("//go:generate " + test.in + "\n")
242+
if !reflect.DeepEqual(got, test.out) {
243+
t.Errorf("split(%q): got %q expected %q", test.in, got, test.out)
244+
}
245+
// Then with CRLFs, thank you Windows.
246+
got = g.split("//go:generate " + test.in + "\r\n")
247+
if !reflect.DeepEqual(got, test.out) {
248+
t.Errorf("split(%q): got %q expected %q", test.in, got, test.out)
249+
}
250+
if got[0] == "-command" { // record commands
251+
g.setShorthand(got)
252+
}
253+
}
254+
}

0 commit comments

Comments
 (0)