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/
+}