├── .gitignore ├── LICENSE ├── README.md ├── cmd └── durationlint │ └── main.go ├── go.mod ├── go.sum ├── pkg └── analyzer │ ├── analyzer.go │ └── analyzer_test.go └── testdata └── src └── p1 └── test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | ./durationlint 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Gabriele Viglianisi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DurationLint 2 | 3 | **NOTE**: this check is currently a work in progress, and needs some more testing. Issues and suggestions are welcome. 4 | 5 | DurationLint disallows usage of untyped literals and constants as time.Duration. 6 | 7 | It prevents the accidental uses of small constants as time.Duration values that result in unintended time.Durations of a few nanoseconds. As an example: 8 | 9 | ```go 10 | package main 11 | import "net/http" 12 | import "time" 13 | 14 | func example() { 15 | // Programmer here mistakenly defined a constant as 5, instead of 5 * time.Seconds 16 | const timeout = 5 17 | 18 | // When using a literal, or an untyped constant like the one above, in a 19 | // place where a time.Duration is expected, the value gets automatically 20 | // promoted, resulting in a 5 nanosecond timeout being used. 21 | // All the following uses are flagged by the check: 22 | _ = http.Client { Timeout: timeout } 23 | var duration time.Duration = timeout 24 | time.Sleep(timeout) 25 | time.Sleep(5) 26 | 27 | // Values typed to time.Duration are allowed, so you can explicitly cast to 28 | // time.Duration to silence the check. 29 | // The following uses are not flagged: 30 | time.Sleep(time.Duration(5)) 31 | time.Sleep(5 * time.Second) 32 | const correctTimeout = 5 * time.Second 33 | time.Sleep(correctTimeout) 34 | } 35 | ``` 36 | 37 | see `./testdata/src/p1/test.go` for more 38 | 39 | ## Known issues 40 | 41 | - To ensure that casting to time.Duration is always possible while allowing the `time` package to be aliased, the check currently ignores calls to functions named `Duration` regardless of their package. 42 | 43 | ## Usage 44 | 45 | ```bash 46 | go install github.com/vigliag/durationlint/cmd/durationlint@latest 47 | cd yourcode 48 | durationlint ./... 49 | ``` 50 | 51 | ## Thanks 52 | 53 | This tool was really easy to write, thanks to the excellent go/analysis package and the following amazing guides: 54 | 55 | - https://disaev.me/p/writing-useful-go-analysis-linter/ 56 | - https://arslan.io/2019/06/13/using-go-analysis-to-write-a-custom-linter/ 57 | 58 | Previous efforts that I could find (prior to the introduction of go/analysis, as far as I can tell) are at: 59 | 60 | - https://github.com/golang/lint/issues/130 61 | - https://github.com/dominikh/go-staticcheck/issues/1 62 | 63 | Also related: 64 | 65 | - https://github.com/charithe/durationcheck 66 | 67 | -------------------------------------------------------------------------------- /cmd/durationlint/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/vigliag/durationlint/pkg/analyzer" 5 | "golang.org/x/tools/go/analysis/singlechecker" 6 | ) 7 | 8 | func main() { 9 | singlechecker.Main(analyzer.Analyzer) 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vigliag/durationlint 2 | 3 | go 1.18 4 | 5 | require golang.org/x/tools v0.1.10 6 | 7 | require ( 8 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect 9 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect 10 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= 2 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= 3 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= 4 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 5 | golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= 6 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 7 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 8 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 9 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | "go/types" 7 | "golang.org/x/tools/go/analysis" 8 | ) 9 | 10 | var Analyzer = &analysis.Analyzer{ 11 | Name: "durationlint", 12 | Doc: "disallows usage of untyped literals and constants as time.Duration", 13 | Run: run, 14 | } 15 | 16 | func run(pass *analysis.Pass) (interface{}, error) { 17 | for _, file := range pass.Files { 18 | // NOTE: as struct and function call expressions can be nested in any 19 | // other assignment and call expressions, we want to always return true 20 | // to continue descending the tree 21 | 22 | ast.Inspect(file, func(node ast.Node) bool { 23 | switch v := node.(type) { 24 | case *ast.KeyValueExpr: 25 | diag := checkAssignment(pass, v.Key, v.Value) 26 | if diag != nil { 27 | pass.Report(*diag) 28 | } 29 | return true 30 | 31 | case *ast.AssignStmt: 32 | for i := range v.Lhs { 33 | diag := checkAssignment(pass, v.Lhs[i], v.Rhs[i]) 34 | if diag != nil { 35 | pass.Report(*diag) 36 | } 37 | } 38 | return true 39 | 40 | case *ast.ValueSpec: 41 | if v.Type == nil { 42 | return true 43 | } 44 | for _, value := range v.Values { 45 | diag := checkAssignment(pass, v.Type, value) 46 | if diag != nil { 47 | pass.Report(*diag) 48 | } 49 | } 50 | return true 51 | 52 | case *ast.CallExpr: 53 | if shouldExcludeCall(v) { 54 | return false 55 | } 56 | for _, arg := range v.Args { 57 | diag := checkArgument(pass, arg) 58 | if diag != nil { 59 | pass.Report(*diag) 60 | } 61 | } 62 | return true 63 | 64 | default: 65 | return true 66 | } 67 | }) 68 | } 69 | return nil, nil 70 | } 71 | 72 | func checkArgument(pass *analysis.Pass, v ast.Expr) *analysis.Diagnostic { 73 | if pass.TypesInfo.TypeOf(v).String() != "time.Duration" { 74 | return nil 75 | } 76 | if !usesUntypedConstants(pass.TypesInfo, v) { 77 | return nil 78 | } 79 | return &analysis.Diagnostic{ 80 | Pos: v.Pos(), 81 | Message: "untyped constant in time.Duration argument", 82 | } 83 | } 84 | 85 | func checkAssignment(pass *analysis.Pass, l ast.Expr, r ast.Expr) *analysis.Diagnostic { 86 | lType := pass.TypesInfo.TypeOf(l) 87 | if lType == nil || lType.String() != "time.Duration" { 88 | return nil 89 | } 90 | if !usesUntypedConstants(pass.TypesInfo, r) { 91 | return nil 92 | } 93 | return &analysis.Diagnostic{Pos: r.Pos(), Message: "untyped constant in time.Duration assignment"} 94 | } 95 | 96 | func usesUntypedConstants(ti *types.Info, e ast.Expr) bool { 97 | switch v := e.(type) { 98 | case *ast.BasicLit: // ex: 1 99 | return v.Value != "0" 100 | case *ast.BinaryExpr: 101 | switch v.Op { 102 | case token.ADD: // ex: 1 + time.Seconds 103 | return usesUntypedConstants(ti, v.X) || usesUntypedConstants(ti, v.Y) 104 | case token.MUL: // ex: 1 * time.Seconds 105 | return usesUntypedConstants(ti, v.X) && usesUntypedConstants(ti, v.Y) 106 | } 107 | case *ast.Ident: // ex: someIdentifier 108 | return hasUntypedConstDeclaration(ti, v) 109 | } 110 | return false 111 | } 112 | 113 | // we only care about untyped `const Name = 123` declarations 114 | // `var Name = 123`, and `a := 123` declarations are already type-checked 115 | // by the compiler 116 | func hasUntypedConstDeclaration(ti *types.Info, identifier *ast.Ident) bool { 117 | decl := identifier.Obj.Decl 118 | 119 | // TODO: we could ignore `var` statements altogether 120 | // Is there a way to distinguish them? 121 | vSpec, ok := decl.(*ast.ValueSpec) // `var` or `const` declaration 122 | if !ok { 123 | return false 124 | } 125 | 126 | // typed const or var declaration, 127 | // we can ignore it as it is already type-checked 128 | if vSpec.Type != nil { 129 | return false 130 | } 131 | 132 | // if it's a multiple declaration (eg. `const a, b = 10, time.Second`) 133 | // we need to find the correct identifier 134 | nameIdx := -1 135 | for i, name := range vSpec.Names { 136 | if name.Name == identifier.Name { 137 | nameIdx = i 138 | break 139 | } 140 | } 141 | 142 | if nameIdx == -1 { 143 | panic("logic error: identifier not found in its declaration") 144 | } 145 | 146 | // skip if the right-hand side is explicitly typed to time.Duration 147 | vType := ti.TypeOf(vSpec.Values[nameIdx]) 148 | if vType.String() != "time.Duration" { 149 | return true 150 | } 151 | 152 | return false 153 | } 154 | 155 | // shouldExcludeCall prevents `time.Duration(10)` from being reported 156 | func shouldExcludeCall(v *ast.CallExpr) bool { 157 | se, ok := v.Fun.(*ast.SelectorExpr) 158 | if !ok { 159 | return false 160 | } 161 | 162 | // NOTE: we don't check the package name in the selector expression, as it 163 | // could have been aliased to something else 164 | return se.Sel.Name == "Duration" 165 | } 166 | -------------------------------------------------------------------------------- /pkg/analyzer/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "golang.org/x/tools/go/analysis/analysistest" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | ) 9 | 10 | func TestAll(t *testing.T) { 11 | wd, err := os.Getwd() 12 | if err != nil { 13 | t.Fatalf("Failed to get wd: %s", err) 14 | } 15 | 16 | testdata := filepath.Join(filepath.Dir(filepath.Dir(wd)), "testdata") 17 | analysistest.Run(t, testdata, Analyzer, "p1") 18 | } 19 | -------------------------------------------------------------------------------- /testdata/src/p1/test.go: -------------------------------------------------------------------------------- 1 | package p1 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | timeAliased "time" 7 | ) 8 | 9 | const untypedConst = 30 10 | const typedConst time.Duration = time.Second 11 | const ignored1, typedConst2 = 10, time.Duration(2) 12 | const ignored2, untypedConst2 = 10, 10 13 | 14 | const typedConstImplicit = time.Second 15 | const derivedConst = typedConst 16 | const derivedUntypedConst = untypedConst 17 | const intTypedConst int = 10 18 | 19 | type TestStruct struct { 20 | DurationField time.Duration 21 | } 22 | 23 | func useTestStruct(testStruct TestStruct) { 24 | } 25 | 26 | func TestFunction() { 27 | var a time.Duration 28 | 29 | // non suspicious 30 | a = 10 * time.Second 31 | a = time.Second 32 | a = time.Duration(10) 33 | a = timeAliased.Duration(10) 34 | a = typedConst 35 | a = typedConstImplicit 36 | a = derivedConst 37 | a = 0 38 | a = typedConst2 39 | _ = 1 40 | 41 | // suspicious 42 | a = 10 // want `untyped constant in time.Duration assignment` 43 | a = untypedConst // want `untyped constant in time.Duration assignment` 44 | a = untypedConst2 // want `untyped constant in time.Duration assignment` 45 | a = derivedUntypedConst // want `untyped constant in time.Duration assignment` 46 | a = 10 + time.Second // want `untyped constant in time.Duration assignment` 47 | b := TestStruct{DurationField: 20} // want `untyped constant in time.Duration assignment` 48 | useTestStruct(TestStruct{DurationField: 20}) // want `untyped constant in time.Duration assignment` 49 | var c, d time.Duration = time.Second, 10 // want `untyped constant in time.Duration assignment` 50 | const e time.Duration = 10 // want `untyped constant in time.Duration assignment` 51 | 52 | _ = a 53 | _ = b 54 | _ = c 55 | _ = d 56 | } 57 | 58 | func TestHttpTimeout() { 59 | const timeout = 5 60 | _ = http.Client{ 61 | Timeout: timeout, // want `untyped constant in time.Duration assignment` 62 | } 63 | _ = http.Client{ 64 | Timeout: 10, // want `untyped constant in time.Duration assignment` 65 | } 66 | } 67 | 68 | func TestTicker() { 69 | t := time.NewTicker(10) // want `untyped constant in time.Duration argument` 70 | t.Stop() 71 | } 72 | 73 | func TestSleep() { 74 | const timeout = 5 75 | const typedTimeout = 5 * time.Nanosecond 76 | 77 | time.Sleep(10) // want `untyped constant in time.Duration argument` 78 | time.Sleep(timeout) // want `untyped constant in time.Duration argument` 79 | time.Sleep(typedTimeout) 80 | time.Sleep(5 * time.Nanosecond) 81 | } 82 | --------------------------------------------------------------------------------