Add staticcheck tool

This commit is contained in:
dwrz
2026-06-06 01:17:20 +00:00
parent 6fb63c8c90
commit ad58cd78ff
315 changed files with 56337 additions and 4 deletions

View File

@@ -0,0 +1,200 @@
// Code generated by generate.go. DO NOT EDIT.
package staticcheck
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/staticcheck/sa1000"
"honnef.co/go/tools/staticcheck/sa1001"
"honnef.co/go/tools/staticcheck/sa1002"
"honnef.co/go/tools/staticcheck/sa1003"
"honnef.co/go/tools/staticcheck/sa1004"
"honnef.co/go/tools/staticcheck/sa1005"
"honnef.co/go/tools/staticcheck/sa1006"
"honnef.co/go/tools/staticcheck/sa1007"
"honnef.co/go/tools/staticcheck/sa1008"
"honnef.co/go/tools/staticcheck/sa1010"
"honnef.co/go/tools/staticcheck/sa1011"
"honnef.co/go/tools/staticcheck/sa1012"
"honnef.co/go/tools/staticcheck/sa1013"
"honnef.co/go/tools/staticcheck/sa1014"
"honnef.co/go/tools/staticcheck/sa1015"
"honnef.co/go/tools/staticcheck/sa1016"
"honnef.co/go/tools/staticcheck/sa1017"
"honnef.co/go/tools/staticcheck/sa1018"
"honnef.co/go/tools/staticcheck/sa1019"
"honnef.co/go/tools/staticcheck/sa1020"
"honnef.co/go/tools/staticcheck/sa1021"
"honnef.co/go/tools/staticcheck/sa1023"
"honnef.co/go/tools/staticcheck/sa1024"
"honnef.co/go/tools/staticcheck/sa1025"
"honnef.co/go/tools/staticcheck/sa1026"
"honnef.co/go/tools/staticcheck/sa1027"
"honnef.co/go/tools/staticcheck/sa1028"
"honnef.co/go/tools/staticcheck/sa1029"
"honnef.co/go/tools/staticcheck/sa1030"
"honnef.co/go/tools/staticcheck/sa1031"
"honnef.co/go/tools/staticcheck/sa1032"
"honnef.co/go/tools/staticcheck/sa2000"
"honnef.co/go/tools/staticcheck/sa2001"
"honnef.co/go/tools/staticcheck/sa2002"
"honnef.co/go/tools/staticcheck/sa2003"
"honnef.co/go/tools/staticcheck/sa3000"
"honnef.co/go/tools/staticcheck/sa3001"
"honnef.co/go/tools/staticcheck/sa4000"
"honnef.co/go/tools/staticcheck/sa4001"
"honnef.co/go/tools/staticcheck/sa4003"
"honnef.co/go/tools/staticcheck/sa4004"
"honnef.co/go/tools/staticcheck/sa4005"
"honnef.co/go/tools/staticcheck/sa4006"
"honnef.co/go/tools/staticcheck/sa4008"
"honnef.co/go/tools/staticcheck/sa4009"
"honnef.co/go/tools/staticcheck/sa4010"
"honnef.co/go/tools/staticcheck/sa4011"
"honnef.co/go/tools/staticcheck/sa4012"
"honnef.co/go/tools/staticcheck/sa4013"
"honnef.co/go/tools/staticcheck/sa4014"
"honnef.co/go/tools/staticcheck/sa4015"
"honnef.co/go/tools/staticcheck/sa4016"
"honnef.co/go/tools/staticcheck/sa4017"
"honnef.co/go/tools/staticcheck/sa4018"
"honnef.co/go/tools/staticcheck/sa4019"
"honnef.co/go/tools/staticcheck/sa4020"
"honnef.co/go/tools/staticcheck/sa4021"
"honnef.co/go/tools/staticcheck/sa4022"
"honnef.co/go/tools/staticcheck/sa4023"
"honnef.co/go/tools/staticcheck/sa4024"
"honnef.co/go/tools/staticcheck/sa4025"
"honnef.co/go/tools/staticcheck/sa4026"
"honnef.co/go/tools/staticcheck/sa4027"
"honnef.co/go/tools/staticcheck/sa4028"
"honnef.co/go/tools/staticcheck/sa4029"
"honnef.co/go/tools/staticcheck/sa4030"
"honnef.co/go/tools/staticcheck/sa4031"
"honnef.co/go/tools/staticcheck/sa4032"
"honnef.co/go/tools/staticcheck/sa5000"
"honnef.co/go/tools/staticcheck/sa5001"
"honnef.co/go/tools/staticcheck/sa5002"
"honnef.co/go/tools/staticcheck/sa5003"
"honnef.co/go/tools/staticcheck/sa5004"
"honnef.co/go/tools/staticcheck/sa5005"
"honnef.co/go/tools/staticcheck/sa5007"
"honnef.co/go/tools/staticcheck/sa5008"
"honnef.co/go/tools/staticcheck/sa5009"
"honnef.co/go/tools/staticcheck/sa5010"
"honnef.co/go/tools/staticcheck/sa5011"
"honnef.co/go/tools/staticcheck/sa5012"
"honnef.co/go/tools/staticcheck/sa6000"
"honnef.co/go/tools/staticcheck/sa6001"
"honnef.co/go/tools/staticcheck/sa6002"
"honnef.co/go/tools/staticcheck/sa6003"
"honnef.co/go/tools/staticcheck/sa6005"
"honnef.co/go/tools/staticcheck/sa6006"
"honnef.co/go/tools/staticcheck/sa9001"
"honnef.co/go/tools/staticcheck/sa9002"
"honnef.co/go/tools/staticcheck/sa9003"
"honnef.co/go/tools/staticcheck/sa9004"
"honnef.co/go/tools/staticcheck/sa9005"
"honnef.co/go/tools/staticcheck/sa9006"
"honnef.co/go/tools/staticcheck/sa9007"
"honnef.co/go/tools/staticcheck/sa9008"
"honnef.co/go/tools/staticcheck/sa9009"
)
var Analyzers = []*lint.Analyzer{
sa1000.SCAnalyzer,
sa1001.SCAnalyzer,
sa1002.SCAnalyzer,
sa1003.SCAnalyzer,
sa1004.SCAnalyzer,
sa1005.SCAnalyzer,
sa1006.SCAnalyzer,
sa1007.SCAnalyzer,
sa1008.SCAnalyzer,
sa1010.SCAnalyzer,
sa1011.SCAnalyzer,
sa1012.SCAnalyzer,
sa1013.SCAnalyzer,
sa1014.SCAnalyzer,
sa1015.SCAnalyzer,
sa1016.SCAnalyzer,
sa1017.SCAnalyzer,
sa1018.SCAnalyzer,
sa1019.SCAnalyzer,
sa1020.SCAnalyzer,
sa1021.SCAnalyzer,
sa1023.SCAnalyzer,
sa1024.SCAnalyzer,
sa1025.SCAnalyzer,
sa1026.SCAnalyzer,
sa1027.SCAnalyzer,
sa1028.SCAnalyzer,
sa1029.SCAnalyzer,
sa1030.SCAnalyzer,
sa1031.SCAnalyzer,
sa1032.SCAnalyzer,
sa2000.SCAnalyzer,
sa2001.SCAnalyzer,
sa2002.SCAnalyzer,
sa2003.SCAnalyzer,
sa3000.SCAnalyzer,
sa3001.SCAnalyzer,
sa4000.SCAnalyzer,
sa4001.SCAnalyzer,
sa4003.SCAnalyzer,
sa4004.SCAnalyzer,
sa4005.SCAnalyzer,
sa4006.SCAnalyzer,
sa4008.SCAnalyzer,
sa4009.SCAnalyzer,
sa4010.SCAnalyzer,
sa4011.SCAnalyzer,
sa4012.SCAnalyzer,
sa4013.SCAnalyzer,
sa4014.SCAnalyzer,
sa4015.SCAnalyzer,
sa4016.SCAnalyzer,
sa4017.SCAnalyzer,
sa4018.SCAnalyzer,
sa4019.SCAnalyzer,
sa4020.SCAnalyzer,
sa4021.SCAnalyzer,
sa4022.SCAnalyzer,
sa4023.SCAnalyzer,
sa4024.SCAnalyzer,
sa4025.SCAnalyzer,
sa4026.SCAnalyzer,
sa4027.SCAnalyzer,
sa4028.SCAnalyzer,
sa4029.SCAnalyzer,
sa4030.SCAnalyzer,
sa4031.SCAnalyzer,
sa4032.SCAnalyzer,
sa5000.SCAnalyzer,
sa5001.SCAnalyzer,
sa5002.SCAnalyzer,
sa5003.SCAnalyzer,
sa5004.SCAnalyzer,
sa5005.SCAnalyzer,
sa5007.SCAnalyzer,
sa5008.SCAnalyzer,
sa5009.SCAnalyzer,
sa5010.SCAnalyzer,
sa5011.SCAnalyzer,
sa5012.SCAnalyzer,
sa6000.SCAnalyzer,
sa6001.SCAnalyzer,
sa6002.SCAnalyzer,
sa6003.SCAnalyzer,
sa6005.SCAnalyzer,
sa6006.SCAnalyzer,
sa9001.SCAnalyzer,
sa9002.SCAnalyzer,
sa9003.SCAnalyzer,
sa9004.SCAnalyzer,
sa9005.SCAnalyzer,
sa9006.SCAnalyzer,
sa9007.SCAnalyzer,
sa9008.SCAnalyzer,
sa9009.SCAnalyzer,
}

View File

@@ -0,0 +1,5 @@
//go:generate go run ../generate.go
// Package staticcheck contains analyzes that find bugs and performance issues.
// Barring the rare false positive, any code flagged by these analyzes needs to be fixed.
package staticcheck

View File

@@ -0,0 +1,370 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains a modified copy of the encoding/json encoder.
// All dynamic behavior has been removed, and reflecttion has been replaced with go/types.
// This allows us to statically find unmarshable types
// with the same rules for tags, shadowing and addressability as encoding/json.
// This is used for SA1026.
package fakejson
import (
"go/types"
"sort"
"strings"
"unicode"
"golang.org/x/exp/typeparams"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/staticcheck/fakereflect"
)
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) string {
if before, _, ok := strings.Cut(tag, ","); ok {
return before
}
return tag
}
func Marshal(v types.Type) *UnsupportedTypeError {
enc := encoder{}
return enc.newTypeEncoder(fakereflect.TypeAndCanAddr{Type: v}, "x")
}
// An UnsupportedTypeError is returned by Marshal when attempting
// to encode an unsupported value type.
type UnsupportedTypeError struct {
Type types.Type
Path string
}
type encoder struct {
// TODO we track addressable and non-addressable instances separately out of an abundance of caution. We don't know
// if this is actually required for correctness.
seenCanAddr typeutil.Map[struct{}]
seenCantAddr typeutil.Map[struct{}]
}
func (enc *encoder) newTypeEncoder(t fakereflect.TypeAndCanAddr, stack string) *UnsupportedTypeError {
var m *typeutil.Map[struct{}]
if t.CanAddr() {
m = &enc.seenCanAddr
} else {
m = &enc.seenCantAddr
}
if _, ok := m.At(t.Type); ok {
return nil
}
m.Set(t.Type, struct{}{})
if t.Implements(knowledge.Interfaces["encoding/json.Marshaler"]) {
return nil
}
if !t.IsPtr() && t.CanAddr() && fakereflect.PtrTo(t).Implements(knowledge.Interfaces["encoding/json.Marshaler"]) {
return nil
}
if t.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
if !t.IsPtr() && t.CanAddr() && fakereflect.PtrTo(t).Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
switch t.Type.Underlying().(type) {
case *types.Basic, *types.Interface:
return nil
case *types.Struct:
return enc.typeFields(t, stack)
case *types.Map:
return enc.newMapEncoder(t, stack)
case *types.Slice:
return enc.newSliceEncoder(t, stack)
case *types.Array:
return enc.newArrayEncoder(t, stack)
case *types.Pointer:
// we don't have to express the pointer dereference in the path; x.f is syntactic sugar for (*x).f
return enc.newTypeEncoder(t.Elem(), stack)
default:
return &UnsupportedTypeError{t.Type, stack}
}
}
func (enc *encoder) newMapEncoder(t fakereflect.TypeAndCanAddr, stack string) *UnsupportedTypeError {
if typeparams.IsTypeParam(t.Key().Type) {
// We don't know enough about the concrete instantiation to say much about the key. The only time we could make
// a definite "this key is bad" statement is if the type parameter is constrained by type terms, none of which
// are tilde terms, none of which are a basic type. In all other cases, the key might implement TextMarshaler.
// It doesn't seem worth checking for that one single case.
return enc.newTypeEncoder(t.Elem(), stack+"[k]")
}
switch t.Key().Type.Underlying().(type) {
case *types.Basic:
default:
if !t.Key().Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return &UnsupportedTypeError{
Type: t.Type,
Path: stack,
}
}
}
return enc.newTypeEncoder(t.Elem(), stack+"[k]")
}
func (enc *encoder) newSliceEncoder(t fakereflect.TypeAndCanAddr, stack string) *UnsupportedTypeError {
// Byte slices get special treatment; arrays don't.
basic, ok := t.Elem().Type.Underlying().(*types.Basic)
if ok && basic.Kind() == types.Uint8 {
p := fakereflect.PtrTo(t.Elem())
if !p.Implements(knowledge.Interfaces["encoding/json.Marshaler"]) && !p.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
}
return enc.newArrayEncoder(t, stack)
}
func (enc *encoder) newArrayEncoder(t fakereflect.TypeAndCanAddr, stack string) *UnsupportedTypeError {
return enc.newTypeEncoder(t.Elem(), stack+"[0]")
}
func isValidTag(s string) bool {
if s == "" {
return false
}
for _, c := range s {
switch {
case strings.ContainsRune("!#$%&()*+-./:;<=>?@[]^_{|}~ ", c):
// Backslash and quote chars are reserved, but
// otherwise any punctuation chars are allowed
// in a tag name.
case !unicode.IsLetter(c) && !unicode.IsDigit(c):
return false
}
}
return true
}
func typeByIndex(t fakereflect.TypeAndCanAddr, index []int) fakereflect.TypeAndCanAddr {
for _, i := range index {
if t.IsPtr() {
t = t.Elem()
}
t = t.Field(i).Type
}
return t
}
func pathByIndex(t fakereflect.TypeAndCanAddr, index []int) string {
var path strings.Builder
for _, i := range index {
if t.IsPtr() {
t = t.Elem()
}
path.WriteString("." + t.Field(i).Name)
t = t.Field(i).Type
}
return path.String()
}
// A field represents a single field found in a struct.
type field struct {
name string
tag bool
index []int
typ fakereflect.TypeAndCanAddr
}
// byIndex sorts field by index sequence.
type byIndex []field
func (x byIndex) Len() int { return len(x) }
func (x byIndex) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byIndex) Less(i, j int) bool {
for k, xik := range x[i].index {
if k >= len(x[j].index) {
return false
}
if xik != x[j].index[k] {
return xik < x[j].index[k]
}
}
return len(x[i].index) < len(x[j].index)
}
// typeFields returns a list of fields that JSON should recognize for the given type.
// The algorithm is breadth-first search over the set of structs to include - the top struct
// and then any reachable anonymous structs.
func (enc *encoder) typeFields(t fakereflect.TypeAndCanAddr, stack string) *UnsupportedTypeError {
// Anonymous fields to explore at the current level and the next.
current := []field{}
next := []field{{typ: t}}
// Count of queued names for current level and the next.
var count, nextCount map[fakereflect.TypeAndCanAddr]int
// Types already visited at an earlier level.
visited := map[fakereflect.TypeAndCanAddr]bool{}
// Fields found.
var fields []field
for len(next) > 0 {
current, next = next, current[:0]
count, nextCount = nextCount, map[fakereflect.TypeAndCanAddr]int{}
for _, f := range current {
if visited[f.typ] {
continue
}
visited[f.typ] = true
// Scan f.typ for fields to include.
for i := 0; i < f.typ.NumField(); i++ {
sf := f.typ.Field(i)
if sf.Anonymous {
t := sf.Type
if t.IsPtr() {
t = t.Elem()
}
if !sf.IsExported() && !t.IsStruct() {
// Ignore embedded fields of unexported non-struct types.
continue
}
// Do not ignore embedded fields of unexported struct types
// since they may have exported fields.
} else if !sf.IsExported() {
// Ignore unexported non-embedded fields.
continue
}
tag := sf.Tag.Get("json")
if tag == "-" {
continue
}
name := parseTag(tag)
if !isValidTag(name) {
name = ""
}
index := make([]int, len(f.index)+1)
copy(index, f.index)
index[len(f.index)] = i
ft := sf.Type
if ft.Name() == "" && ft.IsPtr() {
// Follow pointer.
ft = ft.Elem()
}
// Record found field and index sequence.
if name != "" || !sf.Anonymous || !ft.IsStruct() {
tagged := name != ""
if name == "" {
name = sf.Name
}
field := field{
name: name,
tag: tagged,
index: index,
typ: ft,
}
fields = append(fields, field)
if count[f.typ] > 1 {
// If there were multiple instances, add a second,
// so that the annihilation code will see a duplicate.
// It only cares about the distinction between 1 or 2,
// so don't bother generating any more copies.
fields = append(fields, fields[len(fields)-1])
}
continue
}
// Record new anonymous struct to explore in next round.
nextCount[ft]++
if nextCount[ft] == 1 {
next = append(next, field{name: ft.Name(), index: index, typ: ft})
}
}
}
}
sort.Slice(fields, func(i, j int) bool {
x := fields
// sort field by name, breaking ties with depth, then
// breaking ties with "name came from json tag", then
// breaking ties with index sequence.
if x[i].name != x[j].name {
return x[i].name < x[j].name
}
if len(x[i].index) != len(x[j].index) {
return len(x[i].index) < len(x[j].index)
}
if x[i].tag != x[j].tag {
return x[i].tag
}
return byIndex(x).Less(i, j)
})
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with JSON tags are promoted.
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0]
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break
}
}
if advance == 1 { // Only one field with this name
out = append(out, fi)
continue
}
dominant, ok := dominantField(fields[i : i+advance])
if ok {
out = append(out, dominant)
}
}
fields = out
sort.Sort(byIndex(fields))
for i := range fields {
f := &fields[i]
err := enc.newTypeEncoder(typeByIndex(t, f.index), stack+pathByIndex(t, f.index))
if err != nil {
return err
}
}
return nil
}
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field) (field, bool) {
// The fields are sorted in increasing index-length order, then by presence of tag.
// That means that the first field is the dominant one. We need only check
// for error cases: two fields at top level, either both tagged or neither tagged.
if len(fields) > 1 && len(fields[0].index) == len(fields[1].index) && fields[0].tag == fields[1].tag {
return field{}, false
}
return fields[0], true
}

View File

@@ -0,0 +1,131 @@
package fakereflect
import (
"fmt"
"go/types"
"reflect"
)
type TypeAndCanAddr struct {
Type types.Type
canAddr bool
}
type StructField struct {
Index []int
Name string
Anonymous bool
Tag reflect.StructTag
f *types.Var
Type TypeAndCanAddr
}
func (sf StructField) IsExported() bool { return sf.f.Exported() }
func (t TypeAndCanAddr) Field(i int) StructField {
st := t.Type.Underlying().(*types.Struct)
f := st.Field(i)
return StructField{
f: f,
Index: []int{i},
Name: f.Name(),
Anonymous: f.Anonymous(),
Tag: reflect.StructTag(st.Tag(i)),
Type: TypeAndCanAddr{
Type: f.Type(),
canAddr: t.canAddr,
},
}
}
func (t TypeAndCanAddr) FieldByIndex(index []int) StructField {
f := t.Field(index[0])
for _, idx := range index[1:] {
f = f.Type.Field(idx)
}
f.Index = index
return f
}
func PtrTo(t TypeAndCanAddr) TypeAndCanAddr {
// Note that we don't care about canAddr here because it's irrelevant to all uses of PtrTo
return TypeAndCanAddr{Type: types.NewPointer(t.Type)}
}
func (t TypeAndCanAddr) CanAddr() bool { return t.canAddr }
func (t TypeAndCanAddr) Implements(ityp *types.Interface) bool {
return types.Implements(t.Type, ityp)
}
func (t TypeAndCanAddr) IsSlice() bool {
_, ok := t.Type.Underlying().(*types.Slice)
return ok
}
func (t TypeAndCanAddr) IsArray() bool {
_, ok := t.Type.Underlying().(*types.Array)
return ok
}
func (t TypeAndCanAddr) IsPtr() bool {
_, ok := t.Type.Underlying().(*types.Pointer)
return ok
}
func (t TypeAndCanAddr) IsInterface() bool {
_, ok := t.Type.Underlying().(*types.Interface)
return ok
}
func (t TypeAndCanAddr) IsStruct() bool {
_, ok := t.Type.Underlying().(*types.Struct)
return ok
}
func (t TypeAndCanAddr) Name() string {
named, ok := types.Unalias(t.Type).(*types.Named)
if !ok {
return ""
}
return named.Obj().Name()
}
func (t TypeAndCanAddr) NumField() int {
return t.Type.Underlying().(*types.Struct).NumFields()
}
func (t TypeAndCanAddr) String() string {
return t.Type.String()
}
func (t TypeAndCanAddr) Key() TypeAndCanAddr {
return TypeAndCanAddr{Type: t.Type.Underlying().(*types.Map).Key()}
}
func (t TypeAndCanAddr) Elem() TypeAndCanAddr {
switch typ := t.Type.Underlying().(type) {
case *types.Pointer:
return TypeAndCanAddr{
Type: typ.Elem(),
canAddr: true,
}
case *types.Slice:
return TypeAndCanAddr{
Type: typ.Elem(),
canAddr: true,
}
case *types.Array:
return TypeAndCanAddr{
Type: typ.Elem(),
canAddr: t.canAddr,
}
case *types.Map:
return TypeAndCanAddr{
Type: typ.Elem(),
canAddr: false,
}
default:
panic(fmt.Sprintf("unhandled type %T", typ))
}
}

View File

@@ -0,0 +1,376 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file contains a modified copy of the encoding/xml encoder.
// All dynamic behavior has been removed, and reflecttion has been replaced with go/types.
// This allows us to statically find unmarshable types
// with the same rules for tags, shadowing and addressability as encoding/xml.
// This is used for SA1026 and SA5008.
// NOTE(dh): we do not check CanInterface in various places, which means we'll accept more marshaler implementations than encoding/xml does. This will lead to a small amount of false negatives.
package fakexml
import (
"fmt"
"go/types"
"strings"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/staticcheck/fakereflect"
)
func Marshal(v types.Type) error {
return NewEncoder().Encode(v)
}
type Encoder struct {
// TODO we track addressable and non-addressable instances separately out of an abundance of caution. We don't know
// if this is actually required for correctness.
seenCanAddr typeutil.Map[struct{}]
seenCantAddr typeutil.Map[struct{}]
}
func NewEncoder() *Encoder {
e := &Encoder{}
return e
}
func (enc *Encoder) Encode(v types.Type) error {
rv := fakereflect.TypeAndCanAddr{Type: v}
return enc.marshalValue(rv, nil, nil, "x")
}
func implementsMarshaler(v fakereflect.TypeAndCanAddr) bool {
t := v.Type
obj, _, _ := types.LookupFieldOrMethod(t, false, nil, "MarshalXML")
if obj == nil {
return false
}
fn, ok := obj.(*types.Func)
if !ok {
return false
}
params := fn.Type().(*types.Signature).Params()
if params.Len() != 2 {
return false
}
if !typeutil.IsPointerToTypeWithName(params.At(0).Type(), "encoding/xml.Encoder") {
return false
}
if !typeutil.IsTypeWithName(params.At(1).Type(), "encoding/xml.StartElement") {
return false
}
rets := fn.Type().(*types.Signature).Results()
if rets.Len() != 1 {
return false
}
if !typeutil.IsTypeWithName(rets.At(0).Type(), "error") {
return false
}
return true
}
func implementsMarshalerAttr(v fakereflect.TypeAndCanAddr) bool {
t := v.Type
obj, _, _ := types.LookupFieldOrMethod(t, false, nil, "MarshalXMLAttr")
if obj == nil {
return false
}
fn, ok := obj.(*types.Func)
if !ok {
return false
}
params := fn.Type().(*types.Signature).Params()
if params.Len() != 1 {
return false
}
if !typeutil.IsTypeWithName(params.At(0).Type(), "encoding/xml.Name") {
return false
}
rets := fn.Type().(*types.Signature).Results()
if rets.Len() != 2 {
return false
}
if !typeutil.IsTypeWithName(rets.At(0).Type(), "encoding/xml.Attr") {
return false
}
if !typeutil.IsTypeWithName(rets.At(1).Type(), "error") {
return false
}
return true
}
type CyclicTypeError struct {
Type types.Type
Path string
}
func (err *CyclicTypeError) Error() string {
return "cyclic type"
}
// marshalValue writes one or more XML elements representing val.
// If val was obtained from a struct field, finfo must have its details.
func (e *Encoder) marshalValue(val fakereflect.TypeAndCanAddr, finfo *fieldInfo, startTemplate *StartElement, stack string) error {
var m *typeutil.Map[struct{}]
if val.CanAddr() {
m = &e.seenCanAddr
} else {
m = &e.seenCantAddr
}
if _, ok := m.At(val.Type); ok {
return nil
}
m.Set(val.Type, struct{}{})
// Drill into interfaces and pointers.
seen := map[fakereflect.TypeAndCanAddr]struct{}{}
for val.IsInterface() || val.IsPtr() {
if val.IsInterface() {
return nil
}
val = val.Elem()
if _, ok := seen[val]; ok {
// Loop in type graph, e.g. 'type P *P'
return &CyclicTypeError{val.Type, stack}
}
seen[val] = struct{}{}
}
// Check for marshaler.
if implementsMarshaler(val) {
return nil
}
if val.CanAddr() {
pv := fakereflect.PtrTo(val)
if implementsMarshaler(pv) {
return nil
}
}
// Check for text marshaler.
if val.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
if val.CanAddr() {
pv := fakereflect.PtrTo(val)
if pv.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
}
// Slices and arrays iterate over the elements. They do not have an enclosing tag.
if (val.IsSlice() || val.IsArray()) && !isByteArray(val) && !isByteSlice(val) {
if err := e.marshalValue(val.Elem(), finfo, startTemplate, stack+"[0]"); err != nil {
return err
}
return nil
}
tinfo, err := getTypeInfo(val)
if err != nil {
return err
}
// Create start element.
// Precedence for the XML element name is:
// 0. startTemplate
// 1. XMLName field in underlying struct;
// 2. field name/tag in the struct field; and
// 3. type name
var start StartElement
if startTemplate != nil {
start.Name = startTemplate.Name
start.Attr = append(start.Attr, startTemplate.Attr...)
} else if tinfo.xmlname != nil {
xmlname := tinfo.xmlname
if xmlname.name != "" {
start.Name.Space, start.Name.Local = xmlname.xmlns, xmlname.name
}
}
// Attributes
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
if finfo.flags&fAttr == 0 {
continue
}
fv := finfo.value(val)
name := Name{Space: finfo.xmlns, Local: finfo.name}
if err := e.marshalAttr(&start, name, fv, stack+pathByIndex(val, finfo.idx)); err != nil {
return err
}
}
if val.IsStruct() {
return e.marshalStruct(tinfo, val, stack)
} else {
return e.marshalSimple(val, stack)
}
}
func isSlice(v fakereflect.TypeAndCanAddr) bool {
_, ok := v.Type.Underlying().(*types.Slice)
return ok
}
func isByteSlice(v fakereflect.TypeAndCanAddr) bool {
slice, ok := v.Type.Underlying().(*types.Slice)
if !ok {
return false
}
basic, ok := slice.Elem().Underlying().(*types.Basic)
if !ok {
return false
}
return basic.Kind() == types.Uint8
}
func isByteArray(v fakereflect.TypeAndCanAddr) bool {
slice, ok := v.Type.Underlying().(*types.Array)
if !ok {
return false
}
basic, ok := slice.Elem().Underlying().(*types.Basic)
if !ok {
return false
}
return basic.Kind() == types.Uint8
}
// marshalAttr marshals an attribute with the given name and value, adding to start.Attr.
func (e *Encoder) marshalAttr(start *StartElement, name Name, val fakereflect.TypeAndCanAddr, stack string) error {
if implementsMarshalerAttr(val) {
return nil
}
if val.CanAddr() {
pv := fakereflect.PtrTo(val)
if implementsMarshalerAttr(pv) {
return nil
}
}
if val.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
if val.CanAddr() {
pv := fakereflect.PtrTo(val)
if pv.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
return nil
}
}
// Dereference or skip nil pointer
if val.IsPtr() {
val = val.Elem()
}
// Walk slices.
if isSlice(val) && !isByteSlice(val) {
if err := e.marshalAttr(start, name, val.Elem(), stack+"[0]"); err != nil {
return err
}
return nil
}
if typeutil.IsTypeWithName(val.Type, "encoding/xml.Attr") {
return nil
}
return e.marshalSimple(val, stack)
}
func (e *Encoder) marshalSimple(val fakereflect.TypeAndCanAddr, stack string) error {
switch val.Type.Underlying().(type) {
case *types.Basic, *types.Interface:
return nil
case *types.Slice, *types.Array:
basic, ok := val.Elem().Type.Underlying().(*types.Basic)
if !ok || basic.Kind() != types.Uint8 {
return &UnsupportedTypeError{val.Type, stack}
}
return nil
default:
return &UnsupportedTypeError{val.Type, stack}
}
}
func indirect(vf fakereflect.TypeAndCanAddr) fakereflect.TypeAndCanAddr {
for vf.IsPtr() {
vf = vf.Elem()
}
return vf
}
func pathByIndex(t fakereflect.TypeAndCanAddr, index []int) string {
var path strings.Builder
for _, i := range index {
if t.IsPtr() {
t = t.Elem()
}
path.WriteString("." + t.Field(i).Name)
t = t.Field(i).Type
}
return path.String()
}
func (e *Encoder) marshalStruct(tinfo *typeInfo, val fakereflect.TypeAndCanAddr, stack string) error {
for i := range tinfo.fields {
finfo := &tinfo.fields[i]
if finfo.flags&fAttr != 0 {
continue
}
vf := finfo.value(val)
switch finfo.flags & fMode {
case fCDATA, fCharData:
if vf.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
continue
}
if vf.CanAddr() {
pv := fakereflect.PtrTo(vf)
if pv.Implements(knowledge.Interfaces["encoding.TextMarshaler"]) {
continue
}
}
continue
case fComment:
vf = indirect(vf)
if !(isByteSlice(vf) || isByteArray(vf)) {
return fmt.Errorf("xml: bad type for comment field of %s", val)
}
continue
case fInnerXML:
vf = indirect(vf)
if t, ok := vf.Type.(*types.Slice); (ok && types.Identical(t.Elem(), types.Typ[types.Byte])) || types.Identical(vf.Type, types.Typ[types.String]) {
continue
}
case fElement, fElement | fAny:
}
if err := e.marshalValue(vf, finfo, nil, stack+pathByIndex(val, finfo.idx)); err != nil {
return err
}
}
return nil
}
// UnsupportedTypeError is returned when Marshal encounters a type
// that cannot be converted into XML.
type UnsupportedTypeError struct {
Type types.Type
Path string
}
func (e *UnsupportedTypeError) Error() string {
return fmt.Sprintf("xml: unsupported type %s, via %s ", e.Type, e.Path)
}

View File

@@ -0,0 +1,381 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fakexml
import (
"fmt"
"strconv"
"strings"
"sync"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/staticcheck/fakereflect"
)
// typeInfo holds details for the xml representation of a type.
type typeInfo struct {
xmlname *fieldInfo
fields []fieldInfo
}
// fieldInfo holds details for the xml representation of a single field.
type fieldInfo struct {
idx []int
name string
xmlns string
flags fieldFlags
parents []string
}
type fieldFlags int
const (
fElement fieldFlags = 1 << iota
fAttr
fCDATA
fCharData
fInnerXML
fComment
fAny
fOmitEmpty
fMode = fElement | fAttr | fCDATA | fCharData | fInnerXML | fComment | fAny
xmlName = "XMLName"
)
func (f fieldFlags) String() string {
switch f {
case fAttr:
return "attr"
case fCDATA:
return "cdata"
case fCharData:
return "chardata"
case fInnerXML:
return "innerxml"
case fComment:
return "comment"
case fAny:
return "any"
case fOmitEmpty:
return "omitempty"
case fAny | fAttr:
return "any,attr"
default:
return strconv.Itoa(int(f))
}
}
var tinfoMap sync.Map // map[reflect.Type]*typeInfo
// getTypeInfo returns the typeInfo structure with details necessary
// for marshaling and unmarshaling typ.
func getTypeInfo(typ fakereflect.TypeAndCanAddr) (*typeInfo, error) {
if ti, ok := tinfoMap.Load(typ); ok {
return ti.(*typeInfo), nil
}
tinfo := &typeInfo{}
if typ.IsStruct() && !typeutil.IsTypeWithName(typ.Type, "encoding/xml.Name") {
n := typ.NumField()
for i := range n {
f := typ.Field(i)
if (!f.IsExported() && !f.Anonymous) || f.Tag.Get("xml") == "-" {
continue // Private field
}
// For embedded structs, embed its fields.
if f.Anonymous {
t := f.Type
if t.IsPtr() {
t = t.Elem()
}
if t.IsStruct() {
inner, err := getTypeInfo(t)
if err != nil {
return nil, err
}
if tinfo.xmlname == nil {
tinfo.xmlname = inner.xmlname
}
for _, finfo := range inner.fields {
finfo.idx = append([]int{i}, finfo.idx...)
if err := addFieldInfo(typ, tinfo, &finfo); err != nil {
return nil, err
}
}
continue
}
}
finfo, err := StructFieldInfo(f)
if err != nil {
return nil, err
}
if f.Name == xmlName {
tinfo.xmlname = finfo
continue
}
// Add the field if it doesn't conflict with other fields.
if err := addFieldInfo(typ, tinfo, finfo); err != nil {
return nil, err
}
}
}
ti, _ := tinfoMap.LoadOrStore(typ, tinfo)
return ti.(*typeInfo), nil
}
// StructFieldInfo builds and returns a fieldInfo for f.
func StructFieldInfo(f fakereflect.StructField) (*fieldInfo, error) {
finfo := &fieldInfo{idx: f.Index}
// Split the tag from the xml namespace if necessary.
tag := f.Tag.Get("xml")
if i := strings.Index(tag, " "); i >= 0 {
finfo.xmlns, tag = tag[:i], tag[i+1:]
}
// Parse flags.
tokens := strings.Split(tag, ",")
if len(tokens) == 1 {
finfo.flags = fElement
} else {
tag = tokens[0]
for _, flag := range tokens[1:] {
switch flag {
case "attr":
finfo.flags |= fAttr
case "cdata":
finfo.flags |= fCDATA
case "chardata":
finfo.flags |= fCharData
case "innerxml":
finfo.flags |= fInnerXML
case "comment":
finfo.flags |= fComment
case "any":
finfo.flags |= fAny
case "omitempty":
finfo.flags |= fOmitEmpty
}
}
// Validate the flags used.
switch mode := finfo.flags & fMode; mode {
case 0:
finfo.flags |= fElement
case fAttr, fCDATA, fCharData, fInnerXML, fComment, fAny, fAny | fAttr:
if f.Name == xmlName {
return nil, fmt.Errorf("cannot use option %s on XMLName field", mode)
} else if tag != "" && mode != fAttr {
return nil, fmt.Errorf("cannot specify name together with option ,%s", mode)
}
default:
// This will also catch multiple modes in a single field.
return nil, fmt.Errorf("invalid combination of options: %q", f.Tag.Get("xml"))
}
if finfo.flags&fMode == fAny {
finfo.flags |= fElement
}
if finfo.flags&fOmitEmpty != 0 && finfo.flags&(fElement|fAttr) == 0 {
return nil, fmt.Errorf("can only use omitempty on elements and attributes")
}
}
// Use of xmlns without a name is not allowed.
if finfo.xmlns != "" && tag == "" {
return nil, fmt.Errorf("namespace without name: %q", f.Tag.Get("xml"))
}
if f.Name == xmlName {
// The XMLName field records the XML element name. Don't
// process it as usual because its name should default to
// empty rather than to the field name.
finfo.name = tag
return finfo, nil
}
if tag == "" {
// If the name part of the tag is completely empty, get
// default from XMLName of underlying struct if feasible,
// or field name otherwise.
if xmlname := lookupXMLName(f.Type); xmlname != nil {
finfo.xmlns, finfo.name = xmlname.xmlns, xmlname.name
} else {
finfo.name = f.Name
}
return finfo, nil
}
// Prepare field name and parents.
parents := strings.Split(tag, ">")
if parents[0] == "" {
parents[0] = f.Name
}
if parents[len(parents)-1] == "" {
return nil, fmt.Errorf("trailing '>'")
}
finfo.name = parents[len(parents)-1]
if len(parents) > 1 {
if (finfo.flags & fElement) == 0 {
return nil, fmt.Errorf("%s chain not valid with %s flag", tag, strings.Join(tokens[1:], ","))
}
finfo.parents = parents[:len(parents)-1]
}
// If the field type has an XMLName field, the names must match
// so that the behavior of both marshaling and unmarshaling
// is straightforward and unambiguous.
if finfo.flags&fElement != 0 {
ftyp := f.Type
xmlname := lookupXMLName(ftyp)
if xmlname != nil && xmlname.name != finfo.name {
return nil, fmt.Errorf("name %q conflicts with name %q in %s.XMLName", finfo.name, xmlname.name, ftyp)
}
}
return finfo, nil
}
// lookupXMLName returns the fieldInfo for typ's XMLName field
// in case it exists and has a valid xml field tag, otherwise
// it returns nil.
func lookupXMLName(typ fakereflect.TypeAndCanAddr) (xmlname *fieldInfo) {
seen := map[fakereflect.TypeAndCanAddr]struct{}{}
for typ.IsPtr() {
typ = typ.Elem()
if _, ok := seen[typ]; ok {
// Loop in type graph, e.g. 'type P *P'
return nil
}
seen[typ] = struct{}{}
}
if !typ.IsStruct() {
return nil
}
for i, n := 0, typ.NumField(); i < n; i++ {
f := typ.Field(i)
if f.Name != xmlName {
continue
}
finfo, err := StructFieldInfo(f)
if err == nil && finfo.name != "" {
return finfo
}
// Also consider errors as a non-existent field tag
// and let getTypeInfo itself report the error.
break
}
return nil
}
// addFieldInfo adds finfo to tinfo.fields if there are no
// conflicts, or if conflicts arise from previous fields that were
// obtained from deeper embedded structures than finfo. In the latter
// case, the conflicting entries are dropped.
// A conflict occurs when the path (parent + name) to a field is
// itself a prefix of another path, or when two paths match exactly.
// It is okay for field paths to share a common, shorter prefix.
func addFieldInfo(typ fakereflect.TypeAndCanAddr, tinfo *typeInfo, newf *fieldInfo) error {
var conflicts []int
Loop:
// First, figure all conflicts. Most working code will have none.
for i := range tinfo.fields {
oldf := &tinfo.fields[i]
if oldf.flags&fMode != newf.flags&fMode {
continue
}
if oldf.xmlns != "" && newf.xmlns != "" && oldf.xmlns != newf.xmlns {
continue
}
minl := min(len(newf.parents), len(oldf.parents))
for p := range minl {
if oldf.parents[p] != newf.parents[p] {
continue Loop
}
}
if len(oldf.parents) > len(newf.parents) {
if oldf.parents[len(newf.parents)] == newf.name {
conflicts = append(conflicts, i)
}
} else if len(oldf.parents) < len(newf.parents) {
if newf.parents[len(oldf.parents)] == oldf.name {
conflicts = append(conflicts, i)
}
} else {
if newf.name == oldf.name {
conflicts = append(conflicts, i)
}
}
}
// Without conflicts, add the new field and return.
if conflicts == nil {
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// If any conflict is shallower, ignore the new field.
// This matches the Go field resolution on embedding.
for _, i := range conflicts {
if len(tinfo.fields[i].idx) < len(newf.idx) {
return nil
}
}
// Otherwise, if any of them is at the same depth level, it's an error.
for _, i := range conflicts {
oldf := &tinfo.fields[i]
if len(oldf.idx) == len(newf.idx) {
f1 := typ.FieldByIndex(oldf.idx)
f2 := typ.FieldByIndex(newf.idx)
return &TagPathError{typ, f1.Name, f1.Tag.Get("xml"), f2.Name, f2.Tag.Get("xml")}
}
}
// Otherwise, the new field is shallower, and thus takes precedence,
// so drop the conflicting fields from tinfo and append the new one.
for c := len(conflicts) - 1; c >= 0; c-- {
i := conflicts[c]
copy(tinfo.fields[i:], tinfo.fields[i+1:])
tinfo.fields = tinfo.fields[:len(tinfo.fields)-1]
}
tinfo.fields = append(tinfo.fields, *newf)
return nil
}
// A TagPathError represents an error in the unmarshaling process
// caused by the use of field tags with conflicting paths.
type TagPathError struct {
Struct fakereflect.TypeAndCanAddr
Field1, Tag1 string
Field2, Tag2 string
}
func (e *TagPathError) Error() string {
return fmt.Sprintf("%s field %q with tag %q conflicts with field %q with tag %q", e.Struct, e.Field1, e.Tag1, e.Field2, e.Tag2)
}
// value returns v's field value corresponding to finfo.
// It's equivalent to v.FieldByIndex(finfo.idx), but when passed
// initNilPointers, it initializes and dereferences pointers as necessary.
// When passed dontInitNilPointers and a nil pointer is reached, the function
// returns a zero reflect.Value.
func (finfo *fieldInfo) value(v fakereflect.TypeAndCanAddr) fakereflect.TypeAndCanAddr {
for i, x := range finfo.idx {
if i > 0 {
t := v
if t.IsPtr() && t.Elem().IsStruct() {
v = v.Elem()
}
}
v = v.Field(x).Type
}
return v
}

View File

@@ -0,0 +1,33 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fakexml
// References:
// Annotated XML spec: https://www.xml.com/axml/testaxml.htm
// XML name spaces: https://www.w3.org/TR/REC-xml-names/
// TODO(rsc):
// Test error handling.
// A Name represents an XML name (Local) annotated
// with a name space identifier (Space).
// In tokens returned by Decoder.Token, the Space identifier
// is given as a canonical URL, not the short prefix used
// in the document being parsed.
type Name struct {
Space, Local string
}
// An Attr represents an attribute in an XML element (Name=Value).
type Attr struct {
Name Name
Value string
}
// A StartElement represents an XML start element.
type StartElement struct {
Name Name
Attr []Attr
}

View File

@@ -0,0 +1,46 @@
package sa1000
import (
"go/constant"
"regexp"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1000",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Invalid regular expression`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"regexp.MustCompile": check,
"regexp.Compile": check,
"regexp.Match": check,
"regexp.MatchReader": check,
"regexp.MatchString": check,
}
func check(call *callcheck.Call) {
arg := call.Args[0]
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.String); c != nil {
s := constant.StringVal(c.Value)
if _, err := regexp.Compile(s); err != nil {
arg.Invalid(err.Error())
}
}
}

View File

@@ -0,0 +1,83 @@
package sa1001
import (
"go/ast"
htmltemplate "html/template"
"strings"
texttemplate "text/template"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1001",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Invalid template`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`
(CallExpr
(Symbol
name@(Or
"(*text/template.Template).Parse"
"(*html/template.Template).Parse"))
[s])`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, query) {
name := m.State["name"].(string)
var kind string
switch name {
case "(*text/template.Template).Parse":
kind = "text"
case "(*html/template.Template).Parse":
kind = "html"
}
call := node.(*ast.CallExpr)
sel := call.Fun.(*ast.SelectorExpr)
if !code.IsCallToAny(pass, sel.X, "text/template.New", "html/template.New") {
// TODO(dh): this is a cheap workaround for templates with
// different delims. A better solution with less false
// negatives would use data flow analysis to see where the
// template comes from and where it has been
continue
}
s, ok := code.ExprToString(pass, m.State["s"].(ast.Expr))
if !ok {
continue
}
var err error
switch kind {
case "text":
_, err = texttemplate.New("").Parse(s)
case "html":
_, err = htmltemplate.New("").Parse(s)
}
if err != nil {
// TODO(dominikh): whitelist other parse errors, if any
if strings.Contains(err.Error(), "unexpected") ||
strings.Contains(err.Error(), "bad character") {
report.Report(pass, call.Args[knowledge.Arg("(*text/template.Template).Parse.text")], err.Error())
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,45 @@
package sa1002
import (
"go/constant"
"strings"
"time"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1002",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Invalid format in \'time.Parse\'`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"time.Parse": func(call *callcheck.Call) {
arg := call.Args[knowledge.Arg("time.Parse.layout")]
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.String); c != nil {
s := constant.StringVal(c.Value)
s = strings.Replace(s, "_", " ", -1)
s = strings.Replace(s, "Z", "-", -1)
_, err := time.Parse(s, s)
if err != nil {
arg.Invalid(err.Error())
}
}
},
}

View File

@@ -0,0 +1,92 @@
package sa1003
import (
"fmt"
"go/types"
"go/version"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1003",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkEncodingBinaryRules),
},
Doc: &lint.RawDocumentation{
Title: `Unsupported argument to functions in \'encoding/binary\'`,
Text: `The \'encoding/binary\' package can only serialize types with known sizes.
This precludes the use of the \'int\' and \'uint\' types, as their sizes
differ on different architectures. Furthermore, it doesn't support
serializing maps, channels, strings, or functions.
Before Go 1.8, \'bool\' wasn't supported, either.`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkEncodingBinaryRules = map[string]callcheck.Check{
"encoding/binary.Write": func(call *callcheck.Call) {
arg := call.Args[knowledge.Arg("encoding/binary.Write.data")]
if !CanBinaryMarshal(call.Pass, call.Parent, arg.Value) {
arg.Invalid(fmt.Sprintf("value of type %s cannot be used with binary.Write", arg.Value.Value.Type()))
}
},
}
func CanBinaryMarshal(pass *analysis.Pass, node code.Positioner, v callcheck.Value) bool {
typ := v.Value.Type().Underlying()
if ttyp, ok := typ.(*types.Pointer); ok {
typ = ttyp.Elem().Underlying()
}
if ttyp, ok := types.Unalias(typ).(interface {
Elem() types.Type
}); ok {
if _, ok := ttyp.(*types.Pointer); !ok {
typ = ttyp.Elem()
}
}
return validEncodingBinaryType(pass, node, typ)
}
func validEncodingBinaryType(pass *analysis.Pass, node code.Positioner, typ types.Type) bool {
typ = typ.Underlying()
switch typ := typ.(type) {
case *types.Basic:
switch typ.Kind() {
case types.Uint8, types.Uint16, types.Uint32, types.Uint64,
types.Int8, types.Int16, types.Int32, types.Int64,
types.Float32, types.Float64, types.Complex64, types.Complex128, types.Invalid:
return true
case types.Bool:
return version.Compare(code.StdlibVersion(pass, node), "go1.8") >= 0
}
return false
case *types.Struct:
n := typ.NumFields()
for i := range n {
if !validEncodingBinaryType(pass, node, typ.Field(i).Type()) {
return false
}
}
return true
case *types.Array:
return validEncodingBinaryType(pass, node, typ.Elem())
case *types.Interface:
// we can't determine if it's a valid type or not
return true
}
return false
}

View File

@@ -0,0 +1,72 @@
package sa1004
import (
"fmt"
"go/ast"
"go/constant"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1004",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Suspiciously small untyped constant in \'time.Sleep\'`,
Text: `The \'time\'.Sleep function takes a \'time.Duration\' as its only argument.
Durations are expressed in nanoseconds. Thus, calling \'time.Sleep(1)\'
will sleep for 1 nanosecond. This is a common source of bugs, as sleep
functions in other languages often accept seconds or milliseconds.
The \'time\' package provides constants such as \'time.Second\' to express
large durations. These can be combined with arithmetic to express
arbitrary durations, for example \'5 * time.Second\' for 5 seconds.
If you truly meant to sleep for a tiny amount of time, use
\'n * time.Nanosecond\' to signal to Staticcheck that you did mean to sleep
for some amount of nanoseconds.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkTimeSleepConstantPatternQ = pattern.MustParse(`(CallExpr (Symbol "time.Sleep") lit@(IntegerLiteral value))`)
checkTimeSleepConstantPatternRns = pattern.MustParse(`(BinaryExpr duration "*" (SelectorExpr (Ident "time") (Ident "Nanosecond")))`)
checkTimeSleepConstantPatternRs = pattern.MustParse(`(BinaryExpr duration "*" (SelectorExpr (Ident "time") (Ident "Second")))`)
)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, checkTimeSleepConstantPatternQ) {
n, ok := constant.Int64Val(m.State["value"].(types.TypeAndValue).Value)
if !ok {
continue
}
if n == 0 || n > 120 {
// time.Sleep(0) is a seldom used pattern in concurrency
// tests. >120 might be intentional. 120 was chosen
// because the user could've meant 2 minutes.
continue
}
lit := m.State["lit"].(ast.Node)
report.Report(pass, lit,
fmt.Sprintf("sleeping for %d nanoseconds is probably a bug; be explicit if it isn't", n), report.Fixes(
edit.Fix("Explicitly use nanoseconds", edit.ReplaceWithPattern(pass.Fset, lit, checkTimeSleepConstantPatternRns, pattern.State{"duration": lit})),
edit.Fix("Use seconds", edit.ReplaceWithPattern(pass.Fset, lit, checkTimeSleepConstantPatternRs, pattern.State{"duration": lit}))))
}
return nil, nil
}

View File

@@ -0,0 +1,65 @@
package sa1005
import (
"go/ast"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1005",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Invalid first argument to \'exec.Command\'`,
Text: `\'os/exec\' runs programs directly (using variants of the fork and exec
system calls on Unix systems). This shouldn't be confused with running
a command in a shell. The shell will allow for features such as input
redirection, pipes, and general scripting. The shell is also
responsible for splitting the user's input into a program name and its
arguments. For example, the equivalent to
ls / /tmp
would be
exec.Command("ls", "/", "/tmp")
If you want to run a command in a shell, consider using something like
the following but be aware that not all systems, particularly
Windows, will have a \'/bin/sh\' program:
exec.Command("/bin/sh", "-c", "ls | grep Awesome")`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`(CallExpr (Symbol "os/exec.Command") arg1:_)`)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, query) {
arg1 := m.State["arg1"].(ast.Expr)
val, ok := code.ExprToString(pass, arg1)
if !ok {
continue
}
if !strings.Contains(val, " ") || strings.Contains(val, `\`) || strings.Contains(val, "/") {
continue
}
report.Report(pass, arg1,
"first argument to exec.Command looks like a shell command, but a program name or path are expected")
}
return nil, nil
}

View File

@@ -0,0 +1,115 @@
package sa1006
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1006",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\'Printf\' with dynamic first argument and no further arguments`,
Text: `Using \'fmt.Printf\' with a dynamic first argument can lead to unexpected
output. The first argument is a format string, where certain character
combinations have special meaning. If, for example, a user were to
enter a string such as
Interest rate: 5%
and you printed it with
fmt.Printf(s)
it would lead to the following output:
Interest rate: 5%!(NOVERB).
Similarly, forming the first parameter via string concatenation with
user input should be avoided for the same reason. When printing user
input, either use a variant of \'fmt.Print\', or use the \'%s\' Printf verb
and pass the string as an argument.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query1 = pattern.MustParse(`
(CallExpr
(Symbol
name@(Or
"fmt.Errorf"
"fmt.Printf"
"fmt.Sprintf"
"log.Fatalf"
"log.Panicf"
"log.Printf"
"(*log.Logger).Printf"
"(*testing.common).Logf"
"(*testing.common).Errorf"
"(*testing.common).Fatalf"
"(*testing.common).Skipf"
"(testing.TB).Logf"
"(testing.TB).Errorf"
"(testing.TB).Fatalf"
"(testing.TB).Skipf"))
format:[])
`)
var query2 = pattern.MustParse(`(CallExpr (Symbol "fmt.Fprintf") _:format:[])`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, query1, query2) {
call := node.(*ast.CallExpr)
name, ok := m.State["name"].(string)
if !ok {
name = "fmt.Fprintf"
}
arg := m.State["format"].(ast.Expr)
switch arg.(type) {
case *ast.CallExpr, *ast.Ident:
default:
continue
}
if _, ok := pass.TypesInfo.TypeOf(arg).(*types.Tuple); ok {
// the called function returns multiple values and got
// splatted into the call. for all we know, it is
// returning good arguments.
continue
}
var alt string
if name == "fmt.Errorf" {
// The alternative to fmt.Errorf isn't fmt.Error but errors.New
alt = "errors.New"
} else {
// This can be either a function call like log.Printf or a method call with an
// arbitrarily complex selector, such as foo.bar[0].Printf. In either case,
// all we have to do is remove the final 'f' from the existing call.Fun
// expression.
alt = report.Render(pass, call.Fun)
alt = alt[:len(alt)-1]
}
report.Report(pass, call,
"printf-style function with dynamic format string and no further arguments should use print-style function instead",
report.Fixes(edit.Fix(fmt.Sprintf("Use %s instead of %s", alt, name), edit.ReplaceWithString(call.Fun, alt))))
}
return nil, nil
}

View File

@@ -0,0 +1,43 @@
package sa1007
import (
"fmt"
"go/constant"
"net/url"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1007",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Invalid URL in \'net/url.Parse\'`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"net/url.Parse": func(call *callcheck.Call) {
arg := call.Args[knowledge.Arg("net/url.Parse.rawurl")]
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.String); c != nil {
s := constant.StringVal(c.Value)
_, err := url.Parse(s)
if err != nil {
arg.Invalid(fmt.Sprintf("%q is not a valid URL: %s", s, err))
}
}
},
}

View File

@@ -0,0 +1,111 @@
package sa1008
import (
"fmt"
"go/ast"
"net/http"
"strconv"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1008",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Non-canonical key in \'http.Header\' map`,
Text: `Keys in \'http.Header\' maps are canonical, meaning they follow a specific
combination of uppercase and lowercase letters. Methods such as
\'http.Header.Add\' and \'http.Header.Del\' convert inputs into this canonical
form before manipulating the map.
When manipulating \'http.Header\' maps directly, as opposed to using the
provided methods, care should be taken to stick to canonical form in
order to avoid inconsistencies. The following piece of code
demonstrates one such inconsistency:
h := http.Header{}
h["etag"] = []string{"1234"}
h.Add("etag", "5678")
fmt.Println(h)
// Output:
// map[Etag:[5678] etag:[1234]]
The easiest way of obtaining the canonical form of a key is to use
\'http.CanonicalHeaderKey\'.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node, push bool) bool {
if !push {
return false
}
if assign, ok := node.(*ast.AssignStmt); ok {
// TODO(dh): This risks missing some Header reads, for
// example in `h1["foo"] = h2["foo"]` these edge
// cases are probably rare enough to ignore for now.
for _, expr := range assign.Lhs {
op, ok := expr.(*ast.IndexExpr)
if !ok {
continue
}
if code.IsOfTypeWithName(pass, op.X, "net/http.Header") {
return false
}
}
return true
}
op, ok := node.(*ast.IndexExpr)
if !ok {
return true
}
if !code.IsOfTypeWithName(pass, op.X, "net/http.Header") {
return true
}
s, ok := code.ExprToString(pass, op.Index)
if !ok {
return true
}
canonical := http.CanonicalHeaderKey(s)
if s == canonical {
return true
}
var fix analysis.SuggestedFix
switch op.Index.(type) {
case *ast.BasicLit:
fix = edit.Fix("Canonicalize header key", edit.ReplaceWithString(op.Index, strconv.Quote(canonical)))
case *ast.Ident:
call := &ast.CallExpr{
Fun: edit.Selector("http", "CanonicalHeaderKey"),
Args: []ast.Expr{op.Index},
}
fix = edit.Fix("Wrap in http.CanonicalHeaderKey", edit.ReplaceWithNode(pass.Fset, op.Index, call))
}
msg := fmt.Sprintf("keys in http.Header are canonicalized, %q is not canonical; fix the constant or use http.CanonicalHeaderKey", s)
if fix.Message != "" {
report.Report(pass, op, msg, report.Fixes(fix))
} else {
report.Report(pass, op, msg)
}
return true
}
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.AssignStmt)(nil), (*ast.IndexExpr)(nil)}, fn)
return nil, nil
}

View File

@@ -0,0 +1,53 @@
package sa1010
import (
"fmt"
"go/constant"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1010",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkRegexpFindAllRules),
},
Doc: &lint.RawDocumentation{
Title: `\'(*regexp.Regexp).FindAll\' called with \'n == 0\', which will always return zero results`,
Text: `If \'n >= 0\', the function returns at most \'n\' matches/submatches. To
return all results, specify a negative number.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny, // MergeIfAny if we only flag literals, not named constants
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkRegexpFindAllRules = map[string]callcheck.Check{
"(*regexp.Regexp).FindAll": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllIndex": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllString": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllStringIndex": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllStringSubmatch": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllStringSubmatchIndex": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllSubmatch": RepeatZeroTimes("a FindAll method", 1),
"(*regexp.Regexp).FindAllSubmatchIndex": RepeatZeroTimes("a FindAll method", 1),
}
func RepeatZeroTimes(name string, arg int) callcheck.Check {
return func(call *callcheck.Call) {
arg := call.Args[arg]
if k, ok := arg.Value.Value.(*ir.Const); ok && k.Value.Kind() == constant.Int {
if v, ok := constant.Int64Val(k.Value); ok && v == 0 {
arg.Invalid(fmt.Sprintf("calling %s with n == 0 will return no results, did you mean -1?", name))
}
}
}
}

View File

@@ -0,0 +1,47 @@
package sa1011
import (
"go/constant"
"unicode/utf8"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1011",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkUTF8CutsetRules),
},
Doc: &lint.RawDocumentation{
Title: `Various methods in the \"strings\" package expect valid UTF-8, but invalid input is provided`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkUTF8CutsetRules = map[string]callcheck.Check{
"strings.IndexAny": check,
"strings.LastIndexAny": check,
"strings.ContainsAny": check,
"strings.Trim": check,
"strings.TrimLeft": check,
"strings.TrimRight": check,
}
func check(call *callcheck.Call) {
arg := call.Args[1]
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.String); c != nil {
s := constant.StringVal(c.Value)
if !utf8.ValidString(s) {
arg.Invalid("argument is not a valid UTF-8 encoded string")
}
}
}

View File

@@ -0,0 +1,65 @@
package sa1012
import (
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1012",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `A nil \'context.Context\' is being passed to a function, consider using \'context.TODO\' instead`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkNilContextQ = pattern.MustParse(`(CallExpr fun@(Symbol _) (Builtin "nil"):_)`)
func run(pass *analysis.Pass) (any, error) {
todo := &ast.CallExpr{
Fun: edit.Selector("context", "TODO"),
}
bg := &ast.CallExpr{
Fun: edit.Selector("context", "Background"),
}
for node, m := range code.Matches(pass, checkNilContextQ) {
call := node.(*ast.CallExpr)
fun, ok := m.State["fun"].(*types.Func)
if !ok {
// it might also be a builtin
continue
}
sig := fun.Type().(*types.Signature)
if sig.Params().Len() == 0 {
// Our CallExpr might've matched a method expression, like
// (*T).Foo(nil) here, nil isn't the first argument of
// the Foo method, but the method receiver.
continue
}
if !typeutil.IsTypeWithName(sig.Params().At(0).Type(), "context.Context") {
continue
}
report.Report(pass, call.Args[0],
"do not pass a nil Context, even if a function permits it; pass context.TODO if you are unsure about which Context to use", report.Fixes(
edit.Fix("Use context.TODO", edit.ReplaceWithNode(pass.Fset, call.Args[0], todo)),
edit.Fix("Use context.Background", edit.ReplaceWithNode(pass.Fset, call.Args[0], bg))))
}
return nil, nil
}

View File

@@ -0,0 +1,47 @@
package sa1013
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1013",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\'io.Seeker.Seek\' is being called with the whence constant as the first argument, but it should be the second`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkSeekerQ = pattern.MustParse(`(CallExpr fun@(SelectorExpr _ (Ident "Seek")) [arg1@(SelectorExpr _ (Symbol (Or "io.SeekStart" "io.SeekCurrent" "io.SeekEnd"))) arg2])`)
checkSeekerR = pattern.MustParse(`(CallExpr fun [arg2 arg1])`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkSeekerQ) {
if !code.IsMethod(pass, m.State["fun"].(*ast.SelectorExpr), "Seek", knowledge.Signatures["(io.Seeker).Seek"]) {
continue
}
edits := code.EditMatch(pass, node, m, checkSeekerR)
report.Report(pass, node, "the first argument of io.Seeker is the offset, but an io.Seek* constant is being used instead",
report.Fixes(edit.Fix("Swap arguments", edits...)))
}
return nil, nil
}

View File

@@ -0,0 +1,52 @@
package sa1014
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1014",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkUnmarshalPointerRules),
},
Doc: &lint.RawDocumentation{
Title: `Non-pointer value passed to \'Unmarshal\' or \'Decode\'`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkUnmarshalPointerRules = map[string]callcheck.Check{
"encoding/xml.Unmarshal": unmarshalPointer("xml.Unmarshal", 1),
"(*encoding/xml.Decoder).Decode": unmarshalPointer("Decode", 0),
"(*encoding/xml.Decoder).DecodeElement": unmarshalPointer("DecodeElement", 0),
"encoding/json.Unmarshal": unmarshalPointer("json.Unmarshal", 1),
"(*encoding/json.Decoder).Decode": unmarshalPointer("Decode", 0),
}
func unmarshalPointer(name string, arg int) callcheck.Check {
return func(call *callcheck.Call) {
if !Pointer(call.Args[arg].Value) {
call.Args[arg].Invalid(fmt.Sprintf("%s expects to unmarshal into a pointer, but the provided value is not a pointer", name))
}
}
}
func Pointer(v callcheck.Value) bool {
switch v.Value.Type().Underlying().(type) {
case *types.Pointer, *types.Interface:
return true
}
return false
}

View File

@@ -0,0 +1,69 @@
package sa1015
import (
"go/token"
"go/version"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1015",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Using \'time.Tick\' in a way that will leak. Consider using \'time.NewTicker\', and only use \'time.Tick\' in tests, commands and endless functions`,
Text: `Before Go 1.23, \'time.Ticker\'s had to be closed to be able to be garbage
collected. Since \'time.Tick\' doesn't make it possible to close the underlying
ticker, using it repeatedly would leak memory.
Go 1.23 fixes this by allowing tickers to be collected even if they weren't closed.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
if fn.Pos() == token.NoPos || version.Compare(code.StdlibVersion(pass, fn), "go1.23") >= 0 {
// Beginning with Go 1.23, the GC is able to collect unreferenced, unclosed
// tickers, which makes time.Tick safe(r) to use.
//
// When we don't have a valid position, we err on the side of false negatives.
// This shouldn't actually lead to any false negatives, as no functions
// without valid positions (such as the synthesized init function) should be
// able to use time.Tick.
continue
}
if code.IsMainLike(pass) || code.IsInTest(pass, fn) {
continue
}
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
call, ok := ins.(*ir.Call)
if !ok || !irutil.IsCallTo(call.Common(), "time.Tick") {
continue
}
if !irutil.Terminates(call.Parent()) {
continue
}
report.Report(pass, call, "using time.Tick leaks the underlying ticker, consider using it only in endless functions, tests and the main package, and use time.NewTicker here")
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,115 @@
package sa1016
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1016",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Trapping a signal that cannot be trapped`,
Text: `Not all signals can be intercepted by a process. Specifically, on
UNIX-like systems, the \'syscall.SIGKILL\' and \'syscall.SIGSTOP\' signals are
never passed to the process, but instead handled directly by the
kernel. It is therefore pointless to try and handle these signals.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`
(CallExpr
(Symbol
(Or
"os/signal.Ignore"
"os/signal.Notify"
"os/signal.Reset"))
_)`)
func run(pass *analysis.Pass) (any, error) {
isSignal := func(pass *analysis.Pass, expr ast.Expr, name string) bool {
if expr, ok := expr.(*ast.SelectorExpr); ok {
return code.SelectorName(pass, expr) == name
} else {
return false
}
}
for node := range code.Matches(pass, query) {
call := node.(*ast.CallExpr)
hasSigterm := false
for _, arg := range call.Args {
if conv, ok := arg.(*ast.CallExpr); ok && isSignal(pass, conv.Fun, "os.Signal") {
arg = conv.Args[0]
}
if isSignal(pass, arg, "syscall.SIGTERM") {
hasSigterm = true
break
}
}
for i, arg := range call.Args {
if conv, ok := arg.(*ast.CallExpr); ok && isSignal(pass, conv.Fun, "os.Signal") {
arg = conv.Args[0]
}
if isSignal(pass, arg, "os.Kill") || isSignal(pass, arg, "syscall.SIGKILL") {
var fixes []analysis.SuggestedFix
if !hasSigterm {
nargs := make([]ast.Expr, len(call.Args))
for j, a := range call.Args {
if i == j {
nargs[j] = edit.Selector("syscall", "SIGTERM")
} else {
nargs[j] = a
}
}
ncall := *call
ncall.Args = nargs
fixes = append(fixes, edit.Fix(fmt.Sprintf("Use syscall.SIGTERM instead of %s", report.Render(pass, arg)), edit.ReplaceWithNode(pass.Fset, call, &ncall)))
}
nargs := make([]ast.Expr, 0, len(call.Args))
for j, a := range call.Args {
if i == j {
continue
}
nargs = append(nargs, a)
}
ncall := *call
ncall.Args = nargs
fixes = append(fixes, edit.Fix(fmt.Sprintf("Remove %s from list of arguments", report.Render(pass, arg)), edit.ReplaceWithNode(pass.Fset, call, &ncall)))
report.Report(pass, arg, fmt.Sprintf("%s cannot be trapped (did you mean syscall.SIGTERM?)", report.Render(pass, arg)), report.Fixes(fixes...))
}
if isSignal(pass, arg, "syscall.SIGSTOP") {
nargs := make([]ast.Expr, 0, len(call.Args)-1)
for j, a := range call.Args {
if i == j {
continue
}
nargs = append(nargs, a)
}
ncall := *call
ncall.Args = nargs
report.Report(pass, arg, "syscall.SIGSTOP cannot be trapped", report.Fixes(edit.Fix("Remove syscall.SIGSTOP from list of arguments", edit.ReplaceWithNode(pass.Fset, call, &ncall))))
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,64 @@
package sa1017
import (
"go/constant"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1017",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Channels used with \'os/signal.Notify\' should be buffered`,
Text: `The \'os/signal\' package uses non-blocking channel sends when delivering
signals. If the receiving end of the channel isn't ready and the
channel is either unbuffered or full, the signal will be dropped. To
avoid missing signals, the channel should be buffered and of the
appropriate size. For a channel used for notification of just one
signal value, a buffer of size 1 is sufficient.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"os/signal.Notify": func(call *callcheck.Call) {
arg := call.Args[knowledge.Arg("os/signal.Notify.c")]
if isUnbufferedChannel(arg.Value) {
arg.Invalid("the channel used with signal.Notify should be buffered")
}
},
}
func isUnbufferedChannel(v callcheck.Value) bool {
// TODO(dh): this check of course misses many cases of unbuffered
// channels, such as any in phi or sigma nodes. We'll eventually
// replace this function.
val := v.Value
if ct, ok := val.(*ir.ChangeType); ok {
val = ct.X
}
mk, ok := val.(*ir.MakeChan)
if !ok {
return false
}
if k, ok := mk.Size.(*ir.Const); ok && k.Value.Kind() == constant.Int {
if v, ok := constant.Int64Val(k.Value); ok && v == 0 {
return true
}
}
return false
}

View File

@@ -0,0 +1,47 @@
package sa1018
import (
"fmt"
"go/constant"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1018",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `\'strings.Replace\' called with \'n == 0\', which does nothing`,
Text: `With \'n == 0\', zero instances will be replaced. To replace all
instances, use a negative number, or use \'strings.ReplaceAll\'.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny, // MergeIfAny if we only flag literals, not named constants
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"strings.Replace": check("strings.Replace", 3),
"bytes.Replace": check("bytes.Replace", 3),
}
func check(name string, arg int) callcheck.Check {
return func(call *callcheck.Call) {
arg := call.Args[arg]
if k, ok := arg.Value.Value.(*ir.Const); ok && k.Value.Kind() == constant.Int {
if v, ok := constant.Int64Val(k.Value); ok && v == 0 {
arg.Invalid(fmt.Sprintf("calling %s with n == 0 will return no results, did you mean -1?", name))
}
}
}
}

View File

@@ -0,0 +1,212 @@
package sa1019
import (
"fmt"
"go/ast"
"go/types"
"go/version"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/deprecated"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1019",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, deprecated.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Using a deprecated function, variable, constant or field`,
Since: "2017.1",
Severity: lint.SeverityDeprecated,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func formatGoVersion(s string) string {
return "Go " + strings.TrimPrefix(s, "go")
}
func run(pass *analysis.Pass) (any, error) {
deprs := pass.ResultOf[deprecated.Analyzer].(deprecated.Result)
// Selectors can appear outside of function literals, e.g. when
// declaring package level variables.
isStdlibPath := func(path string) bool {
// Modules with no dot in the first path element are reserved for the standard library and tooling.
// This is the best we can currently do.
// Nobody tells us which import paths are part of the standard library.
//
// We check the entire path instead of just the first path element, because the standard library doesn't contain paths with any dots, anyway.
return !strings.Contains(path, ".")
}
handleDeprecation := func(depr *deprecated.IsDeprecated, node ast.Node, deprecatedObjName string, pkgPath string, tfn types.Object) {
std, ok := knowledge.StdlibDeprecations[deprecatedObjName]
if !ok && isStdlibPath(pkgPath) {
// Deprecated object in the standard library, but we don't know the details of the deprecation.
// Don't flag it at all, to avoid flagging an object that was deprecated in 1.N when targeting 1.N-1.
// See https://staticcheck.dev/issues/1108 for the background on this.
return
}
if ok {
// In the past, we made use of the AlternativeAvailableSince field. If a function was deprecated in Go
// 1.6 and an alternative had been available in Go 1.0, then we'd recommend using the alternative even
// if targeting Go 1.2. The idea was to suggest writing future-proof code by using already-existing
// alternatives. This had a major flaw, however: the user would need to use at least Go 1.6 for
// Staticcheck to know that the function had been deprecated. Thus, targeting Go 1.2 and using Go 1.2
// would behave differently from targeting Go 1.2 and using Go 1.6. This is especially a problem if the
// user tries to ignore the warning. Depending on the Go version in use, the ignore directive may or may
// not match, causing a warning of its own.
//
// To avoid this issue, we no longer try to be smart. We now only compare the targeted version against
// the version that deprecated an object.
//
// Unfortunately, this issue also applies to AlternativeAvailableSince == DeprecatedNeverUse. Even though it
// is only applied to seriously flawed API, such as broken cryptography, users may wish to ignore those
// warnings.
//
// See also https://staticcheck.dev/issues/1318.
if version.Compare(code.StdlibVersion(pass, node), std.DeprecatedSince) == -1 {
return
}
}
if tfn != nil {
if _, ok := deprs.Objects[tfn]; ok {
// functions that are deprecated may use deprecated
// symbols
return
}
}
if ok {
switch std.AlternativeAvailableSince {
case knowledge.DeprecatedNeverUse:
report.Report(pass, node,
fmt.Sprintf("%s has been deprecated since %s because it shouldn't be used: %s",
report.Render(pass, node), formatGoVersion(std.DeprecatedSince), depr.Msg))
case std.DeprecatedSince, knowledge.DeprecatedUseNoLonger:
report.Report(pass, node,
fmt.Sprintf("%s has been deprecated since %s: %s",
report.Render(pass, node), formatGoVersion(std.DeprecatedSince), depr.Msg))
default:
report.Report(pass, node,
fmt.Sprintf("%s has been deprecated since %s and an alternative has been available since %s: %s",
report.Render(pass, node), formatGoVersion(std.DeprecatedSince), formatGoVersion(std.AlternativeAvailableSince), depr.Msg))
}
} else {
report.Report(pass, node, fmt.Sprintf("%s is deprecated: %s", report.Render(pass, node), depr.Msg))
}
}
var tfn types.Object
stack := 0
fn := func(node ast.Node, push bool) bool {
if !push {
stack--
return false
}
stack++
if stack == 1 {
tfn = nil
}
if fn, ok := node.(*ast.FuncDecl); ok {
tfn = pass.TypesInfo.ObjectOf(fn.Name)
}
// FIXME(dh): this misses dot-imported objects
sel, ok := node.(*ast.SelectorExpr)
if !ok {
return true
}
obj := pass.TypesInfo.ObjectOf(sel.Sel)
if obj_, ok := obj.(*types.Func); ok {
obj = obj_.Origin()
}
if obj.Pkg() == nil {
return true
}
if obj.Pkg() == pass.Pkg {
// A package is allowed to use its own deprecated objects
return true
}
// A package "foo" has two related packages "foo_test" and "foo.test", for external tests and the package main
// generated by 'go test' respectively. "foo_test" can import and use "foo", "foo.test" imports and uses "foo"
// and "foo_test".
if strings.TrimSuffix(pass.Pkg.Path(), "_test") == obj.Pkg().Path() {
// foo_test (the external tests of foo) can use objects from foo.
return true
}
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == obj.Pkg().Path() {
// foo.test (the main package of foo's tests) can use objects from foo.
return true
}
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(obj.Pkg().Path(), "_test") {
// foo.test (the main package of foo's tests) can use objects from foo's external tests.
return true
}
if depr, ok := deprs.Objects[obj]; ok {
handleDeprecation(depr, sel, code.SelectorName(pass, sel), obj.Pkg().Path(), tfn)
}
return true
}
fn2 := func(node ast.Node) {
spec := node.(*ast.ImportSpec)
var imp *types.Package
if spec.Name != nil {
imp = pass.TypesInfo.ObjectOf(spec.Name).(*types.PkgName).Imported()
} else {
imp = pass.TypesInfo.Implicits[spec].(*types.PkgName).Imported()
}
p := spec.Path.Value
path := p[1 : len(p)-1]
if depr, ok := deprs.Packages[imp]; ok {
if path == "github.com/golang/protobuf/proto" {
gen, ok := code.Generator(pass, spec.Path.Pos())
if ok && gen == generated.ProtocGenGo {
return
}
}
if strings.TrimSuffix(pass.Pkg.Path(), "_test") == path {
// foo_test can import foo
return
}
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == path {
// foo.test can import foo
return
}
if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(path, "_test") {
// foo.test can import foo_test
return
}
handleDeprecation(depr, spec.Path, path, path, nil)
}
}
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes(nil, fn)
code.Preorder(pass, fn2, (*ast.ImportSpec)(nil))
return nil, nil
}

View File

@@ -0,0 +1,94 @@
package sa1020
import (
"go/constant"
"net"
"strconv"
"strings"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1020",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkListenAddressRules),
},
Doc: &lint.RawDocumentation{
Title: `Using an invalid host:port pair with a \'net.Listen\'-related function`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkListenAddressRules = map[string]callcheck.Check{
"net/http.ListenAndServe": checkValidHostPort(0),
"net/http.ListenAndServeTLS": checkValidHostPort(0),
}
func checkValidHostPort(arg int) callcheck.Check {
return func(call *callcheck.Call) {
if !ValidHostPort(call.Args[arg].Value) {
const MsgInvalidHostPort = "invalid port or service name in host:port pair"
call.Args[arg].Invalid(MsgInvalidHostPort)
}
}
}
func ValidHostPort(v callcheck.Value) bool {
if k := callcheck.ExtractConstExpectKind(v, constant.String); k != nil {
s := constant.StringVal(k.Value)
if s == "" {
return true
}
_, port, err := net.SplitHostPort(s)
if err != nil {
return false
}
// TODO(dh): check hostname
if !validatePort(port) {
return false
}
}
return true
}
func validateServiceName(s string) bool {
if len(s) < 1 || len(s) > 15 {
return false
}
if s[0] == '-' || s[len(s)-1] == '-' {
return false
}
if strings.Contains(s, "--") {
return false
}
hasLetter := false
for _, r := range s {
if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
hasLetter = true
continue
}
if r >= '0' && r <= '9' {
continue
}
return false
}
return hasLetter
}
func validatePort(s string) bool {
n, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return validateServiceName(s)
}
return n >= 0 && n <= 65535
}

View File

@@ -0,0 +1,49 @@
package sa1021
import (
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1021",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Using \'bytes.Equal\' to compare two \'net.IP\'`,
Text: `A \'net.IP\' stores an IPv4 or IPv6 address as a slice of bytes. The
length of the slice for an IPv4 address, however, can be either 4 or
16 bytes long, using different ways of representing IPv4 addresses. In
order to correctly compare two \'net.IP\'s, the \'net.IP.Equal\' method should
be used, as it takes both representations into account.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"bytes.Equal": func(call *callcheck.Call) {
if isConvertedFrom(call.Args[knowledge.Arg("bytes.Equal.a")].Value, "net.IP") &&
isConvertedFrom(call.Args[knowledge.Arg("bytes.Equal.b")].Value, "net.IP") {
call.Invalid("use net.IP.Equal to compare net.IPs, not bytes.Equal")
}
},
}
// ConvertedFrom reports whether value v was converted from type typ.
func isConvertedFrom(v callcheck.Value, typ string) bool {
change, ok := v.Value.(*ir.ChangeType)
return ok && types.TypeString(types.Unalias(change.X.Type()), nil) == typ
}

View File

@@ -0,0 +1,72 @@
package sa1023
import (
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1023",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Modifying the buffer in an \'io.Writer\' implementation`,
Text: `\'Write\' must not modify the slice data, even temporarily.`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// TODO(dh): this might be a good candidate for taint analysis.
// Taint the argument as MUST_NOT_MODIFY, then propagate that
// through functions like bytes.Split
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
sig := fn.Signature
if fn.Name() != "Write" || sig.Recv() == nil {
continue
}
if !types.Identical(sig, knowledge.Signatures["(io.Writer).Write"]) {
continue
}
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
switch ins := ins.(type) {
case *ir.Store:
addr, ok := ins.Addr.(*ir.IndexAddr)
if !ok {
continue
}
if addr.X != fn.Params[1] {
continue
}
report.Report(pass, ins, "io.Writer.Write must not modify the provided buffer, not even temporarily")
case *ir.Call:
if !irutil.IsCallTo(ins.Common(), "append") {
continue
}
if ins.Common().Args[0] != fn.Params[1] {
continue
}
report.Report(pass, ins, "io.Writer.Write must not modify the provided buffer, not even temporarily")
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,75 @@
package sa1024
import (
"go/constant"
"sort"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1024",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `A string cutset contains duplicate characters`,
Text: `The \'strings.TrimLeft\' and \'strings.TrimRight\' functions take cutsets, not
prefixes. A cutset is treated as a set of characters to remove from a
string. For example,
strings.TrimLeft("42133word", "1234")
will result in the string \'"word"\' any characters that are 1, 2, 3 or
4 are cut from the left of the string.
In order to remove one string from another, use \'strings.TrimPrefix\' instead.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"strings.Trim": check,
"strings.TrimLeft": check,
"strings.TrimRight": check,
}
func check(call *callcheck.Call) {
arg := call.Args[1]
if !isUniqueStringCutset(arg.Value) {
const MsgNonUniqueCutset = "cutset contains duplicate characters"
arg.Invalid(MsgNonUniqueCutset)
}
}
func isUniqueStringCutset(v callcheck.Value) bool {
if c := callcheck.ExtractConstExpectKind(v, constant.String); c != nil {
s := constant.StringVal(c.Value)
rs := runeSlice(s)
if len(rs) < 2 {
return true
}
sort.Sort(rs)
for i, r := range rs[1:] {
if rs[i] == r {
return false
}
}
}
return true
}
type runeSlice []rune
func (rs runeSlice) Len() int { return len(rs) }
func (rs runeSlice) Less(i int, j int) bool { return rs[i] < rs[j] }
func (rs runeSlice) Swap(i int, j int) { rs[i], rs[j] = rs[j], rs[i] }

View File

@@ -0,0 +1,92 @@
package sa1025
import (
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1025",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `It is not possible to use \'(*time.Timer).Reset\''s return value correctly`,
Since: "2019.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
call, ok := ins.(*ir.Call)
if !ok {
continue
}
if !irutil.IsCallTo(call.Common(), "(*time.Timer).Reset") {
continue
}
refs := call.Referrers()
if refs == nil {
continue
}
for _, ref := range irutil.FilterDebug(*refs) {
ifstmt, ok := ref.(*ir.If)
if !ok {
continue
}
found := false
for _, succ := range ifstmt.Block().Succs {
if len(succ.Preds) != 1 {
// Merge point, not a branch in the
// syntactical sense.
// FIXME(dh): this is broken for if
// statements a la "if x || y"
continue
}
irutil.Walk(succ, func(b *ir.BasicBlock) bool {
if !succ.Dominates(b) {
// We've reached the end of the branch
return false
}
for _, ins := range b.Instrs {
// TODO(dh): we should check that we're receiving from the
// channel of a time.Timer to further reduce false
// positives. Not a key priority, considering the rarity
// of Reset and the tiny likeliness of a false positive
//
// We intentionally don't handle aliases here, because
// we're only interested in time.Timer.C.
if ins, ok := ins.(*ir.Recv); ok && types.TypeString(ins.Chan.Type(), nil) == "<-chan time.Time" {
found = true
return false
}
}
return true
})
}
if found {
report.Report(pass, call, "it is not possible to use Reset's return value correctly, as there is a race condition between draining the channel and the new timer expiring")
}
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,77 @@
package sa1026
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/staticcheck/fakejson"
"honnef.co/go/tools/staticcheck/fakexml"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1026",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Cannot marshal channels or functions`,
Since: "2019.2",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"encoding/json.Marshal": checkJSON,
"encoding/xml.Marshal": checkXML,
"(*encoding/json.Encoder).Encode": checkJSON,
"(*encoding/xml.Encoder).Encode": checkXML,
}
func checkJSON(call *callcheck.Call) {
arg := call.Args[0]
T := arg.Value.Value.Type()
if err := fakejson.Marshal(T); err != nil {
typ := types.TypeString(err.Type, types.RelativeTo(call.Parent.Pkg.Pkg))
if err.Path == "x" {
arg.Invalid(fmt.Sprintf("trying to marshal unsupported type %s", typ))
} else {
arg.Invalid(fmt.Sprintf("trying to marshal unsupported type %s, via %s", typ, err.Path))
}
}
}
func checkXML(call *callcheck.Call) {
arg := call.Args[0]
T := arg.Value.Value.Type()
if err := fakexml.Marshal(T); err != nil {
switch err := err.(type) {
case *fakexml.UnsupportedTypeError:
typ := types.TypeString(err.Type, types.RelativeTo(call.Parent.Pkg.Pkg))
if err.Path == "x" {
arg.Invalid(fmt.Sprintf("trying to marshal unsupported type %s", typ))
} else {
arg.Invalid(fmt.Sprintf("trying to marshal unsupported type %s, via %s", typ, err.Path))
}
case *fakexml.CyclicTypeError:
typ := types.TypeString(err.Type, types.RelativeTo(call.Parent.Pkg.Pkg))
if err.Path == "x" {
arg.Invalid(fmt.Sprintf("trying to marshal cyclic type %s", typ))
} else {
arg.Invalid(fmt.Sprintf("trying to marshal cyclic type %s, via %s", typ, err.Path))
}
case *fakexml.TagPathError:
// Vet does a better job at reporting this error, because it can flag the actual struct tags, not just the call to Marshal
default:
// These errors get reported by SA5008 instead, which can flag the actual fields, independently of calls to xml.Marshal
}
}
}

View File

@@ -0,0 +1,76 @@
package sa1027
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1027",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkAtomicAlignment),
},
Doc: &lint.RawDocumentation{
Title: `Atomic access to 64-bit variable must be 64-bit aligned`,
Text: `On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to
arrange for 64-bit alignment of 64-bit words accessed atomically. The
first word in a variable or in an allocated struct, array, or slice
can be relied upon to be 64-bit aligned.
You can use the structlayout tool to inspect the alignment of fields
in a struct.`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkAtomicAlignment = map[string]callcheck.Check{
"sync/atomic.AddInt64": checkAtomicAlignmentImpl,
"sync/atomic.AddUint64": checkAtomicAlignmentImpl,
"sync/atomic.CompareAndSwapInt64": checkAtomicAlignmentImpl,
"sync/atomic.CompareAndSwapUint64": checkAtomicAlignmentImpl,
"sync/atomic.LoadInt64": checkAtomicAlignmentImpl,
"sync/atomic.LoadUint64": checkAtomicAlignmentImpl,
"sync/atomic.StoreInt64": checkAtomicAlignmentImpl,
"sync/atomic.StoreUint64": checkAtomicAlignmentImpl,
"sync/atomic.SwapInt64": checkAtomicAlignmentImpl,
"sync/atomic.SwapUint64": checkAtomicAlignmentImpl,
}
func checkAtomicAlignmentImpl(call *callcheck.Call) {
sizes := call.Pass.TypesSizes
if sizes.Sizeof(types.Typ[types.Uintptr]) != 4 {
// Not running on a 32-bit platform
return
}
v, ok := irutil.Flatten(call.Args[0].Value.Value).(*ir.FieldAddr)
if !ok {
// TODO(dh): also check indexing into arrays and slices
return
}
T := v.X.Type().Underlying().(*types.Pointer).Elem().Underlying().(*types.Struct)
fields := make([]*types.Var, 0, T.NumFields())
for i := 0; i < T.NumFields() && i <= v.Field; i++ {
fields = append(fields, T.Field(i))
}
off := sizes.Offsetsof(fields)[v.Field]
if off%8 != 0 {
msg := fmt.Sprintf("address of non 64-bit aligned field %s passed to %s",
T.Field(v.Field).Name(),
irutil.CallName(call.Instr.Common()))
call.Invalid(msg)
}
}

View File

@@ -0,0 +1,57 @@
package sa1028
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1028",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `\'sort.Slice\' can only be used on slices`,
Text: `The first argument of \'sort.Slice\' must be a slice.`,
Since: "2020.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"sort.Slice": check,
"sort.SliceIsSorted": check,
"sort.SliceStable": check,
}
func check(call *callcheck.Call) {
c := call.Instr.Common().StaticCallee()
arg := call.Args[0]
T := arg.Value.Value.Type().Underlying()
switch T.(type) {
case *types.Interface:
// we don't know.
// TODO(dh): if the value is a phi node we can look at its edges
if k, ok := arg.Value.Value.(*ir.Const); ok && k.Value == nil {
// literal nil, e.g. sort.Sort(nil, ...)
arg.Invalid(fmt.Sprintf("cannot call %s on nil literal", c))
}
case *types.Slice:
// this is fine
default:
// this is not fine
arg.Invalid(fmt.Sprintf("%s must only be called on slices, was called on %s", c, T))
}
}

View File

@@ -0,0 +1,61 @@
package sa1029
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1029",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkWithValueKeyRules),
},
Doc: &lint.RawDocumentation{
Title: `Inappropriate key in call to \'context.WithValue\'`,
Text: `The provided key must be comparable and should not be
of type \'string\' or any other built-in type to avoid collisions between
packages using context. Users of \'WithValue\' should define their own
types for keys.
To avoid allocating when assigning to an \'interface{}\',
context keys often have concrete type \'struct{}\'. Alternatively,
exported context key variables' static type should be a pointer or
interface.`,
Since: "2020.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkWithValueKeyRules = map[string]callcheck.Check{
"context.WithValue": checkWithValueKey,
}
func checkWithValueKey(call *callcheck.Call) {
arg := call.Args[1]
T := arg.Value.Value.Type()
if typ, ok := types.Unalias(T).(*types.Basic); ok {
if _, ok := T.(*types.Alias); ok {
arg.Invalid(
fmt.Sprintf("should not use built-in type %s (via alias %s) as key for value; define your own type to avoid collisions", typ, types.TypeString(T, types.RelativeTo(call.Pass.Pkg))))
} else {
arg.Invalid(
fmt.Sprintf("should not use built-in type %s as key for value; define your own type to avoid collisions", typ))
}
}
// TODO(dh): we should probably flag all anonymous structs, as they all risk collisions
if s, ok := T.(*types.Struct); ok && s.NumFields() == 0 {
arg.Invalid("should not use empty anonymous struct as key for value; define your own type to avoid collisions")
} else if !types.Comparable(T) {
arg.Invalid(fmt.Sprintf("keys used with context.WithValue must be comparable, but type %s is not comparable", T))
}
}

View File

@@ -0,0 +1,134 @@
package sa1030
import (
"fmt"
"go/constant"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1030",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Invalid argument in call to a \'strconv\' function`,
Text: `This check validates the format, number base and bit size arguments of
the various parsing and formatting functions in \'strconv\'.`,
Since: "2021.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"strconv.ParseComplex": func(call *callcheck.Call) {
validateComplexBitSize(call.Args[knowledge.Arg("strconv.ParseComplex.bitSize")])
},
"strconv.ParseFloat": func(call *callcheck.Call) {
validateFloatBitSize(call.Args[knowledge.Arg("strconv.ParseFloat.bitSize")])
},
"strconv.ParseInt": func(call *callcheck.Call) {
validateContinuousBitSize(call.Args[knowledge.Arg("strconv.ParseInt.bitSize")], 0, 64)
validateIntBaseAllowZero(call.Args[knowledge.Arg("strconv.ParseInt.base")])
},
"strconv.ParseUint": func(call *callcheck.Call) {
validateContinuousBitSize(call.Args[knowledge.Arg("strconv.ParseUint.bitSize")], 0, 64)
validateIntBaseAllowZero(call.Args[knowledge.Arg("strconv.ParseUint.base")])
},
"strconv.FormatComplex": func(call *callcheck.Call) {
validateComplexFormat(call.Args[knowledge.Arg("strconv.FormatComplex.fmt")])
validateComplexBitSize(call.Args[knowledge.Arg("strconv.FormatComplex.bitSize")])
},
"strconv.FormatFloat": func(call *callcheck.Call) {
validateFloatFormat(call.Args[knowledge.Arg("strconv.FormatFloat.fmt")])
validateFloatBitSize(call.Args[knowledge.Arg("strconv.FormatFloat.bitSize")])
},
"strconv.FormatInt": func(call *callcheck.Call) {
validateIntBase(call.Args[knowledge.Arg("strconv.FormatInt.base")])
},
"strconv.FormatUint": func(call *callcheck.Call) {
validateIntBase(call.Args[knowledge.Arg("strconv.FormatUint.base")])
},
"strconv.AppendFloat": func(call *callcheck.Call) {
validateFloatFormat(call.Args[knowledge.Arg("strconv.AppendFloat.fmt")])
validateFloatBitSize(call.Args[knowledge.Arg("strconv.AppendFloat.bitSize")])
},
"strconv.AppendInt": func(call *callcheck.Call) {
validateIntBase(call.Args[knowledge.Arg("strconv.AppendInt.base")])
},
"strconv.AppendUint": func(call *callcheck.Call) {
validateIntBase(call.Args[knowledge.Arg("strconv.AppendUint.base")])
},
}
func validateDiscreetBitSize(arg *callcheck.Argument, size1 int, size2 int) {
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.Int); c != nil {
val, _ := constant.Int64Val(c.Value)
if val != int64(size1) && val != int64(size2) {
arg.Invalid(fmt.Sprintf("'bitSize' argument is invalid, must be either %d or %d", size1, size2))
}
}
}
func validateComplexBitSize(arg *callcheck.Argument) { validateDiscreetBitSize(arg, 64, 128) }
func validateFloatBitSize(arg *callcheck.Argument) { validateDiscreetBitSize(arg, 32, 64) }
func validateContinuousBitSize(arg *callcheck.Argument, min int, max int) {
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.Int); c != nil {
val, _ := constant.Int64Val(c.Value)
if val < int64(min) || val > int64(max) {
arg.Invalid(fmt.Sprintf("'bitSize' argument is invalid, must be within %d and %d", min, max))
}
}
}
func validateIntBase(arg *callcheck.Argument) {
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.Int); c != nil {
val, _ := constant.Int64Val(c.Value)
if val < 2 {
arg.Invalid("'base' must not be smaller than 2")
}
if val > 36 {
arg.Invalid("'base' must not be larger than 36")
}
}
}
func validateIntBaseAllowZero(arg *callcheck.Argument) {
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.Int); c != nil {
val, _ := constant.Int64Val(c.Value)
if val < 2 && val != 0 {
arg.Invalid("'base' must not be smaller than 2, unless it is 0")
}
if val > 36 {
arg.Invalid("'base' must not be larger than 36")
}
}
}
func validateComplexFormat(arg *callcheck.Argument) {
validateFloatFormat(arg)
}
func validateFloatFormat(arg *callcheck.Argument) {
if c := callcheck.ExtractConstExpectKind(arg.Value, constant.Int); c != nil {
val, _ := constant.Int64Val(c.Value)
switch val {
case 'b', 'e', 'E', 'f', 'g', 'G', 'x', 'X':
default:
arg.Invalid(fmt.Sprintf("'fmt' argument is invalid: unknown format %q", val))
}
}
}

View File

@@ -0,0 +1,81 @@
package sa1031
import (
"go/constant"
"go/token"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1031",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkEncodeRules),
},
Doc: &lint.RawDocumentation{
Title: `Overlapping byte slices passed to an encoder`,
Text: `In an encoding function of the form \'Encode(dst, src)\', \'dst\' and
\'src\' were found to reference the same memory. This can result in
\'src\' bytes being overwritten before they are read, when the encoder
writes more than one byte per \'src\' byte.`,
Since: "2024.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkEncodeRules = map[string]callcheck.Check{
"encoding/ascii85.Encode": checkNonOverlappingDstSrc(knowledge.Arg("encoding/ascii85.Encode.dst"), knowledge.Arg("encoding/ascii85.Encode.src")),
"(*encoding/base32.Encoding).Encode": checkNonOverlappingDstSrc(knowledge.Arg("(*encoding/base32.Encoding).Encode.dst"), knowledge.Arg("(*encoding/base32.Encoding).Encode.src")),
"(*encoding/base64.Encoding).Encode": checkNonOverlappingDstSrc(knowledge.Arg("(*encoding/base64.Encoding).Encode.dst"), knowledge.Arg("(*encoding/base64.Encoding).Encode.src")),
"encoding/hex.Encode": checkNonOverlappingDstSrc(knowledge.Arg("encoding/hex.Encode.dst"), knowledge.Arg("encoding/hex.Encode.src")),
}
func checkNonOverlappingDstSrc(dstArg, srcArg int) callcheck.Check {
return func(call *callcheck.Call) {
dst := call.Args[dstArg]
src := call.Args[srcArg]
_, dstConst := irutil.Flatten(dst.Value.Value).(*ir.Const)
_, srcConst := irutil.Flatten(src.Value.Value).(*ir.Const)
if dstConst || srcConst {
// one of the arguments is nil, therefore overlap is not possible
return
}
if dst.Value == src.Value {
// simple case of f(b, b)
dst.Invalid("overlapping dst and src")
return
}
dstSlice, ok := irutil.Flatten(dst.Value.Value).(*ir.Slice)
if !ok {
return
}
srcSlice, ok := irutil.Flatten(src.Value.Value).(*ir.Slice)
if !ok {
return
}
if irutil.Flatten(dstSlice.X) != irutil.Flatten(srcSlice.X) {
// differing underlying arrays, all is well
return
}
l1 := irutil.Flatten(dstSlice.Low)
l2 := irutil.Flatten(srcSlice.Low)
c1, ok1 := l1.(*ir.Const)
c2, ok2 := l2.(*ir.Const)
if l1 == l2 || (ok1 && ok2 && constant.Compare(c1.Value, token.EQL, c2.Value)) {
// dst and src are the same slice, and have the same lower bound
dst.Invalid("overlapping dst and src")
return
}
}
}

View File

@@ -0,0 +1,79 @@
package sa1032
import (
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA1032",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Wrong order of arguments to \'errors.Is\'`,
Text: `
The first argument of the function \'errors.Is\' is the error
that we have and the second argument is the error we're trying to match against.
For example:
if errors.Is(err, io.EOF) { ... }
This check detects some cases where the two arguments have been swapped. It
flags any calls where the first argument is referring to a package-level error
variable, such as
if errors.Is(io.EOF, err) { /* this is wrong */ }`,
Since: "2024.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"errors.Is": validateIs,
}
func validateIs(call *callcheck.Call) {
if len(call.Args) != 2 {
return
}
global := func(arg *callcheck.Argument) *ir.Global {
v, ok := arg.Value.Value.(*ir.Load)
if !ok {
return nil
}
g, _ := v.X.(*ir.Global)
return g
}
x, y := call.Args[0], call.Args[1]
gx := global(x)
if gx == nil {
return
}
if pkgx := gx.Package().Pkg; pkgx != nil && pkgx.Path() != call.Pass.Pkg.Path() {
// x is a global that's not in this package
if gy := global(y); gy != nil {
if pkgy := gy.Package().Pkg; pkgy != nil && pkgy.Path() != call.Pass.Pkg.Path() {
// Both arguments refer to globals that aren't in this package. This can
// genuinely happen for external tests that check that one error "is"
// another one. net/http's external tests, for example, do
// `errors.Is(http.ErrNotSupported, errors.ErrUnsupported)`.
return
}
}
call.Invalid("arguments have the wrong order")
}
}

View File

@@ -0,0 +1,44 @@
package sa2000
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA2000",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\'sync.WaitGroup.Add\' called inside the goroutine, leading to a race condition`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkWaitgroupAddQ = pattern.MustParse(`
(GoStmt
(CallExpr
(FuncLit
_
call@(CallExpr (Symbol "(*sync.WaitGroup).Add") _):_) _))`)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, checkWaitgroupAddQ) {
call := m.State["call"].(ast.Node)
report.Report(pass, call, fmt.Sprintf("should call %s before starting the goroutine to avoid a race", report.Render(pass, call)))
}
return nil, nil
}

View File

@@ -0,0 +1,111 @@
package sa2001
import (
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA2001",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Empty critical section, did you mean to defer the unlock?`,
Text: `Empty critical sections of the kind
mu.Lock()
mu.Unlock()
are very often a typo, and the following was intended instead:
mu.Lock()
defer mu.Unlock()
Do note that sometimes empty critical sections can be useful, as a
form of signaling to wait on another goroutine. Many times, there are
simpler ways of achieving the same effect. When that isn't the case,
the code should be amply commented to avoid confusion. Combining such
comments with a \'//lint:ignore\' directive can be used to suppress this
rare false positive.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
if pass.Pkg.Path() == "sync_test" {
// exception for the sync package's tests
return nil, nil
}
// Initially it might seem like this check would be easier to
// implement using IR. After all, we're only checking for two
// consecutive method calls. In reality, however, there may be any
// number of other instructions between the lock and unlock, while
// still constituting an empty critical section. For example,
// given `m.x().Lock(); m.x().Unlock()`, there will be a call to
// x(). In the AST-based approach, this has a tiny potential for a
// false positive (the second call to x might be doing work that
// is protected by the mutex). In an IR-based approach, however,
// it would miss a lot of real bugs.
mutexParams := func(s ast.Stmt) (x ast.Expr, funcName string, ok bool) {
expr, ok := s.(*ast.ExprStmt)
if !ok {
return nil, "", false
}
call, ok := astutil.Unparen(expr.X).(*ast.CallExpr)
if !ok {
return nil, "", false
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return nil, "", false
}
fn, ok := pass.TypesInfo.ObjectOf(sel.Sel).(*types.Func)
if !ok {
return nil, "", false
}
sig := fn.Type().(*types.Signature)
if sig.Params().Len() != 0 || sig.Results().Len() != 0 {
return nil, "", false
}
return sel.X, fn.Name(), true
}
fn := func(node ast.Node) {
block := node.(*ast.BlockStmt)
if len(block.List) < 2 {
return
}
for i := range block.List[:len(block.List)-1] {
sel1, method1, ok1 := mutexParams(block.List[i])
sel2, method2, ok2 := mutexParams(block.List[i+1])
if !ok1 || !ok2 || report.Render(pass, sel1) != report.Render(pass, sel2) {
continue
}
if (method1 == "Lock" && method2 == "Unlock") ||
(method1 == "RLock" && method2 == "RUnlock") {
report.Report(pass, block.List[i+1], "empty critical section")
}
}
}
code.Preorder(pass, fn, (*ast.BlockStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,93 @@
package sa2002
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA2002",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Called \'testing.T.FailNow\' or \'SkipNow\' in a goroutine, which isn't allowed`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
gostmt, ok := ins.(*ir.Go)
if !ok {
continue
}
var fn *ir.Function
switch val := gostmt.Call.Value.(type) {
case *ir.Function:
fn = val
case *ir.MakeClosure:
fn = val.Fn.(*ir.Function)
default:
continue
}
if fn.Blocks == nil {
continue
}
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
call, ok := ins.(*ir.Call)
if !ok {
continue
}
if call.Call.IsInvoke() {
continue
}
callee := call.Call.StaticCallee()
if callee == nil {
continue
}
recv := callee.Signature.Recv()
if recv == nil {
continue
}
if !typeutil.IsPointerToTypeWithName(recv.Type(), "testing.common") {
continue
}
fn, ok := call.Call.StaticCallee().Object().(*types.Func)
if !ok {
continue
}
name := fn.Name()
switch name {
case "FailNow", "Fatal", "Fatalf", "SkipNow", "Skip", "Skipf":
default:
continue
}
// TODO(dh): don't report multiple diagnostics
// for multiple calls to T.Fatal, but do
// collect all of them as related information
report.Report(pass, gostmt, fmt.Sprintf("the goroutine calls T.%s, which must be called in the same goroutine as the test", name),
report.Related(call, fmt.Sprintf("call to T.%s", name)))
}
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,87 @@
package sa2003
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA2003",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Deferred \'Lock\' right after locking, likely meant to defer \'Unlock\' instead`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
instrs := irutil.FilterDebug(block.Instrs)
if len(instrs) < 2 {
continue
}
for i, ins := range instrs[:len(instrs)-1] {
call, ok := ins.(*ir.Call)
if !ok {
continue
}
if !irutil.IsCallToAny(call.Common(), "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
continue
}
nins, ok := instrs[i+1].(*ir.Defer)
if !ok {
continue
}
if !irutil.IsCallToAny(&nins.Call, "(*sync.Mutex).Lock", "(*sync.RWMutex).RLock") {
continue
}
if call.Common().Args[0] != nins.Call.Args[0] {
continue
}
name := shortCallName(call.Common())
alt := ""
switch name {
case "Lock":
alt = "Unlock"
case "RLock":
alt = "RUnlock"
}
report.Report(pass, nins, fmt.Sprintf("deferring %s right after having locked already; did you mean to defer %s?", name, alt))
}
}
}
return nil, nil
}
func shortCallName(call *ir.CallCommon) string {
if call.IsInvoke() {
return ""
}
switch v := call.Value.(type) {
case *ir.Function:
fn, ok := v.Object().(*types.Func)
if !ok {
return ""
}
return fn.Name()
case *ir.Builtin:
return v.Name()
}
return ""
}

View File

@@ -0,0 +1,118 @@
package sa3000
import (
"go/ast"
"go/types"
"go/version"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA3000",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `\'TestMain\' doesn't call \'os.Exit\', hiding test failures`,
Text: `Test executables (and in turn \"go test\") exit with a non-zero status
code if any tests failed. When specifying your own \'TestMain\' function,
it is your responsibility to arrange for this, by calling \'os.Exit\' with
the correct code. The correct code is returned by \'(*testing.M).Run\', so
the usual way of implementing \'TestMain\' is to end it with
\'os.Exit(m.Run())\'.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
var (
fnmain ast.Node
callsExit bool
callsRun bool
arg types.Object
)
fn := func(node ast.Node, push bool) bool {
if !push {
if fnmain != nil && node == fnmain {
if !callsExit && callsRun {
report.Report(pass, fnmain, "TestMain should call os.Exit to set exit code")
}
fnmain = nil
callsExit = false
callsRun = false
arg = nil
}
return true
}
switch node := node.(type) {
case *ast.FuncDecl:
if fnmain != nil {
return true
}
if !isTestMain(pass, node) {
return false
}
if version.Compare(code.StdlibVersion(pass, node), "go1.15") >= 0 {
// Beginning with Go 1.15, the test framework will call
// os.Exit for us.
return false
}
fnmain = node
arg = pass.TypesInfo.ObjectOf(node.Type.Params.List[0].Names[0])
return true
case *ast.CallExpr:
if code.IsCallTo(pass, node, "os.Exit") {
callsExit = true
return false
}
sel, ok := node.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
ident, ok := sel.X.(*ast.Ident)
if !ok {
return true
}
if arg != pass.TypesInfo.ObjectOf(ident) {
return true
}
if sel.Sel.Name == "Run" {
callsRun = true
return false
}
return true
default:
lint.ExhaustiveTypeSwitch(node)
return true
}
}
pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.FuncDecl)(nil), (*ast.CallExpr)(nil)}, fn)
return nil, nil
}
func isTestMain(pass *analysis.Pass, decl *ast.FuncDecl) bool {
if decl.Name.Name != "TestMain" {
return false
}
if len(decl.Type.Params.List) != 1 {
return false
}
arg := decl.Type.Params.List[0]
if len(arg.Names) != 1 {
return false
}
return code.IsOfPointerToTypeWithName(pass, arg.Type, "testing.M")
}

View File

@@ -0,0 +1,47 @@
package sa3001
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA3001",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Assigning to \'b.N\' in benchmarks distorts the results`,
Text: `The testing package dynamically sets \'b.N\' to improve the reliability of
benchmarks and uses it in computations to determine the duration of a
single operation. Benchmark code must not alter \'b.N\' as this would
falsify results.`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`(AssignStmt sel@(SelectorExpr selX (Ident "N")) "=" [_] )`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, query) {
assign := node.(*ast.AssignStmt)
if !code.IsOfPointerToTypeWithName(pass, m.State["selX"].(ast.Expr), "testing.B") {
continue
}
report.Report(pass, assign,
fmt.Sprintf("should not assign to %s", report.Render(pass, m.State["sel"])))
}
return nil, nil
}

View File

@@ -0,0 +1,217 @@
package sa4000
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"reflect"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
"golang.org/x/tools/go/ast/edge"
"golang.org/x/tools/go/ast/inspector"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4000",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Binary operator has identical expressions on both sides`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
var isFloat func(T types.Type) bool
isFloat = func(T types.Type) bool {
tset := typeutil.NewTypeSet(T)
if len(tset.Terms) == 0 {
// no terms, so floats are a possibility
return true
}
return tset.Any(func(term *types.Term) bool {
switch typ := term.Type().Underlying().(type) {
case *types.Basic:
kind := typ.Kind()
return kind == types.Float32 || kind == types.Float64
case *types.Array:
return isFloat(typ.Elem())
case *types.Struct:
for field := range typ.Fields() {
if isFloat(field.Type()) {
return true
}
}
return false
default:
return false
}
})
}
// TODO(dh): this check ignores the existence of side-effects and
// happily flags fn() == fn() so far, we've had only two complains
// about false positives, and it's caught several bugs in real
// code.
//
// We special case functions from the math/rand package. Someone ran
// into the following false positive: "rand.Intn(2) - rand.Intn(2), which I wrote to generate values {-1, 0, 1} with {0.25, 0.5, 0.25} probability."
skipComparableCheck := func(c inspector.Cursor) bool {
op, ok := c.Node().(*ast.BinaryExpr)
if !ok {
return false
}
if clit, ok := op.X.(*ast.CompositeLit); !ok || len(clit.Elts) != 0 {
return false
}
if clit, ok := op.Y.(*ast.CompositeLit); !ok || len(clit.Elts) != 0 {
return false
}
// TODO(dh): we should probably skip ParenExprs, but users should
// probably not use unnecessary ParenExprs.
vspec, ok := c.Parent().Node().(*ast.ValueSpec)
if !ok {
return false
}
e, i := c.ParentEdge()
if e != edge.ValueSpec_Values {
return false
}
if vspec.Names[i].Name == "_" {
// `var _ = T{} == T{}` is permitted, as a compile-time
// check that T implements comparable.
return true
}
return false
}
for c := range code.Cursor(pass).Preorder((*ast.BinaryExpr)(nil)) {
node := c.Node()
op := node.(*ast.BinaryExpr)
switch op.Op {
case token.EQL, token.NEQ:
if skipComparableCheck(c) {
continue
}
case token.SUB, token.QUO, token.AND, token.REM, token.OR, token.XOR, token.AND_NOT,
token.LAND, token.LOR, token.LSS, token.GTR, token.LEQ, token.GEQ:
default:
// For some ops, such as + and *, it can make sense to
// have identical operands
continue
}
if isFloat(pass.TypesInfo.TypeOf(op.X)) {
// 'float <op> float' makes sense for several operators.
// We've tried keeping an exact list of operators to allow, but floats keep surprising us. Let's just give up instead.
continue
}
if reflect.TypeOf(op.X) != reflect.TypeOf(op.Y) {
continue
}
if report.Render(pass, op.X) != report.Render(pass, op.Y) {
continue
}
l1, ok1 := op.X.(*ast.BasicLit)
l2, ok2 := op.Y.(*ast.BasicLit)
if ok1 && ok2 && l1.Kind == token.INT && l2.Kind == l1.Kind && l1.Value == "0" && l2.Value == l1.Value && code.IsGenerated(pass, l1.Pos()) {
// cgo generates the following function call:
// _cgoCheckPointer(_cgoBase0, 0 == 0) it uses 0 == 0
// instead of true in case the user shadowed the
// identifier. Ideally we'd restrict this exception to
// calls of _cgoCheckPointer, but it's not worth the
// hassle of keeping track of the stack. <lit> <op> <lit>
// are very rare to begin with, and we're mostly checking
// for them to catch typos such as 1 == 1 where the user
// meant to type i == 1. The odds of a false negative for
// 0 == 0 are slim.
continue
}
if expr, ok := op.X.(*ast.CallExpr); ok {
call := code.CallName(pass, expr)
switch call {
case "math/rand.Int",
"math/rand.Int31",
"math/rand.Int31n",
"math/rand.Int63",
"math/rand.Int63n",
"math/rand.Intn",
"math/rand.Uint32",
"math/rand.Uint64",
"math/rand.ExpFloat64",
"math/rand.Float32",
"math/rand.Float64",
"math/rand.NormFloat64",
"(*math/rand.Rand).Int",
"(*math/rand.Rand).Int31",
"(*math/rand.Rand).Int31n",
"(*math/rand.Rand).Int63",
"(*math/rand.Rand).Int63n",
"(*math/rand.Rand).Intn",
"(*math/rand.Rand).Uint32",
"(*math/rand.Rand).Uint64",
"(*math/rand.Rand).ExpFloat64",
"(*math/rand.Rand).Float32",
"(*math/rand.Rand).Float64",
"(*math/rand.Rand).NormFloat64",
"math/rand/v2.Int",
"math/rand/v2.Int32",
"math/rand/v2.Int32N",
"math/rand/v2.Int64",
"math/rand/v2.Int64N",
"math/rand/v2.IntN",
"math/rand/v2.N",
"math/rand/v2.Uint",
"math/rand/v2.Uint32",
"math/rand/v2.Uint32N",
"math/rand/v2.Uint64",
"math/rand/v2.Uint64N",
"math/rand/v2.UintN",
"math/rand/v2.ExpFloat64",
"math/rand/v2.Float32",
"math/rand/v2.Float64",
"math/rand/v2.NormFloat64",
"(*math/rand/v2.Rand).Int",
"(*math/rand/v2.Rand).Int32",
"(*math/rand/v2.Rand).Int32N",
"(*math/rand/v2.Rand).Int64",
"(*math/rand/v2.Rand).Int64N",
"(*math/rand/v2.Rand).IntN",
"(*math/rand/v2.Rand).N",
"(*math/rand/v2.Rand).Uint",
"(*math/rand/v2.Rand).Uint32",
"(*math/rand/v2.Rand).Uint32N",
"(*math/rand/v2.Rand).Uint64",
"(*math/rand/v2.Rand).Uint64N",
"(*math/rand/v2.Rand).UintN",
"(*math/rand/v2.Rand).ExpFloat64",
"(*math/rand/v2.Rand).Float32",
"(*math/rand/v2.Rand).Float64",
"(*math/rand/v2.Rand).NormFloat64":
continue
}
}
report.Report(pass, op, fmt.Sprintf("identical expressions on the left and right side of the '%s' operator", op.Op))
}
return nil, nil
}

View File

@@ -0,0 +1,52 @@
package sa4001
import (
"go/ast"
"regexp"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4001",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `\'&*x\' gets simplified to \'x\', it does not copy \'x\'`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
// cgo produces code like fn(&*_Cvar_kSomeCallbacks) which we don't
// want to flag.
cgoIdent = regexp.MustCompile(`^_C(func|var)_.+$`)
checkIneffectiveCopyQ1 = pattern.MustParse(`(UnaryExpr "&" (StarExpr obj))`)
checkIneffectiveCopyQ2 = pattern.MustParse(`(StarExpr (UnaryExpr "&" _))`)
)
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
if m, ok := code.Match(pass, checkIneffectiveCopyQ1, node); ok {
if ident, ok := m.State["obj"].(*ast.Ident); !ok || !cgoIdent.MatchString(ident.Name) {
report.Report(pass, node, "&*x will be simplified to x. It will not copy x.")
}
} else if _, ok := code.Match(pass, checkIneffectiveCopyQ2, node); ok {
report.Report(pass, node, "*&x will be simplified to x. It will not copy x.")
}
}
code.Preorder(pass, fn, (*ast.UnaryExpr)(nil), (*ast.StarExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,160 @@
package sa4003
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"math"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4003",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Comparing unsigned values against negative values is pointless`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
isobj := func(expr ast.Expr, name string) bool {
if name == "" {
return false
}
sel, ok := expr.(*ast.SelectorExpr)
if !ok {
return false
}
return typeutil.IsObject(pass.TypesInfo.ObjectOf(sel.Sel), name)
}
fn := func(node ast.Node) {
expr := node.(*ast.BinaryExpr)
tx := pass.TypesInfo.TypeOf(expr.X)
basic, ok := tx.Underlying().(*types.Basic)
if !ok {
return
}
// We only check for the math constants and integer literals, not for all constant expressions. This is to avoid
// false positives when constant values differ under different build tags.
var (
maxMathConst string
minMathConst string
maxLiteral constant.Value
minLiteral constant.Value
)
switch basic.Kind() {
case types.Uint8:
maxMathConst = "math.MaxUint8"
minLiteral = constant.MakeUint64(0)
maxLiteral = constant.MakeUint64(math.MaxUint8)
case types.Uint16:
maxMathConst = "math.MaxUint16"
minLiteral = constant.MakeUint64(0)
maxLiteral = constant.MakeUint64(math.MaxUint16)
case types.Uint32:
maxMathConst = "math.MaxUint32"
minLiteral = constant.MakeUint64(0)
maxLiteral = constant.MakeUint64(math.MaxUint32)
case types.Uint64:
maxMathConst = "math.MaxUint64"
minLiteral = constant.MakeUint64(0)
maxLiteral = constant.MakeUint64(math.MaxUint64)
case types.Uint:
// TODO(dh): we could chose 32 bit vs 64 bit depending on the file's build tags
maxMathConst = "math.MaxUint64"
minLiteral = constant.MakeUint64(0)
maxLiteral = constant.MakeUint64(math.MaxUint64)
case types.Int8:
minMathConst = "math.MinInt8"
maxMathConst = "math.MaxInt8"
minLiteral = constant.MakeInt64(math.MinInt8)
maxLiteral = constant.MakeInt64(math.MaxInt8)
case types.Int16:
minMathConst = "math.MinInt16"
maxMathConst = "math.MaxInt16"
minLiteral = constant.MakeInt64(math.MinInt16)
maxLiteral = constant.MakeInt64(math.MaxInt16)
case types.Int32:
minMathConst = "math.MinInt32"
maxMathConst = "math.MaxInt32"
minLiteral = constant.MakeInt64(math.MinInt32)
maxLiteral = constant.MakeInt64(math.MaxInt32)
case types.Int64:
minMathConst = "math.MinInt64"
maxMathConst = "math.MaxInt64"
minLiteral = constant.MakeInt64(math.MinInt64)
maxLiteral = constant.MakeInt64(math.MaxInt64)
case types.Int:
// TODO(dh): we could chose 32 bit vs 64 bit depending on the file's build tags
minMathConst = "math.MinInt64"
maxMathConst = "math.MaxInt64"
minLiteral = constant.MakeInt64(math.MinInt64)
maxLiteral = constant.MakeInt64(math.MaxInt64)
}
isLiteral := func(expr ast.Expr, c constant.Value) bool {
if c == nil {
return false
}
return code.IsIntegerLiteral(pass, expr, c)
}
isZeroLiteral := func(expr ast.Expr) bool {
return code.IsIntegerLiteral(pass, expr, constant.MakeInt64(0))
}
if (expr.Op == token.GTR || expr.Op == token.GEQ) && (isobj(expr.Y, maxMathConst) || isLiteral(expr.Y, maxLiteral)) ||
(expr.Op == token.LSS || expr.Op == token.LEQ) && (isobj(expr.X, maxMathConst) || isLiteral(expr.X, maxLiteral)) {
report.Report(pass, expr, fmt.Sprintf("no value of type %s is greater than %s", basic, maxMathConst), report.FilterGenerated())
}
if expr.Op == token.LEQ && (isobj(expr.Y, maxMathConst) || isLiteral(expr.Y, maxLiteral)) ||
expr.Op == token.GEQ && (isobj(expr.X, maxMathConst) || isLiteral(expr.X, maxLiteral)) {
report.Report(pass, expr, fmt.Sprintf("every value of type %s is <= %s", basic, maxMathConst), report.FilterGenerated())
}
if (basic.Info() & types.IsUnsigned) != 0 {
if (expr.Op == token.LSS && isZeroLiteral(expr.Y)) ||
(expr.Op == token.GTR && isZeroLiteral(expr.X)) {
report.Report(pass, expr, fmt.Sprintf("no value of type %s is less than 0", basic), report.FilterGenerated())
}
if expr.Op == token.GEQ && isZeroLiteral(expr.Y) ||
expr.Op == token.LEQ && isZeroLiteral(expr.X) {
report.Report(pass, expr, fmt.Sprintf("every value of type %s is >= 0", basic), report.FilterGenerated())
}
} else {
if (expr.Op == token.LSS || expr.Op == token.LEQ) && (isobj(expr.Y, minMathConst) || isLiteral(expr.Y, minLiteral)) ||
(expr.Op == token.GTR || expr.Op == token.GEQ) && (isobj(expr.X, minMathConst) || isLiteral(expr.X, minLiteral)) {
report.Report(pass, expr, fmt.Sprintf("no value of type %s is less than %s", basic, minMathConst), report.FilterGenerated())
}
if expr.Op == token.GEQ && (isobj(expr.Y, minMathConst) || isLiteral(expr.Y, minLiteral)) ||
expr.Op == token.LEQ && (isobj(expr.X, minMathConst) || isLiteral(expr.X, minLiteral)) {
report.Report(pass, expr, fmt.Sprintf("every value of type %s is >= %s", basic, minMathConst), report.FilterGenerated())
}
}
}
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,159 @@
package sa4004
import (
"go/ast"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4004",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `The loop exits unconditionally after one iteration`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// This check detects some, but not all unconditional loop exits.
// We give up in the following cases:
//
// - a goto anywhere in the loop. The goto might skip over our
// return, and we don't check that it doesn't.
//
// - any nested, unlabelled continue, even if it is in another
// loop or closure.
fn := func(node ast.Node) {
var body *ast.BlockStmt
switch fn := node.(type) {
case *ast.FuncDecl:
body = fn.Body
case *ast.FuncLit:
body = fn.Body
default:
lint.ExhaustiveTypeSwitch(node)
}
if body == nil {
return
}
labels := map[types.Object]ast.Stmt{}
ast.Inspect(body, func(node ast.Node) bool {
label, ok := node.(*ast.LabeledStmt)
if !ok {
return true
}
labels[pass.TypesInfo.ObjectOf(label.Label)] = label.Stmt
return true
})
ast.Inspect(body, func(node ast.Node) bool {
var loop ast.Node
var body *ast.BlockStmt
switch node := node.(type) {
case *ast.ForStmt:
body = node.Body
loop = node
case *ast.RangeStmt:
ok := typeutil.All(pass.TypesInfo.TypeOf(node.X), func(term *types.Term) bool {
switch term.Type().Underlying().(type) {
case *types.Slice, *types.Chan, *types.Basic, *types.Pointer, *types.Array:
return true
case *types.Map:
// looping once over a map is a valid pattern for
// getting an arbitrary element.
return false
case *types.Signature:
// we have no idea what semantics the function implements
return false
default:
lint.ExhaustiveTypeSwitch(term.Type().Underlying())
return false
}
})
if !ok {
return true
}
body = node.Body
loop = node
default:
return true
}
if len(body.List) < 2 {
// TODO(dh): is this check needed? when body.List < 2,
// then we can't find both an unconditional exit and a
// branching statement (if, ...). and we don't flag
// unconditional exits if there has been no branching
// in the loop body.
// avoid flagging the somewhat common pattern of using
// a range loop to get the first element in a slice,
// or the first rune in a string.
return true
}
var unconditionalExit ast.Node
hasBranching := false
for _, stmt := range body.List {
switch stmt := stmt.(type) {
case *ast.BranchStmt:
switch stmt.Tok {
case token.BREAK:
if stmt.Label == nil || labels[pass.TypesInfo.ObjectOf(stmt.Label)] == loop {
unconditionalExit = stmt
}
case token.CONTINUE:
if stmt.Label == nil || labels[pass.TypesInfo.ObjectOf(stmt.Label)] == loop {
unconditionalExit = nil
return false
}
}
case *ast.ReturnStmt:
unconditionalExit = stmt
case *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.SwitchStmt, *ast.SelectStmt:
hasBranching = true
}
}
if unconditionalExit == nil || !hasBranching {
return false
}
ast.Inspect(body, func(node ast.Node) bool {
if branch, ok := node.(*ast.BranchStmt); ok {
switch branch.Tok {
case token.GOTO:
unconditionalExit = nil
return false
case token.CONTINUE:
if branch.Label != nil && labels[pass.TypesInfo.ObjectOf(branch.Label)] != loop {
return true
}
unconditionalExit = nil
return false
}
}
return true
})
if unconditionalExit != nil {
report.Report(pass, unconditionalExit, "the surrounding loop is unconditionally terminated")
}
return true
})
}
code.Preorder(pass, fn, (*ast.FuncDecl)(nil), (*ast.FuncLit)(nil))
return nil, nil
}

View File

@@ -0,0 +1,138 @@
package sa4005
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4005",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Field assignment that will never be observed. Did you mean to use a pointer receiver?`,
Since: "2021.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// The analysis only considers the receiver and its first level
// fields. It doesn't look at other parameters, nor at nested
// fields.
//
// The analysis does not detect all kinds of dead stores, only
// those of fields that are never read after the write. That is,
// we do not flag 'a.x = 1; a.x = 2; _ = a.x'. We might explore
// this again if we add support for SROA to go/ir and implement
// https://github.com/dominikh/go-tools/issues/191.
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
fnLoop:
for _, fn := range irpkg.SrcFuncs {
if recv := fn.Signature.Recv(); recv == nil {
continue
} else if _, ok := recv.Type().Underlying().(*types.Struct); !ok {
continue
}
recv := fn.Params[0]
refs := irutil.FilterDebug(*recv.Referrers())
if len(refs) != 1 {
continue
}
store, ok := refs[0].(*ir.Store)
if !ok {
continue
}
alloc, ok := store.Addr.(*ir.Alloc)
if !ok || alloc.Heap {
continue
}
reads := map[int][]ir.Instruction{}
writes := map[int][]ir.Instruction{}
for _, ref := range *alloc.Referrers() {
switch ref := ref.(type) {
case *ir.FieldAddr:
for _, refref := range *ref.Referrers() {
switch refref.(type) {
case *ir.Store:
writes[ref.Field] = append(writes[ref.Field], refref)
case *ir.Load:
reads[ref.Field] = append(reads[ref.Field], refref)
case *ir.DebugRef:
continue
default:
// this should be safe… if the field address
// escapes, then alloc.Heap will be true.
// there should be no instructions left that,
// given this FieldAddr, without escaping, can
// effect a load or store.
continue
}
}
case *ir.Store:
// we could treat this as a store to every field, but
// we don't want to decide the semantics of partial
// struct initializers. should `v = t{x: 1}` also mark
// v.y as being written to?
if ref != store {
continue fnLoop
}
case *ir.Load:
// a load of the entire struct loads every field
for i := 0; i < recv.Type().Underlying().(*types.Struct).NumFields(); i++ {
reads[i] = append(reads[i], ref)
}
case *ir.DebugRef:
continue
default:
continue fnLoop
}
}
offset := func(instr ir.Instruction) int {
for i, other := range instr.Block().Instrs {
if instr == other {
return i
}
}
panic("couldn't find instruction in its block")
}
for field, ws := range writes {
rs := reads[field]
wLoop:
for _, w := range ws {
for _, r := range rs {
if w.Block() == r.Block() {
if offset(r) > offset(w) {
// found a reachable read of our write
continue wLoop
}
} else if irutil.Reachable(w.Block(), r.Block()) {
// found a reachable read of our write
continue wLoop
}
}
fieldName := recv.Type().Underlying().(*types.Struct).Field(field).Name()
report.Report(pass, w, fmt.Sprintf("ineffective assignment to field %s.%s", recv.Type().(interface{ Obj() *types.TypeName }).Obj().Name(), fieldName))
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,155 @@
package sa4006
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4006",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `A value assigned to a variable is never read before being overwritten. Forgotten error check or dead code?`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
if irutil.IsExample(fn) {
continue
}
node := fn.Source()
if node == nil {
continue
}
if gen, ok := code.Generator(pass, node.Pos()); ok && gen == generated.Goyacc {
// Don't flag unused values in code generated by goyacc.
// There may be hundreds of those due to the way the state
// machine is constructed.
continue
}
switchTags := map[ir.Value]struct{}{}
ast.Inspect(node, func(node ast.Node) bool {
s, ok := node.(*ast.SwitchStmt)
if !ok {
return true
}
v, _ := fn.ValueForExpr(s.Tag)
switchTags[v] = struct{}{}
return true
})
// OPT(dh): don't use a map, possibly use a bitset
var hasUse func(v ir.Value, seen map[ir.Value]struct{}) bool
hasUse = func(v ir.Value, seen map[ir.Value]struct{}) bool {
if _, ok := seen[v]; ok {
return false
}
if _, ok := switchTags[v]; ok {
return true
}
refs := v.Referrers()
if refs == nil {
// TODO investigate why refs can be nil
return true
}
for _, ref := range *refs {
switch ref := ref.(type) {
case *ir.DebugRef:
case *ir.Sigma:
if seen == nil {
seen = map[ir.Value]struct{}{}
}
seen[v] = struct{}{}
if hasUse(ref, seen) {
return true
}
case *ir.Phi:
if seen == nil {
seen = map[ir.Value]struct{}{}
}
seen[v] = struct{}{}
if hasUse(ref, seen) {
return true
}
default:
return true
}
}
return false
}
ast.Inspect(node, func(node ast.Node) bool {
assign, ok := node.(*ast.AssignStmt)
if !ok {
return true
}
if len(assign.Lhs) > 1 && len(assign.Rhs) == 1 {
// Either a function call with multiple return values,
// or a comma-ok assignment
val, _ := fn.ValueForExpr(assign.Rhs[0])
if val == nil {
return true
}
refs := val.Referrers()
if refs == nil {
return true
}
for _, ref := range *refs {
ex, ok := ref.(*ir.Extract)
if !ok {
continue
}
if !hasUse(ex, nil) {
lhs := assign.Lhs[ex.Index]
if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" {
continue
}
report.Report(pass, assign, fmt.Sprintf("this value of %s is never used", lhs))
}
}
return true
}
for i, lhs := range assign.Lhs {
rhs := assign.Rhs[i]
if ident, ok := lhs.(*ast.Ident); !ok || ok && ident.Name == "_" {
continue
}
val, _ := fn.ValueForExpr(rhs)
if val == nil {
continue
}
if _, ok := val.(*ir.Const); ok {
// a zero-valued constant, for example in 'foo := []string(nil)'
continue
}
if !hasUse(val, nil) {
report.Report(pass, assign, fmt.Sprintf("this value of %s is never used", lhs))
}
}
return true
})
}
return nil, nil
}

View File

@@ -0,0 +1,114 @@
package sa4008
import (
"go/ast"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4008",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `The variable in the loop condition never changes, are you incrementing the wrong variable?`,
Text: `For example:
for i := 0; i < 10; j++ { ... }
This may also occur when a loop can only execute once because of unconditional
control flow that terminates the loop. For example, when a loop body contains an
unconditional break, return, or panic:
func f() {
panic("oops")
}
func g() {
for i := 0; i < 10; i++ {
// f unconditionally calls panic, which means "i" is
// never incremented.
f()
}
}`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
cb := func(node ast.Node) bool {
loop, ok := node.(*ast.ForStmt)
if !ok {
return true
}
if loop.Init == nil || loop.Cond == nil || loop.Post == nil {
return true
}
init, ok := loop.Init.(*ast.AssignStmt)
if !ok || len(init.Lhs) != 1 || len(init.Rhs) != 1 {
return true
}
cond, ok := loop.Cond.(*ast.BinaryExpr)
if !ok {
return true
}
x, ok := cond.X.(*ast.Ident)
if !ok {
return true
}
lhs, ok := init.Lhs[0].(*ast.Ident)
if !ok {
return true
}
if pass.TypesInfo.ObjectOf(x) != pass.TypesInfo.ObjectOf(lhs) {
return true
}
if _, ok := loop.Post.(*ast.IncDecStmt); !ok {
return true
}
v, isAddr := fn.ValueForExpr(cond.X)
if v == nil || isAddr {
return true
}
switch v := v.(type) {
case *ir.Phi:
ops := v.Operands(nil)
if len(ops) != 2 {
return true
}
_, ok := (*ops[0]).(*ir.Const)
if !ok {
return true
}
sigma, ok := (*ops[1]).(*ir.Sigma)
if !ok {
return true
}
if sigma.X != v {
return true
}
case *ir.Load:
return true
}
report.Report(pass, cond, "variable in loop condition never changes")
return true
}
if source := fn.Source(); source != nil {
ast.Inspect(source, cb)
}
}
return nil, nil
}

View File

@@ -0,0 +1,106 @@
package sa4009
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4009",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `A function argument is overwritten before its first use`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
cb := func(node ast.Node) bool {
var typ *ast.FuncType
var body *ast.BlockStmt
switch fn := node.(type) {
case *ast.FuncDecl:
typ = fn.Type
body = fn.Body
case *ast.FuncLit:
typ = fn.Type
body = fn.Body
}
if body == nil {
return true
}
if len(typ.Params.List) == 0 {
return true
}
for _, field := range typ.Params.List {
for _, arg := range field.Names {
obj := pass.TypesInfo.ObjectOf(arg)
var irobj *ir.Parameter
for _, param := range fn.Params {
if param.Object() == obj {
irobj = param
break
}
}
if irobj == nil {
continue
}
refs := irobj.Referrers()
if refs == nil {
continue
}
if len(irutil.FilterDebug(*refs)) != 0 {
continue
}
var assignment ast.Node
ast.Inspect(body, func(node ast.Node) bool {
if assignment != nil {
return false
}
assign, ok := node.(*ast.AssignStmt)
if !ok {
return true
}
for _, lhs := range assign.Lhs {
ident, ok := lhs.(*ast.Ident)
if !ok {
continue
}
if pass.TypesInfo.ObjectOf(ident) == obj {
assignment = assign
return false
}
}
return true
})
if assignment != nil {
report.Report(pass, arg, fmt.Sprintf("argument %s is overwritten before first use", arg),
report.Related(assignment, fmt.Sprintf("assignment to %s", arg)))
}
}
}
return true
}
if source := fn.Source(); source != nil {
ast.Inspect(source, cb)
}
}
return nil, nil
}

View File

@@ -0,0 +1,216 @@
package sa4010
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4010",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `The result of \'append\' will never be observed anywhere`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
isAppend := func(ins ir.Value) bool {
call, ok := ins.(*ir.Call)
if !ok {
return false
}
if call.Call.IsInvoke() {
return false
}
if builtin, ok := call.Call.Value.(*ir.Builtin); !ok || builtin.Name() != "append" {
return false
}
return true
}
// We have to be careful about aliasing.
// Multiple slices may refer to the same backing array,
// making appends observable even when we don't see the result of append be used anywhere.
//
// We will have to restrict ourselves to slices that have been allocated within the function,
// haven't been sliced,
// and haven't been passed anywhere that could retain them (such as function calls or memory stores).
//
// We check whether an append should be flagged in two steps.
//
// In the first step, we look at the data flow graph, starting in reverse from the argument to append, till we reach the root.
// This graph must only consist of the following instructions:
//
// - phi
// - sigma
// - slice
// - const nil
// - MakeSlice
// - Alloc
// - calls to append
//
// If this step succeeds, we look at all referrers of the values found in the first step, recursively.
// These referrers must either be in the set of values found in the first step,
// be DebugRefs,
// or fulfill the same type requirements as step 1, with the exception of appends, which are forbidden.
//
// If both steps succeed then we know that the backing array hasn't been aliased in an observable manner.
//
// We could relax these restrictions by making use of additional information:
// - if we passed the slice to a function that doesn't retain the slice then we can still flag it
// - if a slice has been sliced but is dead afterwards, we can flag appends to the new slice
// OPT(dh): We could cache the results of both validate functions.
// However, we only use these functions on values that we otherwise want to flag, which are very few.
// Not caching values hasn't increased the runtimes for the standard library nor k8s.
var validateArgument func(v ir.Value, seen map[ir.Value]struct{}) bool
validateArgument = func(v ir.Value, seen map[ir.Value]struct{}) bool {
if _, ok := seen[v]; ok {
// break cycle
return true
}
seen[v] = struct{}{}
switch v := v.(type) {
case *ir.Phi:
for _, edge := range v.Edges {
if !validateArgument(edge, seen) {
return false
}
}
return true
case *ir.Sigma:
return validateArgument(v.X, seen)
case *ir.Slice:
return validateArgument(v.X, seen)
case *ir.Const:
return true
case *ir.MakeSlice:
return true
case *ir.Alloc:
return true
case *ir.Call:
if isAppend(v) {
return validateArgument(v.Call.Args[0], seen)
}
return false
default:
return false
}
}
var validateReferrers func(v ir.Value, seen map[ir.Instruction]struct{}) bool
validateReferrers = func(v ir.Value, seen map[ir.Instruction]struct{}) bool {
for _, ref := range *v.Referrers() {
if _, ok := seen[ref]; ok {
continue
}
seen[ref] = struct{}{}
switch ref.(type) {
case *ir.Phi:
case *ir.Sigma:
case *ir.Slice:
case *ir.Const:
case *ir.MakeSlice:
case *ir.Alloc:
case *ir.DebugRef:
default:
return false
}
if ref, ok := ref.(ir.Value); ok {
if !validateReferrers(ref, seen) {
return false
}
}
}
return true
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
val, ok := ins.(ir.Value)
if !ok || !isAppend(val) {
continue
}
isUsed := false
visited := map[ir.Instruction]bool{}
var walkRefs func(refs []ir.Instruction)
walkRefs = func(refs []ir.Instruction) {
loop:
for _, ref := range refs {
if visited[ref] {
continue
}
visited[ref] = true
if _, ok := ref.(*ir.DebugRef); ok {
continue
}
switch ref := ref.(type) {
case *ir.Phi:
walkRefs(*ref.Referrers())
case *ir.Sigma:
walkRefs(*ref.Referrers())
case ir.Value:
if !isAppend(ref) {
isUsed = true
} else {
walkRefs(*ref.Referrers())
}
case ir.Instruction:
isUsed = true
break loop
}
}
}
refs := val.Referrers()
if refs == nil {
continue
}
walkRefs(*refs)
if isUsed {
continue
}
seen := map[ir.Value]struct{}{}
if !validateArgument(ins.(*ir.Call).Call.Args[0], seen) {
continue
}
seen2 := map[ir.Instruction]struct{}{}
for k := range seen {
// the only values we allow are also instructions, so this type assertion cannot fail
seen2[k.(ir.Instruction)] = struct{}{}
}
seen2[ins] = struct{}{}
failed := false
for v := range seen {
if !validateReferrers(v, seen2) {
failed = true
break
}
}
if !failed {
report.Report(pass, ins, "this result of append is never used, except maybe in other appends")
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,88 @@
package sa4011
import (
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4011",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Break statement with no effect. Did you mean to break out of an outer loop?`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
var body *ast.BlockStmt
switch node := node.(type) {
case *ast.ForStmt:
body = node.Body
case *ast.RangeStmt:
body = node.Body
default:
lint.ExhaustiveTypeSwitch(node)
}
for _, stmt := range body.List {
var blocks [][]ast.Stmt
switch stmt := stmt.(type) {
case *ast.SwitchStmt:
for _, c := range stmt.Body.List {
blocks = append(blocks, c.(*ast.CaseClause).Body)
}
case *ast.SelectStmt:
for _, c := range stmt.Body.List {
blocks = append(blocks, c.(*ast.CommClause).Body)
}
default:
continue
}
for _, body := range blocks {
if len(body) == 0 {
continue
}
lasts := []ast.Stmt{body[len(body)-1]}
// TODO(dh): unfold all levels of nested block
// statements, not just a single level if statement
if ifs, ok := lasts[0].(*ast.IfStmt); ok {
if len(ifs.Body.List) == 0 {
continue
}
lasts[0] = ifs.Body.List[len(ifs.Body.List)-1]
if block, ok := ifs.Else.(*ast.BlockStmt); ok {
if len(block.List) != 0 {
lasts = append(lasts, block.List[len(block.List)-1])
}
}
}
for _, last := range lasts {
branch, ok := last.(*ast.BranchStmt)
if !ok || branch.Tok != token.BREAK || branch.Label != nil {
continue
}
report.Report(pass, branch, "ineffective break statement. Did you mean to break out of the outer loop?")
}
}
}
}
code.Preorder(pass, fn, (*ast.ForStmt)(nil), (*ast.RangeStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,51 @@
package sa4012
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4012",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Comparing a value against NaN even though no value is equal to NaN`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
isNaN := func(v ir.Value) bool {
call, ok := v.(*ir.Call)
if !ok {
return false
}
return irutil.IsCallTo(call.Common(), "math.NaN")
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
ins, ok := ins.(*ir.BinOp)
if !ok {
continue
}
if isNaN(irutil.Flatten(ins.X)) || isNaN(irutil.Flatten(ins.Y)) {
report.Report(pass, ins, "no value is equal to NaN, not even NaN itself")
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,40 @@
package sa4013
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4013",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Negating a boolean twice (\'!!b\') is the same as writing \'b\'. This is either redundant, or a typo.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkDoubleNegationQ = pattern.MustParse(`(UnaryExpr "!" single@(UnaryExpr "!" x))`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkDoubleNegationQ) {
report.Report(pass, node, "negating a boolean twice has no effect; is this a typo?", report.Fixes(
edit.Fix("Turn into single negation", edit.ReplaceWithNode(pass.Fset, node, m.State["single"].(ast.Node))),
edit.Fix("Remove double negation", edit.ReplaceWithNode(pass.Fset, node, m.State["x"].(ast.Node)))))
}
return nil, nil
}

View File

@@ -0,0 +1,78 @@
package sa4014
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4014",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `An if/else if chain has repeated conditions and no side-effects; if the condition didn't match the first time, it won't match the second time, either`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
seen := map[ast.Node]bool{}
var collectConds func(ifstmt *ast.IfStmt, conds []ast.Expr) ([]ast.Expr, bool)
collectConds = func(ifstmt *ast.IfStmt, conds []ast.Expr) ([]ast.Expr, bool) {
seen[ifstmt] = true
// Bail if any if-statement has an Init statement or side effects in its condition
if ifstmt.Init != nil {
return nil, false
}
if code.MayHaveSideEffects(pass, ifstmt.Cond, nil) {
return nil, false
}
conds = append(conds, ifstmt.Cond)
if elsestmt, ok := ifstmt.Else.(*ast.IfStmt); ok {
return collectConds(elsestmt, conds)
}
return conds, true
}
fn := func(node ast.Node) {
ifstmt := node.(*ast.IfStmt)
if seen[ifstmt] {
// this if-statement is part of an if/else-if chain that we've already processed
return
}
if ifstmt.Else == nil {
// there can be at most one condition
return
}
conds, ok := collectConds(ifstmt, nil)
if !ok {
return
}
if len(conds) < 2 {
return
}
counts := map[string]int{}
for _, cond := range conds {
s := report.Render(pass, cond)
counts[s]++
if counts[s] == 2 {
report.Report(pass, cond, "this condition occurs multiple times in this if/else if chain")
}
}
}
code.Preorder(pass, fn, (*ast.IfStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,56 @@
package sa4015
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4015",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(checkMathIntRules),
},
Doc: &lint.RawDocumentation{
Title: `Calling functions like \'math.Ceil\' on floats converted from integers doesn't do anything useful`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkMathIntRules = map[string]callcheck.Check{
"math.Ceil": pointlessIntMath,
"math.Floor": pointlessIntMath,
"math.IsNaN": pointlessIntMath,
"math.Trunc": pointlessIntMath,
"math.IsInf": pointlessIntMath,
}
func pointlessIntMath(call *callcheck.Call) {
if ConvertedFromInt(call.Args[0].Value) {
call.Invalid(fmt.Sprintf("calling %s on a converted integer is pointless", irutil.CallName(call.Instr.Common())))
}
}
func ConvertedFromInt(v callcheck.Value) bool {
conv, ok := v.Value.(*ir.Convert)
if !ok {
return false
}
return typeutil.NewTypeSet(conv.X.Type()).All(func(t *types.Term) bool {
b, ok := t.Type().Underlying().(*types.Basic)
return ok && b.Info()&types.IsInteger != 0
})
}

View File

@@ -0,0 +1,116 @@
package sa4016
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4016",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Certain bitwise operations, such as \'x ^ 0\', do not do anything useful`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny, // MergeIfAny if we only flag literals, not named constants
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
binop := node.(*ast.BinaryExpr)
if !typeutil.All(pass.TypesInfo.TypeOf(binop), func(term *types.Term) bool {
b, ok := term.Type().Underlying().(*types.Basic)
if !ok {
return false
}
return (b.Info() & types.IsInteger) != 0
}) {
return
}
switch binop.Op {
case token.AND, token.OR, token.XOR:
default:
// we do not flag shifts because too often, x<<0 is part
// of a pattern, x<<0, x<<8, x<<16, ...
return
}
if y, ok := binop.Y.(*ast.Ident); ok {
obj, ok := pass.TypesInfo.ObjectOf(y).(*types.Const)
if !ok {
return
}
if obj.Pkg() != pass.Pkg {
// identifier was dot-imported
return
}
if v, _ := constant.Int64Val(obj.Val()); v != 0 {
return
}
path, _ := astutil.PathEnclosingInterval(code.File(pass, obj), obj.Pos(), obj.Pos())
if len(path) < 2 {
return
}
spec, ok := path[1].(*ast.ValueSpec)
if !ok {
return
}
if len(spec.Names) != 1 || len(spec.Values) != 1 {
// TODO(dh): we could support this
return
}
ident, ok := spec.Values[0].(*ast.Ident)
if !ok {
return
}
if !isIota(pass.TypesInfo.ObjectOf(ident)) {
return
}
switch binop.Op {
case token.AND:
report.Report(pass, node,
fmt.Sprintf("%s always equals 0; %s is defined as iota and has value 0, maybe %s is meant to be 1 << iota?", report.Render(pass, binop), report.Render(pass, binop.Y), report.Render(pass, binop.Y)))
case token.OR, token.XOR:
report.Report(pass, node,
fmt.Sprintf("%s always equals %s; %s is defined as iota and has value 0, maybe %s is meant to be 1 << iota?", report.Render(pass, binop), report.Render(pass, binop.X), report.Render(pass, binop.Y), report.Render(pass, binop.Y)))
}
} else if code.IsIntegerLiteral(pass, binop.Y, constant.MakeInt64(0)) {
switch binop.Op {
case token.AND:
report.Report(pass, node, fmt.Sprintf("%s always equals 0", report.Render(pass, binop)))
case token.OR, token.XOR:
report.Report(pass, node, fmt.Sprintf("%s always equals %s", report.Render(pass, binop), report.Render(pass, binop.X)))
}
}
}
code.Preorder(pass, fn, (*ast.BinaryExpr)(nil))
return nil, nil
}
func isIota(obj types.Object) bool {
if obj.Name() != "iota" {
return false
}
c, ok := obj.(*types.Const)
if !ok {
return false
}
return c.Pkg() == nil
}

View File

@@ -0,0 +1,86 @@
package sa4017
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/purity"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4017",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer, purity.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Discarding the return values of a function without side effects, making the call pointless`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
pure := pass.ResultOf[purity.Analyzer].(purity.Result)
fnLoop:
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
if code.IsInTest(pass, fn) {
params := fn.Signature.Params()
for param := range params.Variables() {
if typeutil.IsPointerToTypeWithName(param.Type(), "testing.B") {
// Ignore discarded pure functions in code related
// to benchmarks. Instead of matching BenchmarkFoo
// functions, we match any function accepting a
// *testing.B. Benchmarks sometimes call generic
// functions for doing the actual work, and
// checking for the parameter is a lot easier and
// faster than analyzing call trees.
continue fnLoop
}
}
}
for _, b := range fn.Blocks {
for _, ins := range b.Instrs {
ins, ok := ins.(*ir.Call)
if !ok {
continue
}
refs := ins.Referrers()
if refs == nil || len(irutil.FilterDebug(*refs)) > 0 {
continue
}
callee := ins.Common().StaticCallee()
if callee == nil {
continue
}
if callee.Object() == nil {
// TODO(dh): support anonymous functions
continue
}
if _, ok := pure[callee.Object().(*types.Func)]; ok {
if pass.Pkg.Path() == "fmt_test" && callee.Object().(*types.Func).FullName() == "fmt.Sprintf" {
// special case for benchmarks in the fmt package
continue
}
report.Report(pass, ins, fmt.Sprintf("%s doesn't have side effects and its return value is ignored", callee.Object().Name()))
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,61 @@
package sa4018
import (
"fmt"
"go/ast"
"go/token"
"reflect"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/facts/purity"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4018",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer, purity.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Self-assignment of variables`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
pure := pass.ResultOf[purity.Analyzer].(purity.Result)
fn := func(node ast.Node) {
assign := node.(*ast.AssignStmt)
if assign.Tok != token.ASSIGN || len(assign.Lhs) != len(assign.Rhs) {
return
}
for i, lhs := range assign.Lhs {
rhs := assign.Rhs[i]
if reflect.TypeOf(lhs) != reflect.TypeOf(rhs) {
continue
}
if code.MayHaveSideEffects(pass, lhs, pure) || code.MayHaveSideEffects(pass, rhs, pure) {
continue
}
rlh := report.Render(pass, lhs)
rrh := report.Render(pass, rhs)
if rlh == rrh {
report.Report(pass, assign, fmt.Sprintf("self-assignment of %s to %s", rrh, rlh), report.FilterGenerated())
}
}
}
code.Preorder(pass, fn, (*ast.AssignStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,82 @@
package sa4019
import (
"fmt"
"go/ast"
"sort"
"strings"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4019",
Run: run,
Requires: []*analysis.Analyzer{generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Multiple, identical build constraints in the same file`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func buildTagsIdentical(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
s1s := make([]string, len(s1))
copy(s1s, s1)
sort.Strings(s1s)
s2s := make([]string, len(s2))
copy(s2s, s2)
sort.Strings(s2s)
for i, s := range s1s {
if s != s2s[i] {
return false
}
}
return true
}
func run(pass *analysis.Pass) (any, error) {
for _, f := range pass.Files {
constraints := buildTags(f)
for i, constraint1 := range constraints {
for j, constraint2 := range constraints {
if i >= j {
continue
}
if buildTagsIdentical(constraint1, constraint2) {
msg := fmt.Sprintf("identical build constraints %q and %q",
strings.Join(constraint1, " "),
strings.Join(constraint2, " "))
report.Report(pass, f, msg, report.FilterGenerated(), report.ShortRange())
}
}
}
}
return nil, nil
}
func buildTags(f *ast.File) [][]string {
var out [][]string
for line := range strings.SplitSeq(astutil.Preamble(f), "\n") {
if !strings.HasPrefix(line, "+build ") {
continue
}
line = strings.TrimSpace(strings.TrimPrefix(line, "+build "))
fields := strings.Fields(line)
out = append(out, fields)
}
return out
}

View File

@@ -0,0 +1,172 @@
package sa4020
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4020",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Unreachable case clause in a type switch`,
Text: `In a type switch like the following
type T struct{}
func (T) Read(b []byte) (int, error) { return 0, nil }
var v any = T{}
switch v.(type) {
case io.Reader:
// ...
case T:
// unreachable
}
the second case clause can never be reached because \'T\' implements
\'io.Reader\' and case clauses are evaluated in source order.
Another example:
type T struct{}
func (T) Read(b []byte) (int, error) { return 0, nil }
func (T) Close() error { return nil }
var v any = T{}
switch v.(type) {
case io.Reader:
// ...
case io.ReadCloser:
// unreachable
}
Even though \'T\' has a \'Close\' method and thus implements \'io.ReadCloser\',
\'io.Reader\' will always match first. The method set of \'io.Reader\' is a
subset of \'io.ReadCloser\'. Thus it is impossible to match the second
case without matching the first case.
Structurally equivalent interfaces
A special case of the previous example are structurally identical
interfaces. Given these declarations
type T error
type V error
func doSomething() error {
err, ok := doAnotherThing()
if ok {
return T(err)
}
return U(err)
}
the following type switch will have an unreachable case clause:
switch doSomething().(type) {
case T:
// ...
case V:
// unreachable
}
\'T\' will always match before V because they are structurally equivalent
and therefore \'doSomething()\''s return value implements both.`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// Check if T subsumes V in a type switch. T subsumes V if T is an interface and T's method set is a subset of V's method set.
subsumes := func(T, V types.Type) bool {
if typeparams.IsTypeParam(T) {
return false
}
tIface, ok := T.Underlying().(*types.Interface)
if !ok {
return false
}
return types.Implements(V, tIface)
}
subsumesAny := func(Ts, Vs []types.Type) (types.Type, types.Type, bool) {
for _, T := range Ts {
for _, V := range Vs {
if subsumes(T, V) {
return T, V, true
}
}
}
return nil, nil, false
}
fn := func(node ast.Node) {
tsStmt := node.(*ast.TypeSwitchStmt)
type ccAndTypes struct {
cc *ast.CaseClause
types []types.Type
}
// All asserted types in the order of case clauses.
ccs := make([]ccAndTypes, 0, len(tsStmt.Body.List))
for _, stmt := range tsStmt.Body.List {
cc, _ := stmt.(*ast.CaseClause)
// Exclude the 'default' case.
if len(cc.List) == 0 {
continue
}
Ts := make([]types.Type, 0, len(cc.List))
for _, expr := range cc.List {
// Exclude the 'nil' value from any 'case' statement (it is always reachable).
if typ := pass.TypesInfo.TypeOf(expr); typ != types.Typ[types.UntypedNil] {
Ts = append(Ts, typ)
}
}
ccs = append(ccs, ccAndTypes{cc: cc, types: Ts})
}
if len(ccs) <= 1 {
// Zero or one case clauses, nothing to check.
return
}
// Check if case clauses following cc have types that are subsumed by cc.
for i, cc := range ccs[:len(ccs)-1] {
for _, next := range ccs[i+1:] {
if T, V, yes := subsumesAny(cc.types, next.types); yes {
report.Report(pass, next.cc, fmt.Sprintf("unreachable case clause: %s will always match before %s", T.String(), V.String()),
report.ShortRange())
}
}
}
}
code.Preorder(pass, fn, (*ast.TypeSwitchStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,36 @@
package sa4021
import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4021",
Run: run,
Requires: append([]*analysis.Analyzer{generated.Analyzer}, code.RequiredAnalyzers...),
},
Doc: &lint.RawDocumentation{
Title: `\"x = append(y)\" is equivalent to \"x = y\"`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var checkSingleArgAppendQ = pattern.MustParse(`(CallExpr (Builtin "append") [_])`)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, checkSingleArgAppendQ) {
report.Report(pass, node, "x = append(y) is equivalent to x = y", report.FilterGenerated())
}
return nil, nil
}

View File

@@ -0,0 +1,40 @@
package sa4022
import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4022",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Comparing the address of a variable against nil`,
Text: `Code such as \"if &x == nil\" is meaningless, because taking the address of a variable always yields a non-nil pointer.`,
Since: "2020.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var CheckAddressIsNilQ = pattern.MustParse(
`(BinaryExpr
(UnaryExpr "&" _)
(Or "==" "!=")
(Builtin "nil"))`)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, CheckAddressIsNilQ) {
report.Report(pass, node, "the address of a variable cannot be nil")
}
return nil, nil
}

View File

@@ -0,0 +1,205 @@
package sa4023
import (
"fmt"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/nilness"
"honnef.co/go/tools/analysis/facts/typedness"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/exp/typeparams"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4023",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer, typedness.Analysis, nilness.Analysis},
},
Doc: &lint.RawDocumentation{
Title: `Impossible comparison of interface value with untyped nil`,
Text: `Under the covers, interfaces are implemented as two elements, a
type T and a value V. V is a concrete value such as an int,
struct or pointer, never an interface itself, and has type T. For
instance, if we store the int value 3 in an interface, the
resulting interface value has, schematically, (T=int, V=3). The
value V is also known as the interface's dynamic value, since a
given interface variable might hold different values V (and
corresponding types T) during the execution of the program.
An interface value is nil only if the V and T are both
unset, (T=nil, V is not set), In particular, a nil interface will
always hold a nil type. If we store a nil pointer of type *int
inside an interface value, the inner type will be *int regardless
of the value of the pointer: (T=*int, V=nil). Such an interface
value will therefore be non-nil even when the pointer value V
inside is nil.
This situation can be confusing, and arises when a nil value is
stored inside an interface value such as an error return:
func returnsError() error {
var p *MyError = nil
if bad() {
p = ErrBad
}
return p // Will always return a non-nil error.
}
If all goes well, the function returns a nil p, so the return
value is an error interface value holding (T=*MyError, V=nil).
This means that if the caller compares the returned error to nil,
it will always look as if there was an error even if nothing bad
happened. To return a proper nil error to the caller, the
function must return an explicit nil:
func returnsError() error {
if bad() {
return ErrBad
}
return nil
}
It's a good idea for functions that return errors always to use
the error type in their signature (as we did above) rather than a
concrete type such as \'*MyError\', to help guarantee the error is
created correctly. As an example, \'os.Open\' returns an error even
though, if not nil, it's always of concrete type *os.PathError.
Similar situations to those described here can arise whenever
interfaces are used. Just keep in mind that if any concrete value
has been stored in the interface, the interface will not be nil.
For more information, see The Laws of
Reflection at https://golang.org/doc/articles/laws_of_reflection.html.
This text has been copied from
https://golang.org/doc/faq#nil_error, licensed under the Creative
Commons Attribution 3.0 License.`,
Since: "2020.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny, // TODO should this be MergeIfAll?
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// The comparison 'fn() == nil' can never be true if fn() returns
// an interface value and only returns typed nils. This is usually
// a mistake in the function itself, but all we can say for
// certain is that the comparison is pointless.
//
// Flag results if no untyped nils are being returned, but either
// known typed nils, or typed unknown nilness are being returned.
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR)
typedness := pass.ResultOf[typedness.Analysis].(*typedness.Result)
nilness := pass.ResultOf[nilness.Analysis].(*nilness.Result)
for _, fn := range irpkg.SrcFuncs {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
binop, ok := instr.(*ir.BinOp)
if !ok || !(binop.Op == token.EQL || binop.Op == token.NEQ) {
continue
}
if _, ok := binop.X.Type().Underlying().(*types.Interface); !ok || typeparams.IsTypeParam(binop.X.Type()) {
// TODO support swapped X and Y
continue
}
k, ok := binop.Y.(*ir.Const)
if !ok || !k.IsNil() {
// if binop.X is an interface, then binop.Y can
// only be a Const if its untyped. A typed nil
// constant would first be passed to
// MakeInterface.
continue
}
var idx int
var obj *types.Func
switch x := irutil.Flatten(binop.X).(type) {
case *ir.Call:
callee := x.Call.StaticCallee()
if callee == nil {
continue
}
obj, _ = callee.Object().(*types.Func)
idx = 0
case *ir.Extract:
call, ok := irutil.Flatten(x.Tuple).(*ir.Call)
if !ok {
continue
}
callee := call.Call.StaticCallee()
if callee == nil {
continue
}
obj, _ = callee.Object().(*types.Func)
idx = x.Index
case *ir.MakeInterface:
var qualifier string
switch binop.Op {
case token.EQL:
qualifier = "never"
case token.NEQ:
qualifier = "always"
default:
panic("unreachable")
}
terms, err := typeparams.NormalTerms(x.X.Type())
if len(terms) == 0 || err != nil {
// Type is a type parameter with no type terms (or we couldn't determine the terms). Such a type
// _can_ be nil when put in an interface value.
continue
}
if report.HasRange(x.X) {
report.Report(pass, binop, fmt.Sprintf("this comparison is %s true", qualifier),
report.Related(x.X, "the lhs of the comparison gets its value from here and has a concrete type"))
} else {
// we can't generate related information for this, so make the diagnostic itself slightly more useful
report.Report(pass, binop, fmt.Sprintf("this comparison is %s true; the lhs of the comparison has been assigned a concretely typed value", qualifier))
}
continue
}
if obj == nil {
continue
}
isNil, onlyGlobal := nilness.MayReturnNil(obj, idx)
if typedness.MustReturnTyped(obj, idx) && isNil && !onlyGlobal && !code.IsInTest(pass, binop) {
// Don't flag these comparisons in tests. Tests
// may be explicitly enforcing the invariant that
// a value isn't nil.
var qualifier string
switch binop.Op {
case token.EQL:
qualifier = "never"
case token.NEQ:
qualifier = "always"
default:
panic("unreachable")
}
report.Report(pass, binop, fmt.Sprintf("this comparison is %s true", qualifier),
// TODO support swapped X and Y
report.Related(binop.X, fmt.Sprintf("the lhs of the comparison is the %s return value of this function call", report.Ordinal(idx+1))),
report.Related(obj, fmt.Sprintf("%s never returns a nil interface value", typeutil.FuncName(obj))))
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,58 @@
package sa4024
import (
"fmt"
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4024",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Checking for impossible return value from a builtin function`,
Text: `Return values of the \'len\' and \'cap\' builtins cannot be negative.
See https://golang.org/pkg/builtin/#len and https://golang.org/pkg/builtin/#cap.
Example:
if len(slice) < 0 {
fmt.Println("unreachable code")
}`,
Since: "2021.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var builtinLessThanZeroQ = pattern.MustParse(`
(Or
(BinaryExpr
(IntegerLiteral "0")
">"
(CallExpr builtin@(Builtin (Or "len" "cap")) _))
(BinaryExpr
(CallExpr builtin@(Builtin (Or "len" "cap")) _)
"<"
(IntegerLiteral "0")))
`)
func run(pass *analysis.Pass) (any, error) {
for node, matcher := range code.Matches(pass, builtinLessThanZeroQ) {
builtin := matcher.State["builtin"].(*ast.Ident)
report.Report(pass, node, fmt.Sprintf("builtin function %s does not return negative values", builtin.Name))
}
return nil, nil
}

View File

@@ -0,0 +1,69 @@
package sa4025
import (
"fmt"
"go/ast"
"go/constant"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4025",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: "Integer division of literals that results in zero",
Text: `When dividing two integer constants, the result will
also be an integer. Thus, a division such as \'2 / 3\' results in \'0\'.
This is true for all of the following examples:
_ = 2 / 3
const _ = 2 / 3
const _ float64 = 2 / 3
_ = float64(2 / 3)
Staticcheck will flag such divisions if both sides of the division are
integer literals, as it is highly unlikely that the division was
intended to truncate to zero. Staticcheck will not flag integer
division involving named constants, to avoid noisy positives.
`,
Since: "2021.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var integerDivisionQ = pattern.MustParse(`(BinaryExpr (IntegerLiteral _) "/" (IntegerLiteral _))`)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, integerDivisionQ) {
val := constant.ToInt(pass.TypesInfo.Types[node.(ast.Expr)].Value)
if v, ok := constant.Uint64Val(val); ok && v == 0 {
report.Report(pass, node, fmt.Sprintf("the integer division '%s' results in zero", report.Render(pass, node)))
}
// TODO: we could offer a suggested fix here, but I am not
// sure what it should be. There are many options to choose
// from.
// Note: we experimented with flagging divisions that truncate
// (e.g. 4 / 3), but it ran into false positives in Go's
// 'time' package, which does this, deliberately:
//
// unixToInternal int64 = (1969*365 + 1969/4 - 1969/100 + 1969/400) * secondsPerDay
//
// The check also found a real bug in other code, but I don't
// think we can outright ban this kind of division.
}
return nil, nil
}

View File

@@ -0,0 +1,79 @@
package sa4026
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4026",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: "Go constants cannot express negative zero",
Text: `In IEEE 754 floating point math, zero has a sign and can be positive
or negative. This can be useful in certain numerical code.
Go constants, however, cannot express negative zero. This means that
the literals \'-0.0\' and \'0.0\' have the same ideal value (zero) and
will both represent positive zero at runtime.
To explicitly and reliably create a negative zero, you can use the
\'math.Copysign\' function: \'math.Copysign(0, -1)\'.`,
Since: "2021.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var negativeZeroFloatQ = pattern.MustParse(`
(Or
(UnaryExpr
"-"
(BasicLit "FLOAT" "0.0"))
(UnaryExpr
"-"
(CallExpr conv@(Object (Or "float32" "float64")) lit@(Or (BasicLit "INT" "0") (BasicLit "FLOAT" "0.0"))))
(CallExpr
conv@(Object (Or "float32" "float64"))
(UnaryExpr "-" lit@(BasicLit "INT" "0"))))`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, negativeZeroFloatQ) {
if conv, ok := m.State["conv"].(*types.TypeName); ok {
var replacement string
// TODO(dh): how does this handle type aliases?
if conv.Name() == "float32" {
replacement = `float32(math.Copysign(0, -1))`
} else {
replacement = `math.Copysign(0, -1)`
}
report.Report(pass, node,
fmt.Sprintf("in Go, the floating-point expression '%s' is the same as '%s(%s)', it does not produce a negative zero",
report.Render(pass, node),
conv.Name(),
report.Render(pass, m.State["lit"])),
report.Fixes(edit.Fix("Use math.Copysign to create negative zero", edit.ReplaceWithString(node, replacement))))
} else {
const replacement = `math.Copysign(0, -1)`
report.Report(pass, node,
"in Go, the floating-point literal '-0.0' is the same as '0.0', it does not produce a negative zero",
report.Fixes(edit.Fix("Use math.Copysign to create negative zero", edit.ReplaceWithString(node, replacement))))
}
}
return nil, nil
}

View File

@@ -0,0 +1,57 @@
package sa4027
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4027",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\'(*net/url.URL).Query\' returns a copy, modifying it doesn't change the URL`,
Text: `\'(*net/url.URL).Query\' parses the current value of \'net/url.URL.RawQuery\'
and returns it as a map of type \'net/url.Values\'. Subsequent changes to
this map will not affect the URL unless the map gets encoded and
assigned to the URL's \'RawQuery\'.
As a consequence, the following code pattern is an expensive no-op:
\'u.Query().Add(key, value)\'.`,
Since: "2021.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var ineffectiveURLQueryAddQ = pattern.MustParse(`(CallExpr (SelectorExpr (CallExpr (SelectorExpr recv (Ident "Query")) []) (Ident meth)) _)`)
func run(pass *analysis.Pass) (any, error) {
// TODO(dh): We could make this check more complex and detect
// pointless modifications of net/url.Values in general, but that
// requires us to get the state machine correct, else we'll cause
// false positives.
for node, m := range code.Matches(pass, ineffectiveURLQueryAddQ) {
if !code.IsOfPointerToTypeWithName(pass, m.State["recv"].(ast.Expr), "net/url.URL") {
continue
}
switch m.State["meth"].(string) {
case "Add", "Del", "Set":
default:
continue
}
report.Report(pass, node, "(*net/url.URL).Query returns a copy, modifying it doesn't change the URL")
}
return nil, nil
}

View File

@@ -0,0 +1,35 @@
package sa4028
import (
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4028",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\'x % 1\' is always zero`,
Since: "2022.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny, // MergeIfAny if we only flag literals, not named constants
},
})
var Analyzer = SCAnalyzer.Analyzer
var moduloOneQ = pattern.MustParse(`(BinaryExpr _ "%" (IntegerLiteral "1"))`)
func run(pass *analysis.Pass) (any, error) {
for node := range code.Matches(pass, moduloOneQ) {
report.Report(pass, node, "x % 1 is always zero")
}
return nil, nil
}

View File

@@ -0,0 +1,80 @@
package sa4029
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4029",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: "Ineffective attempt at sorting slice",
Text: `
\'sort.Float64Slice\', \'sort.IntSlice\', and \'sort.StringSlice\' are
types, not functions. Doing \'x = sort.StringSlice(x)\' does nothing,
especially not sort any values. The correct usage is
\'sort.Sort(sort.StringSlice(x))\' or \'sort.StringSlice(x).Sort()\',
but there are more convenient helpers, namely \'sort.Float64s\',
\'sort.Ints\', and \'sort.Strings\'.
`,
Since: "2022.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var ineffectiveSortQ = pattern.MustParse(`(AssignStmt target@(Ident _) "=" (CallExpr typ@(Symbol (Or "sort.Float64Slice" "sort.IntSlice" "sort.StringSlice")) [target]))`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, ineffectiveSortQ) {
_, ok := types.Unalias(pass.TypesInfo.TypeOf(m.State["target"].(ast.Expr))).(*types.Slice)
if !ok {
// Avoid flagging 'x = sort.StringSlice(x)' where TypeOf(x) == sort.StringSlice
continue
}
var alternative string
typeName := types.TypeString(types.Unalias(m.State["typ"].(*types.TypeName).Type()), nil)
switch typeName {
case "sort.Float64Slice":
alternative = "Float64s"
case "sort.IntSlice":
alternative = "Ints"
case "sort.StringSlice":
alternative = "Strings"
default:
panic(fmt.Sprintf("unreachable: %q", typeName))
}
r := &ast.CallExpr{
Fun: &ast.SelectorExpr{
X: &ast.Ident{Name: "sort"},
Sel: &ast.Ident{Name: alternative},
},
Args: []ast.Expr{m.State["target"].(ast.Expr)},
}
report.Report(pass, node,
fmt.Sprintf("%s is a type, not a function, and %s doesn't sort your values; consider using sort.%s instead",
typeName,
report.Render(pass, node.(*ast.AssignStmt).Rhs[0]),
alternative),
report.Fixes(edit.Fix(fmt.Sprintf("Replace with call to sort.%s", alternative), edit.ReplaceWithNode(pass.Fset, node, r))))
}
return nil, nil
}

View File

@@ -0,0 +1,70 @@
package sa4030
import (
"fmt"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4030",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: "Ineffective attempt at generating random number",
Text: `
Functions in the \'math/rand\' package that accept upper limits, such
as \'Intn\', generate random numbers in the half-open interval [0,n). In
other words, the generated numbers will be \'>= 0\' and \'< n\' they
don't include \'n\'. \'rand.Intn(1)\' therefore doesn't generate \'0\'
or \'1\', it always generates \'0\'.`,
Since: "2022.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var ineffectiveRandIntQ = pattern.MustParse(`
(CallExpr
(Symbol
name@(Or
"math/rand.Int31n"
"math/rand.Int63n"
"math/rand.Intn"
"(*math/rand.Rand).Int31n"
"(*math/rand.Rand).Int63n"
"(*math/rand.Rand).Intn"
"math/rand/v2.Int32N"
"math/rand/v2.Int64N"
"math/rand/v2.IntN"
"math/rand/v2.N"
"math/rand/v2.Uint32N"
"math/rand/v2.Uint64N"
"math/rand/v2.UintN"
"(*math/rand/v2.Rand).Int32N"
"(*math/rand/v2.Rand).Int64N"
"(*math/rand/v2.Rand).IntN"
"(*math/rand/v2.Rand).Uint32N"
"(*math/rand/v2.Rand).Uint64N"
"(*math/rand/v2.Rand).UintN"))
[(IntegerLiteral "1")])`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, ineffectiveRandIntQ) {
report.Report(pass, node,
fmt.Sprintf("%s(n) generates a random value 0 <= x < n; that is, the generated values don't include n; %s therefore always returns 0",
m.State["name"], report.Render(pass, node)))
}
return nil, nil
}

View File

@@ -0,0 +1,162 @@
package sa4031
import (
"fmt"
"go/ast"
"go/token"
"go/types"
"sort"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/pattern"
"honnef.co/go/tools/staticcheck/sa4022"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4031",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer, inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Checking never-nil value against nil`,
Since: "2022.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var allocationNilCheckQ = pattern.MustParse(`(IfStmt _ cond@(BinaryExpr lhs op@(Or "==" "!=") (Builtin "nil")) _ _)`)
func run(pass *analysis.Pass) (any, error) {
irpkg := pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg
var path []ast.Node
fn := func(node ast.Node, stack []ast.Node) {
m, ok := code.Match(pass, allocationNilCheckQ, node)
if !ok {
return
}
cond := m.State["cond"].(ast.Node)
if _, ok := code.Match(pass, sa4022.CheckAddressIsNilQ, cond); ok {
// Don't duplicate diagnostics reported by SA4022
return
}
lhs := m.State["lhs"].(ast.Expr)
path = path[:0]
for i := len(stack) - 1; i >= 0; i-- {
path = append(path, stack[i])
}
irfn := ir.EnclosingFunction(irpkg, path)
if irfn == nil {
// For example for functions named "_", because we don't generate IR for them.
return
}
v, isAddr := irfn.ValueForExpr(lhs)
if isAddr {
return
}
seen := map[ir.Value]struct{}{}
var values []ir.Value
var neverNil func(v ir.Value, track bool) bool
neverNil = func(v ir.Value, track bool) bool {
if _, ok := seen[v]; ok {
return true
}
seen[v] = struct{}{}
switch v := v.(type) {
case *ir.MakeClosure, *ir.Function:
if track {
values = append(values, v)
}
return true
case *ir.MakeChan, *ir.MakeMap, *ir.MakeSlice, *ir.Alloc:
if track {
values = append(values, v)
}
return true
case *ir.Slice:
if track {
values = append(values, v)
}
return neverNil(v.X, false)
case *ir.FieldAddr:
if track {
values = append(values, v)
}
return neverNil(v.X, false)
case *ir.Sigma:
return neverNil(v.X, true)
case *ir.Phi:
for _, e := range v.Edges {
if !neverNil(e, true) {
return false
}
}
return true
default:
return false
}
}
if !neverNil(v, true) {
return
}
var qualifier string
if op := m.State["op"].(token.Token); op == token.EQL {
qualifier = "never"
} else {
qualifier = "always"
}
fallback := fmt.Sprintf("this nil check is %s true", qualifier)
sort.Slice(values, func(i, j int) bool { return values[i].Pos() < values[j].Pos() })
if ident, ok := m.State["lhs"].(*ast.Ident); ok {
if _, ok := pass.TypesInfo.ObjectOf(ident).(*types.Var); ok {
var opts []report.Option
if v.Parent() == irfn {
if len(values) == 1 {
opts = append(opts, report.Related(values[0], fmt.Sprintf("this is the value of %s", ident.Name)))
} else {
for _, vv := range values {
opts = append(opts, report.Related(vv, fmt.Sprintf("this is one of the value of %s", ident.Name)))
}
}
}
switch v.(type) {
case *ir.MakeClosure, *ir.Function:
report.Report(pass, cond, "the checked variable contains a function and is never nil; did you mean to call it?", opts...)
default:
report.Report(pass, cond, fallback, opts...)
}
} else {
if _, ok := v.(*ir.Function); ok {
report.Report(pass, cond, "functions are never nil; did you mean to call it?")
} else {
report.Report(pass, cond, fallback)
}
}
} else {
if _, ok := v.(*ir.Function); ok {
report.Report(pass, cond, "functions are never nil; did you mean to call it?")
} else {
report.Report(pass, cond, fallback)
}
}
}
code.PreorderStack(pass, fn, (*ast.IfStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,221 @@
package sa4032
import (
"fmt"
"go/ast"
"go/build/constraint"
"go/constant"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/pattern"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA4032",
Run: CheckImpossibleGOOSGOARCH,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Comparing \'runtime.GOOS\' or \'runtime.GOARCH\' against impossible value`,
Since: "2024.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
goosComparisonQ = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOOS") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
goarchComparisonQ = pattern.MustParse(`(BinaryExpr (Symbol "runtime.GOARCH") op@(Or "==" "!=") lit@(BasicLit "STRING" _))`)
)
func CheckImpossibleGOOSGOARCH(pass *analysis.Pass) (any, error) {
// TODO(dh): validate GOOS and GOARCH together. that is,
// given '(linux && amd64) || (windows && mips)',
// flag 'if runtime.GOOS == "linux" && runtime.GOARCH == "mips"'
//
// We can't use our IR for the control flow graph, because go/types constant folds constant comparisons, so
// 'runtime.GOOS == "windows"' will just become 'false'. We can't use the AST-based CFG builder from x/tools,
// because it doesn't model branch conditions.
if !code.CouldMatchAny(pass, goarchComparisonQ, goosComparisonQ) {
return nil, nil
}
for _, f := range pass.Files {
expr, ok := code.BuildConstraints(pass, f)
if !ok {
continue
}
ast.Inspect(f, func(node ast.Node) bool {
if m, ok := code.Match(pass, goosComparisonQ, node); ok {
tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
goos := constant.StringVal(tv.Value)
if _, ok := knowledge.KnownGOOS[goos]; !ok {
// Don't try to reason about GOOS values we don't know about. Maybe the user is using a newer
// version of Go that supports a new target, or maybe they run a fork of Go.
return true
}
sat, ok := validateGOOSComparison(expr, goos)
if !ok {
return true
}
if !sat {
// Note that we do not have to worry about constraints that can never be satisfied, such as 'linux
// && windows'. Packages with such files will not be passed to Staticcheck in the first place,
// precisely because the constraints aren't satisfiable.
report.Report(pass, node,
fmt.Sprintf("due to the file's build constraints, runtime.GOOS will never equal %q", goos))
}
} else if m, ok := code.Match(pass, goarchComparisonQ, node); ok {
tv := pass.TypesInfo.Types[m.State["lit"].(ast.Expr)]
goarch := constant.StringVal(tv.Value)
if _, ok := knowledge.KnownGOARCH[goarch]; !ok {
// Don't try to reason about GOARCH values we don't know about. Maybe the user is using a newer
// version of Go that supports a new target, or maybe they run a fork of Go.
return true
}
sat, ok := validateGOARCHComparison(expr, goarch)
if !ok {
return true
}
if !sat {
// Note that we do not have to worry about constraints that can never be satisfied, such as 'amd64
// && mips'. Packages with such files will not be passed to Staticcheck in the first place,
// precisely because the constraints aren't satisfiable.
report.Report(pass, node,
fmt.Sprintf("due to the file's build constraints, runtime.GOARCH will never equal %q", goarch))
}
}
return true
})
}
return nil, nil
}
func validateGOOSComparison(expr constraint.Expr, goos string) (sat bool, didCheck bool) {
matchGoosTag := func(tag string, goos string) (ok bool, goosTag bool) {
switch tag {
case "aix",
"android",
"dragonfly",
"freebsd",
"hurd",
"illumos",
"ios",
"js",
"netbsd",
"openbsd",
"plan9",
"wasip1",
"windows":
return goos == tag, true
case "darwin":
return (goos == "darwin" || goos == "ios"), true
case "linux":
return (goos == "linux" || goos == "android"), true
case "solaris":
return (goos == "solaris" || goos == "illumos"), true
case "unix":
return (goos == "aix" ||
goos == "android" ||
goos == "darwin" ||
goos == "dragonfly" ||
goos == "freebsd" ||
goos == "hurd" ||
goos == "illumos" ||
goos == "ios" ||
goos == "linux" ||
goos == "netbsd" ||
goos == "openbsd" ||
goos == "solaris"), true
default:
return false, false
}
}
return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
return matchGoosTag(tag, goos)
})
}
func validateGOARCHComparison(expr constraint.Expr, goarch string) (sat bool, didCheck bool) {
matchGoarchTag := func(tag string, goarch string) (ok bool, goosTag bool) {
switch tag {
case "386",
"amd64",
"arm",
"arm64",
"loong64",
"mips",
"mipsle",
"mips64",
"mips64le",
"ppc64",
"ppc64le",
"riscv64",
"s390x",
"sparc64",
"wasm":
return goarch == tag, true
default:
return false, false
}
}
return validateTagComparison(expr, func(tag string) (matched bool, special bool) {
return matchGoarchTag(tag, goarch)
})
}
func validateTagComparison(expr constraint.Expr, matchSpecialTag func(tag string) (matched bool, special bool)) (sat bool, didCheck bool) {
otherTags := map[string]int{}
// Collect all tags that aren't known architecture-based tags
b := expr.Eval(func(tag string) bool {
ok, special := matchSpecialTag(tag)
if !special {
// Assign an ID to this tag, but only if we haven't seen it before. For the expression 'foo && foo', this
// callback will be called twice for the 'foo' tag.
if _, ok := otherTags[tag]; !ok {
otherTags[tag] = len(otherTags)
}
}
return ok
})
if b || len(otherTags) == 0 {
// We're done. Either the formula can be satisfied regardless of the values of non-special tags, if any,
// or there aren't any non-special tags and the formula cannot be satisfied.
return b, true
}
if len(otherTags) > 10 {
// We have to try 2**len(otherTags) combinations of tags. 2**10 is about the worst we're willing to try.
return false, false
}
// Try all permutations of otherTags. If any evaluates to true, then the expression is satisfiable.
for bits := 0; bits < 1<<len(otherTags); bits++ {
b := expr.Eval(func(tag string) bool {
ok, special := matchSpecialTag(tag)
if special {
return ok
}
return bits&(1<<otherTags[tag]) != 0
})
if b {
return true, true
}
}
return false, true
}

View File

@@ -0,0 +1,49 @@
package sa5000
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5000",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Assignment to nil map`,
Since: "2017.1",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, block := range fn.Blocks {
for _, ins := range block.Instrs {
mu, ok := ins.(*ir.MapUpdate)
if !ok {
continue
}
c, ok := irutil.Flatten(mu.Map).(*ir.Const)
if !ok {
continue
}
if c.Value != nil {
continue
}
report.Report(pass, mu, "assignment to nil map")
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,109 @@
package sa5001
import (
"fmt"
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5001",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Deferring \'Close\' before checking for a possible error`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
block := node.(*ast.BlockStmt)
if len(block.List) < 2 {
return
}
for i, stmt := range block.List {
if i == len(block.List)-1 {
break
}
assign, ok := stmt.(*ast.AssignStmt)
if !ok {
continue
}
if len(assign.Rhs) != 1 {
continue
}
if len(assign.Lhs) < 2 {
continue
}
if lhs, ok := assign.Lhs[len(assign.Lhs)-1].(*ast.Ident); ok && lhs.Name == "_" {
continue
}
call, ok := assign.Rhs[0].(*ast.CallExpr)
if !ok {
continue
}
sig, ok := pass.TypesInfo.TypeOf(call.Fun).(*types.Signature)
if !ok {
continue
}
if sig.Results().Len() < 2 {
continue
}
last := sig.Results().At(sig.Results().Len() - 1)
// FIXME(dh): check that it's error from universe, not
// another type of the same name
if last.Type().String() != "error" {
continue
}
lhs, ok := assign.Lhs[0].(*ast.Ident)
if !ok {
continue
}
def, ok := block.List[i+1].(*ast.DeferStmt)
if !ok {
continue
}
sel, ok := def.Call.Fun.(*ast.SelectorExpr)
if !ok {
continue
}
ident, ok := selectorX(sel).(*ast.Ident)
if !ok {
continue
}
if pass.TypesInfo.ObjectOf(ident) != pass.TypesInfo.ObjectOf(lhs) {
continue
}
if sel.Sel.Name != "Close" {
continue
}
report.Report(pass, def, fmt.Sprintf("should check error returned from %s() before deferring %s",
report.Render(pass, call.Fun), report.Render(pass, def.Call)))
}
}
code.Preorder(pass, fn, (*ast.BlockStmt)(nil))
return nil, nil
}
func selectorX(sel *ast.SelectorExpr) ast.Node {
switch x := sel.X.(type) {
case *ast.SelectorExpr:
return selectorX(x)
default:
return x
}
}

View File

@@ -0,0 +1,74 @@
package sa5002
import (
"go/ast"
"go/constant"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5002",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `The empty for loop (\"for {}\") spins and can block the scheduler`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
loop := node.(*ast.ForStmt)
if len(loop.Body.List) != 0 || loop.Post != nil {
return
}
if loop.Init != nil {
// TODO(dh): this isn't strictly necessary, it just makes
// the check easier.
return
}
// An empty loop is bad news in two cases: 1) The loop has no
// condition. In that case, it's just a loop that spins
// forever and as fast as it can, keeping a core busy. 2) The
// loop condition only consists of variable or field reads and
// operators on those. The only way those could change their
// value is with unsynchronised access, which constitutes a
// data race.
//
// If the condition contains any function calls, its behaviour
// is dynamic and the loop might terminate. Similarly for
// channel receives.
if loop.Cond != nil {
if code.MayHaveSideEffects(pass, loop.Cond, nil) {
return
}
if ident, ok := loop.Cond.(*ast.Ident); ok {
if k, ok := pass.TypesInfo.ObjectOf(ident).(*types.Const); ok {
if !constant.BoolVal(k.Val()) {
// don't flag `for false {}` loops. They're a debug aid.
return
}
}
}
report.Report(pass, loop, "loop condition never changes or has a race condition")
}
report.Report(pass, loop, "this loop will spin, using 100% CPU", report.ShortRange())
}
code.Preorder(pass, fn, (*ast.ForStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,74 @@
package sa5003
import (
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5003",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Defers in infinite loops will never execute`,
Text: `Defers are scoped to the surrounding function, not the surrounding
block. In a function that never returns, i.e. one containing an
infinite loop, defers will never execute.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
mightExit := false
var defers []ast.Stmt
loop := node.(*ast.ForStmt)
if loop.Cond != nil {
return
}
fn2 := func(node ast.Node) bool {
switch stmt := node.(type) {
case *ast.ReturnStmt:
mightExit = true
return false
case *ast.BranchStmt:
// TODO(dominikh): if this sees a break in a switch or
// select, it doesn't check if it breaks the loop or
// just the select/switch. This causes some false
// negatives.
if stmt.Tok == token.BREAK {
mightExit = true
return false
}
case *ast.DeferStmt:
defers = append(defers, stmt)
case *ast.FuncLit:
// Don't look into function bodies
return false
}
return true
}
ast.Inspect(loop.Body, fn2)
if mightExit {
return
}
for _, stmt := range defers {
report.Report(pass, stmt, "defers in this infinite loop will never run")
}
}
code.Preorder(pass, fn, (*ast.ForStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,49 @@
package sa5004
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5004",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `\"for { select { ...\" with an empty default branch spins`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var query = pattern.MustParse(`(ForStmt nil nil nil (SelectStmt body))`)
func run(pass *analysis.Pass) (any, error) {
for _, m := range code.Matches(pass, query) {
for _, c := range m.State["body"].([]ast.Stmt) {
// FIXME this leaves behind an empty line, and possibly
// comments in the default branch. We can't easily fix
// either.
if comm, ok := c.(*ast.CommClause); ok && comm.Comm == nil && len(comm.Body) == 0 {
report.Report(pass, comm,
"should not have an empty default case in a for+select loop; the loop will spin",
report.Fixes(edit.Fix("Remove empty default branch", edit.Delete(comm))))
// there can only be one default case
break
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,90 @@
package sa5005
import (
"fmt"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5005",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `The finalizer references the finalized object, preventing garbage collection`,
Text: `A finalizer is a function associated with an object that runs when the
garbage collector is ready to collect said object, that is when the
object is no longer referenced by anything.
If the finalizer references the object, however, it will always remain
as the final reference to that object, preventing the garbage
collector from collecting the object. The finalizer will never run,
and the object will never be collected, leading to a memory leak. That
is why the finalizer should instead use its first argument to operate
on the object. That way, the number of references can temporarily go
to zero before the object is being passed to the finalizer.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
cb := func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function) {
if callee.RelString(nil) != "runtime.SetFinalizer" {
return
}
arg0 := site.Common().Args[knowledge.Arg("runtime.SetFinalizer.obj")]
if iface, ok := arg0.(*ir.MakeInterface); ok {
arg0 = iface.X
}
load, ok := arg0.(*ir.Load)
if !ok {
return
}
v, ok := load.X.(*ir.Alloc)
if !ok {
return
}
arg1 := site.Common().Args[knowledge.Arg("runtime.SetFinalizer.finalizer")]
if iface, ok := arg1.(*ir.MakeInterface); ok {
arg1 = iface.X
}
mc, ok := arg1.(*ir.MakeClosure)
if !ok {
return
}
for _, b := range mc.Bindings {
if b == v {
pos := report.DisplayPosition(pass.Fset, mc.Fn.Pos())
report.Report(pass, site, fmt.Sprintf("the finalizer closes over the object, preventing the finalizer from ever running (at %s)", pos))
}
}
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
eachCall(fn, cb)
}
return nil, nil
}
func eachCall(fn *ir.Function, cb func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function)) {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
if site, ok := instr.(ir.CallInstruction); ok {
if g := site.Common().StaticCallee(); g != nil {
cb(fn, site, g)
}
}
}
}
}

View File

@@ -0,0 +1,77 @@
package sa5007
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5007",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Infinite recursive call`,
Text: `A function that calls itself recursively needs to have an exit
condition. Otherwise it will recurse forever, until the system runs
out of memory.
This issue can be caused by simple bugs such as forgetting to add an
exit condition. It can also happen "on purpose". Some languages have
tail call optimization which makes certain infinite recursive calls
safe to use. Go, however, does not implement TCO, and as such a loop
should be used instead.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
eachCall(fn, func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function) {
if callee != fn {
return
}
if _, ok := site.(*ir.Go); ok {
// Recursively spawning goroutines doesn't consume
// stack space infinitely, so don't flag it.
return
}
block := site.Block()
for _, b := range fn.Blocks {
if block.Dominates(b) {
continue
}
if len(b.Instrs) == 0 {
continue
}
if _, ok := b.Control().(*ir.Return); ok {
return
}
}
report.Report(pass, site, "infinite recursive call")
})
}
return nil, nil
}
func eachCall(fn *ir.Function, cb func(caller *ir.Function, site ir.CallInstruction, callee *ir.Function)) {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
if site, ok := instr.(ir.CallInstruction); ok {
if g := site.Common().StaticCallee(); g != nil {
cb(fn, site, g)
}
}
}
}
}

View File

@@ -0,0 +1,288 @@
// Copyright 2021 The Go Authors. All rights reserved.
// This file is a modified copy of Go's encoding/json/v2/field.go
package sa5008
import (
"fmt"
"go/ast"
"go/types"
"io"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
)
func validateJSONTag(pass *analysis.Pass, field *ast.Field, tag string) {
hasTag := tag != ""
tagOrig := tag
// Check whether this field is explicitly ignored.
if tag == "-" {
return
}
// Check whether this field is unexported and not embedded,
// which Go reflection cannot mutate for the sake of serialization.
//
// An embedded field of an unexported type is still capable of
// forwarding exported fields, which may be JSON serialized.
// This technically operates on the edge of what is permissible by
// the Go language, but the most recent decision is to permit this.
//
// See https://go.dev/issue/24153 and https://go.dev/issue/32772.
anonymous := len(field.Names) == 0
if !anonymous && !field.Names[0].IsExported() {
// Tag options specified on an unexported field suggests user error.
if hasTag {
report.Report(pass, field.Tag,
fmt.Sprintf("unexported struct field cannot have non-ignored `json:%q` tag", tag))
}
return
}
if len(tag) > 0 && !strings.HasPrefix(tag, ",") {
// For better compatibility with v1, accept almost any unescaped name.
n := len(tag) - len(strings.TrimLeftFunc(tag, func(r rune) bool {
return !strings.ContainsRune(",\\'\"`", r) // reserve comma, backslash, and quotes
}))
name := tag[:n]
// If the next character is not a comma, then the name is either
// malformed (if n > 0) or a single-quoted name.
// In either case, call consumeTagOption to handle it further.
var err error
if !strings.HasPrefix(tag[n:], ",") && len(name) != len(tag) {
name, n, err = consumeTagOption(tag)
if err != nil {
report.Report(pass, field.Tag, fmt.Sprintf("malformed `json` tag: %v", err))
}
}
if !utf8.ValidString(name) {
report.Report(pass, field.Tag,
fmt.Sprintf("invalid UTF-8 in JSON object name %q", name))
name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError
}
if name == "-" && tag[0] == '-' {
// TODO(dh): offer automatic fix
report.Report(pass, field.Tag,
fmt.Sprintf("should encoding/json ignore this field or name it \"-\"? Either use `json:\"-\"` to ignore the field or use `json:\"'-'%s` to specify %q as the name",
strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name))
}
tag = tag[n:]
}
// Handle any additional tag options (if any).
var wasFormat bool
seenOpts := make(map[string]bool)
for len(tag) > 0 {
// Consume comma delimiter.
if tag[0] != ',' {
report.Report(pass, field.Tag,
fmt.Sprintf("malformed `json` tag: invalid character %q before next option (expecting ',')",
tag[0]))
} else {
tag = tag[len(","):]
if len(tag) == 0 {
report.Report(pass, field.Tag, "malformed `json` tag: invalid trailing ',' character")
break
}
}
// Consume and process the tag option.
opt, n, err := consumeTagOption(tag)
if err != nil {
report.Report(pass, field.Tag, fmt.Sprintf("malformed `json` tag: %v", err))
}
rawOpt := tag[:n]
tag = tag[n:]
switch {
case wasFormat:
report.Report(pass, field.Tag, "`format` tag option was not specified last")
case strings.HasPrefix(rawOpt, "'") && strings.TrimFunc(opt, isLetterOrDigit) == "":
// TODO(dh): offer automatic fix
report.Report(pass, field.Tag,
fmt.Sprintf("unnecessarily quoted appearance of `%s` tag option; specify `%s` instead", rawOpt, opt))
}
switch opt {
case "case":
if !strings.HasPrefix(tag, ":") {
// TODO(dh): offer automatic fix
report.Report(pass, field.Tag,
"missing value for `case` tag option; specify `case:ignore` or `case:strict` instead")
break
}
tag = tag[len(":"):]
opt, n, err := consumeTagOption(tag)
if err != nil {
report.Report(pass, field.Tag,
fmt.Sprintf("malformed value for `case` tag option: %v", err))
break
}
rawOpt := tag[:n]
tag = tag[n:]
if strings.HasPrefix(rawOpt, "'") {
// TODO(dh): offer automatic fix
report.Report(pass, field.Tag,
fmt.Sprintf("unnecessarily quoted appearance of `case:%s` tag option; specify `case:%s` instead",
rawOpt, opt))
}
switch opt {
case "ignore":
case "strict":
default:
report.Report(pass, field.Tag,
fmt.Sprintf("invalid appearance of unknown `case:%s` tag value", rawOpt))
}
case "inline":
case "unknown":
case "omitzero":
case "omitempty":
case "string":
const msg = "invalid appearance of `string` tag option; it is only intended for fields of numeric types or pointers to those"
tset := typeutil.NewTypeSet(pass.TypesInfo.TypeOf(field.Type))
if len(tset.Terms) == 0 {
// TODO(dh): improve message, call out the use of type parameters
report.Report(pass, field.Tag, msg)
continue
}
for _, term := range tset.Terms {
T := typeutil.Dereference(term.Type().Underlying())
for _, term2 := range typeutil.NewTypeSet(T).Terms {
basic, ok := term2.Type().Underlying().(*types.Basic)
// We accept bools and strings because v1 of encoding/json
// supports those. We don't mention that in the message,
// however, because their support is accidental, and v2
// doesn't support it.
if !ok || (basic.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat|types.IsString)) == 0 {
// TODO(dh): improve message, show how we arrived at the type
report.Report(pass, field.Tag, msg)
}
}
}
case "format":
if !strings.HasPrefix(tag, ":") {
report.Report(pass, field.Tag, "missing value for `format` tag option")
break
}
tag = tag[len(":"):]
_, n, err := consumeTagOption(tag)
if err != nil {
report.Report(pass, field.Tag,
fmt.Sprintf("malformed value for `format` tag option: %v", err))
break
}
tag = tag[n:]
wasFormat = true
default:
// Reject keys that resemble one of the supported options.
// This catches invalid mutants such as "omitEmpty" or "omit_empty".
normOpt := strings.ReplaceAll(strings.ToLower(opt), "_", "")
switch normOpt {
case "case", "inline", "unknown", "omitzero", "omitempty", "string", "format":
report.Report(pass, field.Tag,
fmt.Sprintf("invalid appearance of `%s` tag option; specify `%s` instead",
opt, normOpt))
default:
report.Report(pass, field.Tag,
fmt.Sprintf("invalid appearance of unknown `%s` tag option", opt))
}
}
// Reject duplicates.
if seenOpts[opt] {
report.Report(pass, field.Tag,
fmt.Sprintf("duplicate appearance of `%s` tag option", rawOpt))
}
seenOpts[opt] = true
}
if seenOpts["inline"] && seenOpts["unknown"] {
report.Report(pass, field.Tag,
"field cannot have both `inline` and `unknown` specified")
}
// TODO(dh): implement more restrictions for types of inlined and unknown
// fields, including recursive restrictions:
//
// - Go struct field %s cannot have any options other than `inline` or `unknown` specified
// - inlined Go struct field %s of type %s with `unknown` tag must be a Go map of string key or a jsontext.Value
// - inlined Go struct field %s is not exported
// - inlined map field %s of type %s must have a string key that does not implement marshal or unmarshal methods
// - inlined Go struct field %s of type %s must be a Go struct, Go map of string key, or jsontext.Value
}
// consumeTagOption consumes the next option,
// which is either a Go identifier or a single-quoted string.
// If the next option is invalid, it returns all of in until the next comma,
// and reports an error.
func consumeTagOption(in string) (string, int, error) {
// For legacy compatibility with v1, assume options are comma-separated.
i := strings.IndexByte(in, ',')
if i < 0 {
i = len(in)
}
switch r, _ := utf8.DecodeRuneInString(in); {
// Option as a Go identifier.
case r == '_' || unicode.IsLetter(r):
n := len(in) - len(strings.TrimLeftFunc(in, isLetterOrDigit))
return in[:n], n, nil
// Option as a single-quoted string.
case r == '\'':
// The grammar is nearly identical to a double-quoted Go string literal,
// but uses single quotes as the terminators. The reason for a custom
// grammar is because both backtick and double quotes cannot be used
// verbatim in a struct tag.
//
// Convert a single-quoted string to a double-quote string and rely on
// strconv.Unquote to handle the rest.
var inEscape bool
b := []byte{'"'}
n := len(`'`)
for len(in) > n {
r, rn := utf8.DecodeRuneInString(in[n:])
switch {
case inEscape:
if r == '\'' {
b = b[:len(b)-1] // remove escape character: `\'` => `'`
}
inEscape = false
case r == '\\':
inEscape = true
case r == '"':
b = append(b, '\\') // insert escape character: `"` => `\"`
case r == '\'':
b = append(b, '"')
n += len(`'`)
out, err := strconv.Unquote(string(b))
if err != nil {
return in[:i], i, fmt.Errorf("invalid single-quoted string: %s", in[:n])
}
return out, n, nil
}
b = append(b, in[n:][:rn]...)
n += rn
}
if n > 10 {
n = 10 // limit the amount of context printed in the error
}
//lint:ignore ST1005 The ellipsis denotes truncated text
return in[:i], i, fmt.Errorf("single-quoted string not terminated: %s...", in[:n])
case len(in) == 0:
return in[:i], i, io.ErrUnexpectedEOF
default:
return in[:i], i, fmt.Errorf("invalid character %q at start of option (expecting Unicode letter or single quote)", r)
}
}
func isLetterOrDigit(r rune) bool {
return r == '_' || unicode.IsLetter(r) || unicode.IsNumber(r)
}

View File

@@ -0,0 +1,130 @@
package sa5008
import (
"fmt"
"go/ast"
"go/types"
"strings"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/staticcheck/fakereflect"
"honnef.co/go/tools/staticcheck/fakexml"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5008",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Invalid struct tag`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
importsGoFlags := false
// we use the AST instead of (*types.Package).Imports to work
// around vendored packages in GOPATH mode. A vendored package's
// path will include the vendoring subtree as a prefix.
for _, f := range pass.Files {
for _, imp := range f.Imports {
v := imp.Path.Value
if v[1:len(v)-1] == "github.com/jessevdk/go-flags" {
importsGoFlags = true
break
}
}
}
fn := func(node ast.Node) {
structNode := node.(*ast.StructType)
T := pass.TypesInfo.Types[structNode].Type.(*types.Struct)
rt := fakereflect.TypeAndCanAddr{
Type: T,
}
for i, field := range structNode.Fields.List {
if field.Tag == nil {
continue
}
tags, err := parseStructTag(field.Tag.Value[1 : len(field.Tag.Value)-1])
if err != nil {
report.Report(pass, field.Tag, fmt.Sprintf("unparseable struct tag: %s", err))
continue
}
for k, v := range tags {
if len(v) > 1 {
isGoFlagsTag := importsGoFlags &&
(k == "choice" || k == "optional-value" || k == "default")
if !isGoFlagsTag {
report.Report(pass, field.Tag, fmt.Sprintf("duplicate struct tag %q", k))
}
}
switch k {
case "json":
checkJSONTag(pass, field, v[0])
case "xml":
if _, err := fakexml.StructFieldInfo(rt.Field(i)); err != nil {
report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: %s", err))
}
checkXMLTag(pass, field, v[0])
}
}
}
}
code.Preorder(pass, fn, (*ast.StructType)(nil))
return nil, nil
}
func checkJSONTag(pass *analysis.Pass, field *ast.Field, tag string) {
if pass.Pkg.Path() == "encoding/json" ||
pass.Pkg.Path() == "encoding/json_test" ||
pass.Pkg.Path() == "encoding/json/v2" ||
pass.Pkg.Path() == "encoding/json/v2_test" {
// don't flag malformed JSON tags in the encoding/json
// package; it knows what it is doing, and it is testing
// itself.
return
}
//lint:ignore SA9003 TODO(dh): should we flag empty tags?
if len(tag) == 0 {
}
validateJSONTag(pass, field, tag)
}
func checkXMLTag(pass *analysis.Pass, field *ast.Field, tag string) {
//lint:ignore SA9003 TODO(dh): should we flag empty tags?
if len(tag) == 0 {
}
fields := strings.Split(tag, ",")
counts := map[string]int{}
for _, s := range fields[1:] {
switch s {
case "attr", "chardata", "cdata", "innerxml", "comment":
counts[s]++
case "omitempty", "any":
counts[s]++
case "":
default:
report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: unknown option %q", s))
}
}
for k, v := range counts {
if v > 1 {
report.Report(pass, field.Tag, fmt.Sprintf("invalid XML tag: duplicate option %q", k))
}
}
}

View File

@@ -0,0 +1,58 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Copyright 2019 Dominik Honnef. All rights reserved.
package sa5008
import "strconv"
func parseStructTag(tag string) (map[string][]string, error) {
// FIXME(dh): detect missing closing quote
out := map[string][]string{}
for tag != "" {
// Skip leading space.
i := 0
for i < len(tag) && tag[i] == ' ' {
i++
}
tag = tag[i:]
if tag == "" {
break
}
// Scan to colon. A space, a quote or a control character is a syntax error.
// Strictly speaking, control chars include the range [0x7f, 0x9f], not just
// [0x00, 0x1f], but in practice, we ignore the multi-byte control characters
// as it is simpler to inspect the tag's bytes than the tag's runes.
i = 0
for i < len(tag) && tag[i] > ' ' && tag[i] != ':' && tag[i] != '"' && tag[i] != 0x7f {
i++
}
if i == 0 || i+1 >= len(tag) || tag[i] != ':' || tag[i+1] != '"' {
break
}
name := string(tag[:i])
tag = tag[i+1:]
// Scan quoted string to find value.
i = 1
for i < len(tag) && tag[i] != '"' {
if tag[i] == '\\' {
i++
}
i++
}
if i >= len(tag) {
break
}
qvalue := string(tag[:i+1])
tag = tag[i+1:]
value, err := strconv.Unquote(qvalue)
if err != nil {
return nil, err
}
out[name] = append(out[name], value)
}
return out, nil
}

View File

@@ -0,0 +1,409 @@
package sa5009
import (
"fmt"
"go/constant"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"honnef.co/go/tools/printf"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5009",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Invalid Printf call`,
Since: "2019.2",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
// TODO(dh): detect printf wrappers
var rules = map[string]callcheck.Check{
"fmt.Errorf": func(call *callcheck.Call) { check(call, 0, 1) },
"fmt.Printf": func(call *callcheck.Call) { check(call, 0, 1) },
"fmt.Sprintf": func(call *callcheck.Call) { check(call, 0, 1) },
"fmt.Fprintf": func(call *callcheck.Call) { check(call, 1, 2) },
"golang.org/x/xerrors.Errorf": func(call *callcheck.Call) { check(call, 0, 1) },
}
type verbFlag int
const (
isInt verbFlag = 1 << iota
isBool
isFP
isString
isPointer
// Verbs that accept "pseudo pointers" will sometimes dereference
// non-nil pointers. For example, %x on a non-nil *struct will print the
// individual fields, but on a nil pointer it will print the address.
isPseudoPointer
isSlice
isAny
noRecurse
)
var verbs = [...]verbFlag{
'b': isPseudoPointer | isInt | isFP,
'c': isInt,
'd': isPseudoPointer | isInt,
'e': isFP,
'E': isFP,
'f': isFP,
'F': isFP,
'g': isFP,
'G': isFP,
'o': isPseudoPointer | isInt,
'O': isPseudoPointer | isInt,
'p': isSlice | isPointer | noRecurse,
'q': isInt | isString,
's': isString,
't': isBool,
'T': isAny,
'U': isInt,
'v': isAny,
'X': isPseudoPointer | isInt | isFP | isString,
'x': isPseudoPointer | isInt | isFP | isString,
}
func check(call *callcheck.Call, fIdx, vIdx int) {
f := call.Args[fIdx]
var args []ir.Value
switch v := call.Args[vIdx].Value.Value.(type) {
case *ir.Slice:
var ok bool
args, ok = irutil.Vararg(v)
if !ok {
// We don't know what the actual arguments to the function are
return
}
case *ir.Const:
// nil, i.e. no arguments
default:
// We don't know what the actual arguments to the function are
return
}
checkImpl(f, f.Value.Value, args)
}
func checkImpl(carg *callcheck.Argument, f ir.Value, args []ir.Value) {
var msCache *typeutil.MethodSetCache
if f.Parent() != nil {
msCache = &f.Parent().Prog.MethodSets
}
elem := func(T types.Type, verb rune) ([]types.Type, bool) {
if verbs[verb]&noRecurse != 0 {
return []types.Type{T}, false
}
switch T := T.(type) {
case *types.Slice:
if verbs[verb]&isSlice != 0 {
return []types.Type{T}, false
}
if verbs[verb]&isString != 0 && types.Identical(T.Elem().Underlying(), types.Typ[types.Byte]) {
return []types.Type{T}, false
}
return []types.Type{T.Elem()}, true
case *types.Map:
key := T.Key()
val := T.Elem()
return []types.Type{key, val}, true
case *types.Struct:
out := make([]types.Type, 0, T.NumFields())
for field := range T.Fields() {
out = append(out, field.Type())
}
return out, true
case *types.Array:
return []types.Type{T.Elem()}, true
default:
return []types.Type{T}, false
}
}
isInfo := func(T types.Type, info types.BasicInfo) bool {
basic, ok := T.Underlying().(*types.Basic)
return ok && basic.Info()&info != 0
}
isFormatter := func(T types.Type, ms *types.MethodSet) bool {
sel := ms.Lookup(nil, "Format")
if sel == nil {
return false
}
fn, ok := sel.Obj().(*types.Func)
if !ok {
// should be unreachable
return false
}
sig := fn.Type().(*types.Signature)
if sig.Params().Len() != 2 {
return false
}
// TODO(dh): check the types of the arguments for more
// precision
if sig.Results().Len() != 0 {
return false
}
return true
}
var seen typeutil.Map[struct{}]
var checkType func(verb rune, T types.Type, top bool) bool
checkType = func(verb rune, T types.Type, top bool) bool {
if top {
seen = typeutil.Map[struct{}]{}
}
if _, ok := seen.At(T); ok {
return true
}
seen.Set(T, struct{}{})
if int(verb) >= len(verbs) {
// Unknown verb
return true
}
flags := verbs[verb]
if flags == 0 {
// Unknown verb
return true
}
ms := msCache.MethodSet(T)
if isFormatter(T, ms) {
// the value is responsible for formatting itself
return true
}
if flags&isString != 0 && (types.Implements(T, knowledge.Interfaces["fmt.Stringer"]) || types.Implements(T, knowledge.Interfaces["error"])) {
// Check for stringer early because we're about to dereference
return true
}
T = T.Underlying()
if flags&(isPointer|isPseudoPointer) == 0 && top {
T = typeutil.Dereference(T)
}
if flags&isPseudoPointer != 0 && top {
t := typeutil.Dereference(T)
if _, ok := t.Underlying().(*types.Struct); ok {
T = t
}
}
if _, ok := T.(*types.Interface); ok {
// We don't know what's in the interface
return true
}
var info types.BasicInfo
if flags&isInt != 0 {
info |= types.IsInteger
}
if flags&isBool != 0 {
info |= types.IsBoolean
}
if flags&isFP != 0 {
info |= types.IsFloat | types.IsComplex
}
if flags&isString != 0 {
info |= types.IsString
}
if info != 0 && isInfo(T, info) {
return true
}
if flags&isString != 0 {
isStringyElem := func(typ types.Type) bool {
if typ, ok := typ.Underlying().(*types.Basic); ok {
return typ.Kind() == types.Byte
}
return false
}
switch T := T.(type) {
case *types.Slice:
if isStringyElem(T.Elem()) {
return true
}
case *types.Array:
if isStringyElem(T.Elem()) {
return true
}
}
if types.Implements(T, knowledge.Interfaces["fmt.Stringer"]) || types.Implements(T, knowledge.Interfaces["error"]) {
return true
}
}
if flags&isPointer != 0 && typeutil.IsPointerLike(T) {
return true
}
if flags&isPseudoPointer != 0 {
switch U := T.Underlying().(type) {
case *types.Pointer:
if !top {
return true
}
if _, ok := U.Elem().Underlying().(*types.Struct); !ok {
// TODO(dh): can this condition ever be false? For
// *T, if T is a struct, we'll already have
// dereferenced it, meaning the *types.Pointer
// branch couldn't have been taken. For T that
// aren't structs, this condition will always
// evaluate to true.
return true
}
case *types.Chan, *types.Signature:
// Channels and functions are always treated as
// pointers and never recursed into.
return true
case *types.Basic:
if U.Kind() == types.UnsafePointer {
return true
}
case *types.Interface:
// we will already have bailed if the type is an
// interface.
panic("unreachable")
default:
// other pointer-like types, such as maps or slices,
// will be printed element-wise.
}
}
if flags&isSlice != 0 {
if _, ok := T.(*types.Slice); ok {
return true
}
}
if flags&isAny != 0 {
return true
}
elems, ok := elem(T.Underlying(), verb)
if !ok {
return false
}
for _, elem := range elems {
if !checkType(verb, elem, false) {
return false
}
}
return true
}
k, ok := irutil.Flatten(f).(*ir.Const)
if !ok {
return
}
actions, err := printf.Parse(constant.StringVal(k.Value))
if err != nil {
carg.Invalid("couldn't parse format string")
return
}
ptr := 1
hasExplicit := false
checkStar := func(verb printf.Verb, star printf.Argument) bool {
if star, ok := star.(printf.Star); ok {
idx := 0
if star.Index == -1 {
idx = ptr
ptr++
} else {
hasExplicit = true
idx = star.Index
ptr = star.Index + 1
}
if idx == 0 {
carg.Invalid(fmt.Sprintf("Printf format %s reads invalid arg 0; indices are 1-based", verb.Raw))
return false
}
if idx > len(args) {
carg.Invalid(
fmt.Sprintf("Printf format %s reads arg #%d, but call has only %d args",
verb.Raw, idx, len(args)))
return false
}
if arg, ok := args[idx-1].(*ir.MakeInterface); ok {
if !isInfo(arg.X.Type(), types.IsInteger) {
carg.Invalid(fmt.Sprintf("Printf format %s reads non-int arg #%d as argument of *", verb.Raw, idx))
}
}
}
return true
}
// We only report one problem per format string. Making a
// mistake with an index tends to invalidate all future
// implicit indices.
for _, action := range actions {
verb, ok := action.(printf.Verb)
if !ok {
continue
}
if !checkStar(verb, verb.Width) || !checkStar(verb, verb.Precision) {
return
}
off := ptr
if verb.Value != -1 {
hasExplicit = true
off = verb.Value
}
if off > len(args) {
carg.Invalid(
fmt.Sprintf("Printf format %s reads arg #%d, but call has only %d args",
verb.Raw, off, len(args)))
return
} else if verb.Value == 0 && verb.Letter != '%' {
carg.Invalid(fmt.Sprintf("Printf format %s reads invalid arg 0; indices are 1-based", verb.Raw))
return
} else if off != 0 {
arg, ok := args[off-1].(*ir.MakeInterface)
if ok {
if !checkType(verb.Letter, arg.X.Type(), true) {
carg.Invalid(fmt.Sprintf("Printf format %s has arg #%d of wrong type %s",
verb.Raw, ptr, args[ptr-1].(*ir.MakeInterface).X.Type()))
return
}
}
}
switch verb.Value {
case -1:
// Consume next argument
ptr++
case 0:
// Don't consume any arguments
default:
ptr = verb.Value + 1
}
}
if !hasExplicit && ptr <= len(args) {
carg.Invalid(fmt.Sprintf("Printf call needs %d args but has %d args", ptr-1, len(args)))
}
}

View File

@@ -0,0 +1,115 @@
package sa5010
import (
"fmt"
"go/types"
"strings"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5010",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Impossible type assertion`,
Text: `Some type assertions can be statically proven to be
impossible. This is the case when the method sets of both
arguments of the type assertion conflict with each other, for
example by containing the same method with different
signatures.
The Go compiler already applies this check when asserting from an
interface value to a concrete type. If the concrete type misses
methods from the interface, or if function signatures don't match,
then the type assertion can never succeed.
This check applies the same logic when asserting from one interface to
another. If both interface types contain the same method but with
different signatures, then the type assertion can never succeed,
either.`,
Since: "2020.1",
Severity: lint.SeverityWarning,
// Technically this should be MergeIfAll, but the Go compiler
// already flags some impossible type assertions, so
// MergeIfAny is consistent with the compiler.
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
type entry struct {
l, r *types.Func
}
msc := &pass.ResultOf[buildir.Analyzer].(*buildir.IR).Pkg.Prog.MethodSets
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, b := range fn.Blocks {
instrLoop:
for _, instr := range b.Instrs {
assert, ok := instr.(*ir.TypeAssert)
if !ok {
continue
}
var wrong []entry
left := assert.X.Type()
right := assert.AssertedType
righti, ok := right.Underlying().(*types.Interface)
if !ok {
// We only care about interface->interface
// assertions. The Go compiler already catches
// impossible interface->concrete assertions.
continue
}
ms := msc.MethodSet(left)
for mr := range righti.Methods() {
sel := ms.Lookup(mr.Pkg(), mr.Name())
if sel == nil {
continue
}
ml := sel.Obj().(*types.Func)
if ml.Origin() != ml || mr.Origin() != mr {
// Give up when we see generics.
//
// TODO(dh): support generics once go/types gets an
// exported API for type unification.
continue instrLoop
}
if types.AssignableTo(ml.Type(), mr.Type()) {
continue
}
wrong = append(wrong, entry{ml, mr})
}
if len(wrong) != 0 {
var s strings.Builder
s.WriteString(fmt.Sprintf("impossible type assertion; %s and %s contradict each other:",
types.TypeString(left, types.RelativeTo(pass.Pkg)),
types.TypeString(right, types.RelativeTo(pass.Pkg))))
for _, e := range wrong {
s.WriteString(fmt.Sprintf("\n\twrong type for %s method", e.l.Name()))
s.WriteString(fmt.Sprintf("\n\t\thave %s", e.l.Type()))
s.WriteString(fmt.Sprintf("\n\t\twant %s", e.r.Type()))
}
report.Report(pass, assert, s.String())
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,221 @@
package sa5011
import (
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5011",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Possible nil pointer dereference`,
Text: `A pointer is being dereferenced unconditionally, while
also being checked against nil in another place. This suggests that
the pointer may be nil and dereferencing it may panic. This is
commonly a result of improperly ordered code or missing return
statements. Consider the following examples:
func fn(x *int) {
fmt.Println(*x)
// This nil check is equally important for the previous dereference
if x != nil {
foo(*x)
}
}
func TestFoo(t *testing.T) {
x := compute()
if x == nil {
t.Errorf("nil pointer received")
}
// t.Errorf does not abort the test, so if x is nil, the next line will panic.
foo(*x)
}
Staticcheck tries to deduce which functions abort control flow.
For example, it is aware that a function will not continue
execution after a call to \'panic\' or \'log.Fatal\'. However, sometimes
this detection fails, in particular in the presence of
conditionals. Consider the following example:
func Log(msg string, level int) {
fmt.Println(msg)
if level == levelFatal {
os.Exit(1)
}
}
func Fatal(msg string) {
Log(msg, levelFatal)
}
func fn(x *int) {
if x == nil {
Fatal("unexpected nil pointer")
}
fmt.Println(*x)
}
Staticcheck will flag the dereference of \'x\', even though it is perfectly
safe. Staticcheck is not able to deduce that a call to
Fatal will exit the program. For the time being, the easiest
workaround is to modify the definition of Fatal like so:
func Fatal(msg string) {
Log(msg, levelFatal)
panic("unreachable")
}
We also hard-code functions from common logging packages such as
logrus. Please file an issue if we're missing support for a
popular package.`,
Since: "2020.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
// This is an extremely trivial check that doesn't try to reason
// about control flow. That is, phis and sigmas do not propagate
// any information. As such, we can flag this:
//
// _ = *x
// if x == nil { return }
//
// but we cannot flag this:
//
// if x == nil { println(x) }
// _ = *x
//
// but we can flag this, because the if's body doesn't use x:
//
// if x == nil { println("this is bad") }
// _ = *x
//
// nor many other variations of conditional uses of or assignments to x.
//
// However, even this trivial implementation finds plenty of
// real-world bugs, such as dereference before nil pointer check,
// or using t.Error instead of t.Fatal when encountering nil
// pointers.
//
// On the flip side, our naive implementation avoids false positives in branches, such as
//
// if x != nil { _ = *x }
//
// due to the same lack of propagating information through sigma
// nodes. x inside the branch will be independent of the x in the
// nil pointer check.
//
//
// We could implement a more powerful check, but then we'd be
// getting false positives instead of false negatives because
// we're incapable of deducing relationships between variables.
// For example, a function might return a pointer and an error,
// and the error being nil guarantees that the pointer is not nil.
// Depending on the surrounding code, the pointer may still end up
// being checked against nil in one place, and guarded by a check
// on the error in another, which would lead to us marking some
// loads as unsafe.
//
// Unfortunately, simply hard-coding the relationship between
// return values wouldn't eliminate all false positives, either.
// Many other more subtle relationships exist. An abridged example
// from real code:
//
// if a == nil && b == nil { return }
// c := fn(a)
// if c != "" { _ = *a }
//
// where `fn` is guaranteed to return a non-empty string if a
// isn't nil.
//
// We choose to err on the side of false negatives.
isNilConst := func(v ir.Value) bool {
if typeutil.IsPointerLike(v.Type()) {
if k, ok := v.(*ir.Const); ok {
return k.IsNil()
}
}
return false
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
maybeNil := map[ir.Value]ir.Instruction{}
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
// Originally we looked at all ir.BinOp, but that would lead to calls like 'assert(x != nil)' causing false positives.
// Restrict ourselves to actual if statements, as these are more likely to affect control flow in a way we can observe.
if instr, ok := instr.(*ir.If); ok {
if cond, ok := instr.Cond.(*ir.BinOp); ok {
if isNilConst(cond.X) {
maybeNil[cond.Y] = cond
}
if isNilConst(cond.Y) {
maybeNil[cond.X] = cond
}
}
}
}
}
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
var ptr ir.Value
switch instr := instr.(type) {
case *ir.Load:
ptr = instr.X
case *ir.Store:
ptr = instr.Addr
case *ir.IndexAddr:
ptr = instr.X
if typeutil.All(ptr.Type(), func(term *types.Term) bool {
if _, ok := term.Type().Underlying().(*types.Slice); ok {
return true
}
return false
}) {
// indexing a nil slice does not cause a nil pointer panic
//
// Note: This also works around the bad lowering of range loops over slices
// (https://github.com/dominikh/go-tools/issues/1053)
continue
}
case *ir.FieldAddr:
ptr = instr.X
}
if ptr != nil {
switch ptr.(type) {
case *ir.Alloc, *ir.FieldAddr, *ir.IndexAddr:
// these cannot be nil
continue
}
if r, ok := maybeNil[ptr]; ok {
report.Report(pass, instr, "possible nil pointer dereference",
report.Related(r, "this check suggests that the pointer can be nil"))
}
}
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,286 @@
package sa5012
import (
"fmt"
"go/ast"
"go/constant"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA5012",
Run: run,
FactTypes: []analysis.Fact{new(evenElements)},
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: "Passing odd-sized slice to function expecting even size",
Text: `Some functions that take slices as parameters expect the slices to have an even number of elements.
Often, these functions treat elements in a slice as pairs.
For example, \'strings.NewReplacer\' takes pairs of old and new strings,
and calling it with an odd number of elements would be an error.`,
Since: "2020.2",
Severity: lint.SeverityError,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
type evenElements struct{}
func (evenElements) AFact() {}
func (evenElements) String() string { return "needs even elements" }
func findSliceLength(v ir.Value) int {
// TODO(dh): VRP would help here
v = irutil.Flatten(v)
val := func(v ir.Value) int {
if v, ok := v.(*ir.Const); ok {
return int(v.Int64())
}
return -1
}
switch v := v.(type) {
case *ir.Slice:
low := 0
high := -1
if v.Low != nil {
low = val(v.Low)
}
if v.High != nil {
high = val(v.High)
} else {
switch vv := v.X.(type) {
case *ir.Alloc:
high = int(typeutil.Dereference(vv.Type()).Underlying().(*types.Array).Len())
case *ir.Slice:
high = findSliceLength(vv)
}
}
if low == -1 || high == -1 {
return -1
}
return high - low
default:
return -1
}
}
func flagSliceLens(pass *analysis.Pass) {
var tag evenElements
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, b := range fn.Blocks {
for _, instr := range b.Instrs {
call, ok := instr.(ir.CallInstruction)
if !ok {
continue
}
callee := call.Common().StaticCallee()
if callee == nil {
continue
}
for argi, arg := range call.Common().Args {
if callee.Signature.Recv() != nil {
if argi == 0 {
continue
}
argi--
}
_, ok := arg.Type().Underlying().(*types.Slice)
if !ok {
continue
}
param := callee.Signature.Params().At(argi)
if !pass.ImportObjectFact(param, &tag) {
continue
}
// TODO handle stubs
// we know the argument has to have even length.
// now let's try to find its length
if n := findSliceLength(arg); n > -1 && n%2 != 0 {
src := call.Source().(*ast.CallExpr).Args[argi]
sig := call.Common().Signature()
var label string
if argi == sig.Params().Len()-1 && sig.Variadic() {
label = "variadic argument"
} else {
label = "argument"
}
// Note that param.Name() is guaranteed to not
// be empty, otherwise the function couldn't
// have enforced its length.
report.Report(pass, src, fmt.Sprintf("%s %q is expected to have even number of elements, but has %d elements", label, param.Name(), n))
}
}
}
}
}
}
func findSliceLenChecks(pass *analysis.Pass) {
// mark all function parameters that have to be of even length
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, b := range fn.Blocks {
// all paths go through this block
if !b.Dominates(fn.Exit) {
continue
}
// if foo % 2 != 0
ifi, ok := b.Control().(*ir.If)
if !ok {
continue
}
cmp, ok := ifi.Cond.(*ir.BinOp)
if !ok {
continue
}
var needle uint64
switch cmp.Op {
case token.NEQ:
// look for != 0
needle = 0
case token.EQL:
// look for == 1
needle = 1
default:
continue
}
rem, ok1 := cmp.X.(*ir.BinOp)
k, ok2 := cmp.Y.(*ir.Const)
if ok1 != ok2 {
continue
}
if !ok1 {
rem, ok1 = cmp.Y.(*ir.BinOp)
k, ok2 = cmp.X.(*ir.Const)
}
if !ok1 || !ok2 || rem.Op != token.REM || k.Value.Kind() != constant.Int || k.Uint64() != needle {
continue
}
k, ok = rem.Y.(*ir.Const)
if !ok || k.Value.Kind() != constant.Int || k.Uint64() != 2 {
continue
}
// if len(foo) % 2 != 0
call, ok := rem.X.(*ir.Call)
if !ok || !irutil.IsCallTo(call.Common(), "len") {
continue
}
// we're checking the length of a parameter that is a slice
// TODO(dh): support parameters that have flown through sigmas and phis
param, ok := call.Call.Args[0].(*ir.Parameter)
if !ok {
continue
}
if !typeutil.All(param.Type(), typeutil.IsSlice) {
continue
}
// if len(foo) % 2 != 0 then panic
if _, ok := b.Succs[0].Control().(*ir.Panic); !ok {
continue
}
pass.ExportObjectFact(param.Object(), new(evenElements))
}
}
}
func findIndirectSliceLenChecks(pass *analysis.Pass) {
seen := map[*ir.Function]struct{}{}
var doFunction func(fn *ir.Function)
doFunction = func(fn *ir.Function) {
if _, ok := seen[fn]; ok {
return
}
seen[fn] = struct{}{}
for _, b := range fn.Blocks {
// all paths go through this block
if !b.Dominates(fn.Exit) {
continue
}
for _, instr := range b.Instrs {
call, ok := instr.(*ir.Call)
if !ok {
continue
}
callee := call.Call.StaticCallee()
if callee == nil {
continue
}
if callee.Pkg == fn.Pkg || callee.Pkg == nil {
doFunction(callee)
}
for argi, arg := range call.Call.Args {
if callee.Signature.Recv() != nil {
if argi == 0 {
continue
}
argi--
}
// TODO(dh): support parameters that have flown through length-preserving instructions
param, ok := arg.(*ir.Parameter)
if !ok {
continue
}
if !typeutil.All(param.Type(), typeutil.IsSlice) {
continue
}
// We can't use callee.Params to look up the
// parameter, because Params is not populated for
// external functions. In our modular analysis.
// any function in any package that isn't the
// current package is considered "external", as it
// has been loaded from export data only.
sigParams := callee.Signature.Params()
if !pass.ImportObjectFact(sigParams.At(argi), new(evenElements)) {
continue
}
pass.ExportObjectFact(param.Object(), new(evenElements))
}
}
}
}
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
doFunction(fn)
}
}
func run(pass *analysis.Pass) (any, error) {
findSliceLenChecks(pass)
findIndirectSliceLenChecks(pass)
flagSliceLens(pass)
return nil, nil
}

View File

@@ -0,0 +1,57 @@
package sa6000
import (
"fmt"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6000",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Using \'regexp.Match\' or related in a loop, should use \'regexp.Compile\'`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"regexp.Match": check("regexp.Match"),
"regexp.MatchReader": check("regexp.MatchReader"),
"regexp.MatchString": check("regexp.MatchString"),
}
func check(name string) callcheck.Check {
return func(call *callcheck.Call) {
if callcheck.ExtractConst(call.Args[0].Value) == nil {
return
}
if !isInLoop(call.Instr.Block()) {
return
}
call.Invalid(fmt.Sprintf("calling %s in a loop has poor performance, consider using regexp.Compile", name))
}
}
func isInLoop(b *ir.BasicBlock) bool {
sets := irutil.FindLoops(b.Parent())
for _, set := range sets {
if set.Has(b) {
return true
}
}
return false
}

View File

@@ -0,0 +1,124 @@
package sa6001
import (
"go/ast"
"go/types"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6001",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Missing an optimization opportunity when indexing maps by byte slices`,
Text: `Map keys must be comparable, which precludes the use of byte slices.
This usually leads to using string keys and converting byte slices to
strings.
Normally, a conversion of a byte slice to a string needs to copy the data and
causes allocations. The compiler, however, recognizes \'m[string(b)]\' and
uses the data of \'b\' directly, without copying it, because it knows that
the data can't change during the map lookup. This leads to the
counter-intuitive situation that
k := string(b)
println(m[k])
println(m[k])
will be less efficient than
println(m[string(b)])
println(m[string(b)])
because the first version needs to copy and allocate, while the second
one does not.
For some history on this optimization, check out commit
f5f5a8b6209f84961687d993b93ea0d397f5d5bf in the Go repository.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
for _, b := range fn.Blocks {
insLoop:
for _, ins := range b.Instrs {
var fromType types.Type
var toType types.Type
// find []byte -> string conversions
switch ins := ins.(type) {
case *ir.Convert:
fromType = ins.X.Type()
toType = ins.Type()
case *ir.MultiConvert:
fromType = ins.X.Type()
toType = ins.Type()
default:
continue
}
if toType != types.Universe.Lookup("string").Type() {
continue
}
tset := typeutil.NewTypeSet(fromType)
// If at least one of the types is []byte, then it's more efficient to inline the conversion
if !tset.Any(func(term *types.Term) bool {
s, ok := term.Type().Underlying().(*types.Slice)
return ok && s.Elem().Underlying() == types.Universe.Lookup("byte").Type()
}) {
continue
}
refs := ins.Referrers()
// need at least two (DebugRef) references: the
// conversion and the *ast.Ident
if refs == nil || len(*refs) < 2 {
continue
}
ident := false
// skip first reference, that's the conversion itself
for _, ref := range (*refs)[1:] {
switch ref := ref.(type) {
case *ir.DebugRef:
if _, ok := ref.Expr.(*ast.Ident); !ok {
// the string seems to be used somewhere
// unexpected; the default branch should
// catch this already, but be safe
continue insLoop
} else {
ident = true
}
case *ir.MapLookup:
default:
// the string is used somewhere else than a
// map lookup
continue insLoop
}
}
// the result of the conversion wasn't assigned to an
// identifier
if !ident {
continue
}
report.Report(pass, ins, "m[string(key)] would be more efficient than k := string(key); m[k]")
}
}
}
return nil, nil
}

View File

@@ -0,0 +1,52 @@
package sa6002
import (
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6002",
Requires: []*analysis.Analyzer{buildir.Analyzer},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Storing non-pointer values in \'sync.Pool\' allocates memory`,
Text: `A \'sync.Pool\' is used to avoid unnecessary allocations and reduce the
amount of work the garbage collector has to do.
When passing a value that is not a pointer to a function that accepts
an interface, the value needs to be placed on the heap, which means an
additional allocation. Slices are a common thing to put in sync.Pools,
and they're structs with 3 fields (length, capacity, and a pointer to
an array). In order to avoid the extra allocation, one should store a
pointer to the slice instead.
See the comments on https://go-review.googlesource.com/c/go/+/24371
that discuss this problem.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
"(*sync.Pool).Put": func(call *callcheck.Call) {
arg := call.Args[knowledge.Arg("(*sync.Pool).Put.x")]
typ := arg.Value.Value.Type()
_, isSlice := typ.Underlying().(*types.Slice)
if !typeutil.IsPointerLike(typ) || isSlice {
arg.Invalid("argument should be pointer-like to avoid allocations")
}
},
}

View File

@@ -0,0 +1,42 @@
package sa6003
import (
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/internal/sharedcheck"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6003",
Run: sharedcheck.CheckRangeStringRunes,
Requires: []*analysis.Analyzer{buildir.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Converting a string to a slice of runes before ranging over it`,
Text: `You may want to loop over the runes in a string. Instead of converting
the string to a slice of runes and looping over that, you can loop
over the string itself. That is,
for _, r := range s {}
and
for _, r := range []rune(s) {}
will yield the same values. The first version, however, will be faster
and avoid unnecessary memory allocations.
Do note that if you are interested in the indices, ranging over a
string and over a slice of runes will yield different indices. The
first one yields byte offsets, while the second one yields indices in
the slice of runes.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer

View File

@@ -0,0 +1,75 @@
package sa6005
import (
"go/ast"
"go/token"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6005",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Inefficient string comparison with \'strings.ToLower\' or \'strings.ToUpper\'`,
Text: `Converting two strings to the same case and comparing them like so
if strings.ToLower(s1) == strings.ToLower(s2) {
...
}
is significantly more expensive than comparing them with
\'strings.EqualFold(s1, s2)\'. This is due to memory usage as well as
computational complexity.
\'strings.ToLower\' will have to allocate memory for the new strings, as
well as convert both strings fully, even if they differ on the very
first byte. strings.EqualFold, on the other hand, compares the strings
one character at a time. It doesn't need to create two intermediate
strings and can return as soon as the first non-matching character has
been found.
For a more in-depth explanation of this issue, see
https://blog.digitalocean.com/how-to-efficiently-compare-strings-in-go/`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
var (
checkToLowerToUpperComparisonQ = pattern.MustParse(`
(BinaryExpr
(CallExpr fun@(Symbol (Or "strings.ToLower" "strings.ToUpper")) [a])
tok@(Or "==" "!=")
(CallExpr fun [b]))`)
checkToLowerToUpperComparisonR = pattern.MustParse(`(CallExpr (SelectorExpr (Ident "strings") (Ident "EqualFold")) [a b])`)
)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, checkToLowerToUpperComparisonQ) {
rn := pattern.NodeToAST(checkToLowerToUpperComparisonR.Root, m.State).(ast.Expr)
if m.State["tok"].(token.Token) == token.NEQ {
rn = &ast.UnaryExpr{
Op: token.NOT,
X: rn,
}
}
report.Report(pass, node,
"should use strings.EqualFold instead",
report.Fixes(edit.Fix("Replace with strings.EqualFold", edit.ReplaceWithNode(pass.Fset, node, rn))))
}
return nil, nil
}

View File

@@ -0,0 +1,46 @@
package sa6006
import (
"go/ast"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/pattern"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA6006",
Run: run,
Requires: code.RequiredAnalyzers,
},
Doc: &lint.RawDocumentation{
Title: `Using io.WriteString to write \'[]byte\'`,
Text: `Using io.WriteString to write a slice of bytes, as in
io.WriteString(w, string(b))
is both unnecessary and inefficient. Converting from \'[]byte\' to \'string\'
has to allocate and copy the data, and we could simply use \'w.Write(b)\'
instead.`,
Since: "2024.1",
},
})
var Analyzer = SCAnalyzer.Analyzer
var ioWriteStringConversion = pattern.MustParse(`(CallExpr (Symbol "io.WriteString") [_ (CallExpr (Builtin "string") [arg])])`)
func run(pass *analysis.Pass) (any, error) {
for node, m := range code.Matches(pass, ioWriteStringConversion) {
if !code.IsOfStringConvertibleByteSlice(pass, m.State["arg"].(ast.Expr)) {
continue
}
report.Report(pass, node, "use io.Writer.Write instead of converting from []byte to string to use io.WriteString")
}
return nil, nil
}

View File

@@ -0,0 +1,69 @@
package sa9001
import (
"go/ast"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9001",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Defers in range loops may not run when you expect them to`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
loop := node.(*ast.RangeStmt)
typ := pass.TypesInfo.TypeOf(loop.X)
_, ok := typeutil.CoreType(typ).(*types.Chan)
if !ok {
return
}
stmts := []*ast.DeferStmt{}
exits := false
fn2 := func(node ast.Node) bool {
switch stmt := node.(type) {
case *ast.DeferStmt:
stmts = append(stmts, stmt)
case *ast.FuncLit:
// Don't look into function bodies
return false
case *ast.ReturnStmt:
exits = true
case *ast.BranchStmt:
exits = node.(*ast.BranchStmt).Tok == token.BREAK
}
return true
}
ast.Inspect(loop.Body, fn2)
if exits {
return
}
for _, stmt := range stmts {
report.Report(pass, stmt, "defers in this range loop won't run unless the channel gets closed")
}
}
code.Preorder(pass, fn, (*ast.RangeStmt)(nil))
return nil, nil
}

View File

@@ -0,0 +1,63 @@
package sa9002
import (
"fmt"
"go/ast"
"strconv"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/types/typeutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9002",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Using a non-octal \'os.FileMode\' that looks like it was meant to be in octal.`,
Since: "2017.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
call := node.(*ast.CallExpr)
for _, arg := range call.Args {
lit, ok := arg.(*ast.BasicLit)
if !ok {
continue
}
if !typeutil.IsTypeWithName(pass.TypesInfo.TypeOf(lit), "os.FileMode") &&
!typeutil.IsTypeWithName(pass.TypesInfo.TypeOf(lit), "io/fs.FileMode") {
continue
}
if len(lit.Value) == 3 &&
lit.Value[0] != '0' &&
lit.Value[0] >= '0' && lit.Value[0] <= '7' &&
lit.Value[1] >= '0' && lit.Value[1] <= '7' &&
lit.Value[2] >= '0' && lit.Value[2] <= '7' {
v, err := strconv.ParseInt(lit.Value, 10, 64)
if err != nil {
continue
}
report.Report(pass, arg, fmt.Sprintf("file mode '%s' evaluates to %#o; did you mean '0%s'?", lit.Value, v, lit.Value),
report.Fixes(edit.Fix("Fix octal literal", edit.ReplaceWithString(arg, "0"+lit.Value))))
}
}
}
code.Preorder(pass, fn, (*ast.CallExpr)(nil))
return nil, nil
}

View File

@@ -0,0 +1,62 @@
package sa9003
import (
"go/ast"
"golang.org/x/tools/go/analysis"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ir/irutil"
"honnef.co/go/tools/internal/passes/buildir"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9003",
Run: run,
Requires: []*analysis.Analyzer{buildir.Analyzer, generated.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Empty body in an if or else branch`,
Since: "2017.1",
NonDefault: true,
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
for _, fn := range pass.ResultOf[buildir.Analyzer].(*buildir.IR).SrcFuncs {
if fn.Source() == nil {
continue
}
if irutil.IsExample(fn) {
continue
}
cb := func(node ast.Node) bool {
ifstmt, ok := node.(*ast.IfStmt)
if !ok {
return true
}
if ifstmt.Else != nil {
b, ok := ifstmt.Else.(*ast.BlockStmt)
if !ok || len(b.List) != 0 {
return true
}
report.Report(pass, ifstmt.Else, "empty branch", report.FilterGenerated(), report.ShortRange())
}
if len(ifstmt.Body.List) != 0 {
return true
}
report.Report(pass, ifstmt, "empty branch", report.FilterGenerated(), report.ShortRange())
return true
}
if source := fn.Source(); source != nil {
ast.Inspect(source, cb)
}
}
return nil, nil
}

View File

@@ -0,0 +1,185 @@
package sa9004
import (
"go/ast"
"go/token"
"go/types"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/edit"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/go/ast/astutil"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9004",
Run: run,
Requires: []*analysis.Analyzer{inspect.Analyzer},
},
Doc: &lint.RawDocumentation{
Title: `Only the first constant has an explicit type`,
Text: `In a constant declaration such as the following:
const (
First byte = 1
Second = 2
)
the constant Second does not have the same type as the constant First.
This construct shouldn't be confused with
const (
First byte = iota
Second
)
where \'First\' and \'Second\' do indeed have the same type. The type is only
passed on when no explicit value is assigned to the constant.
When declaring enumerations with explicit values it is therefore
important not to write
const (
EnumFirst EnumType = 1
EnumSecond = 2
EnumThird = 3
)
This discrepancy in types can cause various confusing behaviors and
bugs.
Wrong type in variable declarations
The most obvious issue with such incorrect enumerations expresses
itself as a compile error:
package pkg
const (
EnumFirst uint8 = 1
EnumSecond = 2
)
func fn(useFirst bool) {
x := EnumSecond
if useFirst {
x = EnumFirst
}
}
fails to compile with
./const.go:11:5: cannot use EnumFirst (type uint8) as type int in assignment
Losing method sets
A more subtle issue occurs with types that have methods and optional
interfaces. Consider the following:
package main
import "fmt"
type Enum int
func (e Enum) String() string {
return "an enum"
}
const (
EnumFirst Enum = 1
EnumSecond = 2
)
func main() {
fmt.Println(EnumFirst)
fmt.Println(EnumSecond)
}
This code will output
an enum
2
as \'EnumSecond\' has no explicit type, and thus defaults to \'int\'.`,
Since: "2019.1",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAny,
},
})
var Analyzer = SCAnalyzer.Analyzer
func run(pass *analysis.Pass) (any, error) {
fn := func(node ast.Node) {
decl := node.(*ast.GenDecl)
if !decl.Lparen.IsValid() {
return
}
if decl.Tok != token.CONST {
return
}
groups := astutil.GroupSpecs(pass.Fset, decl.Specs)
groupLoop:
for _, group := range groups {
if len(group) < 2 {
continue
}
if group[0].(*ast.ValueSpec).Type == nil {
// first constant doesn't have a type
continue groupLoop
}
firstType := pass.TypesInfo.TypeOf(group[0].(*ast.ValueSpec).Values[0])
for i, spec := range group {
spec := spec.(*ast.ValueSpec)
if i > 0 && spec.Type != nil {
continue groupLoop
}
if len(spec.Names) != 1 || len(spec.Values) != 1 {
continue groupLoop
}
if !types.ConvertibleTo(pass.TypesInfo.TypeOf(spec.Values[0]), firstType) {
continue groupLoop
}
switch v := spec.Values[0].(type) {
case *ast.BasicLit:
case *ast.UnaryExpr:
if _, ok := v.X.(*ast.BasicLit); !ok {
continue groupLoop
}
default:
// if it's not a literal it might be typed, such as
// time.Microsecond = 1000 * Nanosecond
continue groupLoop
}
}
var edits []analysis.TextEdit
typ := group[0].(*ast.ValueSpec).Type
for _, spec := range group[1:] {
nspec := *spec.(*ast.ValueSpec)
nspec.Type = typ
// The position of `spec` node excludes comments (if any).
// However, on generating the source back from the node, the comments are included. Setting `Comment` to nil ensures deduplication of comments.
nspec.Comment = nil
edits = append(edits, edit.ReplaceWithNode(pass.Fset, spec, &nspec))
}
report.Report(pass, group[0],
"only the first constant in this group has an explicit type",
report.Fixes(edit.Fix("Add type to all constants in group", edits...)))
}
}
code.Preorder(pass, fn, (*ast.GenDecl)(nil))
return nil, nil
}

View File

@@ -0,0 +1,94 @@
package sa9005
import (
"fmt"
"go/types"
"honnef.co/go/tools/analysis/callcheck"
"honnef.co/go/tools/analysis/code"
"honnef.co/go/tools/analysis/facts/generated"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/go/types/typeutil"
"honnef.co/go/tools/internal/passes/buildir"
"honnef.co/go/tools/knowledge"
"golang.org/x/tools/go/analysis"
)
var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
Analyzer: &analysis.Analyzer{
Name: "SA9005",
Requires: []*analysis.Analyzer{
buildir.Analyzer,
// Filtering generated code because it may include empty structs generated from data models.
generated.Analyzer,
},
Run: callcheck.Analyzer(rules),
},
Doc: &lint.RawDocumentation{
Title: `Trying to marshal a struct with no public fields nor custom marshaling`,
Text: `
The \'encoding/json\' and \'encoding/xml\' packages only operate on exported
fields in structs, not unexported ones. It is usually an error to try
to (un)marshal structs that only consist of unexported fields.
This check will not flag calls involving types that define custom
marshaling behavior, e.g. via \'MarshalJSON\' methods. It will also not
flag empty structs.`,
Since: "2019.2",
Severity: lint.SeverityWarning,
MergeIf: lint.MergeIfAll,
},
})
var Analyzer = SCAnalyzer.Analyzer
var rules = map[string]callcheck.Check{
// TODO(dh): should we really flag XML? Even an empty struct
// produces a non-zero amount of data, namely its type name.
// Let's see if we encounter any false positives.
//
// Also, should we flag gob?
"encoding/json.Marshal": check(knowledge.Arg("json.Marshal.v"), "MarshalJSON", "MarshalText"),
"encoding/xml.Marshal": check(knowledge.Arg("xml.Marshal.v"), "MarshalXML", "MarshalText"),
"(*encoding/json.Encoder).Encode": check(knowledge.Arg("(*encoding/json.Encoder).Encode.v"), "MarshalJSON", "MarshalText"),
"(*encoding/xml.Encoder).Encode": check(knowledge.Arg("(*encoding/xml.Encoder).Encode.v"), "MarshalXML", "MarshalText"),
"encoding/json.Unmarshal": check(knowledge.Arg("json.Unmarshal.v"), "UnmarshalJSON", "UnmarshalText"),
"encoding/xml.Unmarshal": check(knowledge.Arg("xml.Unmarshal.v"), "UnmarshalXML", "UnmarshalText"),
"(*encoding/json.Decoder).Decode": check(knowledge.Arg("(*encoding/json.Decoder).Decode.v"), "UnmarshalJSON", "UnmarshalText"),
"(*encoding/xml.Decoder).Decode": check(knowledge.Arg("(*encoding/xml.Decoder).Decode.v"), "UnmarshalXML", "UnmarshalText"),
}
func check(argN int, meths ...string) callcheck.Check {
return func(call *callcheck.Call) {
if code.IsGenerated(call.Pass, call.Instr.Pos()) {
return
}
arg := call.Args[argN]
T := arg.Value.Value.Type()
Ts, ok := typeutil.Dereference(T).Underlying().(*types.Struct)
if !ok {
return
}
if Ts.NumFields() == 0 {
return
}
fields := typeutil.FlattenFields(Ts)
for _, field := range fields {
if field.Var.Exported() {
return
}
}
// OPT(dh): we could use a method set cache here
ms := call.Instr.Parent().Prog.MethodSets.MethodSet(T)
// TODO(dh): we're not checking the signature, which can cause false negatives.
// This isn't a huge problem, however, since vet complains about incorrect signatures.
for _, meth := range meths {
if ms.Lookup(nil, meth) != nil {
return
}
}
arg.Invalid(fmt.Sprintf("struct type '%s' doesn't have any exported fields, nor custom marshaling", typeutil.Dereference(T)))
}
}

Some files were not shown because too many files have changed in this diff Show More