Skip to content

Commit 0040183

Browse files
committed
flag: panic if a flag is defined after being set
As part of developing #57411, we ran into cases where a flag was defined in one package init and Set in another package init, and there was no init ordering implied by the spec between those two packages. Changes in initialization ordering as part of #57411 caused a Set to happen before the definition, which makes the Set silently fail. This CL makes the Set fail loudly in that situation. Currently Set *does* fail kinda quietly in that situation, in that it returns an error. (It seems that no one checks the error from Set, at least for string flags.) Ian suggsted that instead we panic at the definition site if there was previously a Set called on that (at the time undefined) flag. So Set on an undefined flag is ok and returns an error (as before), but defining a flag which has already been Set causes a panic. (The API for flag definition has no way to return an error, and does already panic in some situations like a duplicate definition.) Update #57411 Change-Id: I39b5a49006f9469de0b7f3fe092afe3a352e4fcb Reviewed-on: https://go-review.googlesource.com/c/go/+/480215 Run-TryBot: Keith Randall <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 1c17981 commit 0040183

File tree

2 files changed

+41
-3
lines changed

2 files changed

+41
-3
lines changed

src/flag/flag.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import (
8989
"io"
9090
"os"
9191
"reflect"
92+
"runtime"
9293
"sort"
9394
"strconv"
9495
"strings"
@@ -399,7 +400,8 @@ type FlagSet struct {
399400
formal map[string]*Flag
400401
args []string // arguments after flags
401402
errorHandling ErrorHandling
402-
output io.Writer // nil means stderr; use Output() accessor
403+
output io.Writer // nil means stderr; use Output() accessor
404+
undef map[string]string // flags which didn't exist at the time of Set
403405
}
404406

405407
// A Flag represents the state of a flag.
@@ -490,8 +492,29 @@ func Lookup(name string) *Flag {
490492

491493
// Set sets the value of the named flag.
492494
func (f *FlagSet) Set(name, value string) error {
495+
return f.set(name, value)
496+
}
497+
func (f *FlagSet) set(name, value string) error {
493498
flag, ok := f.formal[name]
494499
if !ok {
500+
// Remember that a flag that isn't defined is being set.
501+
// We return an error in this case, but in addition if
502+
// subsequently that flag is defined, we want to panic
503+
// at the definition point.
504+
// This is a problem which occurs if both the definition
505+
// and the Set call are in init code and for whatever
506+
// reason the init code changes evaluation order.
507+
// See issue 57411.
508+
_, file, line, ok := runtime.Caller(2)
509+
if !ok {
510+
file = "?"
511+
line = 0
512+
}
513+
if f.undef == nil {
514+
f.undef = map[string]string{}
515+
}
516+
f.undef[name] = fmt.Sprintf("%s:%d", file, line)
517+
495518
return fmt.Errorf("no such flag -%v", name)
496519
}
497520
err := flag.Value.Set(value)
@@ -507,7 +530,7 @@ func (f *FlagSet) Set(name, value string) error {
507530

508531
// Set sets the value of the named command-line flag.
509532
func Set(name, value string) error {
510-
return CommandLine.Set(name, value)
533+
return CommandLine.set(name, value)
511534
}
512535

513536
// isZeroValue determines whether the string represents the zero
@@ -1004,6 +1027,9 @@ func (f *FlagSet) Var(value Value, name string, usage string) {
10041027
}
10051028
panic(msg) // Happens only if flags are declared with identical names
10061029
}
1030+
if pos := f.undef[name]; pos != "" {
1031+
panic(fmt.Sprintf("flag %s set at %s before being defined", name, pos))
1032+
}
10071033
if f.formal == nil {
10081034
f.formal = make(map[string]*Flag)
10091035
}

src/flag/flag_test.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io"
1313
"os"
1414
"os/exec"
15+
"regexp"
1516
"runtime"
1617
"sort"
1718
"strconv"
@@ -726,7 +727,7 @@ func mustPanic(t *testing.T, testName string, expected string, f func()) {
726727
case nil:
727728
t.Errorf("%s\n: expected panic(%q), but did not panic", testName, expected)
728729
case string:
729-
if msg != expected {
730+
if ok, _ := regexp.MatchString(expected, msg); !ok {
730731
t.Errorf("%s\n: expected panic(%q), but got panic(%q)", testName, expected, msg)
731732
}
732733
default:
@@ -844,3 +845,14 @@ func TestUserDefinedBoolFunc(t *testing.T) {
844845
t.Errorf(`got %q; error should contain "test error"`, errMsg)
845846
}
846847
}
848+
849+
func TestDefineAfterSet(t *testing.T) {
850+
flags := NewFlagSet("test", ContinueOnError)
851+
// Set by itself doesn't panic.
852+
flags.Set("myFlag", "value")
853+
854+
// Define-after-set panics.
855+
mustPanic(t, "DefineAfterSet", "flag myFlag set at .*/flag_test.go:.* before being defined", func() {
856+
_ = flags.String("myFlag", "default", "usage")
857+
})
858+
}

0 commit comments

Comments
 (0)