Resolve #146: prealloc linter support
This commit is contained in:
parent
815d13148f
commit
894ba0df9f
@ -128,6 +128,15 @@ linters-settings:
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 30
|
||||
prealloc:
|
||||
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||
# For most programs usage of prealloc will be a premature optimization.
|
||||
|
||||
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||
# True by default.
|
||||
simple: true
|
||||
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||
|
||||
|
||||
linters:
|
||||
@ -136,7 +145,8 @@ linters:
|
||||
- govet
|
||||
enable-all: false
|
||||
disable:
|
||||
maligned
|
||||
- maligned
|
||||
- prealloc
|
||||
disable-all: false
|
||||
presets:
|
||||
- bugs
|
||||
|
@ -25,3 +25,4 @@ linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- maligned
|
||||
- prealloc
|
8
Gopkg.lock
generated
8
Gopkg.lock
generated
@ -126,6 +126,12 @@
|
||||
packages = ["."]
|
||||
revision = "b1d89398deca2fd3f8578e5a9551e819bd01ca5f"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/prealloc"
|
||||
packages = ["."]
|
||||
revision = "215b22d4de21190b80ce05e7d8466677c1aa3223"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
name = "github.com/golangci/revgrep"
|
||||
@ -431,6 +437,6 @@
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "9844330f4bbfb7620dd00fce3bf03f3aae60f82de82764e817881e76a20888f6"
|
||||
inputs-digest = "41fa8e7a45473f77e2e6b40c275982a7a58dad227b21501ed145f41a1aa20bc9"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
16
README.md
16
README.md
@ -127,6 +127,7 @@ misspell: Finds commonly misspelled English words in comments [fast: true]
|
||||
lll: Reports long lines [fast: true]
|
||||
unparam: Reports unused function parameters [fast: false]
|
||||
nakedret: Finds naked returns in functions greater than a specified function length [fast: true]
|
||||
prealloc: Finds slice declarations that could potentially be preallocated [fast: true]
|
||||
```
|
||||
|
||||
Pass `-E/--enable` to enable linter and `-D/--disable` to disable:
|
||||
@ -313,6 +314,7 @@ golangci-lint linters
|
||||
- [lll](https://github.com/walle/lll) - Reports long lines
|
||||
- [unparam](https://github.com/mvdan/unparam) - Reports unused function parameters
|
||||
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns in functions greater than a specified function length
|
||||
- [prealloc](https://github.com/alexkohler/prealloc) - Finds slice declarations that could potentially be preallocated
|
||||
|
||||
# Configuration
|
||||
The config file has lower priority than command-line options. If the same bool/string/int option is provided on the command-line
|
||||
@ -539,6 +541,15 @@ linters-settings:
|
||||
nakedret:
|
||||
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
|
||||
max-func-lines: 30
|
||||
prealloc:
|
||||
# XXX: we don't recommend using this linter before doing performance profiling.
|
||||
# For most programs usage of prealloc will be a premature optimization.
|
||||
|
||||
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
|
||||
# True by default.
|
||||
simple: true
|
||||
range-loops: true # Report preallocation suggestions on range loops, true by default
|
||||
for-loops: false # Report preallocation suggestions on for loops, false by default
|
||||
|
||||
|
||||
linters:
|
||||
@ -547,7 +558,8 @@ linters:
|
||||
- govet
|
||||
enable-all: false
|
||||
disable:
|
||||
maligned
|
||||
- maligned
|
||||
- prealloc
|
||||
disable-all: false
|
||||
presets:
|
||||
- bugs
|
||||
@ -620,6 +632,7 @@ linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- maligned
|
||||
- prealloc
|
||||
```
|
||||
|
||||
# False Positives
|
||||
@ -726,6 +739,7 @@ There is the most valuable changes log:
|
||||
1. Add support of the next linters:
|
||||
* unparam
|
||||
* misspell
|
||||
* prealloc
|
||||
* nakedret
|
||||
* lll
|
||||
* depguard
|
||||
|
@ -388,6 +388,7 @@ There is the most valuable changes log:
|
||||
1. Add support of the next linters:
|
||||
* unparam
|
||||
* misspell
|
||||
* prealloc
|
||||
* nakedret
|
||||
* lll
|
||||
* depguard
|
||||
|
@ -163,19 +163,26 @@ type LintersSettings struct {
|
||||
Lll LllSettings
|
||||
Unparam UnparamSettings
|
||||
Nakedret NakedretSettings
|
||||
Prealloc PreallocSettings
|
||||
}
|
||||
|
||||
type LllSettings struct {
|
||||
LineLength int `mapstructure:"line-length"`
|
||||
}
|
||||
|
||||
type UnparamSettings struct {
|
||||
CheckExported bool `mapstructure:"check-exported"`
|
||||
Algo string
|
||||
}
|
||||
|
||||
type NakedretSettings struct {
|
||||
MaxFuncLines int `mapstructure:"max-func-lines"`
|
||||
}
|
||||
|
||||
type UnparamSettings struct {
|
||||
CheckExported bool `mapstructure:"check-exported"`
|
||||
Algo string
|
||||
type PreallocSettings struct {
|
||||
Simple bool
|
||||
RangeLoops bool `mapstructure:"range-loops"`
|
||||
ForLoops bool `mapstructure:"for-loops"`
|
||||
}
|
||||
|
||||
var defaultLintersSettings = LintersSettings{
|
||||
@ -188,6 +195,11 @@ var defaultLintersSettings = LintersSettings{
|
||||
Nakedret: NakedretSettings{
|
||||
MaxFuncLines: 30,
|
||||
},
|
||||
Prealloc: PreallocSettings{
|
||||
Simple: true,
|
||||
RangeLoops: true,
|
||||
ForLoops: false,
|
||||
},
|
||||
}
|
||||
|
||||
type Linters struct {
|
||||
|
39
pkg/golinters/prealloc.go
Normal file
39
pkg/golinters/prealloc.go
Normal file
@ -0,0 +1,39 @@
|
||||
package golinters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/lint/linter"
|
||||
"github.com/golangci/golangci-lint/pkg/result"
|
||||
"github.com/golangci/prealloc"
|
||||
)
|
||||
|
||||
type Prealloc struct{}
|
||||
|
||||
func (Prealloc) Name() string {
|
||||
return "prealloc"
|
||||
}
|
||||
|
||||
func (Prealloc) Desc() string {
|
||||
return "Finds slice declarations that could potentially be preallocated"
|
||||
}
|
||||
|
||||
func (lint Prealloc) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
|
||||
var res []result.Issue
|
||||
|
||||
s := &lintCtx.Settings().Prealloc
|
||||
for _, f := range lintCtx.ASTCache.GetAllValidFiles() {
|
||||
hints := prealloc.Check([]*ast.File{f.F}, s.Simple, s.RangeLoops, s.ForLoops)
|
||||
for _, hint := range hints {
|
||||
res = append(res, result.Issue{
|
||||
Pos: f.Fset.Position(hint.Pos),
|
||||
Text: fmt.Sprintf("Consider preallocating %s", formatCode(hint.DeclaredSliceName, lintCtx.Cfg)),
|
||||
FromLinter: lint.Name(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
@ -181,6 +181,10 @@ func GetAllSupportedLinterConfigs() []linter.Config {
|
||||
WithPresets(linter.PresetComplexity).
|
||||
WithSpeed(10).
|
||||
WithURL("https://github.com/alexkohler/nakedret"),
|
||||
linter.NewConfig(golinters.Prealloc{}).
|
||||
WithPresets(linter.PresetPerformance).
|
||||
WithSpeed(8).
|
||||
WithURL("https://github.com/alexkohler/prealloc"),
|
||||
}
|
||||
|
||||
if os.Getenv("GOLANGCI_COM_RUN") == "1" {
|
||||
@ -193,6 +197,7 @@ func GetAllSupportedLinterConfigs() []linter.Config {
|
||||
golinters.Lll{}.Name(): true, // annoying
|
||||
golinters.Unparam{}.Name(): true, // need to check it first
|
||||
golinters.Nakedret{}.Name(): true, // annoying
|
||||
golinters.Prealloc{}.Name(): true, // annoying
|
||||
}
|
||||
return enableLinterConfigs(lcs, func(lc *linter.Config) bool {
|
||||
return !disabled[lc.Linter.Name()]
|
||||
|
11
test/testdata/prealloc.go
vendored
Normal file
11
test/testdata/prealloc.go
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// args: -Eprealloc
|
||||
package testdata
|
||||
|
||||
func Prealloc(source []int) []int {
|
||||
var dest []int // ERROR "Consider preallocating `dest`"
|
||||
for _, v := range source {
|
||||
dest = append(dest, v)
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
21
vendor/github.com/golangci/prealloc/LICENSE
generated
vendored
Normal file
21
vendor/github.com/golangci/prealloc/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Alex Kohler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
198
vendor/github.com/golangci/prealloc/README.md
generated
vendored
Normal file
198
vendor/github.com/golangci/prealloc/README.md
generated
vendored
Normal file
@ -0,0 +1,198 @@
|
||||
# prealloc
|
||||
|
||||
prealloc is a Go static analysis tool to find slice declarations that could potentially be preallocated.
|
||||
|
||||
## Installation
|
||||
|
||||
go get -u github.com/alexkohler/prealloc
|
||||
|
||||
## Usage
|
||||
|
||||
Similar to other Go static analysis tools (such as golint, go vet), prealloc can be invoked with one or more filenames, directories, or packages named by its import path. Prealloc also supports the `...` wildcard.
|
||||
|
||||
prealloc [flags] files/directories/packages
|
||||
|
||||
### Flags
|
||||
- **-simple** (default true) - Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. Setting this to false may increase false positives.
|
||||
- **-rangeloops** (default true) - Report preallocation suggestions on range loops.
|
||||
- **-forloops** (default false) - Report preallocation suggestions on for loops. This is false by default due to there generally being weirder things happening inside for loops (at least from what I've observed in the Standard Library).
|
||||
- **-set_exit_status** (default false) - Set exit status to 1 if any issues are found.
|
||||
|
||||
## Purpose
|
||||
|
||||
While the [Go *does* attempt to avoid reallocation by growing the capacity in advance](https://github.com/golang/go/blob/87e48c5afdcf5e01bb2b7f51b7643e8901f4b7f9/src/runtime/slice.go#L100-L112), this sometimes isn't enough for longer slices. If the size of a slice is known at the time of its creation, it should be specified.
|
||||
|
||||
Consider the following benchmark: (this can be found in prealloc_test.go in this repo)
|
||||
|
||||
```Go
|
||||
import "testing"
|
||||
|
||||
func BenchmarkNoPreallocate(b *testing.B) {
|
||||
existing := make([]int64, 10, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Don't preallocate our initial slice
|
||||
var init []int64
|
||||
for _, element := range existing {
|
||||
init = append(init, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPreallocate(b *testing.B) {
|
||||
existing := make([]int64, 10, 10)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Preallocate our initial slice
|
||||
init := make([]int64, 0, len(existing))
|
||||
for _, element := range existing {
|
||||
init = append(init, element)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```Bash
|
||||
$ go test -bench=. -benchmem
|
||||
goos: linux
|
||||
goarch: amd64
|
||||
BenchmarkNoPreallocate-4 3000000 510 ns/op 248 B/op 5 allocs/op
|
||||
BenchmarkPreallocate-4 20000000 111 ns/op 80 B/op 1 allocs/op
|
||||
```
|
||||
|
||||
As you can see, not preallocating can cause a performance hit, primarily due to Go having to reallocate the underlying array. The pattern benchmarked above is common in Go: declare a slice, then write some sort of range or for loop that appends or indexes into it. The purpose of this tool is to flag slice/loop declarations like the one in `BenchmarkNoPreallocate`.
|
||||
|
||||
## Example
|
||||
|
||||
Some examples from the Go 1.9.2 source:
|
||||
|
||||
```Bash
|
||||
$ prealloc go/src/....
|
||||
archive/tar/reader_test.go:854 Consider preallocating ss
|
||||
archive/zip/zip_test.go:201 Consider preallocating all
|
||||
cmd/api/goapi.go:301 Consider preallocating missing
|
||||
cmd/api/goapi.go:476 Consider preallocating files
|
||||
cmd/asm/internal/asm/endtoend_test.go:345 Consider preallocating extra
|
||||
cmd/cgo/main.go:60 Consider preallocating ks
|
||||
cmd/cgo/ast.go:149 Consider preallocating pieces
|
||||
cmd/compile/internal/ssa/flagalloc.go:64 Consider preallocating oldSched
|
||||
cmd/compile/internal/ssa/regalloc.go:719 Consider preallocating phis
|
||||
cmd/compile/internal/ssa/regalloc.go:718 Consider preallocating oldSched
|
||||
cmd/compile/internal/ssa/regalloc.go:1674 Consider preallocating oldSched
|
||||
cmd/compile/internal/ssa/gen/rulegen.go:145 Consider preallocating ops
|
||||
cmd/compile/internal/ssa/gen/rulegen.go:145 Consider preallocating ops
|
||||
cmd/dist/build.go:893 Consider preallocating all
|
||||
cmd/dist/build.go:1246 Consider preallocating plats
|
||||
cmd/dist/build.go:1264 Consider preallocating results
|
||||
cmd/dist/buildgo.go:59 Consider preallocating list
|
||||
cmd/doc/pkg.go:363 Consider preallocating names
|
||||
cmd/fix/typecheck.go:219 Consider preallocating b
|
||||
cmd/go/internal/base/path.go:34 Consider preallocating out
|
||||
cmd/go/internal/get/get.go:175 Consider preallocating out
|
||||
cmd/go/internal/load/pkg.go:1894 Consider preallocating dirent
|
||||
cmd/go/internal/work/build.go:2402 Consider preallocating absOfiles
|
||||
cmd/go/internal/work/build.go:2731 Consider preallocating absOfiles
|
||||
cmd/internal/objfile/pe.go:48 Consider preallocating syms
|
||||
cmd/internal/objfile/pe.go:38 Consider preallocating addrs
|
||||
cmd/internal/objfile/goobj.go:43 Consider preallocating syms
|
||||
cmd/internal/objfile/elf.go:35 Consider preallocating syms
|
||||
cmd/link/internal/ld/lib.go:1070 Consider preallocating argv
|
||||
cmd/vet/all/main.go:91 Consider preallocating pp
|
||||
database/sql/sql.go:66 Consider preallocating list
|
||||
debug/macho/file.go:506 Consider preallocating all
|
||||
internal/trace/order.go:55 Consider preallocating batches
|
||||
mime/quotedprintable/reader_test.go:191 Consider preallocating outcomes
|
||||
net/dnsclient_unix_test.go:954 Consider preallocating confLines
|
||||
net/interface_solaris.go:85 Consider preallocating ifat
|
||||
net/interface_linux_test.go:91 Consider preallocating ifmat4
|
||||
net/interface_linux_test.go:100 Consider preallocating ifmat6
|
||||
net/internal/socktest/switch.go:34 Consider preallocating st
|
||||
os/os_windows_test.go:766 Consider preallocating args
|
||||
runtime/pprof/internal/profile/filter.go:77 Consider preallocating lines
|
||||
runtime/pprof/internal/profile/profile.go:554 Consider preallocating names
|
||||
text/template/parse/node.go:189 Consider preallocating decl
|
||||
```
|
||||
|
||||
```Go
|
||||
// cmd/api/goapi.go:301
|
||||
var missing []string
|
||||
for feature := range optionalSet {
|
||||
missing = append(missing, feature)
|
||||
}
|
||||
|
||||
// cmd/fix/typecheck.go:219
|
||||
var b []ast.Expr
|
||||
for _, x := range a {
|
||||
b = append(b, x)
|
||||
}
|
||||
|
||||
// net/internal/socktest/switch.go:34
|
||||
var st []Stat
|
||||
sw.smu.RLock()
|
||||
for _, s := range sw.stats {
|
||||
ns := *s
|
||||
st = append(st, ns)
|
||||
}
|
||||
sw.smu.RUnlock()
|
||||
|
||||
// cmd/api/goapi.go:301
|
||||
var missing []string
|
||||
for feature := range optionalSet {
|
||||
missing = append(missing, feature)
|
||||
}
|
||||
```
|
||||
|
||||
Even if the size the slice is being preallocated to is small, there's still a performance gain to be had in explicitly specifying the capacity rather than leaving it up to `append` to discover that it needs to preallocate. Of course, preallocation doesn't need to be done *everywhere*. This tool's job is just to help suggest places where one should consider preallocating.
|
||||
|
||||
## How do I fix prealloc's suggestions?
|
||||
|
||||
During the declaration of your slice, rather than using the zero value of the slice with `var`, initialize it with Go's built-in `make` function, passing the appropriate type and length. This length will generally be whatever you are ranging over. Fixing the examples from above would look like so:
|
||||
|
||||
```Go
|
||||
// cmd/api/goapi.go:301
|
||||
missing := make([]string, 0, len(optionalSet))
|
||||
for feature := range optionalSet {
|
||||
missing = append(missing, feature)
|
||||
}
|
||||
|
||||
// cmd/fix/typecheck.go:219
|
||||
b := make([]ast.Expr, 0, len(a))
|
||||
for _, x := range a {
|
||||
b = append(b, x)
|
||||
}
|
||||
|
||||
// net/internal/socktest/switch.go:34
|
||||
st := make([]Stat, 0, len(sw.stats))
|
||||
sw.smu.RLock()
|
||||
for _, s := range sw.stats {
|
||||
ns := *s
|
||||
st = append(st, ns)
|
||||
}
|
||||
sw.smu.RUnlock()
|
||||
|
||||
// cmd/api/goapi.go:301
|
||||
missing := make ([]string, 0, len(optionalSet))
|
||||
for feature := range optionalSet {
|
||||
missing = append(missing, feature)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## TODO
|
||||
|
||||
- Configuration on whether or not to run on test files
|
||||
- Support for embedded ifs (currently, prealloc will only find breaks/returns/continues/gotos if they are in a single if block, I'd like to expand this to supporting multiple if blocks in the future).
|
||||
- Globbing support (e.g. prealloc *.go)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests welcome!
|
||||
|
||||
|
||||
## Other static analysis tools
|
||||
|
||||
If you've enjoyed prealloc, take a look at my other static anaylsis tools!
|
||||
- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns.
|
||||
- [unimport](https://github.com/alexkohler/unimport) - Finds unnecessary import aliases.
|
310
vendor/github.com/golangci/prealloc/import.go
generated
vendored
Normal file
310
vendor/github.com/golangci/prealloc/import.go
generated
vendored
Normal file
@ -0,0 +1,310 @@
|
||||
package prealloc
|
||||
|
||||
/*
|
||||
|
||||
This file holds a direct copy of the import path matching code of
|
||||
https://github.com/golang/go/blob/master/src/cmd/go/main.go. It can be
|
||||
replaced when https://golang.org/issue/8768 is resolved.
|
||||
|
||||
It has been updated to follow upstream changes in a few ways.
|
||||
|
||||
*/
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/build"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var buildContext = build.Default
|
||||
|
||||
var (
|
||||
goroot = filepath.Clean(runtime.GOROOT())
|
||||
gorootSrc = filepath.Join(goroot, "src")
|
||||
)
|
||||
|
||||
// importPathsNoDotExpansion returns the import paths to use for the given
|
||||
// command line, but it does no ... expansion.
|
||||
func importPathsNoDotExpansion(args []string) []string {
|
||||
if len(args) == 0 {
|
||||
return []string{"."}
|
||||
}
|
||||
var out []string
|
||||
for _, a := range args {
|
||||
// Arguments are supposed to be import paths, but
|
||||
// as a courtesy to Windows developers, rewrite \ to /
|
||||
// in command-line arguments. Handles .\... and so on.
|
||||
if filepath.Separator == '\\' {
|
||||
a = strings.Replace(a, `\`, `/`, -1)
|
||||
}
|
||||
|
||||
// Put argument in canonical form, but preserve leading ./.
|
||||
if strings.HasPrefix(a, "./") {
|
||||
a = "./" + path.Clean(a)
|
||||
if a == "./." {
|
||||
a = "."
|
||||
}
|
||||
} else {
|
||||
a = path.Clean(a)
|
||||
}
|
||||
if a == "all" || a == "std" {
|
||||
out = append(out, allPackages(a)...)
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// importPaths returns the import paths to use for the given command line.
|
||||
func importPaths(args []string) []string {
|
||||
args = importPathsNoDotExpansion(args)
|
||||
var out []string
|
||||
for _, a := range args {
|
||||
if strings.Contains(a, "...") {
|
||||
if build.IsLocalImport(a) {
|
||||
out = append(out, allPackagesInFS(a)...)
|
||||
} else {
|
||||
out = append(out, allPackages(a)...)
|
||||
}
|
||||
continue
|
||||
}
|
||||
out = append(out, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// matchPattern(pattern)(name) reports whether
|
||||
// name matches pattern. Pattern is a limited glob
|
||||
// pattern in which '...' means 'any string' and there
|
||||
// is no other special syntax.
|
||||
func matchPattern(pattern string) func(name string) bool {
|
||||
re := regexp.QuoteMeta(pattern)
|
||||
re = strings.Replace(re, `\.\.\.`, `.*`, -1)
|
||||
// Special case: foo/... matches foo too.
|
||||
if strings.HasSuffix(re, `/.*`) {
|
||||
re = re[:len(re)-len(`/.*`)] + `(/.*)?`
|
||||
}
|
||||
reg := regexp.MustCompile(`^` + re + `$`)
|
||||
return func(name string) bool {
|
||||
return reg.MatchString(name)
|
||||
}
|
||||
}
|
||||
|
||||
// hasPathPrefix reports whether the path s begins with the
|
||||
// elements in prefix.
|
||||
func hasPathPrefix(s, prefix string) bool {
|
||||
switch {
|
||||
default:
|
||||
return false
|
||||
case len(s) == len(prefix):
|
||||
return s == prefix
|
||||
case len(s) > len(prefix):
|
||||
if prefix != "" && prefix[len(prefix)-1] == '/' {
|
||||
return strings.HasPrefix(s, prefix)
|
||||
}
|
||||
return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
|
||||
}
|
||||
}
|
||||
|
||||
// treeCanMatchPattern(pattern)(name) reports whether
|
||||
// name or children of name can possibly match pattern.
|
||||
// Pattern is the same limited glob accepted by matchPattern.
|
||||
func treeCanMatchPattern(pattern string) func(name string) bool {
|
||||
wildCard := false
|
||||
if i := strings.Index(pattern, "..."); i >= 0 {
|
||||
wildCard = true
|
||||
pattern = pattern[:i]
|
||||
}
|
||||
return func(name string) bool {
|
||||
return len(name) <= len(pattern) && hasPathPrefix(pattern, name) ||
|
||||
wildCard && strings.HasPrefix(name, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
// allPackages returns all the packages that can be found
|
||||
// under the $GOPATH directories and $GOROOT matching pattern.
|
||||
// The pattern is either "all" (all packages), "std" (standard packages)
|
||||
// or a path including "...".
|
||||
func allPackages(pattern string) []string {
|
||||
pkgs := matchPackages(pattern)
|
||||
if len(pkgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func matchPackages(pattern string) []string {
|
||||
match := func(string) bool { return true }
|
||||
treeCanMatch := func(string) bool { return true }
|
||||
if pattern != "all" && pattern != "std" {
|
||||
match = matchPattern(pattern)
|
||||
treeCanMatch = treeCanMatchPattern(pattern)
|
||||
}
|
||||
|
||||
have := map[string]bool{
|
||||
"builtin": true, // ignore pseudo-package that exists only for documentation
|
||||
}
|
||||
if !buildContext.CgoEnabled {
|
||||
have["runtime/cgo"] = true // ignore during walk
|
||||
}
|
||||
var pkgs []string
|
||||
|
||||
// Commands
|
||||
cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator)
|
||||
filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() || path == cmd {
|
||||
return nil
|
||||
}
|
||||
name := path[len(cmd):]
|
||||
if !treeCanMatch(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
// Commands are all in cmd/, not in subdirectories.
|
||||
if strings.Contains(name, string(filepath.Separator)) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// We use, e.g., cmd/gofmt as the pseudo import path for gofmt.
|
||||
name = "cmd/" + name
|
||||
if have[name] {
|
||||
return nil
|
||||
}
|
||||
have[name] = true
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
_, err = buildContext.ImportDir(path, 0)
|
||||
if err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); !noGo {
|
||||
log.Print(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
|
||||
for _, src := range buildContext.SrcDirs() {
|
||||
if (pattern == "std" || pattern == "cmd") && src != gorootSrc {
|
||||
continue
|
||||
}
|
||||
src = filepath.Clean(src) + string(filepath.Separator)
|
||||
root := src
|
||||
if pattern == "cmd" {
|
||||
root += "cmd" + string(filepath.Separator)
|
||||
}
|
||||
filepath.Walk(root, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() || path == src {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Avoid .foo, _foo, testdata and vendor directory trees.
|
||||
_, elem := filepath.Split(path)
|
||||
if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
name := filepath.ToSlash(path[len(src):])
|
||||
if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") {
|
||||
// The name "std" is only the standard library.
|
||||
// If the name is cmd, it's the root of the command tree.
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if !treeCanMatch(name) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if have[name] {
|
||||
return nil
|
||||
}
|
||||
have[name] = true
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
_, err = buildContext.ImportDir(path, 0)
|
||||
if err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); noGo {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// allPackagesInFS is like allPackages but is passed a pattern
|
||||
// beginning ./ or ../, meaning it should scan the tree rooted
|
||||
// at the given directory. There are ... in the pattern too.
|
||||
func allPackagesInFS(pattern string) []string {
|
||||
pkgs := matchPackagesInFS(pattern)
|
||||
if len(pkgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern)
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
func matchPackagesInFS(pattern string) []string {
|
||||
// Find directory to begin the scan.
|
||||
// Could be smarter but this one optimization
|
||||
// is enough for now, since ... is usually at the
|
||||
// end of a path.
|
||||
i := strings.Index(pattern, "...")
|
||||
dir, _ := path.Split(pattern[:i])
|
||||
|
||||
// pattern begins with ./ or ../.
|
||||
// path.Clean will discard the ./ but not the ../.
|
||||
// We need to preserve the ./ for pattern matching
|
||||
// and in the returned import paths.
|
||||
prefix := ""
|
||||
if strings.HasPrefix(pattern, "./") {
|
||||
prefix = "./"
|
||||
}
|
||||
match := matchPattern(pattern)
|
||||
|
||||
var pkgs []string
|
||||
filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil || !fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if path == dir {
|
||||
// filepath.Walk starts at dir and recurses. For the recursive case,
|
||||
// the path is the result of filepath.Join, which calls filepath.Clean.
|
||||
// The initial case is not Cleaned, though, so we do this explicitly.
|
||||
//
|
||||
// This converts a path like "./io/" to "io". Without this step, running
|
||||
// "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io
|
||||
// package, because prepending the prefix "./" to the unclean path would
|
||||
// result in "././io", and match("././io") returns false.
|
||||
path = filepath.Clean(path)
|
||||
}
|
||||
|
||||
// Avoid .foo, _foo, testdata and vendor directory trees, but do not avoid "." or "..".
|
||||
_, elem := filepath.Split(path)
|
||||
dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".."
|
||||
if dot || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
name := prefix + filepath.ToSlash(path)
|
||||
if !match(name) {
|
||||
return nil
|
||||
}
|
||||
if _, err = build.ImportDir(path, 0); err != nil {
|
||||
if _, noGo := err.(*build.NoGoError); !noGo {
|
||||
log.Print(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pkgs = append(pkgs, name)
|
||||
return nil
|
||||
})
|
||||
return pkgs
|
||||
}
|
403
vendor/github.com/golangci/prealloc/prealloc.go
generated
vendored
Normal file
403
vendor/github.com/golangci/prealloc/prealloc.go
generated
vendored
Normal file
@ -0,0 +1,403 @@
|
||||
package prealloc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Support: (in order of priority)
|
||||
// * Full make suggestion with type?
|
||||
// * Test flag
|
||||
// * Embedded ifs?
|
||||
// * Use an import rather than the duplcated import.go
|
||||
|
||||
const (
|
||||
pwd = "./"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
log.Printf("Usage of %s:\n", os.Args[0])
|
||||
log.Printf("\nprealloc [flags] # runs on package in current directory\n")
|
||||
log.Printf("\nprealloc [flags] [packages]\n")
|
||||
log.Printf("Flags:\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
type sliceDeclaration struct {
|
||||
name string
|
||||
// sType string
|
||||
genD *ast.GenDecl
|
||||
}
|
||||
|
||||
type returnsVisitor struct {
|
||||
// flags
|
||||
simple bool
|
||||
includeRangeLoops bool
|
||||
includeForLoops bool
|
||||
// visitor fields
|
||||
sliceDeclarations []*sliceDeclaration
|
||||
preallocHints []Hint
|
||||
returnsInsideOfLoop bool
|
||||
arrayTypes []string
|
||||
}
|
||||
|
||||
func NoMain() {
|
||||
|
||||
// Remove log timestamp
|
||||
log.SetFlags(0)
|
||||
|
||||
simple := flag.Bool("simple", true, "Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them")
|
||||
includeRangeLoops := flag.Bool("rangeloops", true, "Report preallocation suggestions on range loops")
|
||||
includeForLoops := flag.Bool("forloops", false, "Report preallocation suggestions on for loops")
|
||||
setExitStatus := flag.Bool("set_exit_status", false, "Set exit status to 1 if any issues are found")
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
hints, err := checkForPreallocations(flag.Args(), simple, includeRangeLoops, includeForLoops)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
for _, hint := range hints {
|
||||
log.Println(hint)
|
||||
}
|
||||
if *setExitStatus && len(hints) > 0 {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func checkForPreallocations(args []string, simple, includeRangeLoops *bool, includeForLoops *bool) ([]Hint, error) {
|
||||
|
||||
fset := token.NewFileSet()
|
||||
|
||||
files, err := parseInput(args, fset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse input %v", err)
|
||||
}
|
||||
|
||||
if simple == nil {
|
||||
return nil, errors.New("simple nil")
|
||||
}
|
||||
|
||||
if includeRangeLoops == nil {
|
||||
return nil, errors.New("includeRangeLoops nil")
|
||||
}
|
||||
|
||||
if includeForLoops == nil {
|
||||
return nil, errors.New("includeForLoops nil")
|
||||
}
|
||||
|
||||
hints := []Hint{}
|
||||
for _, f := range files {
|
||||
retVis := &returnsVisitor{
|
||||
simple: *simple,
|
||||
includeRangeLoops: *includeRangeLoops,
|
||||
includeForLoops: *includeForLoops,
|
||||
}
|
||||
ast.Walk(retVis, f)
|
||||
// if simple is true, then we actually have to check if we had returns
|
||||
// inside of our loop. Otherwise, we can just report all messages.
|
||||
if !retVis.simple || !retVis.returnsInsideOfLoop {
|
||||
hints = append(hints, retVis.preallocHints...)
|
||||
}
|
||||
}
|
||||
|
||||
return hints, nil
|
||||
}
|
||||
|
||||
func Check(files []*ast.File, simple, includeRangeLoops, includeForLoops bool) []Hint {
|
||||
hints := []Hint{}
|
||||
for _, f := range files {
|
||||
retVis := &returnsVisitor{
|
||||
simple: simple,
|
||||
includeRangeLoops: includeRangeLoops,
|
||||
includeForLoops: includeForLoops,
|
||||
}
|
||||
ast.Walk(retVis, f)
|
||||
|
||||
// if simple is true, then we actually have to check if we had returns
|
||||
// inside of our loop. Otherwise, we can just report all messages.
|
||||
if !retVis.simple || !retVis.returnsInsideOfLoop {
|
||||
hints = append(hints, retVis.preallocHints...)
|
||||
}
|
||||
}
|
||||
|
||||
return hints
|
||||
}
|
||||
|
||||
func parseInput(args []string, fset *token.FileSet) ([]*ast.File, error) {
|
||||
var directoryList []string
|
||||
var fileMode bool
|
||||
files := make([]*ast.File, 0)
|
||||
|
||||
if len(args) == 0 {
|
||||
directoryList = append(directoryList, pwd)
|
||||
} else {
|
||||
for _, arg := range args {
|
||||
if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) {
|
||||
|
||||
for _, dirname := range allPackagesInFS(arg) {
|
||||
directoryList = append(directoryList, dirname)
|
||||
}
|
||||
|
||||
} else if isDir(arg) {
|
||||
directoryList = append(directoryList, arg)
|
||||
|
||||
} else if exists(arg) {
|
||||
if strings.HasSuffix(arg, ".go") {
|
||||
fileMode = true
|
||||
f, err := parser.ParseFile(fset, arg, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, f)
|
||||
} else {
|
||||
return nil, fmt.Errorf("invalid file %v specified", arg)
|
||||
}
|
||||
} else {
|
||||
|
||||
//TODO clean this up a bit
|
||||
imPaths := importPaths([]string{arg})
|
||||
for _, importPath := range imPaths {
|
||||
pkg, err := build.Import(importPath, ".", 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var stringFiles []string
|
||||
stringFiles = append(stringFiles, pkg.GoFiles...)
|
||||
// files = append(files, pkg.CgoFiles...)
|
||||
stringFiles = append(stringFiles, pkg.TestGoFiles...)
|
||||
if pkg.Dir != "." {
|
||||
for i, f := range stringFiles {
|
||||
stringFiles[i] = filepath.Join(pkg.Dir, f)
|
||||
}
|
||||
}
|
||||
|
||||
fileMode = true
|
||||
for _, stringFile := range stringFiles {
|
||||
f, err := parser.ParseFile(fset, stringFile, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we're not in file mode, then we need to grab each and every package in each directory
|
||||
// we can to grab all the files
|
||||
if !fileMode {
|
||||
for _, fpath := range directoryList {
|
||||
pkgs, err := parser.ParseDir(fset, fpath, nil, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, pkg := range pkgs {
|
||||
for _, f := range pkg.Files {
|
||||
files = append(files, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func isDir(filename string) bool {
|
||||
fi, err := os.Stat(filename)
|
||||
return err == nil && fi.IsDir()
|
||||
}
|
||||
|
||||
func exists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *returnsVisitor) Visit(node ast.Node) ast.Visitor {
|
||||
|
||||
v.sliceDeclarations = nil
|
||||
v.returnsInsideOfLoop = false
|
||||
|
||||
switch n := node.(type) {
|
||||
case *ast.TypeSpec:
|
||||
if _, ok := n.Type.(*ast.ArrayType); ok {
|
||||
if n.Name != nil {
|
||||
v.arrayTypes = append(v.arrayTypes, n.Name.Name)
|
||||
}
|
||||
}
|
||||
case *ast.FuncDecl:
|
||||
if n.Body != nil {
|
||||
for _, stmt := range n.Body.List {
|
||||
switch s := stmt.(type) {
|
||||
// Find non pre-allocated slices
|
||||
case *ast.DeclStmt:
|
||||
genD, ok := s.Decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if genD.Tok == token.TYPE {
|
||||
for _, spec := range genD.Specs {
|
||||
tSpec, ok := spec.(*ast.TypeSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := tSpec.Type.(*ast.ArrayType); ok {
|
||||
if tSpec.Name != nil {
|
||||
v.arrayTypes = append(v.arrayTypes, tSpec.Name.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if genD.Tok == token.VAR {
|
||||
for _, spec := range genD.Specs {
|
||||
vSpec, ok := spec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var isArrType bool
|
||||
switch val := vSpec.Type.(type) {
|
||||
case *ast.ArrayType:
|
||||
isArrType = true
|
||||
case *ast.Ident:
|
||||
isArrType = contains(v.arrayTypes, val.Name)
|
||||
}
|
||||
if isArrType {
|
||||
if vSpec.Names != nil {
|
||||
/*atID, ok := arrayType.Elt.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}*/
|
||||
|
||||
// We should handle multiple slices declared on same line e.g. var mySlice1, mySlice2 []uint32
|
||||
for _, vName := range vSpec.Names {
|
||||
v.sliceDeclarations = append(v.sliceDeclarations, &sliceDeclaration{name: vName.Name /*sType: atID.Name,*/, genD: genD})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.RangeStmt:
|
||||
if v.includeRangeLoops {
|
||||
if len(v.sliceDeclarations) == 0 {
|
||||
continue
|
||||
}
|
||||
if s.Body != nil {
|
||||
v.handleLoops(s.Body)
|
||||
}
|
||||
}
|
||||
|
||||
case *ast.ForStmt:
|
||||
if v.includeForLoops {
|
||||
if len(v.sliceDeclarations) == 0 {
|
||||
continue
|
||||
}
|
||||
if s.Body != nil {
|
||||
v.handleLoops(s.Body)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// handleLoops is a helper function to share the logic required for both *ast.RangeLoops and *ast.ForLoops
|
||||
func (v *returnsVisitor) handleLoops(blockStmt *ast.BlockStmt) {
|
||||
|
||||
for _, stmt := range blockStmt.List {
|
||||
switch bodyStmt := stmt.(type) {
|
||||
case *ast.AssignStmt:
|
||||
asgnStmt := bodyStmt
|
||||
for _, expr := range asgnStmt.Rhs {
|
||||
callExpr, ok := expr.(*ast.CallExpr)
|
||||
if !ok {
|
||||
continue // should this be break? comes back to multi-call support I think
|
||||
}
|
||||
ident, ok := callExpr.Fun.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ident.Name == "append" {
|
||||
// see if this append is appending the slice we found
|
||||
for _, lhsExpr := range asgnStmt.Lhs {
|
||||
lhsIdent, ok := lhsExpr.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, sliceDecl := range v.sliceDeclarations {
|
||||
if sliceDecl.name == lhsIdent.Name {
|
||||
// This is a potential mark, we just need to make sure there are no returns/continues in the
|
||||
// range loop.
|
||||
// now we just need to grab whatever we're ranging over
|
||||
/*sxIdent, ok := s.X.(*ast.Ident)
|
||||
if !ok {
|
||||
continue
|
||||
}*/
|
||||
|
||||
v.preallocHints = append(v.preallocHints, Hint{
|
||||
Pos: sliceDecl.genD.Pos(),
|
||||
DeclaredSliceName: sliceDecl.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
case *ast.IfStmt:
|
||||
ifStmt := bodyStmt
|
||||
if ifStmt.Body != nil {
|
||||
for _, ifBodyStmt := range ifStmt.Body.List {
|
||||
// TODO should probably handle embedded ifs here
|
||||
switch /*ift :=*/ ifBodyStmt.(type) {
|
||||
case *ast.BranchStmt, *ast.ReturnStmt:
|
||||
v.returnsInsideOfLoop = true
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Hint stores the information about an occurence of a slice that could be
|
||||
// preallocated.
|
||||
type Hint struct {
|
||||
Pos token.Pos
|
||||
DeclaredSliceName string
|
||||
}
|
||||
|
||||
func (h Hint) String() string {
|
||||
return fmt.Sprintf("%v: Consider preallocating %v", h.Pos, h.DeclaredSliceName)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user