147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package lint
|
|
|
|
import "testing"
|
|
|
|
// TestABIExpectedArgSize checks the Go ABI0 argument-area size computation
|
|
// against hand-verified signatures (the same layouts the go-flac kernels use).
|
|
func TestABIExpectedArgSize(t *testing.T) {
|
|
cases := []struct {
|
|
sig string
|
|
want int64
|
|
}{
|
|
{"func f()", 0},
|
|
{"func f(a int, b int)", 16},
|
|
{"func f(a int32)", 4}, // no results: no word-boundary padding
|
|
{"func f(a int32) (r int32)", 12}, // results begin on a word boundary: 4 -> 8, +4
|
|
{"func f(a int) int", 16},
|
|
{"func f(s []int32, p *[32]uint16) (x uint64, ok bool)", 41},
|
|
{"func f(left, right []int32, sums *[4]uint64)", 56},
|
|
{"func f(src []byte, dst []int32)", 48},
|
|
{"func f(a bool, b int64)", 16}, // bool at 0, int64 aligned to 8
|
|
{"func f(x struct{ a int32; b int64 })", 16},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := abiExpectedArgSize(c.sig)
|
|
if !ok {
|
|
t.Errorf("%s: could not compute size", c.sig)
|
|
continue
|
|
}
|
|
if got != c.want {
|
|
t.Errorf("%s: size = %d, want %d", c.sig, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestABIUnknownTypeSkipped(t *testing.T) {
|
|
// A bare named type of unknown size must abort the check rather than guess.
|
|
// (A *pointer* to a named type is still 8 bytes and is fine.)
|
|
if _, ok := abiExpectedArgSize("func f(s Stream)"); ok {
|
|
t.Error("bare named type should make the size undecidable")
|
|
}
|
|
if _, ok := abiExpectedArgSize("func f(s *Stream)"); !ok {
|
|
t.Error("pointer to a named type is decidable (8 bytes)")
|
|
}
|
|
}
|
|
|
|
// TestABIArgSizeRule checks the lint rule end to end.
|
|
func TestABIArgSizeRule(t *testing.T) {
|
|
// Matching: the declared arg size agrees with the signature.
|
|
clean := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"// func f(a int, b int)\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0-16\n"+
|
|
"\tMOVQ a+0(FP), AX\n"+
|
|
"\tRET\n")
|
|
if codes(clean)[CodeABIArgSize] != 0 {
|
|
t.Fatalf("matching arg size should not warn: %+v", clean)
|
|
}
|
|
|
|
// Mismatching: declared 8, signature implies 16.
|
|
bad := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"// func f(a int, b int)\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0-8\n"+
|
|
"\tMOVQ a+0(FP), AX\n"+
|
|
"\tRET\n")
|
|
if codes(bad)[CodeABIArgSize] != 1 {
|
|
t.Fatalf("mismatching arg size should warn once: %+v", bad)
|
|
}
|
|
}
|
|
|
|
// TestABIArgSizeSkipsRegisterABI verifies the check does not fire for functions
|
|
// that declare a zero arg area (register ABI) or never touch FP.
|
|
func TestABIArgSizeSkipsRegisterABI(t *testing.T) {
|
|
diags := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"// func f(a int, b int)\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0-0\n"+
|
|
"\tMOVQ AX, BX\n"+
|
|
"\tRET\n")
|
|
if codes(diags)[CodeABIArgSize] != 0 {
|
|
t.Fatalf("register-ABI function must not be checked: %+v", diags)
|
|
}
|
|
}
|
|
|
|
// TestABI0ArgsSkipsABIInternal verifies the frame-read check does not fire for
|
|
// a TEXT declared <ABIInternal>: runtime·memmove<ABIInternal> and friends read
|
|
// their arguments from the register file by declaration, which is the correct
|
|
// spelling there, not the register-args port bug the rule hunts.
|
|
func TestABI0ArgsSkipsABIInternal(t *testing.T) {
|
|
diags := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"// func memmove(to, from unsafe.Pointer, n uintptr)\n"+
|
|
"TEXT ·memmove<ABIInternal>(SB), NOSPLIT, $0-24\n"+
|
|
"\tMOVQ AX, DI\n"+
|
|
"\tMOVQ BX, SI\n"+
|
|
"\tMOVQ CX, BX\n"+
|
|
"\tRET\n")
|
|
if codes(diags)[CodeABI0RegisterArgs] != 0 {
|
|
t.Fatalf("ABIInternal TEXT must not be checked against the FP frame: %+v", diags)
|
|
}
|
|
}
|
|
|
|
// TestUnreachableCode exercises the dead-code detection and its guard rails.
|
|
func TestUnreachableCode(t *testing.T) {
|
|
// Code after a RET is unreachable.
|
|
dead := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
|
"\tRET\n"+
|
|
"\tMOVQ AX, BX\n")
|
|
if codes(dead)[CodeUnreachable] != 1 {
|
|
t.Fatalf("code after RET should be unreachable: %+v", dead)
|
|
}
|
|
|
|
// A label after the RET makes the following code reachable again.
|
|
live := lintSrc(t, "#include \"textflag.h\"\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
|
"\tRET\n"+
|
|
"again:\n"+
|
|
"\tJMP again\n")
|
|
if codes(live)[CodeUnreachable] != 0 {
|
|
t.Fatalf("code after a label is reachable: %+v", live)
|
|
}
|
|
}
|
|
|
|
// TestUnreachableGuards verifies the analysis is suppressed where reachability
|
|
// cannot be determined statically.
|
|
func TestUnreachableGuards(t *testing.T) {
|
|
// A PC-relative jump defeats the analysis for the whole function.
|
|
pcrel := lintSrcArch(t, "f_amd64.s", "#include \"textflag.h\"\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
|
"\tJCC 2(PC)\n"+
|
|
"\tRET\n"+
|
|
"\tMOVQ AX, BX\n")
|
|
if codes(pcrel)[CodeUnreachable] != 0 {
|
|
t.Fatalf("PC-relative functions must be skipped: %+v", pcrel)
|
|
}
|
|
|
|
// A register-indirect branch (riscv JALR) defeats the analysis too.
|
|
indirect := lintSrcArch(t, "f_riscv64.s", "#include \"textflag.h\"\n"+
|
|
"TEXT ·f(SB), NOSPLIT, $0\n"+
|
|
"\tJALR X1, X5\n"+
|
|
"\tRET\n"+
|
|
"\tMOV X1, X2\n")
|
|
if codes(indirect)[CodeUnreachable] != 0 {
|
|
t.Fatalf("indirect-branch functions must be skipped: %+v", indirect)
|
|
}
|
|
}
|