Add unitless duration lint check.
Fixes #130.
Signed-off-by: David Symonds <dsymonds@golang.org>
diff --git a/lint.go b/lint.go
index 7de99bc..11f6ed7 100644
--- a/lint.go
+++ b/lint.go
@@ -21,6 +21,7 @@
"unicode"
"unicode/utf8"
+ "golang.org/x/tools/go/exact"
"golang.org/x/tools/go/gcimporter"
"golang.org/x/tools/go/types"
)
@@ -187,6 +188,7 @@
f.lintErrorReturn()
f.lintUnexportedReturn()
f.lintTimeNames()
+ f.lintDurationUnits()
}
type link string
@@ -1396,6 +1398,70 @@
})
}
+func (f *file) lintDurationUnits() {
+ f.walk(func(node ast.Node) bool {
+ e, ok := node.(ast.Expr)
+ if !ok {
+ return true
+ }
+
+ tv := f.pkg.typesInfo.Types[e]
+ if !f.pkg.isNamedType(tv.Type, "time", "Duration") {
+ return true
+ }
+ if tv.Value == nil {
+ return true
+ }
+ // OK for 0 to be unitless; non-constants are OK.
+ if x, ok := exact.Int64Val(tv.Value); !ok || x == 0 {
+ return true
+ }
+
+ // e has a constant value. What matters now is how it was defined.
+ // Its definition might be spread across many expressions:
+ //
+ // const (
+ // a = iota
+ // b
+ // c = b * time.Second
+ // e = c * 10
+ // )
+ //
+ // Rather than attempt to reconstruct e's origins,
+ // just handle the common cases.
+ switch e.(type) {
+ default:
+ // Assume that any non-literal, non-call expression is ok.
+ // In particular, allow 3 * time.Second, which is an *ast.BinExpr.
+ // This means that we fail to flag cases like
+ // time.Duration(15) + time.Duration(30) and
+ // time.Sleep(x) where 'const x = 10'.
+ return false
+ case *ast.CallExpr:
+ // Descend into call expressions in order to catch time.Duration(15).
+ return true
+ case *ast.BasicLit:
+ // A basic literal cannot possibly use time package constants.
+ }
+
+ f.errorf(e, 0.8, category("time"), `time.Duration expression "%v" should use time package constants`, f.render(e))
+ // TODO: Generate a ReplacementLine with an appropriately sized unit.
+ // For example, 3e9 should become 3 * time.Second. The unit part is easy.
+ // The challenge is figuring out exactly what else is on the line
+ // and handling the case in which there are multiple expressions
+ // to replace on the same line.
+ // The latter opens up API questions. If there are multiple
+ // problems on the same line, should they all generate identical
+ // replacements, or one replacement per problem?
+ // The former would mean that this linter has to maintain state across
+ // expressions.
+ // Also, the test framework does not yet have a means to represent
+ // multiple matches or multiple replacements on a single line.
+
+ return true
+ })
+}
+
func receiverType(fn *ast.FuncDecl) string {
switch e := fn.Recv.List[0].Type.(type) {
case *ast.Ident:
diff --git a/testdata/time.go b/testdata/time.go
index 9271acb..44cda9d 100644
--- a/testdata/time.go
+++ b/testdata/time.go
@@ -11,3 +11,32 @@
var rpcTimeoutMsec = flag.Duration("rpc_timeout", 100*time.Millisecond, "some flag") // MATCH /Msec.*\*time.Duration/
var timeoutSecs = 5 * time.Second // MATCH /Secs.*time.Duration/
+
+var delay = flag.Duration("delay", 5, "wait impatiently") // MATCH /time.*expression.*"5".*constants/
+
+func f(...time.Duration) {}
+
+var (
+ _ = time.Duration(15) // MATCH /time.*expression.*"15".*constants/
+ _ = time.Duration(15) + time.Duration(30) // Not caught, but rare in real code; see comments in lintDurationUnits
+ _ = []time.Duration{
+ 1, // MATCH /time.*expression.*"1".*constants/
+ 10, // MATCH /time.*expression.*"10".*constants/
+ 1 * time.Nanosecond,
+ 10 * 10 * 10 * 10,
+ time.Hour,
+ 0, // 0 is OK
+ }
+ abc = f(17) // MATCH /time.*expression.*"17".*constants/
+)
+
+const x = 10
+
+func g() {
+ time.Sleep(
+ 12, // MATCH /time.*expression.*"12".*constants/
+ )
+ time.Sleep(x) // Not caught; see comments in lintDurationUnits
+ _ = f(1, // MATCH /time.*expression.*"1".*constants/
+ 1e12) // MATCH /time.*expression.*"1e12".*constants/
+}