mirror of
https://github.com/DNSCrypt/dnscrypt-proxy.git
synced 2025-04-03 13:17:37 +03:00
200 lines
4.9 KiB
Go
200 lines
4.9 KiB
Go
// makezero provides a linter for appends to slices initialized with non-zero length.
|
|
package makezero
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/printer"
|
|
"go/token"
|
|
"go/types"
|
|
"log"
|
|
"regexp"
|
|
)
|
|
|
|
type Issue interface {
|
|
Details() string
|
|
Position() token.Position
|
|
String() string
|
|
}
|
|
|
|
type AppendIssue struct {
|
|
name string
|
|
position token.Position
|
|
}
|
|
|
|
func (a AppendIssue) Details() string {
|
|
return fmt.Sprintf("append to slice `%s` with non-zero initialized length", a.name)
|
|
}
|
|
|
|
func (a AppendIssue) Position() token.Position {
|
|
return a.position
|
|
}
|
|
|
|
func (a AppendIssue) String() string { return toString(a) }
|
|
|
|
type MustHaveNonZeroInitLenIssue struct {
|
|
name string
|
|
position token.Position
|
|
}
|
|
|
|
func (i MustHaveNonZeroInitLenIssue) Details() string {
|
|
return fmt.Sprintf("slice `%s` does not have non-zero initial length", i.name)
|
|
}
|
|
|
|
func (i MustHaveNonZeroInitLenIssue) Position() token.Position {
|
|
return i.position
|
|
}
|
|
|
|
func (i MustHaveNonZeroInitLenIssue) String() string { return toString(i) }
|
|
|
|
func toString(i Issue) string {
|
|
return fmt.Sprintf("%s at %s", i.Details(), i.Position())
|
|
}
|
|
|
|
type visitor struct {
|
|
initLenMustBeZero bool
|
|
|
|
comments []*ast.CommentGroup // comments to apply during this visit
|
|
info *types.Info
|
|
|
|
nonZeroLengthSliceDecls map[interface{}]struct{}
|
|
fset *token.FileSet
|
|
issues []Issue
|
|
}
|
|
|
|
type Linter struct {
|
|
initLenMustBeZero bool
|
|
}
|
|
|
|
func NewLinter(initialLengthMustBeZero bool) *Linter {
|
|
return &Linter{
|
|
initLenMustBeZero: initialLengthMustBeZero,
|
|
}
|
|
}
|
|
|
|
func (l Linter) Run(fset *token.FileSet, info *types.Info, nodes ...ast.Node) ([]Issue, error) {
|
|
var issues []Issue // nolint:prealloc // don't know how many there will be
|
|
for _, node := range nodes {
|
|
var comments []*ast.CommentGroup
|
|
if file, ok := node.(*ast.File); ok {
|
|
comments = file.Comments
|
|
}
|
|
visitor := visitor{
|
|
nonZeroLengthSliceDecls: make(map[interface{}]struct{}),
|
|
initLenMustBeZero: l.initLenMustBeZero,
|
|
info: info,
|
|
fset: fset,
|
|
comments: comments,
|
|
}
|
|
ast.Walk(&visitor, node)
|
|
issues = append(issues, visitor.issues...)
|
|
}
|
|
return issues, nil
|
|
}
|
|
|
|
func (v *visitor) Visit(node ast.Node) ast.Visitor {
|
|
switch node := node.(type) {
|
|
case *ast.CallExpr:
|
|
fun, ok := node.Fun.(*ast.Ident)
|
|
if !ok || fun.Name != "append" {
|
|
break
|
|
}
|
|
if sliceIdent, ok := node.Args[0].(*ast.Ident); ok &&
|
|
v.hasNonZeroInitialLength(sliceIdent) &&
|
|
!v.hasNoLintOnSameLine(fun) {
|
|
v.issues = append(v.issues, AppendIssue{name: sliceIdent.Name, position: v.fset.Position(fun.Pos())})
|
|
}
|
|
case *ast.AssignStmt:
|
|
for i, right := range node.Rhs {
|
|
if right, ok := right.(*ast.CallExpr); ok {
|
|
fun, ok := right.Fun.(*ast.Ident)
|
|
if !ok || fun.Name != "make" {
|
|
continue
|
|
}
|
|
left := node.Lhs[i]
|
|
if len(right.Args) == 2 {
|
|
// ignore if not a slice or it has explicit zero length
|
|
if !v.isSlice(right.Args[0]) {
|
|
break
|
|
} else if lit, ok := right.Args[1].(*ast.BasicLit); ok && lit.Kind == token.INT && lit.Value == "0" {
|
|
break
|
|
}
|
|
if v.initLenMustBeZero && !v.hasNoLintOnSameLine(fun) {
|
|
v.issues = append(v.issues, MustHaveNonZeroInitLenIssue{
|
|
name: v.textFor(left),
|
|
position: v.fset.Position(node.Pos()),
|
|
})
|
|
}
|
|
v.recordNonZeroLengthSlices(left)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (v *visitor) textFor(node ast.Node) string {
|
|
typeBuf := new(bytes.Buffer)
|
|
if err := printer.Fprint(typeBuf, v.fset, node); err != nil {
|
|
log.Fatalf("ERROR: unable to print type: %s", err)
|
|
}
|
|
return typeBuf.String()
|
|
}
|
|
|
|
func (v *visitor) hasNonZeroInitialLength(ident *ast.Ident) bool {
|
|
if ident.Obj == nil {
|
|
log.Printf("WARNING: could not determine with %q at %s is a slice (missing object type)",
|
|
ident.Name, v.fset.Position(ident.Pos()).String())
|
|
return false
|
|
}
|
|
_, exists := v.nonZeroLengthSliceDecls[ident.Obj.Decl]
|
|
return exists
|
|
}
|
|
|
|
func (v *visitor) recordNonZeroLengthSlices(node ast.Node) {
|
|
ident, ok := node.(*ast.Ident)
|
|
if !ok {
|
|
return
|
|
}
|
|
if ident.Obj == nil {
|
|
return
|
|
}
|
|
v.nonZeroLengthSliceDecls[ident.Obj.Decl] = struct{}{}
|
|
}
|
|
|
|
func (v *visitor) isSlice(node ast.Node) bool {
|
|
// determine type if this is a user-defined type
|
|
if ident, ok := node.(*ast.Ident); ok {
|
|
obj := ident.Obj
|
|
if obj == nil {
|
|
if v.info != nil {
|
|
_, ok := v.info.ObjectOf(ident).Type().(*types.Slice)
|
|
return ok
|
|
}
|
|
return false
|
|
}
|
|
spec, ok := obj.Decl.(*ast.TypeSpec)
|
|
if !ok {
|
|
return false
|
|
}
|
|
node = spec.Type
|
|
}
|
|
|
|
if node, ok := node.(*ast.ArrayType); ok {
|
|
return node.Len == nil // only slices have zero length
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (v *visitor) hasNoLintOnSameLine(node ast.Node) bool {
|
|
var nolint = regexp.MustCompile(`^\s*nozero\b`)
|
|
nodePos := v.fset.Position(node.Pos())
|
|
for _, c := range v.comments {
|
|
commentPos := v.fset.Position(c.Pos())
|
|
if commentPos.Line == nodePos.Line && nolint.MatchString(c.Text()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|