Skip to content
3 changes: 2 additions & 1 deletion .github/workflows/release.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 3 additions & 38 deletions pkg/linters/appendbytestring/appendbytestring.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package appendbytestring
import (
"fmt"
"go/ast"
"go/types"

"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/analysis/passes/inspect"
Expand Down Expand Up @@ -67,7 +66,7 @@ func run(pass *analysis.Pass) (any, error) {
}

// The first argument must be []byte.
if !isByteSlice(pass, call.Args[0]) {
if !astutil.IsByteSlice(pass, call.Args[0]) {
return
}

Expand All @@ -76,14 +75,14 @@ func run(pass *analysis.Pass) (any, error) {
if !ok {
return
}
if !isByteSliceConversion(pass, conv) {
if !astutil.IsByteSliceConversion(pass, conv) {
return
}
if len(conv.Args) != 1 {
return
}
strArg := conv.Args[0]
if !isStringType(pass, strArg) {
if !astutil.IsStringType(pass, strArg) {
return
}

Expand All @@ -103,40 +102,6 @@ func run(pass *analysis.Pass) (any, error) {
return nil, nil
}

// isByteSlice reports whether expr has type []byte.
func isByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
sl, ok := t.Underlying().(*types.Slice)
if !ok {
return false
}
elem, ok := sl.Elem().(*types.Basic)
return ok && elem.Kind() == types.Byte
}

// isByteSliceConversion reports whether conv is a []byte/[]uint8 conversion expression.
func isByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
// A type conversion has a type expression as the call function.
funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
if !ok || !funTypeInfo.IsType() {
return false
}
return isByteSlice(pass, conv)
}

// isStringType reports whether expr has type string.
func isStringType(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
basic, ok := t.Underlying().(*types.Basic)
return ok && basic.Kind() == types.String
}

// buildFix returns a SuggestedFix rewriting append(b, []byte(s)...) to append(b, s...).
func buildFix(pass *analysis.Pass, conv *ast.CallExpr, strArg ast.Expr) []analysis.SuggestedFix {
sText := astutil.NodeText(pass.Fset, strArg)
Expand Down
16 changes: 1 addition & 15 deletions pkg/linters/bytescomparestring/bytescomparestring.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,23 +276,9 @@ func extractByteSliceStringConv(pass *analysis.Pass, expr ast.Expr) (ast.Expr, b

// The argument must be []byte (or []uint8).
arg := call.Args[0]
if !isByteSlice(pass, arg) {
if !astutil.IsByteSlice(pass, arg) {
return nil, false
}

return arg, true
}

// isByteSlice reports whether expr has underlying type []byte ([]uint8).
func isByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
sl, ok := t.Underlying().(*types.Slice)
if !ok {
return false
}
elem, ok := sl.Elem().(*types.Basic)
return ok && elem.Kind() == types.Byte
}
33 changes: 33 additions & 0 deletions pkg/linters/internal/astutil/astutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,39 @@ func QualifierShadowed(pkg *types.Package, pos token.Pos, name, importPath strin
return pkgName.Imported().Path() != importPath
}

// IsByteSlice reports whether expr has underlying type []byte ([]uint8).
func IsByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
sl, ok := t.Underlying().(*types.Slice)
if !ok {
return false
}
elem, ok := sl.Elem().(*types.Basic)
return ok && elem.Kind() == types.Byte
}

// IsByteSliceConversion reports whether conv is a []byte or []uint8 conversion expression.
func IsByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
if !ok || !funTypeInfo.IsType() {
return false
}
return IsByteSlice(pass, conv)
}

// IsStringType reports whether expr has underlying type string (or a named string type).
func IsStringType(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
basic, ok := t.Underlying().(*types.Basic)
return ok && basic.Kind() == types.String
}

// ConstIntValue returns the integer constant value of expr, if it is a
// constant integer.
func ConstIntValue(pass *analysis.Pass, expr ast.Expr) (int64, bool) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] The three new exported helpers have no tests — a gap that matters now they are shared API surface.

💡 Suggested test skeleton
// astutil_test.go
func TestIsByteSlice(t *testing.T) {
    // arrange: build a *analysis.Pass with TypesInfo pointing at a []byte expr
    // act:     call IsByteSlice(pass, expr)
    // assert:  returns true for []byte / []uint8, false for string / int
}

func TestIsByteSliceConversion(t *testing.T) {
    // assert: true only when the call function is a []byte type expression
    // assert: false when it is a plain function call
}

func TestIsStringType(t *testing.T) {
    // assert: true for predeclared string and named-string types
    // assert: false for []byte / int
}

Because these predicates are now called from three linters, a single misunderstanding about what counts as "underlying type" will silently affect all three.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added focused unit coverage for IsByteSlice, IsByteSliceConversion, and IsStringType in pkg/linters/internal/astutil/astutil_test.go (including positive/negative cases and named string type handling) in 77555eb.

Expand Down
115 changes: 115 additions & 0 deletions pkg/linters/internal/astutil/astutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,37 @@ package astutil
import (
"go/ast"
"go/constant"
"go/importer"
"go/parser"
"go/token"
"go/types"
"testing"

"golang.org/x/tools/go/analysis"
)

func typecheckSnippet(t *testing.T, src string) (*analysis.Pass, *ast.File) {
t.Helper()

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "snippet.go", src, parser.SkipObjectResolution)
if err != nil {
t.Fatalf("ParseFile() error = %v", err)
}

info := &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
}
cfg := types.Config{Importer: importer.Default()}
if _, err := cfg.Check("example.com/p", fset, []*ast.File{file}, info); err != nil {
t.Fatalf("type checking failed: %v", err)
}

return &analysis.Pass{TypesInfo: info}, file
}

func TestRhsExprForIndex(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -424,3 +448,94 @@ func TestBuildContainsFix(t *testing.T) {
t.Fatalf("negated Message = %q, want %q", fixes[0].Message, "negated message")
}
}

func TestByteStringTypeHelpers(t *testing.T) {
t.Parallel()

const src = `package p

func g(s string) []byte { return nil }

func f(s string, b []byte) {
type myString string
var ms myString

_ = []byte(s)
_ = g(s)
_ = b
_ = s
_ = ms
}
`

pass, file := typecheckSnippet(t, src)

var fn *ast.FuncDecl
for _, decl := range file.Decls {
decl, ok := decl.(*ast.FuncDecl)
if ok && decl.Name.Name == "f" {
fn = decl
break
}
}
if fn == nil || fn.Body == nil {
t.Fatal("failed to find function f in test snippet")
}

var rhsExprs []ast.Expr
for _, stmt := range fn.Body.List {
assign, ok := stmt.(*ast.AssignStmt)
if !ok || len(assign.Rhs) != 1 {
continue
}
rhsExprs = append(rhsExprs, assign.Rhs[0])
}
if len(rhsExprs) != 5 {
t.Fatalf("found %d assignment expressions in f, want 5", len(rhsExprs))
}

byteConv, ok := rhsExprs[0].(*ast.CallExpr)
if !ok {
t.Fatalf("rhs[0] type = %T, want *ast.CallExpr", rhsExprs[0])
}
gCall, ok := rhsExprs[1].(*ast.CallExpr)
if !ok {
t.Fatalf("rhs[1] type = %T, want *ast.CallExpr", rhsExprs[1])
}
bIdent, ok := rhsExprs[2].(*ast.Ident)
if !ok {
t.Fatalf("rhs[2] type = %T, want *ast.Ident", rhsExprs[2])
}
sIdent, ok := rhsExprs[3].(*ast.Ident)
if !ok {
t.Fatalf("rhs[3] type = %T, want *ast.Ident", rhsExprs[3])
}
msIdent, ok := rhsExprs[4].(*ast.Ident)
if !ok {
t.Fatalf("rhs[4] type = %T, want *ast.Ident", rhsExprs[4])
}

if !IsByteSlice(pass, bIdent) {
t.Fatal("IsByteSlice(b) = false, want true")
}
if IsByteSlice(pass, sIdent) {
t.Fatal("IsByteSlice(s) = true, want false")
}

if !IsByteSliceConversion(pass, byteConv) {
t.Fatal("IsByteSliceConversion([]byte(s)) = false, want true")
}
if IsByteSliceConversion(pass, gCall) {
t.Fatal("IsByteSliceConversion(g(s)) = true, want false")
}

if !IsStringType(pass, sIdent) {
t.Fatal("IsStringType(s) = false, want true")
}
if !IsStringType(pass, msIdent) {
t.Fatal("IsStringType(ms) = false, want true for named string")
}
if IsStringType(pass, bIdent) {
t.Fatal("IsStringType(b) = true, want false")
}
}
37 changes: 2 additions & 35 deletions pkg/linters/writebytestring/writebytestring.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,11 @@ func run(pass *analysis.Pass) (any, error) {
if !ok {
return
}
if !isByteSliceConversion(pass, conv) {
if !astutil.IsByteSliceConversion(pass, conv) {
return
}
strArg := conv.Args[0]
if !isStringType(pass, strArg) {
if !astutil.IsStringType(pass, strArg) {
return
}

Expand Down Expand Up @@ -146,39 +146,6 @@ func run(pass *analysis.Pass) (any, error) {
return nil, nil
}

// isByteSliceConversion reports whether conv is a []byte or []uint8 conversion expression.
func isByteSliceConversion(pass *analysis.Pass, conv *ast.CallExpr) bool {
funTypeInfo, ok := pass.TypesInfo.Types[conv.Fun]
if !ok || !funTypeInfo.IsType() {
return false
}
return isByteSlice(pass, conv)
}

// isByteSlice reports whether expr has type []byte ([]uint8).
func isByteSlice(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
sl, ok := t.Underlying().(*types.Slice)
if !ok {
return false
}
elem, ok := sl.Elem().(*types.Basic)
return ok && elem.Kind() == types.Byte
}

// isStringType reports whether expr has type string (or named string type).
func isStringType(pass *analysis.Pass, expr ast.Expr) bool {
t := pass.TypesInfo.TypeOf(expr)
if t == nil {
return false
}
basic, ok := t.Underlying().(*types.Basic)
return ok && basic.Kind() == types.String
}

// isExactString reports whether t is the predeclared string type, not a named
// type whose underlying type is string. io.WriteString(w Writer, s string)
// requires a predeclared string; named string types need an explicit string(...)
Expand Down