pikeman 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +21 -0
- data/CONTRIBUTING.md +15 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +36 -0
- data/LICENSE +27 -0
- data/LICENSE.txt +21 -0
- data/Makefile +39 -0
- data/README.md +85 -0
- data/Rakefile +10 -0
- data/VERSION +1 -0
- data/bin/console +14 -0
- data/bin/pikeman +15 -0
- data/bin/setup +8 -0
- data/golint/golint.go +213 -0
- data/golint/import.go +309 -0
- data/golint/version.go +3 -0
- data/lib/pikeman.rb +34 -0
- data/lib/pikeman/version.rb +3 -0
- data/lint.go +1708 -0
- data/lint_test.go +317 -0
- data/misc/emacs/golint.el +54 -0
- data/misc/vim/ftplugin/go/lint.vim +31 -0
- data/pikeman.gemspec +28 -0
- data/testdata/4.go +38 -0
- data/testdata/5_test.go +17 -0
- data/testdata/blank-import-lib.go +39 -0
- data/testdata/blank-import-lib_test.go +25 -0
- data/testdata/blank-import-main.go +14 -0
- data/testdata/broken.go +9 -0
- data/testdata/common-methods.go +16 -0
- data/testdata/const-block.go +36 -0
- data/testdata/context.go +24 -0
- data/testdata/contextkeytypes.go +38 -0
- data/testdata/else-multi.go +18 -0
- data/testdata/else.go +23 -0
- data/testdata/error-return.go +43 -0
- data/testdata/errorf.go +40 -0
- data/testdata/errors.go +38 -0
- data/testdata/iferr.go +101 -0
- data/testdata/import-dot.go +8 -0
- data/testdata/inc.go +14 -0
- data/testdata/names.go +116 -0
- data/testdata/pkg-caps.go +4 -0
- data/testdata/pkg-doc1.go +3 -0
- data/testdata/pkg-doc2.go +5 -0
- data/testdata/pkg-doc3.go +7 -0
- data/testdata/pkg-doc4.go +7 -0
- data/testdata/pkg-doc5.go +9 -0
- data/testdata/pkg-main.go +5 -0
- data/testdata/range.go +43 -0
- data/testdata/receiver-names.go +49 -0
- data/testdata/sort.go +20 -0
- data/testdata/stutter.go +25 -0
- data/testdata/time.go +13 -0
- data/testdata/unexp-return.go +46 -0
- data/testdata/var-decl.go +86 -0
- metadata +172 -0
data/golint/import.go
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
/*
|
4
|
+
|
5
|
+
This file holds a direct copy of the import path matching code of
|
6
|
+
https://github.com/golang/go/blob/master/src/cmd/go/main.go. It can be
|
7
|
+
replaced when https://golang.org/issue/8768 is resolved.
|
8
|
+
|
9
|
+
It has been updated to follow upstream changes in a few ways.
|
10
|
+
|
11
|
+
*/
|
12
|
+
|
13
|
+
import (
|
14
|
+
"fmt"
|
15
|
+
"go/build"
|
16
|
+
"log"
|
17
|
+
"os"
|
18
|
+
"path"
|
19
|
+
"path/filepath"
|
20
|
+
"regexp"
|
21
|
+
"runtime"
|
22
|
+
"strings"
|
23
|
+
)
|
24
|
+
|
25
|
+
var (
|
26
|
+
buildContext = build.Default
|
27
|
+
goroot = filepath.Clean(runtime.GOROOT())
|
28
|
+
gorootSrc = filepath.Join(goroot, "src")
|
29
|
+
)
|
30
|
+
|
31
|
+
// importPathsNoDotExpansion returns the import paths to use for the given
|
32
|
+
// command line, but it does no ... expansion.
|
33
|
+
func importPathsNoDotExpansion(args []string) []string {
|
34
|
+
if len(args) == 0 {
|
35
|
+
return []string{"."}
|
36
|
+
}
|
37
|
+
var out []string
|
38
|
+
for _, a := range args {
|
39
|
+
// Arguments are supposed to be import paths, but
|
40
|
+
// as a courtesy to Windows developers, rewrite \ to /
|
41
|
+
// in command-line arguments. Handles .\... and so on.
|
42
|
+
if filepath.Separator == '\\' {
|
43
|
+
a = strings.Replace(a, `\`, `/`, -1)
|
44
|
+
}
|
45
|
+
|
46
|
+
// Put argument in canonical form, but preserve leading ./.
|
47
|
+
if strings.HasPrefix(a, "./") {
|
48
|
+
a = "./" + path.Clean(a)
|
49
|
+
if a == "./." {
|
50
|
+
a = "."
|
51
|
+
}
|
52
|
+
} else {
|
53
|
+
a = path.Clean(a)
|
54
|
+
}
|
55
|
+
if a == "all" || a == "std" {
|
56
|
+
out = append(out, allPackages(a)...)
|
57
|
+
continue
|
58
|
+
}
|
59
|
+
out = append(out, a)
|
60
|
+
}
|
61
|
+
return out
|
62
|
+
}
|
63
|
+
|
64
|
+
// importPaths returns the import paths to use for the given command line.
|
65
|
+
func importPaths(args []string) []string {
|
66
|
+
args = importPathsNoDotExpansion(args)
|
67
|
+
var out []string
|
68
|
+
for _, a := range args {
|
69
|
+
if strings.Contains(a, "...") {
|
70
|
+
if build.IsLocalImport(a) {
|
71
|
+
out = append(out, allPackagesInFS(a)...)
|
72
|
+
} else {
|
73
|
+
out = append(out, allPackages(a)...)
|
74
|
+
}
|
75
|
+
continue
|
76
|
+
}
|
77
|
+
out = append(out, a)
|
78
|
+
}
|
79
|
+
return out
|
80
|
+
}
|
81
|
+
|
82
|
+
// matchPattern(pattern)(name) reports whether
|
83
|
+
// name matches pattern. Pattern is a limited glob
|
84
|
+
// pattern in which '...' means 'any string' and there
|
85
|
+
// is no other special syntax.
|
86
|
+
func matchPattern(pattern string) func(name string) bool {
|
87
|
+
re := regexp.QuoteMeta(pattern)
|
88
|
+
re = strings.Replace(re, `\.\.\.`, `.*`, -1)
|
89
|
+
// Special case: foo/... matches foo too.
|
90
|
+
if strings.HasSuffix(re, `/.*`) {
|
91
|
+
re = re[:len(re)-len(`/.*`)] + `(/.*)?`
|
92
|
+
}
|
93
|
+
reg := regexp.MustCompile(`^` + re + `$`)
|
94
|
+
return func(name string) bool {
|
95
|
+
return reg.MatchString(name)
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
// hasPathPrefix reports whether the path s begins with the
|
100
|
+
// elements in prefix.
|
101
|
+
func hasPathPrefix(s, prefix string) bool {
|
102
|
+
switch {
|
103
|
+
default:
|
104
|
+
return false
|
105
|
+
case len(s) == len(prefix):
|
106
|
+
return s == prefix
|
107
|
+
case len(s) > len(prefix):
|
108
|
+
if prefix != "" && prefix[len(prefix)-1] == '/' {
|
109
|
+
return strings.HasPrefix(s, prefix)
|
110
|
+
}
|
111
|
+
return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
// treeCanMatchPattern(pattern)(name) reports whether
|
116
|
+
// name or children of name can possibly match pattern.
|
117
|
+
// Pattern is the same limited glob accepted by matchPattern.
|
118
|
+
func treeCanMatchPattern(pattern string) func(name string) bool {
|
119
|
+
wildCard := false
|
120
|
+
if i := strings.Index(pattern, "..."); i >= 0 {
|
121
|
+
wildCard = true
|
122
|
+
pattern = pattern[:i]
|
123
|
+
}
|
124
|
+
return func(name string) bool {
|
125
|
+
return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
|
126
|
+
wildCard && strings.HasPrefix(name, pattern)
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
// allPackages returns all the packages that can be found
|
131
|
+
// under the $GOPATH directories and $GOROOT matching pattern.
|
132
|
+
// The pattern is either "all" (all packages), "std" (standard packages)
|
133
|
+
// or a path including "...".
|
134
|
+
func allPackages(pattern string) []string {
|
135
|
+
pkgs := matchPackages(pattern)
|
136
|
+
if len(pkgs) == 0 {
|
137
|
+
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
138
|
+
}
|
139
|
+
return pkgs
|
140
|
+
}
|
141
|
+
|
142
|
+
func matchPackages(pattern string) []string {
|
143
|
+
match := func(string) bool { return true }
|
144
|
+
treeCanMatch := func(string) bool { return true }
|
145
|
+
if pattern != "all" && pattern != "std" {
|
146
|
+
match = matchPattern(pattern)
|
147
|
+
treeCanMatch = treeCanMatchPattern(pattern)
|
148
|
+
}
|
149
|
+
|
150
|
+
have := map[string]bool{
|
151
|
+
"builtin": true, // ignore pseudo-package that exists only for documentation
|
152
|
+
}
|
153
|
+
if !buildContext.CgoEnabled {
|
154
|
+
have["runtime/cgo"] = true // ignore during walk
|
155
|
+
}
|
156
|
+
var pkgs []string
|
157
|
+
|
158
|
+
// Commands
|
159
|
+
cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator)
|
160
|
+
filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error {
|
161
|
+
if err != nil || !fi.IsDir() || path == cmd {
|
162
|
+
return nil
|
163
|
+
}
|
164
|
+
name := path[len(cmd):]
|
165
|
+
if !treeCanMatch(name) {
|
166
|
+
return filepath.SkipDir
|
167
|
+
}
|
168
|
+
// Commands are all in cmd/, not in subdirectories.
|
169
|
+
if strings.Contains(name, string(filepath.Separator)) {
|
170
|
+
return filepath.SkipDir
|
171
|
+
}
|
172
|
+
|
173
|
+
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
|
174
|
+
name = "cmd/" + name
|
175
|
+
if have[name] {
|
176
|
+
return nil
|
177
|
+
}
|
178
|
+
have[name] = true
|
179
|
+
if !match(name) {
|
180
|
+
return nil
|
181
|
+
}
|
182
|
+
_, err = buildContext.ImportDir(path, 0)
|
183
|
+
if err != nil {
|
184
|
+
if _, noGo := err.(*build.NoGoError); !noGo {
|
185
|
+
log.Print(err)
|
186
|
+
}
|
187
|
+
return nil
|
188
|
+
}
|
189
|
+
pkgs = append(pkgs, name)
|
190
|
+
return nil
|
191
|
+
})
|
192
|
+
|
193
|
+
for _, src := range buildContext.SrcDirs() {
|
194
|
+
if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
|
195
|
+
continue
|
196
|
+
}
|
197
|
+
src = filepath.Clean(src) + string(filepath.Separator)
|
198
|
+
root := src
|
199
|
+
if pattern == "cmd" {
|
200
|
+
root += "cmd" + string(filepath.Separator)
|
201
|
+
}
|
202
|
+
filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
203
|
+
if err != nil || !fi.IsDir() || path == src {
|
204
|
+
return nil
|
205
|
+
}
|
206
|
+
|
207
|
+
// Avoid .foo, _foo, and testdata directory trees.
|
208
|
+
_, elem := filepath.Split(path)
|
209
|
+
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
|
210
|
+
return filepath.SkipDir
|
211
|
+
}
|
212
|
+
|
213
|
+
name := filepath.ToSlash(path[len(src):])
|
214
|
+
if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") {
|
215
|
+
// The name "std" is only the standard library.
|
216
|
+
// If the name is cmd, it's the root of the command tree.
|
217
|
+
return filepath.SkipDir
|
218
|
+
}
|
219
|
+
if !treeCanMatch(name) {
|
220
|
+
return filepath.SkipDir
|
221
|
+
}
|
222
|
+
if have[name] {
|
223
|
+
return nil
|
224
|
+
}
|
225
|
+
have[name] = true
|
226
|
+
if !match(name) {
|
227
|
+
return nil
|
228
|
+
}
|
229
|
+
_, err = buildContext.ImportDir(path, 0)
|
230
|
+
if err != nil {
|
231
|
+
if _, noGo := err.(*build.NoGoError); noGo {
|
232
|
+
return nil
|
233
|
+
}
|
234
|
+
}
|
235
|
+
pkgs = append(pkgs, name)
|
236
|
+
return nil
|
237
|
+
})
|
238
|
+
}
|
239
|
+
return pkgs
|
240
|
+
}
|
241
|
+
|
242
|
+
// allPackagesInFS is like allPackages but is passed a pattern
|
243
|
+
// beginning ./ or ../, meaning it should scan the tree rooted
|
244
|
+
// at the given directory. There are ... in the pattern too.
|
245
|
+
func allPackagesInFS(pattern string) []string {
|
246
|
+
pkgs := matchPackagesInFS(pattern)
|
247
|
+
if len(pkgs) == 0 {
|
248
|
+
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
249
|
+
}
|
250
|
+
return pkgs
|
251
|
+
}
|
252
|
+
|
253
|
+
func matchPackagesInFS(pattern string) []string {
|
254
|
+
// Find directory to begin the scan.
|
255
|
+
// Could be smarter but this one optimization
|
256
|
+
// is enough for now, since ... is usually at the
|
257
|
+
// end of a path.
|
258
|
+
i := strings.Index(pattern, "...")
|
259
|
+
dir, _ := path.Split(pattern[:i])
|
260
|
+
|
261
|
+
// pattern begins with ./ or ../.
|
262
|
+
// path.Clean will discard the ./ but not the ../.
|
263
|
+
// We need to preserve the ./ for pattern matching
|
264
|
+
// and in the returned import paths.
|
265
|
+
prefix := ""
|
266
|
+
if strings.HasPrefix(pattern, "./") {
|
267
|
+
prefix = "./"
|
268
|
+
}
|
269
|
+
match := matchPattern(pattern)
|
270
|
+
|
271
|
+
var pkgs []string
|
272
|
+
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
|
273
|
+
if err != nil || !fi.IsDir() {
|
274
|
+
return nil
|
275
|
+
}
|
276
|
+
if path == dir {
|
277
|
+
// filepath.Walk starts at dir and recurses. For the recursive case,
|
278
|
+
// the path is the result of filepath.Join, which calls filepath.Clean.
|
279
|
+
// The initial case is not Cleaned, though, so we do this explicitly.
|
280
|
+
//
|
281
|
+
// This converts a path like "./io/" to "io". Without this step, running
|
282
|
+
// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io
|
283
|
+
// package, because prepending the prefix "./" to the unclean path would
|
284
|
+
// result in "././io", and match("././io") returns false.
|
285
|
+
path = filepath.Clean(path)
|
286
|
+
}
|
287
|
+
|
288
|
+
// Avoid .foo, _foo, and testdata directory trees, but do not avoid "." or "..".
|
289
|
+
_, elem := filepath.Split(path)
|
290
|
+
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
|
291
|
+
if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
|
292
|
+
return filepath.SkipDir
|
293
|
+
}
|
294
|
+
|
295
|
+
name := prefix + filepath.ToSlash(path)
|
296
|
+
if !match(name) {
|
297
|
+
return nil
|
298
|
+
}
|
299
|
+
if _, err = build.ImportDir(path, 0); err != nil {
|
300
|
+
if _, noGo := err.(*build.NoGoError); !noGo {
|
301
|
+
log.Print(err)
|
302
|
+
}
|
303
|
+
return nil
|
304
|
+
}
|
305
|
+
pkgs = append(pkgs, name)
|
306
|
+
return nil
|
307
|
+
})
|
308
|
+
return pkgs
|
309
|
+
}
|
data/golint/version.go
ADDED
data/lib/pikeman.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "pikeman/version"
|
2
|
+
require "cli/kit"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Pikeman
|
6
|
+
Error = Struct.new(:filename, :line, :column, :text, :link, :confidence, :linetext, :category) do
|
7
|
+
def initialize(filename:, line:, column:, text:, link:, confidence:, linetext:, category:)
|
8
|
+
super(filename, line, column, text, link, confidence, linetext, category)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.lint_file(absolute_path, _config_absolute_path = nil)
|
13
|
+
out_and_err, _stat = CLI::Kit::System.capture2e(binary, "-format", "json", absolute_path)
|
14
|
+
out_and_err.each_line.map do |line|
|
15
|
+
data = JSON.parse(line)
|
16
|
+
Error.new(
|
17
|
+
filename: data.fetch("filename"),
|
18
|
+
line: data.fetch("line"),
|
19
|
+
column: data.fetch("column"),
|
20
|
+
text: data.fetch("text"),
|
21
|
+
link: data.fetch("link"),
|
22
|
+
confidence: data.fetch("confidence"),
|
23
|
+
linetext: data.fetch("linetext"),
|
24
|
+
category: data.fetch("category")
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def self.binary
|
32
|
+
File.join(File.dirname(__dir__), "bin", "pikeman")
|
33
|
+
end
|
34
|
+
end
|
data/lint.go
ADDED
@@ -0,0 +1,1708 @@
|
|
1
|
+
// Copyright (c) 2013 The Go Authors. All rights reserved.
|
2
|
+
//
|
3
|
+
// Use of this source code is governed by a BSD-style
|
4
|
+
// license that can be found in the LICENSE file or at
|
5
|
+
// https://developers.google.com/open-source/licenses/bsd.
|
6
|
+
|
7
|
+
// Package lint contains a linter for Go source code.
|
8
|
+
package lint
|
9
|
+
|
10
|
+
import (
|
11
|
+
"bufio"
|
12
|
+
"bytes"
|
13
|
+
"fmt"
|
14
|
+
"go/ast"
|
15
|
+
"go/parser"
|
16
|
+
"go/printer"
|
17
|
+
"go/token"
|
18
|
+
"go/types"
|
19
|
+
"regexp"
|
20
|
+
"sort"
|
21
|
+
"strconv"
|
22
|
+
"strings"
|
23
|
+
"unicode"
|
24
|
+
"unicode/utf8"
|
25
|
+
|
26
|
+
"golang.org/x/tools/go/gcexportdata"
|
27
|
+
)
|
28
|
+
|
29
|
+
const styleGuideBase = "https://golang.org/wiki/CodeReviewComments"
|
30
|
+
|
31
|
+
// A Linter lints Go source code.
|
32
|
+
type Linter struct {
|
33
|
+
}
|
34
|
+
|
35
|
+
// Problem represents a problem in some source code.
|
36
|
+
type Problem struct {
|
37
|
+
Position token.Position // position in source file
|
38
|
+
Text string // the prose that describes the problem
|
39
|
+
Link string // (optional) the link to the style guide for the problem
|
40
|
+
Confidence float64 // a value in (0,1] estimating the confidence in this problem's correctness
|
41
|
+
LineText string // the source line
|
42
|
+
Category string // a short name for the general category of the problem
|
43
|
+
|
44
|
+
// If the problem has a suggested fix (the minority case),
|
45
|
+
// ReplacementLine is a full replacement for the relevant line of the source file.
|
46
|
+
ReplacementLine string
|
47
|
+
}
|
48
|
+
|
49
|
+
func (p *Problem) String() string {
|
50
|
+
if p.Link != "" {
|
51
|
+
return p.Text + "\n\n" + p.Link
|
52
|
+
}
|
53
|
+
return p.Text
|
54
|
+
}
|
55
|
+
|
56
|
+
type byPosition []Problem
|
57
|
+
|
58
|
+
func (p byPosition) Len() int { return len(p) }
|
59
|
+
func (p byPosition) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
60
|
+
|
61
|
+
func (p byPosition) Less(i, j int) bool {
|
62
|
+
pi, pj := p[i].Position, p[j].Position
|
63
|
+
|
64
|
+
if pi.Filename != pj.Filename {
|
65
|
+
return pi.Filename < pj.Filename
|
66
|
+
}
|
67
|
+
if pi.Line != pj.Line {
|
68
|
+
return pi.Line < pj.Line
|
69
|
+
}
|
70
|
+
if pi.Column != pj.Column {
|
71
|
+
return pi.Column < pj.Column
|
72
|
+
}
|
73
|
+
|
74
|
+
return p[i].Text < p[j].Text
|
75
|
+
}
|
76
|
+
|
77
|
+
// Lint lints src.
|
78
|
+
func (l *Linter) Lint(filename string, src []byte) ([]Problem, error) {
|
79
|
+
return l.LintFiles(map[string][]byte{filename: src})
|
80
|
+
}
|
81
|
+
|
82
|
+
// LintFiles lints a set of files of a single package.
|
83
|
+
// The argument is a map of filename to source.
|
84
|
+
func (l *Linter) LintFiles(files map[string][]byte) ([]Problem, error) {
|
85
|
+
pkg := &pkg{
|
86
|
+
fset: token.NewFileSet(),
|
87
|
+
files: make(map[string]*file),
|
88
|
+
}
|
89
|
+
var pkgName string
|
90
|
+
for filename, src := range files {
|
91
|
+
if isGenerated(src) {
|
92
|
+
continue // See issue #239
|
93
|
+
}
|
94
|
+
f, err := parser.ParseFile(pkg.fset, filename, src, parser.ParseComments)
|
95
|
+
if err != nil {
|
96
|
+
return nil, err
|
97
|
+
}
|
98
|
+
if pkgName == "" {
|
99
|
+
pkgName = f.Name.Name
|
100
|
+
} else if f.Name.Name != pkgName {
|
101
|
+
return nil, fmt.Errorf("%s is in package %s, not %s", filename, f.Name.Name, pkgName)
|
102
|
+
}
|
103
|
+
pkg.files[filename] = &file{
|
104
|
+
pkg: pkg,
|
105
|
+
f: f,
|
106
|
+
fset: pkg.fset,
|
107
|
+
src: src,
|
108
|
+
filename: filename,
|
109
|
+
}
|
110
|
+
}
|
111
|
+
if len(pkg.files) == 0 {
|
112
|
+
return nil, nil
|
113
|
+
}
|
114
|
+
return pkg.lint(), nil
|
115
|
+
}
|
116
|
+
|
117
|
+
var (
|
118
|
+
genHdr = []byte("// Code generated ")
|
119
|
+
genFtr = []byte(" DO NOT EDIT.")
|
120
|
+
)
|
121
|
+
|
122
|
+
// isGenerated reports whether the source file is generated code
|
123
|
+
// according the rules from https://golang.org/s/generatedcode.
|
124
|
+
func isGenerated(src []byte) bool {
|
125
|
+
sc := bufio.NewScanner(bytes.NewReader(src))
|
126
|
+
for sc.Scan() {
|
127
|
+
b := sc.Bytes()
|
128
|
+
if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) {
|
129
|
+
return true
|
130
|
+
}
|
131
|
+
}
|
132
|
+
return false
|
133
|
+
}
|
134
|
+
|
135
|
+
// pkg represents a package being linted.
|
136
|
+
type pkg struct {
|
137
|
+
fset *token.FileSet
|
138
|
+
files map[string]*file
|
139
|
+
|
140
|
+
typesPkg *types.Package
|
141
|
+
typesInfo *types.Info
|
142
|
+
|
143
|
+
// sortable is the set of types in the package that implement sort.Interface.
|
144
|
+
sortable map[string]bool
|
145
|
+
// main is whether this is a "main" package.
|
146
|
+
main bool
|
147
|
+
|
148
|
+
problems []Problem
|
149
|
+
}
|
150
|
+
|
151
|
+
func (p *pkg) lint() []Problem {
|
152
|
+
if err := p.typeCheck(); err != nil {
|
153
|
+
/* TODO(dsymonds): Consider reporting these errors when golint operates on entire packages.
|
154
|
+
if e, ok := err.(types.Error); ok {
|
155
|
+
pos := p.fset.Position(e.Pos)
|
156
|
+
conf := 1.0
|
157
|
+
if strings.Contains(e.Msg, "can't find import: ") {
|
158
|
+
// Golint is probably being run in a context that doesn't support
|
159
|
+
// typechecking (e.g. package files aren't found), so don't warn about it.
|
160
|
+
conf = 0
|
161
|
+
}
|
162
|
+
if conf > 0 {
|
163
|
+
p.errorfAt(pos, conf, category("typechecking"), e.Msg)
|
164
|
+
}
|
165
|
+
|
166
|
+
// TODO(dsymonds): Abort if !e.Soft?
|
167
|
+
}
|
168
|
+
*/
|
169
|
+
}
|
170
|
+
|
171
|
+
p.scanSortable()
|
172
|
+
p.main = p.isMain()
|
173
|
+
|
174
|
+
for _, f := range p.files {
|
175
|
+
f.lint()
|
176
|
+
}
|
177
|
+
|
178
|
+
sort.Sort(byPosition(p.problems))
|
179
|
+
|
180
|
+
return p.problems
|
181
|
+
}
|
182
|
+
|
183
|
+
// file represents a file being linted.
|
184
|
+
type file struct {
|
185
|
+
pkg *pkg
|
186
|
+
f *ast.File
|
187
|
+
fset *token.FileSet
|
188
|
+
src []byte
|
189
|
+
filename string
|
190
|
+
}
|
191
|
+
|
192
|
+
func (f *file) isTest() bool { return strings.HasSuffix(f.filename, "_test.go") }
|
193
|
+
|
194
|
+
func (f *file) lint() {
|
195
|
+
f.lintPackageComment()
|
196
|
+
f.lintImports()
|
197
|
+
f.lintBlankImports()
|
198
|
+
f.lintExported()
|
199
|
+
f.lintNames()
|
200
|
+
f.lintVarDecls()
|
201
|
+
f.lintElses()
|
202
|
+
f.lintIfError()
|
203
|
+
f.lintRanges()
|
204
|
+
f.lintErrorf()
|
205
|
+
f.lintErrors()
|
206
|
+
f.lintErrorStrings()
|
207
|
+
f.lintReceiverNames()
|
208
|
+
f.lintIncDec()
|
209
|
+
f.lintErrorReturn()
|
210
|
+
f.lintUnexportedReturn()
|
211
|
+
f.lintTimeNames()
|
212
|
+
f.lintContextKeyTypes()
|
213
|
+
f.lintContextArgs()
|
214
|
+
}
|
215
|
+
|
216
|
+
type link string
|
217
|
+
type category string
|
218
|
+
|
219
|
+
// The variadic arguments may start with link and category types,
|
220
|
+
// and must end with a format string and any arguments.
|
221
|
+
// It returns the new Problem.
|
222
|
+
func (f *file) errorf(n ast.Node, confidence float64, args ...interface{}) *Problem {
|
223
|
+
pos := f.fset.Position(n.Pos())
|
224
|
+
if pos.Filename == "" {
|
225
|
+
pos.Filename = f.filename
|
226
|
+
}
|
227
|
+
return f.pkg.errorfAt(pos, confidence, args...)
|
228
|
+
}
|
229
|
+
|
230
|
+
func (p *pkg) errorfAt(pos token.Position, confidence float64, args ...interface{}) *Problem {
|
231
|
+
problem := Problem{
|
232
|
+
Position: pos,
|
233
|
+
Confidence: confidence,
|
234
|
+
}
|
235
|
+
if pos.Filename != "" {
|
236
|
+
// The file might not exist in our mapping if a //line directive was encountered.
|
237
|
+
if f, ok := p.files[pos.Filename]; ok {
|
238
|
+
problem.LineText = srcLine(f.src, pos)
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
argLoop:
|
243
|
+
for len(args) > 1 { // always leave at least the format string in args
|
244
|
+
switch v := args[0].(type) {
|
245
|
+
case link:
|
246
|
+
problem.Link = string(v)
|
247
|
+
case category:
|
248
|
+
problem.Category = string(v)
|
249
|
+
default:
|
250
|
+
break argLoop
|
251
|
+
}
|
252
|
+
args = args[1:]
|
253
|
+
}
|
254
|
+
|
255
|
+
problem.Text = fmt.Sprintf(args[0].(string), args[1:]...)
|
256
|
+
|
257
|
+
p.problems = append(p.problems, problem)
|
258
|
+
return &p.problems[len(p.problems)-1]
|
259
|
+
}
|
260
|
+
|
261
|
+
var newImporter = func(fset *token.FileSet) types.ImporterFrom {
|
262
|
+
return gcexportdata.NewImporter(fset, make(map[string]*types.Package))
|
263
|
+
}
|
264
|
+
|
265
|
+
func (p *pkg) typeCheck() error {
|
266
|
+
config := &types.Config{
|
267
|
+
// By setting a no-op error reporter, the type checker does as much work as possible.
|
268
|
+
Error: func(error) {},
|
269
|
+
Importer: newImporter(p.fset),
|
270
|
+
}
|
271
|
+
info := &types.Info{
|
272
|
+
Types: make(map[ast.Expr]types.TypeAndValue),
|
273
|
+
Defs: make(map[*ast.Ident]types.Object),
|
274
|
+
Uses: make(map[*ast.Ident]types.Object),
|
275
|
+
Scopes: make(map[ast.Node]*types.Scope),
|
276
|
+
}
|
277
|
+
var anyFile *file
|
278
|
+
var astFiles []*ast.File
|
279
|
+
for _, f := range p.files {
|
280
|
+
anyFile = f
|
281
|
+
astFiles = append(astFiles, f.f)
|
282
|
+
}
|
283
|
+
pkg, err := config.Check(anyFile.f.Name.Name, p.fset, astFiles, info)
|
284
|
+
// Remember the typechecking info, even if config.Check failed,
|
285
|
+
// since we will get partial information.
|
286
|
+
p.typesPkg = pkg
|
287
|
+
p.typesInfo = info
|
288
|
+
return err
|
289
|
+
}
|
290
|
+
|
291
|
+
func (p *pkg) typeOf(expr ast.Expr) types.Type {
|
292
|
+
if p.typesInfo == nil {
|
293
|
+
return nil
|
294
|
+
}
|
295
|
+
return p.typesInfo.TypeOf(expr)
|
296
|
+
}
|
297
|
+
|
298
|
+
func (p *pkg) isNamedType(typ types.Type, importPath, name string) bool {
|
299
|
+
n, ok := typ.(*types.Named)
|
300
|
+
if !ok {
|
301
|
+
return false
|
302
|
+
}
|
303
|
+
tn := n.Obj()
|
304
|
+
return tn != nil && tn.Pkg() != nil && tn.Pkg().Path() == importPath && tn.Name() == name
|
305
|
+
}
|
306
|
+
|
307
|
+
// scopeOf returns the tightest scope encompassing id.
|
308
|
+
func (p *pkg) scopeOf(id *ast.Ident) *types.Scope {
|
309
|
+
var scope *types.Scope
|
310
|
+
if obj := p.typesInfo.ObjectOf(id); obj != nil {
|
311
|
+
scope = obj.Parent()
|
312
|
+
}
|
313
|
+
if scope == p.typesPkg.Scope() {
|
314
|
+
// We were given a top-level identifier.
|
315
|
+
// Use the file-level scope instead of the package-level scope.
|
316
|
+
pos := id.Pos()
|
317
|
+
for _, f := range p.files {
|
318
|
+
if f.f.Pos() <= pos && pos < f.f.End() {
|
319
|
+
scope = p.typesInfo.Scopes[f.f]
|
320
|
+
break
|
321
|
+
}
|
322
|
+
}
|
323
|
+
}
|
324
|
+
return scope
|
325
|
+
}
|
326
|
+
|
327
|
+
func (p *pkg) scanSortable() {
|
328
|
+
p.sortable = make(map[string]bool)
|
329
|
+
|
330
|
+
// bitfield for which methods exist on each type.
|
331
|
+
const (
|
332
|
+
Len = 1 << iota
|
333
|
+
Less
|
334
|
+
Swap
|
335
|
+
)
|
336
|
+
nmap := map[string]int{"Len": Len, "Less": Less, "Swap": Swap}
|
337
|
+
has := make(map[string]int)
|
338
|
+
for _, f := range p.files {
|
339
|
+
f.walk(func(n ast.Node) bool {
|
340
|
+
fn, ok := n.(*ast.FuncDecl)
|
341
|
+
if !ok || fn.Recv == nil || len(fn.Recv.List) == 0 {
|
342
|
+
return true
|
343
|
+
}
|
344
|
+
// TODO(dsymonds): We could check the signature to be more precise.
|
345
|
+
recv := receiverType(fn)
|
346
|
+
if i, ok := nmap[fn.Name.Name]; ok {
|
347
|
+
has[recv] |= i
|
348
|
+
}
|
349
|
+
return false
|
350
|
+
})
|
351
|
+
}
|
352
|
+
for typ, ms := range has {
|
353
|
+
if ms == Len|Less|Swap {
|
354
|
+
p.sortable[typ] = true
|
355
|
+
}
|
356
|
+
}
|
357
|
+
}
|
358
|
+
|
359
|
+
func (p *pkg) isMain() bool {
|
360
|
+
for _, f := range p.files {
|
361
|
+
if f.isMain() {
|
362
|
+
return true
|
363
|
+
}
|
364
|
+
}
|
365
|
+
return false
|
366
|
+
}
|
367
|
+
|
368
|
+
func (f *file) isMain() bool {
|
369
|
+
if f.f.Name.Name == "main" {
|
370
|
+
return true
|
371
|
+
}
|
372
|
+
return false
|
373
|
+
}
|
374
|
+
|
375
|
+
// lintPackageComment checks package comments. It complains if
|
376
|
+
// there is no package comment, or if it is not of the right form.
|
377
|
+
// This has a notable false positive in that a package comment
|
378
|
+
// could rightfully appear in a different file of the same package,
|
379
|
+
// but that's not easy to fix since this linter is file-oriented.
|
380
|
+
func (f *file) lintPackageComment() {
|
381
|
+
if f.isTest() {
|
382
|
+
return
|
383
|
+
}
|
384
|
+
|
385
|
+
const ref = styleGuideBase + "#package-comments"
|
386
|
+
prefix := "Package " + f.f.Name.Name + " "
|
387
|
+
|
388
|
+
// Look for a detached package comment.
|
389
|
+
// First, scan for the last comment that occurs before the "package" keyword.
|
390
|
+
var lastCG *ast.CommentGroup
|
391
|
+
for _, cg := range f.f.Comments {
|
392
|
+
if cg.Pos() > f.f.Package {
|
393
|
+
// Gone past "package" keyword.
|
394
|
+
break
|
395
|
+
}
|
396
|
+
lastCG = cg
|
397
|
+
}
|
398
|
+
if lastCG != nil && strings.HasPrefix(lastCG.Text(), prefix) {
|
399
|
+
endPos := f.fset.Position(lastCG.End())
|
400
|
+
pkgPos := f.fset.Position(f.f.Package)
|
401
|
+
if endPos.Line+1 < pkgPos.Line {
|
402
|
+
// There isn't a great place to anchor this error;
|
403
|
+
// the start of the blank lines between the doc and the package statement
|
404
|
+
// is at least pointing at the location of the problem.
|
405
|
+
pos := token.Position{
|
406
|
+
Filename: endPos.Filename,
|
407
|
+
// Offset not set; it is non-trivial, and doesn't appear to be needed.
|
408
|
+
Line: endPos.Line + 1,
|
409
|
+
Column: 1,
|
410
|
+
}
|
411
|
+
f.pkg.errorfAt(pos, 0.9, link(ref), category("comments"), "package comment is detached; there should be no blank lines between it and the package statement")
|
412
|
+
return
|
413
|
+
}
|
414
|
+
}
|
415
|
+
|
416
|
+
if f.f.Doc == nil {
|
417
|
+
f.errorf(f.f, 0.2, link(ref), category("comments"), "should have a package comment, unless it's in another file for this package")
|
418
|
+
return
|
419
|
+
}
|
420
|
+
s := f.f.Doc.Text()
|
421
|
+
if ts := strings.TrimLeft(s, " \t"); ts != s {
|
422
|
+
f.errorf(f.f.Doc, 1, link(ref), category("comments"), "package comment should not have leading space")
|
423
|
+
s = ts
|
424
|
+
}
|
425
|
+
// Only non-main packages need to keep to this form.
|
426
|
+
if !f.pkg.main && !strings.HasPrefix(s, prefix) {
|
427
|
+
f.errorf(f.f.Doc, 1, link(ref), category("comments"), `package comment should be of the form "%s..."`, prefix)
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
// lintBlankImports complains if a non-main package has blank imports that are
|
432
|
+
// not documented.
|
433
|
+
func (f *file) lintBlankImports() {
|
434
|
+
// In package main and in tests, we don't complain about blank imports.
|
435
|
+
if f.pkg.main || f.isTest() {
|
436
|
+
return
|
437
|
+
}
|
438
|
+
|
439
|
+
// The first element of each contiguous group of blank imports should have
|
440
|
+
// an explanatory comment of some kind.
|
441
|
+
for i, imp := range f.f.Imports {
|
442
|
+
pos := f.fset.Position(imp.Pos())
|
443
|
+
|
444
|
+
if !isBlank(imp.Name) {
|
445
|
+
continue // Ignore non-blank imports.
|
446
|
+
}
|
447
|
+
if i > 0 {
|
448
|
+
prev := f.f.Imports[i-1]
|
449
|
+
prevPos := f.fset.Position(prev.Pos())
|
450
|
+
if isBlank(prev.Name) && prevPos.Line+1 == pos.Line {
|
451
|
+
continue // A subsequent blank in a group.
|
452
|
+
}
|
453
|
+
}
|
454
|
+
|
455
|
+
// This is the first blank import of a group.
|
456
|
+
if imp.Doc == nil && imp.Comment == nil {
|
457
|
+
ref := ""
|
458
|
+
f.errorf(imp, 1, link(ref), category("imports"), "a blank import should be only in a main or test package, or have a comment justifying it")
|
459
|
+
}
|
460
|
+
}
|
461
|
+
}
|
462
|
+
|
463
|
+
// lintImports examines import blocks.
|
464
|
+
func (f *file) lintImports() {
|
465
|
+
for i, is := range f.f.Imports {
|
466
|
+
_ = i
|
467
|
+
if is.Name != nil && is.Name.Name == "." && !f.isTest() {
|
468
|
+
f.errorf(is, 1, link(styleGuideBase+"#import-dot"), category("imports"), "should not use dot imports")
|
469
|
+
}
|
470
|
+
|
471
|
+
}
|
472
|
+
}
|
473
|
+
|
474
|
+
const docCommentsLink = styleGuideBase + "#doc-comments"
|
475
|
+
|
476
|
+
// lintExported examines the exported names.
|
477
|
+
// It complains if any required doc comments are missing,
|
478
|
+
// or if they are not of the right form. The exact rules are in
|
479
|
+
// lintFuncDoc, lintTypeDoc and lintValueSpecDoc; this function
|
480
|
+
// also tracks the GenDecl structure being traversed to permit
|
481
|
+
// doc comments for constants to be on top of the const block.
|
482
|
+
// It also complains if the names stutter when combined with
|
483
|
+
// the package name.
|
484
|
+
func (f *file) lintExported() {
|
485
|
+
if f.isTest() {
|
486
|
+
return
|
487
|
+
}
|
488
|
+
|
489
|
+
var lastGen *ast.GenDecl // last GenDecl entered.
|
490
|
+
|
491
|
+
// Set of GenDecls that have already had missing comments flagged.
|
492
|
+
genDeclMissingComments := make(map[*ast.GenDecl]bool)
|
493
|
+
|
494
|
+
f.walk(func(node ast.Node) bool {
|
495
|
+
switch v := node.(type) {
|
496
|
+
case *ast.GenDecl:
|
497
|
+
if v.Tok == token.IMPORT {
|
498
|
+
return false
|
499
|
+
}
|
500
|
+
// token.CONST, token.TYPE or token.VAR
|
501
|
+
lastGen = v
|
502
|
+
return true
|
503
|
+
case *ast.FuncDecl:
|
504
|
+
f.lintFuncDoc(v)
|
505
|
+
if v.Recv == nil {
|
506
|
+
// Only check for stutter on functions, not methods.
|
507
|
+
// Method names are not used package-qualified.
|
508
|
+
f.checkStutter(v.Name, "func")
|
509
|
+
}
|
510
|
+
// Don't proceed inside funcs.
|
511
|
+
return false
|
512
|
+
case *ast.TypeSpec:
|
513
|
+
// inside a GenDecl, which usually has the doc
|
514
|
+
doc := v.Doc
|
515
|
+
if doc == nil {
|
516
|
+
doc = lastGen.Doc
|
517
|
+
}
|
518
|
+
f.lintTypeDoc(v, doc)
|
519
|
+
f.checkStutter(v.Name, "type")
|
520
|
+
// Don't proceed inside types.
|
521
|
+
return false
|
522
|
+
case *ast.ValueSpec:
|
523
|
+
f.lintValueSpecDoc(v, lastGen, genDeclMissingComments)
|
524
|
+
return false
|
525
|
+
}
|
526
|
+
return true
|
527
|
+
})
|
528
|
+
}
|
529
|
+
|
530
|
+
var (
|
531
|
+
allCapsRE = regexp.MustCompile(`^[A-Z0-9_]+$`)
|
532
|
+
anyCapsRE = regexp.MustCompile(`[A-Z]`)
|
533
|
+
)
|
534
|
+
|
535
|
+
// knownNameExceptions is a set of names that are known to be exempt from naming checks.
|
536
|
+
// This is usually because they are constrained by having to match names in the
|
537
|
+
// standard library.
|
538
|
+
var knownNameExceptions = map[string]bool{
|
539
|
+
"LastInsertId": true, // must match database/sql
|
540
|
+
"kWh": true,
|
541
|
+
}
|
542
|
+
|
543
|
+
// lintNames examines all names in the file.
|
544
|
+
// It complains if any use underscores or incorrect known initialisms.
|
545
|
+
func (f *file) lintNames() {
|
546
|
+
// Package names need slightly different handling than other names.
|
547
|
+
if strings.Contains(f.f.Name.Name, "_") && !strings.HasSuffix(f.f.Name.Name, "_test") {
|
548
|
+
f.errorf(f.f, 1, link("http://golang.org/doc/effective_go.html#package-names"), category("naming"), "don't use an underscore in package name")
|
549
|
+
}
|
550
|
+
if anyCapsRE.MatchString(f.f.Name.Name) {
|
551
|
+
f.errorf(f.f, 1, link("http://golang.org/doc/effective_go.html#package-names"), category("mixed-caps"), "don't use MixedCaps in package name; %s should be %s", f.f.Name.Name, strings.ToLower(f.f.Name.Name))
|
552
|
+
}
|
553
|
+
|
554
|
+
check := func(id *ast.Ident, thing string) {
|
555
|
+
if id.Name == "_" {
|
556
|
+
return
|
557
|
+
}
|
558
|
+
if knownNameExceptions[id.Name] {
|
559
|
+
return
|
560
|
+
}
|
561
|
+
|
562
|
+
// Handle two common styles from other languages that don't belong in Go.
|
563
|
+
if len(id.Name) >= 5 && allCapsRE.MatchString(id.Name) && strings.Contains(id.Name, "_") {
|
564
|
+
f.errorf(id, 0.8, link(styleGuideBase+"#mixed-caps"), category("naming"), "don't use ALL_CAPS in Go names; use CamelCase")
|
565
|
+
return
|
566
|
+
}
|
567
|
+
if len(id.Name) > 2 && id.Name[0] == 'k' && id.Name[1] >= 'A' && id.Name[1] <= 'Z' {
|
568
|
+
should := string(id.Name[1]+'a'-'A') + id.Name[2:]
|
569
|
+
f.errorf(id, 0.8, link(styleGuideBase+"#mixed-caps"), category("naming"), "don't use leading k in Go names; %s %s should be %s", thing, id.Name, should)
|
570
|
+
}
|
571
|
+
|
572
|
+
should := lintName(id.Name)
|
573
|
+
if id.Name == should {
|
574
|
+
return
|
575
|
+
}
|
576
|
+
|
577
|
+
if len(id.Name) > 2 && strings.Contains(id.Name[1:], "_") {
|
578
|
+
f.errorf(id, 0.9, link("http://golang.org/doc/effective_go.html#mixed-caps"), category("naming"), "don't use underscores in Go names; %s %s should be %s", thing, id.Name, should)
|
579
|
+
return
|
580
|
+
}
|
581
|
+
f.errorf(id, 0.8, link(styleGuideBase+"#initialisms"), category("naming"), "%s %s should be %s", thing, id.Name, should)
|
582
|
+
}
|
583
|
+
checkList := func(fl *ast.FieldList, thing string) {
|
584
|
+
if fl == nil {
|
585
|
+
return
|
586
|
+
}
|
587
|
+
for _, f := range fl.List {
|
588
|
+
for _, id := range f.Names {
|
589
|
+
check(id, thing)
|
590
|
+
}
|
591
|
+
}
|
592
|
+
}
|
593
|
+
f.walk(func(node ast.Node) bool {
|
594
|
+
switch v := node.(type) {
|
595
|
+
case *ast.AssignStmt:
|
596
|
+
if v.Tok == token.ASSIGN {
|
597
|
+
return true
|
598
|
+
}
|
599
|
+
for _, exp := range v.Lhs {
|
600
|
+
if id, ok := exp.(*ast.Ident); ok {
|
601
|
+
check(id, "var")
|
602
|
+
}
|
603
|
+
}
|
604
|
+
case *ast.FuncDecl:
|
605
|
+
if f.isTest() && (strings.HasPrefix(v.Name.Name, "Example") || strings.HasPrefix(v.Name.Name, "Test") || strings.HasPrefix(v.Name.Name, "Benchmark")) {
|
606
|
+
return true
|
607
|
+
}
|
608
|
+
|
609
|
+
thing := "func"
|
610
|
+
if v.Recv != nil {
|
611
|
+
thing = "method"
|
612
|
+
}
|
613
|
+
|
614
|
+
// Exclude naming warnings for functions that are exported to C but
|
615
|
+
// not exported in the Go API.
|
616
|
+
// See https://github.com/golang/lint/issues/144.
|
617
|
+
if ast.IsExported(v.Name.Name) || !isCgoExported(v) {
|
618
|
+
check(v.Name, thing)
|
619
|
+
}
|
620
|
+
|
621
|
+
checkList(v.Type.Params, thing+" parameter")
|
622
|
+
checkList(v.Type.Results, thing+" result")
|
623
|
+
case *ast.GenDecl:
|
624
|
+
if v.Tok == token.IMPORT {
|
625
|
+
return true
|
626
|
+
}
|
627
|
+
var thing string
|
628
|
+
switch v.Tok {
|
629
|
+
case token.CONST:
|
630
|
+
thing = "const"
|
631
|
+
case token.TYPE:
|
632
|
+
thing = "type"
|
633
|
+
case token.VAR:
|
634
|
+
thing = "var"
|
635
|
+
}
|
636
|
+
for _, spec := range v.Specs {
|
637
|
+
switch s := spec.(type) {
|
638
|
+
case *ast.TypeSpec:
|
639
|
+
check(s.Name, thing)
|
640
|
+
case *ast.ValueSpec:
|
641
|
+
for _, id := range s.Names {
|
642
|
+
check(id, thing)
|
643
|
+
}
|
644
|
+
}
|
645
|
+
}
|
646
|
+
case *ast.InterfaceType:
|
647
|
+
// Do not check interface method names.
|
648
|
+
// They are often constrainted by the method names of concrete types.
|
649
|
+
for _, x := range v.Methods.List {
|
650
|
+
ft, ok := x.Type.(*ast.FuncType)
|
651
|
+
if !ok { // might be an embedded interface name
|
652
|
+
continue
|
653
|
+
}
|
654
|
+
checkList(ft.Params, "interface method parameter")
|
655
|
+
checkList(ft.Results, "interface method result")
|
656
|
+
}
|
657
|
+
case *ast.RangeStmt:
|
658
|
+
if v.Tok == token.ASSIGN {
|
659
|
+
return true
|
660
|
+
}
|
661
|
+
if id, ok := v.Key.(*ast.Ident); ok {
|
662
|
+
check(id, "range var")
|
663
|
+
}
|
664
|
+
if id, ok := v.Value.(*ast.Ident); ok {
|
665
|
+
check(id, "range var")
|
666
|
+
}
|
667
|
+
case *ast.StructType:
|
668
|
+
for _, f := range v.Fields.List {
|
669
|
+
for _, id := range f.Names {
|
670
|
+
check(id, "struct field")
|
671
|
+
}
|
672
|
+
}
|
673
|
+
}
|
674
|
+
return true
|
675
|
+
})
|
676
|
+
}
|
677
|
+
|
678
|
+
// lintName returns a different name if it should be different.
|
679
|
+
func lintName(name string) (should string) {
|
680
|
+
// Fast path for simple cases: "_" and all lowercase.
|
681
|
+
if name == "_" {
|
682
|
+
return name
|
683
|
+
}
|
684
|
+
allLower := true
|
685
|
+
for _, r := range name {
|
686
|
+
if !unicode.IsLower(r) {
|
687
|
+
allLower = false
|
688
|
+
break
|
689
|
+
}
|
690
|
+
}
|
691
|
+
if allLower {
|
692
|
+
return name
|
693
|
+
}
|
694
|
+
|
695
|
+
// Split camelCase at any lower->upper transition, and split on underscores.
|
696
|
+
// Check each word for common initialisms.
|
697
|
+
runes := []rune(name)
|
698
|
+
w, i := 0, 0 // index of start of word, scan
|
699
|
+
for i+1 <= len(runes) {
|
700
|
+
eow := false // whether we hit the end of a word
|
701
|
+
if i+1 == len(runes) {
|
702
|
+
eow = true
|
703
|
+
} else if runes[i+1] == '_' {
|
704
|
+
// underscore; shift the remainder forward over any run of underscores
|
705
|
+
eow = true
|
706
|
+
n := 1
|
707
|
+
for i+n+1 < len(runes) && runes[i+n+1] == '_' {
|
708
|
+
n++
|
709
|
+
}
|
710
|
+
|
711
|
+
// Leave at most one underscore if the underscore is between two digits
|
712
|
+
if i+n+1 < len(runes) && unicode.IsDigit(runes[i]) && unicode.IsDigit(runes[i+n+1]) {
|
713
|
+
n--
|
714
|
+
}
|
715
|
+
|
716
|
+
copy(runes[i+1:], runes[i+n+1:])
|
717
|
+
runes = runes[:len(runes)-n]
|
718
|
+
} else if unicode.IsLower(runes[i]) && !unicode.IsLower(runes[i+1]) {
|
719
|
+
// lower->non-lower
|
720
|
+
eow = true
|
721
|
+
}
|
722
|
+
i++
|
723
|
+
if !eow {
|
724
|
+
continue
|
725
|
+
}
|
726
|
+
|
727
|
+
// [w,i) is a word.
|
728
|
+
word := string(runes[w:i])
|
729
|
+
if u := strings.ToUpper(word); commonInitialisms[u] {
|
730
|
+
// Keep consistent case, which is lowercase only at the start.
|
731
|
+
if w == 0 && unicode.IsLower(runes[w]) {
|
732
|
+
u = strings.ToLower(u)
|
733
|
+
}
|
734
|
+
// All the common initialisms are ASCII,
|
735
|
+
// so we can replace the bytes exactly.
|
736
|
+
copy(runes[w:], []rune(u))
|
737
|
+
} else if w > 0 && strings.ToLower(word) == word {
|
738
|
+
// already all lowercase, and not the first word, so uppercase the first character.
|
739
|
+
runes[w] = unicode.ToUpper(runes[w])
|
740
|
+
}
|
741
|
+
w = i
|
742
|
+
}
|
743
|
+
return string(runes)
|
744
|
+
}
|
745
|
+
|
746
|
+
// commonInitialisms is a set of common initialisms.
|
747
|
+
// Only add entries that are highly unlikely to be non-initialisms.
|
748
|
+
// For instance, "ID" is fine (Freudian code is rare), but "AND" is not.
|
749
|
+
var commonInitialisms = map[string]bool{
|
750
|
+
"ACL": true,
|
751
|
+
"API": true,
|
752
|
+
"ASCII": true,
|
753
|
+
"CPU": true,
|
754
|
+
"CSS": true,
|
755
|
+
"DNS": true,
|
756
|
+
"EOF": true,
|
757
|
+
"GUID": true,
|
758
|
+
"HTML": true,
|
759
|
+
"HTTP": true,
|
760
|
+
"HTTPS": true,
|
761
|
+
"ID": true,
|
762
|
+
"IP": true,
|
763
|
+
"JSON": true,
|
764
|
+
"LHS": true,
|
765
|
+
"QPS": true,
|
766
|
+
"RAM": true,
|
767
|
+
"RHS": true,
|
768
|
+
"RPC": true,
|
769
|
+
"SLA": true,
|
770
|
+
"SMTP": true,
|
771
|
+
"SQL": true,
|
772
|
+
"SSH": true,
|
773
|
+
"TCP": true,
|
774
|
+
"TLS": true,
|
775
|
+
"TTL": true,
|
776
|
+
"UDP": true,
|
777
|
+
"UI": true,
|
778
|
+
"UID": true,
|
779
|
+
"UUID": true,
|
780
|
+
"URI": true,
|
781
|
+
"URL": true,
|
782
|
+
"UTF8": true,
|
783
|
+
"VM": true,
|
784
|
+
"XML": true,
|
785
|
+
"XMPP": true,
|
786
|
+
"XSRF": true,
|
787
|
+
"XSS": true,
|
788
|
+
}
|
789
|
+
|
790
|
+
// lintTypeDoc examines the doc comment on a type.
|
791
|
+
// It complains if they are missing from an exported type,
|
792
|
+
// or if they are not of the standard form.
|
793
|
+
func (f *file) lintTypeDoc(t *ast.TypeSpec, doc *ast.CommentGroup) {
|
794
|
+
if !ast.IsExported(t.Name.Name) {
|
795
|
+
return
|
796
|
+
}
|
797
|
+
if doc == nil {
|
798
|
+
f.errorf(t, 1, link(docCommentsLink), category("comments"), "exported type %v should have comment or be unexported", t.Name)
|
799
|
+
return
|
800
|
+
}
|
801
|
+
|
802
|
+
s := doc.Text()
|
803
|
+
articles := [...]string{"A", "An", "The"}
|
804
|
+
for _, a := range articles {
|
805
|
+
if strings.HasPrefix(s, a+" ") {
|
806
|
+
s = s[len(a)+1:]
|
807
|
+
break
|
808
|
+
}
|
809
|
+
}
|
810
|
+
if !strings.HasPrefix(s, t.Name.Name+" ") {
|
811
|
+
f.errorf(doc, 1, link(docCommentsLink), category("comments"), `comment on exported type %v should be of the form "%v ..." (with optional leading article)`, t.Name, t.Name)
|
812
|
+
}
|
813
|
+
}
|
814
|
+
|
815
|
+
var commonMethods = map[string]bool{
|
816
|
+
"Error": true,
|
817
|
+
"Read": true,
|
818
|
+
"ServeHTTP": true,
|
819
|
+
"String": true,
|
820
|
+
"Write": true,
|
821
|
+
}
|
822
|
+
|
823
|
+
// lintFuncDoc examines doc comments on functions and methods.
|
824
|
+
// It complains if they are missing, or not of the right form.
|
825
|
+
// It has specific exclusions for well-known methods (see commonMethods above).
|
826
|
+
func (f *file) lintFuncDoc(fn *ast.FuncDecl) {
|
827
|
+
if !ast.IsExported(fn.Name.Name) {
|
828
|
+
// func is unexported
|
829
|
+
return
|
830
|
+
}
|
831
|
+
kind := "function"
|
832
|
+
name := fn.Name.Name
|
833
|
+
if fn.Recv != nil && len(fn.Recv.List) > 0 {
|
834
|
+
// method
|
835
|
+
kind = "method"
|
836
|
+
recv := receiverType(fn)
|
837
|
+
if !ast.IsExported(recv) {
|
838
|
+
// receiver is unexported
|
839
|
+
return
|
840
|
+
}
|
841
|
+
if commonMethods[name] {
|
842
|
+
return
|
843
|
+
}
|
844
|
+
switch name {
|
845
|
+
case "Len", "Less", "Swap":
|
846
|
+
if f.pkg.sortable[recv] {
|
847
|
+
return
|
848
|
+
}
|
849
|
+
}
|
850
|
+
name = recv + "." + name
|
851
|
+
}
|
852
|
+
if fn.Doc == nil {
|
853
|
+
f.errorf(fn, 1, link(docCommentsLink), category("comments"), "exported %s %s should have comment or be unexported", kind, name)
|
854
|
+
return
|
855
|
+
}
|
856
|
+
s := fn.Doc.Text()
|
857
|
+
prefix := fn.Name.Name + " "
|
858
|
+
if !strings.HasPrefix(s, prefix) {
|
859
|
+
f.errorf(fn.Doc, 1, link(docCommentsLink), category("comments"), `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix)
|
860
|
+
}
|
861
|
+
}
|
862
|
+
|
863
|
+
// lintValueSpecDoc examines package-global variables and constants.
|
864
|
+
// It complains if they are not individually declared,
|
865
|
+
// or if they are not suitably documented in the right form (unless they are in a block that is commented).
|
866
|
+
func (f *file) lintValueSpecDoc(vs *ast.ValueSpec, gd *ast.GenDecl, genDeclMissingComments map[*ast.GenDecl]bool) {
|
867
|
+
kind := "var"
|
868
|
+
if gd.Tok == token.CONST {
|
869
|
+
kind = "const"
|
870
|
+
}
|
871
|
+
|
872
|
+
if len(vs.Names) > 1 {
|
873
|
+
// Check that none are exported except for the first.
|
874
|
+
for _, n := range vs.Names[1:] {
|
875
|
+
if ast.IsExported(n.Name) {
|
876
|
+
f.errorf(vs, 1, category("comments"), "exported %s %s should have its own declaration", kind, n.Name)
|
877
|
+
return
|
878
|
+
}
|
879
|
+
}
|
880
|
+
}
|
881
|
+
|
882
|
+
// Only one name.
|
883
|
+
name := vs.Names[0].Name
|
884
|
+
if !ast.IsExported(name) {
|
885
|
+
return
|
886
|
+
}
|
887
|
+
|
888
|
+
if vs.Doc == nil && gd.Doc == nil {
|
889
|
+
if genDeclMissingComments[gd] {
|
890
|
+
return
|
891
|
+
}
|
892
|
+
block := ""
|
893
|
+
if kind == "const" && gd.Lparen.IsValid() {
|
894
|
+
block = " (or a comment on this block)"
|
895
|
+
}
|
896
|
+
f.errorf(vs, 1, link(docCommentsLink), category("comments"), "exported %s %s should have comment%s or be unexported", kind, name, block)
|
897
|
+
genDeclMissingComments[gd] = true
|
898
|
+
return
|
899
|
+
}
|
900
|
+
// If this GenDecl has parens and a comment, we don't check its comment form.
|
901
|
+
if gd.Lparen.IsValid() && gd.Doc != nil {
|
902
|
+
return
|
903
|
+
}
|
904
|
+
// The relevant text to check will be on either vs.Doc or gd.Doc.
|
905
|
+
// Use vs.Doc preferentially.
|
906
|
+
doc := vs.Doc
|
907
|
+
if doc == nil {
|
908
|
+
doc = gd.Doc
|
909
|
+
}
|
910
|
+
prefix := name + " "
|
911
|
+
if !strings.HasPrefix(doc.Text(), prefix) {
|
912
|
+
f.errorf(doc, 1, link(docCommentsLink), category("comments"), `comment on exported %s %s should be of the form "%s..."`, kind, name, prefix)
|
913
|
+
}
|
914
|
+
}
|
915
|
+
|
916
|
+
func (f *file) checkStutter(id *ast.Ident, thing string) {
|
917
|
+
pkg, name := f.f.Name.Name, id.Name
|
918
|
+
if !ast.IsExported(name) {
|
919
|
+
// unexported name
|
920
|
+
return
|
921
|
+
}
|
922
|
+
// A name stutters if the package name is a strict prefix
|
923
|
+
// and the next character of the name starts a new word.
|
924
|
+
if len(name) <= len(pkg) {
|
925
|
+
// name is too short to stutter.
|
926
|
+
// This permits the name to be the same as the package name.
|
927
|
+
return
|
928
|
+
}
|
929
|
+
if !strings.EqualFold(pkg, name[:len(pkg)]) {
|
930
|
+
return
|
931
|
+
}
|
932
|
+
// We can assume the name is well-formed UTF-8.
|
933
|
+
// If the next rune after the package name is uppercase or an underscore
|
934
|
+
// the it's starting a new word and thus this name stutters.
|
935
|
+
rem := name[len(pkg):]
|
936
|
+
if next, _ := utf8.DecodeRuneInString(rem); next == '_' || unicode.IsUpper(next) {
|
937
|
+
f.errorf(id, 0.8, link(styleGuideBase+"#package-names"), category("naming"), "%s name will be used as %s.%s by other packages, and that stutters; consider calling this %s", thing, pkg, name, rem)
|
938
|
+
}
|
939
|
+
}
|
940
|
+
|
941
|
+
// zeroLiteral is a set of ast.BasicLit values that are zero values.
|
942
|
+
// It is not exhaustive.
|
943
|
+
var zeroLiteral = map[string]bool{
|
944
|
+
"false": true, // bool
|
945
|
+
// runes
|
946
|
+
`'\x00'`: true,
|
947
|
+
`'\000'`: true,
|
948
|
+
// strings
|
949
|
+
`""`: true,
|
950
|
+
"``": true,
|
951
|
+
// numerics
|
952
|
+
"0": true,
|
953
|
+
"0.": true,
|
954
|
+
"0.0": true,
|
955
|
+
"0i": true,
|
956
|
+
}
|
957
|
+
|
958
|
+
// lintVarDecls examines variable declarations. It complains about declarations with
|
959
|
+
// redundant LHS types that can be inferred from the RHS.
|
960
|
+
func (f *file) lintVarDecls() {
|
961
|
+
var lastGen *ast.GenDecl // last GenDecl entered.
|
962
|
+
|
963
|
+
f.walk(func(node ast.Node) bool {
|
964
|
+
switch v := node.(type) {
|
965
|
+
case *ast.GenDecl:
|
966
|
+
if v.Tok != token.CONST && v.Tok != token.VAR {
|
967
|
+
return false
|
968
|
+
}
|
969
|
+
lastGen = v
|
970
|
+
return true
|
971
|
+
case *ast.ValueSpec:
|
972
|
+
if lastGen.Tok == token.CONST {
|
973
|
+
return false
|
974
|
+
}
|
975
|
+
if len(v.Names) > 1 || v.Type == nil || len(v.Values) == 0 {
|
976
|
+
return false
|
977
|
+
}
|
978
|
+
rhs := v.Values[0]
|
979
|
+
// An underscore var appears in a common idiom for compile-time interface satisfaction,
|
980
|
+
// as in "var _ Interface = (*Concrete)(nil)".
|
981
|
+
if isIdent(v.Names[0], "_") {
|
982
|
+
return false
|
983
|
+
}
|
984
|
+
// If the RHS is a zero value, suggest dropping it.
|
985
|
+
zero := false
|
986
|
+
if lit, ok := rhs.(*ast.BasicLit); ok {
|
987
|
+
zero = zeroLiteral[lit.Value]
|
988
|
+
} else if isIdent(rhs, "nil") {
|
989
|
+
zero = true
|
990
|
+
}
|
991
|
+
if zero {
|
992
|
+
f.errorf(rhs, 0.9, category("zero-value"), "should drop = %s from declaration of var %s; it is the zero value", f.render(rhs), v.Names[0])
|
993
|
+
return false
|
994
|
+
}
|
995
|
+
lhsTyp := f.pkg.typeOf(v.Type)
|
996
|
+
rhsTyp := f.pkg.typeOf(rhs)
|
997
|
+
|
998
|
+
if !validType(lhsTyp) || !validType(rhsTyp) {
|
999
|
+
// Type checking failed (often due to missing imports).
|
1000
|
+
return false
|
1001
|
+
}
|
1002
|
+
|
1003
|
+
if !types.Identical(lhsTyp, rhsTyp) {
|
1004
|
+
// Assignment to a different type is not redundant.
|
1005
|
+
return false
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
// The next three conditions are for suppressing the warning in situations
|
1009
|
+
// where we were unable to typecheck.
|
1010
|
+
|
1011
|
+
// If the LHS type is an interface, don't warn, since it is probably a
|
1012
|
+
// concrete type on the RHS. Note that our feeble lexical check here
|
1013
|
+
// will only pick up interface{} and other literal interface types;
|
1014
|
+
// that covers most of the cases we care to exclude right now.
|
1015
|
+
if _, ok := v.Type.(*ast.InterfaceType); ok {
|
1016
|
+
return false
|
1017
|
+
}
|
1018
|
+
// If the RHS is an untyped const, only warn if the LHS type is its default type.
|
1019
|
+
if defType, ok := f.isUntypedConst(rhs); ok && !isIdent(v.Type, defType) {
|
1020
|
+
return false
|
1021
|
+
}
|
1022
|
+
|
1023
|
+
f.errorf(v.Type, 0.8, category("type-inference"), "should omit type %s from declaration of var %s; it will be inferred from the right-hand side", f.render(v.Type), v.Names[0])
|
1024
|
+
return false
|
1025
|
+
}
|
1026
|
+
return true
|
1027
|
+
})
|
1028
|
+
}
|
1029
|
+
|
1030
|
+
func validType(T types.Type) bool {
|
1031
|
+
return T != nil &&
|
1032
|
+
T != types.Typ[types.Invalid] &&
|
1033
|
+
!strings.Contains(T.String(), "invalid type") // good but not foolproof
|
1034
|
+
}
|
1035
|
+
|
1036
|
+
// lintElses examines else blocks. It complains about any else block whose if block ends in a return.
|
1037
|
+
func (f *file) lintElses() {
|
1038
|
+
// We don't want to flag if { } else if { } else { } constructions.
|
1039
|
+
// They will appear as an IfStmt whose Else field is also an IfStmt.
|
1040
|
+
// Record such a node so we ignore it when we visit it.
|
1041
|
+
ignore := make(map[*ast.IfStmt]bool)
|
1042
|
+
|
1043
|
+
f.walk(func(node ast.Node) bool {
|
1044
|
+
ifStmt, ok := node.(*ast.IfStmt)
|
1045
|
+
if !ok || ifStmt.Else == nil {
|
1046
|
+
return true
|
1047
|
+
}
|
1048
|
+
if ignore[ifStmt] {
|
1049
|
+
return true
|
1050
|
+
}
|
1051
|
+
if elseif, ok := ifStmt.Else.(*ast.IfStmt); ok {
|
1052
|
+
ignore[elseif] = true
|
1053
|
+
return true
|
1054
|
+
}
|
1055
|
+
if _, ok := ifStmt.Else.(*ast.BlockStmt); !ok {
|
1056
|
+
// only care about elses without conditions
|
1057
|
+
return true
|
1058
|
+
}
|
1059
|
+
if len(ifStmt.Body.List) == 0 {
|
1060
|
+
return true
|
1061
|
+
}
|
1062
|
+
shortDecl := false // does the if statement have a ":=" initialization statement?
|
1063
|
+
if ifStmt.Init != nil {
|
1064
|
+
if as, ok := ifStmt.Init.(*ast.AssignStmt); ok && as.Tok == token.DEFINE {
|
1065
|
+
shortDecl = true
|
1066
|
+
}
|
1067
|
+
}
|
1068
|
+
lastStmt := ifStmt.Body.List[len(ifStmt.Body.List)-1]
|
1069
|
+
if _, ok := lastStmt.(*ast.ReturnStmt); ok {
|
1070
|
+
extra := ""
|
1071
|
+
if shortDecl {
|
1072
|
+
extra = " (move short variable declaration to its own line if necessary)"
|
1073
|
+
}
|
1074
|
+
f.errorf(ifStmt.Else, 1, link(styleGuideBase+"#indent-error-flow"), category("indent"), "if block ends with a return statement, so drop this else and outdent its block"+extra)
|
1075
|
+
}
|
1076
|
+
return true
|
1077
|
+
})
|
1078
|
+
}
|
1079
|
+
|
1080
|
+
// lintRanges examines range clauses. It complains about redundant constructions.
|
1081
|
+
func (f *file) lintRanges() {
|
1082
|
+
f.walk(func(node ast.Node) bool {
|
1083
|
+
rs, ok := node.(*ast.RangeStmt)
|
1084
|
+
if !ok {
|
1085
|
+
return true
|
1086
|
+
}
|
1087
|
+
|
1088
|
+
if isIdent(rs.Key, "_") && (rs.Value == nil || isIdent(rs.Value, "_")) {
|
1089
|
+
p := f.errorf(rs.Key, 1, category("range-loop"), "should omit values from range; this loop is equivalent to `for range ...`")
|
1090
|
+
|
1091
|
+
newRS := *rs // shallow copy
|
1092
|
+
newRS.Value = nil
|
1093
|
+
newRS.Key = nil
|
1094
|
+
p.ReplacementLine = f.firstLineOf(&newRS, rs)
|
1095
|
+
|
1096
|
+
return true
|
1097
|
+
}
|
1098
|
+
|
1099
|
+
if isIdent(rs.Value, "_") {
|
1100
|
+
p := f.errorf(rs.Value, 1, category("range-loop"), "should omit 2nd value from range; this loop is equivalent to `for %s %s range ...`", f.render(rs.Key), rs.Tok)
|
1101
|
+
|
1102
|
+
newRS := *rs // shallow copy
|
1103
|
+
newRS.Value = nil
|
1104
|
+
p.ReplacementLine = f.firstLineOf(&newRS, rs)
|
1105
|
+
}
|
1106
|
+
|
1107
|
+
return true
|
1108
|
+
})
|
1109
|
+
}
|
1110
|
+
|
1111
|
+
// lintErrorf examines errors.New and testing.Error calls. It complains if its only argument is an fmt.Sprintf invocation.
|
1112
|
+
func (f *file) lintErrorf() {
|
1113
|
+
f.walk(func(node ast.Node) bool {
|
1114
|
+
ce, ok := node.(*ast.CallExpr)
|
1115
|
+
if !ok || len(ce.Args) != 1 {
|
1116
|
+
return true
|
1117
|
+
}
|
1118
|
+
isErrorsNew := isPkgDot(ce.Fun, "errors", "New")
|
1119
|
+
var isTestingError bool
|
1120
|
+
se, ok := ce.Fun.(*ast.SelectorExpr)
|
1121
|
+
if ok && se.Sel.Name == "Error" {
|
1122
|
+
if typ := f.pkg.typeOf(se.X); typ != nil {
|
1123
|
+
isTestingError = typ.String() == "*testing.T"
|
1124
|
+
}
|
1125
|
+
}
|
1126
|
+
if !isErrorsNew && !isTestingError {
|
1127
|
+
return true
|
1128
|
+
}
|
1129
|
+
arg := ce.Args[0]
|
1130
|
+
ce, ok = arg.(*ast.CallExpr)
|
1131
|
+
if !ok || !isPkgDot(ce.Fun, "fmt", "Sprintf") {
|
1132
|
+
return true
|
1133
|
+
}
|
1134
|
+
errorfPrefix := "fmt"
|
1135
|
+
if isTestingError {
|
1136
|
+
errorfPrefix = f.render(se.X)
|
1137
|
+
}
|
1138
|
+
p := f.errorf(node, 1, category("errors"), "should replace %s(fmt.Sprintf(...)) with %s.Errorf(...)", f.render(se), errorfPrefix)
|
1139
|
+
|
1140
|
+
m := f.srcLineWithMatch(ce, `^(.*)`+f.render(se)+`\(fmt\.Sprintf\((.*)\)\)(.*)$`)
|
1141
|
+
if m != nil {
|
1142
|
+
p.ReplacementLine = m[1] + errorfPrefix + ".Errorf(" + m[2] + ")" + m[3]
|
1143
|
+
}
|
1144
|
+
|
1145
|
+
return true
|
1146
|
+
})
|
1147
|
+
}
|
1148
|
+
|
1149
|
+
// lintErrors examines global error vars. It complains if they aren't named in the standard way.
|
1150
|
+
func (f *file) lintErrors() {
|
1151
|
+
for _, decl := range f.f.Decls {
|
1152
|
+
gd, ok := decl.(*ast.GenDecl)
|
1153
|
+
if !ok || gd.Tok != token.VAR {
|
1154
|
+
continue
|
1155
|
+
}
|
1156
|
+
for _, spec := range gd.Specs {
|
1157
|
+
spec := spec.(*ast.ValueSpec)
|
1158
|
+
if len(spec.Names) != 1 || len(spec.Values) != 1 {
|
1159
|
+
continue
|
1160
|
+
}
|
1161
|
+
ce, ok := spec.Values[0].(*ast.CallExpr)
|
1162
|
+
if !ok {
|
1163
|
+
continue
|
1164
|
+
}
|
1165
|
+
if !isPkgDot(ce.Fun, "errors", "New") && !isPkgDot(ce.Fun, "fmt", "Errorf") {
|
1166
|
+
continue
|
1167
|
+
}
|
1168
|
+
|
1169
|
+
id := spec.Names[0]
|
1170
|
+
prefix := "err"
|
1171
|
+
if id.IsExported() {
|
1172
|
+
prefix = "Err"
|
1173
|
+
}
|
1174
|
+
if !strings.HasPrefix(id.Name, prefix) {
|
1175
|
+
f.errorf(id, 0.9, category("naming"), "error var %s should have name of the form %sFoo", id.Name, prefix)
|
1176
|
+
}
|
1177
|
+
}
|
1178
|
+
}
|
1179
|
+
}
|
1180
|
+
|
1181
|
+
func lintErrorString(s string) (isClean bool, conf float64) {
|
1182
|
+
const basicConfidence = 0.8
|
1183
|
+
const capConfidence = basicConfidence - 0.2
|
1184
|
+
first, firstN := utf8.DecodeRuneInString(s)
|
1185
|
+
last, _ := utf8.DecodeLastRuneInString(s)
|
1186
|
+
if last == '.' || last == ':' || last == '!' || last == '\n' {
|
1187
|
+
return false, basicConfidence
|
1188
|
+
}
|
1189
|
+
if unicode.IsUpper(first) {
|
1190
|
+
// People use proper nouns and exported Go identifiers in error strings,
|
1191
|
+
// so decrease the confidence of warnings for capitalization.
|
1192
|
+
if len(s) <= firstN {
|
1193
|
+
return false, capConfidence
|
1194
|
+
}
|
1195
|
+
// Flag strings starting with something that doesn't look like an initialism.
|
1196
|
+
if second, _ := utf8.DecodeRuneInString(s[firstN:]); !unicode.IsUpper(second) {
|
1197
|
+
return false, capConfidence
|
1198
|
+
}
|
1199
|
+
}
|
1200
|
+
return true, 0
|
1201
|
+
}
|
1202
|
+
|
1203
|
+
// lintErrorStrings examines error strings.
|
1204
|
+
// It complains if they are capitalized or end in punctuation or a newline.
|
1205
|
+
func (f *file) lintErrorStrings() {
|
1206
|
+
f.walk(func(node ast.Node) bool {
|
1207
|
+
ce, ok := node.(*ast.CallExpr)
|
1208
|
+
if !ok {
|
1209
|
+
return true
|
1210
|
+
}
|
1211
|
+
if !isPkgDot(ce.Fun, "errors", "New") && !isPkgDot(ce.Fun, "fmt", "Errorf") {
|
1212
|
+
return true
|
1213
|
+
}
|
1214
|
+
if len(ce.Args) < 1 {
|
1215
|
+
return true
|
1216
|
+
}
|
1217
|
+
str, ok := ce.Args[0].(*ast.BasicLit)
|
1218
|
+
if !ok || str.Kind != token.STRING {
|
1219
|
+
return true
|
1220
|
+
}
|
1221
|
+
s, _ := strconv.Unquote(str.Value) // can assume well-formed Go
|
1222
|
+
if s == "" {
|
1223
|
+
return true
|
1224
|
+
}
|
1225
|
+
clean, conf := lintErrorString(s)
|
1226
|
+
if clean {
|
1227
|
+
return true
|
1228
|
+
}
|
1229
|
+
|
1230
|
+
f.errorf(str, conf, link(styleGuideBase+"#error-strings"), category("errors"),
|
1231
|
+
"error strings should not be capitalized or end with punctuation or a newline")
|
1232
|
+
return true
|
1233
|
+
})
|
1234
|
+
}
|
1235
|
+
|
1236
|
+
// lintReceiverNames examines receiver names. It complains about inconsistent
|
1237
|
+
// names used for the same type and names such as "this".
|
1238
|
+
func (f *file) lintReceiverNames() {
|
1239
|
+
typeReceiver := map[string]string{}
|
1240
|
+
f.walk(func(n ast.Node) bool {
|
1241
|
+
fn, ok := n.(*ast.FuncDecl)
|
1242
|
+
if !ok || fn.Recv == nil || len(fn.Recv.List) == 0 {
|
1243
|
+
return true
|
1244
|
+
}
|
1245
|
+
names := fn.Recv.List[0].Names
|
1246
|
+
if len(names) < 1 {
|
1247
|
+
return true
|
1248
|
+
}
|
1249
|
+
name := names[0].Name
|
1250
|
+
const ref = styleGuideBase + "#receiver-names"
|
1251
|
+
if name == "_" {
|
1252
|
+
f.errorf(n, 1, link(ref), category("naming"), `receiver name should not be an underscore, omit the name if it is unused`)
|
1253
|
+
return true
|
1254
|
+
}
|
1255
|
+
if name == "this" || name == "self" {
|
1256
|
+
f.errorf(n, 1, link(ref), category("naming"), `receiver name should be a reflection of its identity; don't use generic names such as "this" or "self"`)
|
1257
|
+
return true
|
1258
|
+
}
|
1259
|
+
recv := receiverType(fn)
|
1260
|
+
if prev, ok := typeReceiver[recv]; ok && prev != name {
|
1261
|
+
f.errorf(n, 1, link(ref), category("naming"), "receiver name %s should be consistent with previous receiver name %s for %s", name, prev, recv)
|
1262
|
+
return true
|
1263
|
+
}
|
1264
|
+
typeReceiver[recv] = name
|
1265
|
+
return true
|
1266
|
+
})
|
1267
|
+
}
|
1268
|
+
|
1269
|
+
// lintIncDec examines statements that increment or decrement a variable.
|
1270
|
+
// It complains if they don't use x++ or x--.
|
1271
|
+
func (f *file) lintIncDec() {
|
1272
|
+
f.walk(func(n ast.Node) bool {
|
1273
|
+
as, ok := n.(*ast.AssignStmt)
|
1274
|
+
if !ok {
|
1275
|
+
return true
|
1276
|
+
}
|
1277
|
+
if len(as.Lhs) != 1 {
|
1278
|
+
return true
|
1279
|
+
}
|
1280
|
+
if !isOne(as.Rhs[0]) {
|
1281
|
+
return true
|
1282
|
+
}
|
1283
|
+
var suffix string
|
1284
|
+
switch as.Tok {
|
1285
|
+
case token.ADD_ASSIGN:
|
1286
|
+
suffix = "++"
|
1287
|
+
case token.SUB_ASSIGN:
|
1288
|
+
suffix = "--"
|
1289
|
+
default:
|
1290
|
+
return true
|
1291
|
+
}
|
1292
|
+
f.errorf(as, 0.8, category("unary-op"), "should replace %s with %s%s", f.render(as), f.render(as.Lhs[0]), suffix)
|
1293
|
+
return true
|
1294
|
+
})
|
1295
|
+
}
|
1296
|
+
|
1297
|
+
// lintErrorReturn examines function declarations that return an error.
|
1298
|
+
// It complains if the error isn't the last parameter.
|
1299
|
+
func (f *file) lintErrorReturn() {
|
1300
|
+
f.walk(func(n ast.Node) bool {
|
1301
|
+
fn, ok := n.(*ast.FuncDecl)
|
1302
|
+
if !ok || fn.Type.Results == nil {
|
1303
|
+
return true
|
1304
|
+
}
|
1305
|
+
ret := fn.Type.Results.List
|
1306
|
+
if len(ret) <= 1 {
|
1307
|
+
return true
|
1308
|
+
}
|
1309
|
+
// An error return parameter should be the last parameter.
|
1310
|
+
// Flag any error parameters found before the last.
|
1311
|
+
for _, r := range ret[:len(ret)-1] {
|
1312
|
+
if isIdent(r.Type, "error") {
|
1313
|
+
f.errorf(fn, 0.9, category("arg-order"), "error should be the last type when returning multiple items")
|
1314
|
+
break // only flag one
|
1315
|
+
}
|
1316
|
+
}
|
1317
|
+
return true
|
1318
|
+
})
|
1319
|
+
}
|
1320
|
+
|
1321
|
+
// lintUnexportedReturn examines exported function declarations.
|
1322
|
+
// It complains if any return an unexported type.
|
1323
|
+
func (f *file) lintUnexportedReturn() {
|
1324
|
+
f.walk(func(n ast.Node) bool {
|
1325
|
+
fn, ok := n.(*ast.FuncDecl)
|
1326
|
+
if !ok {
|
1327
|
+
return true
|
1328
|
+
}
|
1329
|
+
if fn.Type.Results == nil {
|
1330
|
+
return false
|
1331
|
+
}
|
1332
|
+
if !fn.Name.IsExported() {
|
1333
|
+
return false
|
1334
|
+
}
|
1335
|
+
thing := "func"
|
1336
|
+
if fn.Recv != nil && len(fn.Recv.List) > 0 {
|
1337
|
+
thing = "method"
|
1338
|
+
if !ast.IsExported(receiverType(fn)) {
|
1339
|
+
// Don't report exported methods of unexported types,
|
1340
|
+
// such as private implementations of sort.Interface.
|
1341
|
+
return false
|
1342
|
+
}
|
1343
|
+
}
|
1344
|
+
for _, ret := range fn.Type.Results.List {
|
1345
|
+
typ := f.pkg.typeOf(ret.Type)
|
1346
|
+
if exportedType(typ) {
|
1347
|
+
continue
|
1348
|
+
}
|
1349
|
+
f.errorf(ret.Type, 0.8, category("unexported-type-in-api"),
|
1350
|
+
"exported %s %s returns unexported type %s, which can be annoying to use",
|
1351
|
+
thing, fn.Name.Name, typ)
|
1352
|
+
break // only flag one
|
1353
|
+
}
|
1354
|
+
return false
|
1355
|
+
})
|
1356
|
+
}
|
1357
|
+
|
1358
|
+
// exportedType reports whether typ is an exported type.
|
1359
|
+
// It is imprecise, and will err on the side of returning true,
|
1360
|
+
// such as for composite types.
|
1361
|
+
func exportedType(typ types.Type) bool {
|
1362
|
+
switch T := typ.(type) {
|
1363
|
+
case *types.Named:
|
1364
|
+
// Builtin types have no package.
|
1365
|
+
return T.Obj().Pkg() == nil || T.Obj().Exported()
|
1366
|
+
case *types.Map:
|
1367
|
+
return exportedType(T.Key()) && exportedType(T.Elem())
|
1368
|
+
case interface {
|
1369
|
+
Elem() types.Type
|
1370
|
+
}: // array, slice, pointer, chan
|
1371
|
+
return exportedType(T.Elem())
|
1372
|
+
}
|
1373
|
+
// Be conservative about other types, such as struct, interface, etc.
|
1374
|
+
return true
|
1375
|
+
}
|
1376
|
+
|
1377
|
+
// timeSuffixes is a list of name suffixes that imply a time unit.
|
1378
|
+
// This is not an exhaustive list.
|
1379
|
+
var timeSuffixes = []string{
|
1380
|
+
"Sec", "Secs", "Seconds",
|
1381
|
+
"Msec", "Msecs",
|
1382
|
+
"Milli", "Millis", "Milliseconds",
|
1383
|
+
"Usec", "Usecs", "Microseconds",
|
1384
|
+
"MS", "Ms",
|
1385
|
+
}
|
1386
|
+
|
1387
|
+
func (f *file) lintTimeNames() {
|
1388
|
+
f.walk(func(node ast.Node) bool {
|
1389
|
+
v, ok := node.(*ast.ValueSpec)
|
1390
|
+
if !ok {
|
1391
|
+
return true
|
1392
|
+
}
|
1393
|
+
for _, name := range v.Names {
|
1394
|
+
origTyp := f.pkg.typeOf(name)
|
1395
|
+
// Look for time.Duration or *time.Duration;
|
1396
|
+
// the latter is common when using flag.Duration.
|
1397
|
+
typ := origTyp
|
1398
|
+
if pt, ok := typ.(*types.Pointer); ok {
|
1399
|
+
typ = pt.Elem()
|
1400
|
+
}
|
1401
|
+
if !f.pkg.isNamedType(typ, "time", "Duration") {
|
1402
|
+
continue
|
1403
|
+
}
|
1404
|
+
suffix := ""
|
1405
|
+
for _, suf := range timeSuffixes {
|
1406
|
+
if strings.HasSuffix(name.Name, suf) {
|
1407
|
+
suffix = suf
|
1408
|
+
break
|
1409
|
+
}
|
1410
|
+
}
|
1411
|
+
if suffix == "" {
|
1412
|
+
continue
|
1413
|
+
}
|
1414
|
+
f.errorf(v, 0.9, category("time"), "var %s is of type %v; don't use unit-specific suffix %q", name.Name, origTyp, suffix)
|
1415
|
+
}
|
1416
|
+
return true
|
1417
|
+
})
|
1418
|
+
}
|
1419
|
+
|
1420
|
+
// lintContextKeyTypes checks for call expressions to context.WithValue with
|
1421
|
+
// basic types used for the key argument.
|
1422
|
+
// See: https://golang.org/issue/17293
|
1423
|
+
func (f *file) lintContextKeyTypes() {
|
1424
|
+
f.walk(func(node ast.Node) bool {
|
1425
|
+
switch node := node.(type) {
|
1426
|
+
case *ast.CallExpr:
|
1427
|
+
f.checkContextKeyType(node)
|
1428
|
+
}
|
1429
|
+
|
1430
|
+
return true
|
1431
|
+
})
|
1432
|
+
}
|
1433
|
+
|
1434
|
+
// checkContextKeyType reports an error if the call expression calls
|
1435
|
+
// context.WithValue with a key argument of basic type.
|
1436
|
+
func (f *file) checkContextKeyType(x *ast.CallExpr) {
|
1437
|
+
sel, ok := x.Fun.(*ast.SelectorExpr)
|
1438
|
+
if !ok {
|
1439
|
+
return
|
1440
|
+
}
|
1441
|
+
pkg, ok := sel.X.(*ast.Ident)
|
1442
|
+
if !ok || pkg.Name != "context" {
|
1443
|
+
return
|
1444
|
+
}
|
1445
|
+
if sel.Sel.Name != "WithValue" {
|
1446
|
+
return
|
1447
|
+
}
|
1448
|
+
|
1449
|
+
// key is second argument to context.WithValue
|
1450
|
+
if len(x.Args) != 3 {
|
1451
|
+
return
|
1452
|
+
}
|
1453
|
+
key := f.pkg.typesInfo.Types[x.Args[1]]
|
1454
|
+
|
1455
|
+
if ktyp, ok := key.Type.(*types.Basic); ok && ktyp.Kind() != types.Invalid {
|
1456
|
+
f.errorf(x, 1.0, category("context"), fmt.Sprintf("should not use basic type %s as key in context.WithValue", key.Type))
|
1457
|
+
}
|
1458
|
+
}
|
1459
|
+
|
1460
|
+
// lintContextArgs examines function declarations that contain an
|
1461
|
+
// argument with a type of context.Context
|
1462
|
+
// It complains if that argument isn't the first parameter.
|
1463
|
+
func (f *file) lintContextArgs() {
|
1464
|
+
f.walk(func(n ast.Node) bool {
|
1465
|
+
fn, ok := n.(*ast.FuncDecl)
|
1466
|
+
if !ok || len(fn.Type.Params.List) <= 1 {
|
1467
|
+
return true
|
1468
|
+
}
|
1469
|
+
// A context.Context should be the first parameter of a function.
|
1470
|
+
// Flag any that show up after the first.
|
1471
|
+
for _, arg := range fn.Type.Params.List[1:] {
|
1472
|
+
if isPkgDot(arg.Type, "context", "Context") {
|
1473
|
+
f.errorf(fn, 0.9, link("https://golang.org/pkg/context/"), category("arg-order"), "context.Context should be the first parameter of a function")
|
1474
|
+
break // only flag one
|
1475
|
+
}
|
1476
|
+
}
|
1477
|
+
return true
|
1478
|
+
})
|
1479
|
+
}
|
1480
|
+
|
1481
|
+
// containsComments returns whether the interval [start, end) contains any
|
1482
|
+
// comments without "// MATCH " prefix.
|
1483
|
+
func (f *file) containsComments(start, end token.Pos) bool {
|
1484
|
+
for _, cgroup := range f.f.Comments {
|
1485
|
+
comments := cgroup.List
|
1486
|
+
if comments[0].Slash >= end {
|
1487
|
+
// All comments starting with this group are after end pos.
|
1488
|
+
return false
|
1489
|
+
}
|
1490
|
+
if comments[len(comments)-1].Slash < start {
|
1491
|
+
// Comments group ends before start pos.
|
1492
|
+
continue
|
1493
|
+
}
|
1494
|
+
for _, c := range comments {
|
1495
|
+
if start <= c.Slash && c.Slash < end && !strings.HasPrefix(c.Text, "// MATCH ") {
|
1496
|
+
return true
|
1497
|
+
}
|
1498
|
+
}
|
1499
|
+
}
|
1500
|
+
return false
|
1501
|
+
}
|
1502
|
+
|
1503
|
+
func (f *file) lintIfError() {
|
1504
|
+
f.walk(func(node ast.Node) bool {
|
1505
|
+
switch v := node.(type) {
|
1506
|
+
case *ast.BlockStmt:
|
1507
|
+
for i := 0; i < len(v.List)-1; i++ {
|
1508
|
+
// if var := whatever; var != nil { return var }
|
1509
|
+
s, ok := v.List[i].(*ast.IfStmt)
|
1510
|
+
if !ok || s.Body == nil || len(s.Body.List) != 1 || s.Else != nil {
|
1511
|
+
continue
|
1512
|
+
}
|
1513
|
+
assign, ok := s.Init.(*ast.AssignStmt)
|
1514
|
+
if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.DEFINE || assign.Tok == token.ASSIGN) {
|
1515
|
+
continue
|
1516
|
+
}
|
1517
|
+
id, ok := assign.Lhs[0].(*ast.Ident)
|
1518
|
+
if !ok {
|
1519
|
+
continue
|
1520
|
+
}
|
1521
|
+
expr, ok := s.Cond.(*ast.BinaryExpr)
|
1522
|
+
if !ok || expr.Op != token.NEQ {
|
1523
|
+
continue
|
1524
|
+
}
|
1525
|
+
if lhs, ok := expr.X.(*ast.Ident); !ok || lhs.Name != id.Name {
|
1526
|
+
continue
|
1527
|
+
}
|
1528
|
+
if rhs, ok := expr.Y.(*ast.Ident); !ok || rhs.Name != "nil" {
|
1529
|
+
continue
|
1530
|
+
}
|
1531
|
+
r, ok := s.Body.List[0].(*ast.ReturnStmt)
|
1532
|
+
if !ok || len(r.Results) != 1 {
|
1533
|
+
continue
|
1534
|
+
}
|
1535
|
+
if r, ok := r.Results[0].(*ast.Ident); !ok || r.Name != id.Name {
|
1536
|
+
continue
|
1537
|
+
}
|
1538
|
+
|
1539
|
+
// return nil
|
1540
|
+
r, ok = v.List[i+1].(*ast.ReturnStmt)
|
1541
|
+
if !ok || len(r.Results) != 1 {
|
1542
|
+
continue
|
1543
|
+
}
|
1544
|
+
if r, ok := r.Results[0].(*ast.Ident); !ok || r.Name != "nil" {
|
1545
|
+
continue
|
1546
|
+
}
|
1547
|
+
|
1548
|
+
// check if there are any comments explaining the construct, don't emit an error if there are some.
|
1549
|
+
if f.containsComments(s.Pos(), r.Pos()) {
|
1550
|
+
continue
|
1551
|
+
}
|
1552
|
+
|
1553
|
+
f.errorf(v.List[i], 0.9, "redundant if ...; err != nil check, just return error instead.")
|
1554
|
+
}
|
1555
|
+
}
|
1556
|
+
return true
|
1557
|
+
})
|
1558
|
+
}
|
1559
|
+
|
1560
|
+
// receiverType returns the named type of the method receiver, sans "*",
|
1561
|
+
// or "invalid-type" if fn.Recv is ill formed.
|
1562
|
+
func receiverType(fn *ast.FuncDecl) string {
|
1563
|
+
switch e := fn.Recv.List[0].Type.(type) {
|
1564
|
+
case *ast.Ident:
|
1565
|
+
return e.Name
|
1566
|
+
case *ast.StarExpr:
|
1567
|
+
if id, ok := e.X.(*ast.Ident); ok {
|
1568
|
+
return id.Name
|
1569
|
+
}
|
1570
|
+
}
|
1571
|
+
// The parser accepts much more than just the legal forms.
|
1572
|
+
return "invalid-type"
|
1573
|
+
}
|
1574
|
+
|
1575
|
+
func (f *file) walk(fn func(ast.Node) bool) {
|
1576
|
+
ast.Walk(walker(fn), f.f)
|
1577
|
+
}
|
1578
|
+
|
1579
|
+
func (f *file) render(x interface{}) string {
|
1580
|
+
var buf bytes.Buffer
|
1581
|
+
if err := printer.Fprint(&buf, f.fset, x); err != nil {
|
1582
|
+
panic(err)
|
1583
|
+
}
|
1584
|
+
return buf.String()
|
1585
|
+
}
|
1586
|
+
|
1587
|
+
func (f *file) debugRender(x interface{}) string {
|
1588
|
+
var buf bytes.Buffer
|
1589
|
+
if err := ast.Fprint(&buf, f.fset, x, nil); err != nil {
|
1590
|
+
panic(err)
|
1591
|
+
}
|
1592
|
+
return buf.String()
|
1593
|
+
}
|
1594
|
+
|
1595
|
+
// walker adapts a function to satisfy the ast.Visitor interface.
|
1596
|
+
// The function return whether the walk should proceed into the node's children.
|
1597
|
+
type walker func(ast.Node) bool
|
1598
|
+
|
1599
|
+
func (w walker) Visit(node ast.Node) ast.Visitor {
|
1600
|
+
if w(node) {
|
1601
|
+
return w
|
1602
|
+
}
|
1603
|
+
return nil
|
1604
|
+
}
|
1605
|
+
|
1606
|
+
func isIdent(expr ast.Expr, ident string) bool {
|
1607
|
+
id, ok := expr.(*ast.Ident)
|
1608
|
+
return ok && id.Name == ident
|
1609
|
+
}
|
1610
|
+
|
1611
|
+
// isBlank returns whether id is the blank identifier "_".
|
1612
|
+
// If id == nil, the answer is false.
|
1613
|
+
func isBlank(id *ast.Ident) bool { return id != nil && id.Name == "_" }
|
1614
|
+
|
1615
|
+
func isPkgDot(expr ast.Expr, pkg, name string) bool {
|
1616
|
+
sel, ok := expr.(*ast.SelectorExpr)
|
1617
|
+
return ok && isIdent(sel.X, pkg) && isIdent(sel.Sel, name)
|
1618
|
+
}
|
1619
|
+
|
1620
|
+
func isOne(expr ast.Expr) bool {
|
1621
|
+
lit, ok := expr.(*ast.BasicLit)
|
1622
|
+
return ok && lit.Kind == token.INT && lit.Value == "1"
|
1623
|
+
}
|
1624
|
+
|
1625
|
+
func isCgoExported(f *ast.FuncDecl) bool {
|
1626
|
+
if f.Recv != nil || f.Doc == nil {
|
1627
|
+
return false
|
1628
|
+
}
|
1629
|
+
|
1630
|
+
cgoExport := regexp.MustCompile(fmt.Sprintf("(?m)^//export %s$", regexp.QuoteMeta(f.Name.Name)))
|
1631
|
+
for _, c := range f.Doc.List {
|
1632
|
+
if cgoExport.MatchString(c.Text) {
|
1633
|
+
return true
|
1634
|
+
}
|
1635
|
+
}
|
1636
|
+
return false
|
1637
|
+
}
|
1638
|
+
|
1639
|
+
var basicTypeKinds = map[types.BasicKind]string{
|
1640
|
+
types.UntypedBool: "bool",
|
1641
|
+
types.UntypedInt: "int",
|
1642
|
+
types.UntypedRune: "rune",
|
1643
|
+
types.UntypedFloat: "float64",
|
1644
|
+
types.UntypedComplex: "complex128",
|
1645
|
+
types.UntypedString: "string",
|
1646
|
+
}
|
1647
|
+
|
1648
|
+
// isUntypedConst reports whether expr is an untyped constant,
|
1649
|
+
// and indicates what its default type is.
|
1650
|
+
// scope may be nil.
|
1651
|
+
func (f *file) isUntypedConst(expr ast.Expr) (defType string, ok bool) {
|
1652
|
+
// Re-evaluate expr outside of its context to see if it's untyped.
|
1653
|
+
// (An expr evaluated within, for example, an assignment context will get the type of the LHS.)
|
1654
|
+
exprStr := f.render(expr)
|
1655
|
+
tv, err := types.Eval(f.fset, f.pkg.typesPkg, expr.Pos(), exprStr)
|
1656
|
+
if err != nil {
|
1657
|
+
return "", false
|
1658
|
+
}
|
1659
|
+
if b, ok := tv.Type.(*types.Basic); ok {
|
1660
|
+
if dt, ok := basicTypeKinds[b.Kind()]; ok {
|
1661
|
+
return dt, true
|
1662
|
+
}
|
1663
|
+
}
|
1664
|
+
|
1665
|
+
return "", false
|
1666
|
+
}
|
1667
|
+
|
1668
|
+
// firstLineOf renders the given node and returns its first line.
|
1669
|
+
// It will also match the indentation of another node.
|
1670
|
+
func (f *file) firstLineOf(node, match ast.Node) string {
|
1671
|
+
line := f.render(node)
|
1672
|
+
if i := strings.Index(line, "\n"); i >= 0 {
|
1673
|
+
line = line[:i]
|
1674
|
+
}
|
1675
|
+
return f.indentOf(match) + line
|
1676
|
+
}
|
1677
|
+
|
1678
|
+
func (f *file) indentOf(node ast.Node) string {
|
1679
|
+
line := srcLine(f.src, f.fset.Position(node.Pos()))
|
1680
|
+
for i, r := range line {
|
1681
|
+
switch r {
|
1682
|
+
case ' ', '\t':
|
1683
|
+
default:
|
1684
|
+
return line[:i]
|
1685
|
+
}
|
1686
|
+
}
|
1687
|
+
return line // unusual or empty line
|
1688
|
+
}
|
1689
|
+
|
1690
|
+
func (f *file) srcLineWithMatch(node ast.Node, pattern string) (m []string) {
|
1691
|
+
line := srcLine(f.src, f.fset.Position(node.Pos()))
|
1692
|
+
line = strings.TrimSuffix(line, "\n")
|
1693
|
+
rx := regexp.MustCompile(pattern)
|
1694
|
+
return rx.FindStringSubmatch(line)
|
1695
|
+
}
|
1696
|
+
|
1697
|
+
// srcLine returns the complete line at p, including the terminating newline.
|
1698
|
+
func srcLine(src []byte, p token.Position) string {
|
1699
|
+
// Run to end of line in both directions if not at line start/end.
|
1700
|
+
lo, hi := p.Offset, p.Offset+1
|
1701
|
+
for lo > 0 && src[lo-1] != '\n' {
|
1702
|
+
lo--
|
1703
|
+
}
|
1704
|
+
for hi < len(src) && src[hi-1] != '\n' {
|
1705
|
+
hi++
|
1706
|
+
}
|
1707
|
+
return string(src[lo:hi])
|
1708
|
+
}
|