325 lines
8.2 KiB
Go
325 lines
8.2 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package lint
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/arch"
|
|
"sourcedock.dev/petrbalvin/gasm-sdk/parser"
|
|
)
|
|
|
|
func TestNoFrameFrameSize(t *testing.T) {
|
|
// NOFRAME against a positive frame: the flag states an arrangement the
|
|
// directive does not describe.
|
|
diags := lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT|NOFRAME, $8
|
|
MOVQ $1, AX
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeNoFrameFrameSize] != 1 {
|
|
t.Fatalf("want one noframe-frame-size, got %+v", diags)
|
|
}
|
|
|
|
// The valid spelling: NOFRAME with a zero frame.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT|NOFRAME, $0
|
|
MOVQ $1, AX
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeNoFrameFrameSize] != 0 {
|
|
t.Fatalf("NOFRAME with $0 must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A negative frame with NOFRAME is the arm64 BSD syscall-stub pattern
|
|
// (GOROOT's sys_netbsd_arm64.s runtime·open), not a defect.
|
|
diags = lintSrcArch(t, "f_arm64.s", `
|
|
#include "textflag.h"
|
|
TEXT ·open(SB),NOSPLIT|NOFRAME,$-8
|
|
MOVD name+0(FP), R0
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeNoFrameFrameSize] != 0 {
|
|
t.Fatalf("NOFRAME with a negative frame must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A frame without the flag is the ordinary spelling.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $8
|
|
MOVQ $1, AX
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeNoFrameFrameSize] != 0 {
|
|
t.Fatalf("a frame without NOFRAME must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// -disable silences the rule.
|
|
f, _ := parser.Parse("t_amd64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT|NOFRAME, $8\n\tRET\n")
|
|
diags = File(f, Config{Arch: arch.AMD64, Disable: map[string]bool{CodeNoFrameFrameSize: true}})
|
|
if codes(diags)[CodeNoFrameFrameSize] != 0 {
|
|
t.Fatalf("disabled rule must stay silent: %+v", diags)
|
|
}
|
|
}
|
|
|
|
func TestMissingArgSize(t *testing.T) {
|
|
// Without NOSPLIT and without an argument area, the object records
|
|
// ArgsSizeUnknown even though the signature states the size.
|
|
diags := lintSrc(t, `
|
|
// func f(a int) int
|
|
TEXT ·f(SB), $0
|
|
MOVQ a+0(FP), AX
|
|
MOVQ AX, ret+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingArgSize] != 1 {
|
|
t.Fatalf("want one missing-argsize, got %+v", diags)
|
|
}
|
|
|
|
// The declared area matching the signature is clean.
|
|
diags = lintSrc(t, `
|
|
// func f(a int) int
|
|
TEXT ·f(SB), $0-16
|
|
MOVQ a+0(FP), AX
|
|
MOVQ AX, ret+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingArgSize] != 0 {
|
|
t.Fatalf("a declared argument area must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// NOSPLIT functions omit the area throughout GOROOT; not flagged.
|
|
diags = lintSrc(t, `
|
|
// func f(a int) int
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
MOVQ a+0(FP), AX
|
|
MOVQ AX, ret+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingArgSize] != 0 {
|
|
t.Fatalf("NOSPLIT without an area must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// No signature, no opinion.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), $0
|
|
MOVQ $1, AX
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingArgSize] != 0 {
|
|
t.Fatalf("signature-less function must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A signature whose argument area is empty implies nothing to declare.
|
|
diags = lintSrc(t, `
|
|
// func f()
|
|
TEXT ·f(SB), $0
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingArgSize] != 0 {
|
|
t.Fatalf("zero-size signature must not be flagged: %+v", diags)
|
|
}
|
|
}
|
|
|
|
// TestMissingRetFallthroughEnd pins the second shape of missing-ret: the
|
|
// body knows how to return, but its tail can run off the end, and the
|
|
// toolchain appends nothing, so execution continues into the next TEXT.
|
|
func TestMissingRetFallthroughEnd(t *testing.T) {
|
|
diags := lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
CMPQ AX, $0
|
|
JNE done
|
|
RET
|
|
done:
|
|
MOVQ $2, BX
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 1 {
|
|
t.Fatalf("want one missing-ret for the fall-through tail, got %+v", diags)
|
|
}
|
|
|
|
// Ending in RET is clean, trailing labels included.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
CMPQ AX, $0
|
|
JNE done
|
|
MOVQ $1, AX
|
|
done:
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("RET last must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// Ending in an unconditional branch is a tail call, not a defect.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
MOVQ $1, AX
|
|
JMP ·g(SB)
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("a tail call must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A conditional branch last, with a RET earlier, still falls through.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
JE done
|
|
RET
|
|
done:
|
|
CMPQ AX, $0
|
|
JNE done
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 1 {
|
|
t.Fatalf("conditional-branch tail must be flagged: %+v", diags)
|
|
}
|
|
|
|
// GC annotations after the terminator carry no control flow.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
RET
|
|
FUNCDATA $1, ·sm(SB)
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("FUNCDATA after RET must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// Trapping tails: the runtime's abort paths end in INT (amd64) or BRK
|
|
// (arm64); they never return, so no RET is needed.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·abort(SB), NOSPLIT, $0
|
|
INT $3
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("INT tail must not be flagged: %+v", diags)
|
|
}
|
|
diags = lintSrcArch(t, "f_arm64.s", `
|
|
#include "textflag.h"
|
|
TEXT ·abort(SB), NOSPLIT, $0
|
|
BRK
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("BRK tail must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A file that includes non-textflag headers may hide the terminator in
|
|
// a macro, so the heuristic stays silent there.
|
|
diags = lintSrcArch(t, "f_amd64.s", `
|
|
#include "go_tls.h"
|
|
TEXT ·f(SB), NOSPLIT, $0
|
|
MOVQ $1, AX
|
|
`)
|
|
if codes(diags)[CodeMissingRet] != 0 {
|
|
t.Fatalf("macro-using files must not be flagged: %+v", diags)
|
|
}
|
|
}
|
|
|
|
func TestInvalidFlagPlacement(t *testing.T) {
|
|
// NOSPLIT is a TEXT flag; on GLOBL it does nothing.
|
|
diags := lintSrc(t, `
|
|
#include "textflag.h"
|
|
GLOBL ·tab(SB), NOSPLIT, $8
|
|
`)
|
|
if codes(diags)[CodeInvalidFlag] != 1 {
|
|
t.Fatalf("want one invalid-textflag for the misplaced NOSPLIT, got %+v", diags)
|
|
}
|
|
|
|
// RODATA belongs to data declarations, not to TEXT.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), RODATA, $0
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeInvalidFlag] != 1 {
|
|
t.Fatalf("want one invalid-textflag for RODATA on TEXT, got %+v", diags)
|
|
}
|
|
|
|
// Correct placements stay silent, on both directives.
|
|
diags = lintSrc(t, `
|
|
#include "textflag.h"
|
|
TEXT ·f(SB), NOSPLIT|DUPOK, $0
|
|
RET
|
|
GLOBL ·tab(SB), RODATA|NOPTR, $8
|
|
DATA ·tab+0(SB)/8, $0
|
|
`)
|
|
if codes(diags)[CodeInvalidFlag] != 0 {
|
|
t.Fatalf("correct flag placements must not be flagged: %+v", diags)
|
|
}
|
|
}
|
|
|
|
func TestUnnamedResult(t *testing.T) {
|
|
// The signature names its results, the body uses the generic spelling.
|
|
diags := lintSrc(t, `
|
|
// func kevent(kq int, ch unsafe.Pointer) (n int, err error)
|
|
TEXT ·kevent(SB), NOSPLIT, $0-40
|
|
MOVL kq+0(FP), AX
|
|
MOVQ ch+8(FP), BX
|
|
MOVL AX, ret+16(FP)
|
|
MOVL $0, err+24(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeUnnamedResult] != 1 {
|
|
t.Fatalf("want one unnamed-result hint, got %+v", diags)
|
|
}
|
|
|
|
// Referencing the result by its declared name is the clean spelling.
|
|
diags = lintSrc(t, `
|
|
// func kevent(kq int, ch unsafe.Pointer) (n int, err error)
|
|
TEXT ·kevent(SB), NOSPLIT, $0-40
|
|
MOVL kq+0(FP), AX
|
|
MOVQ ch+8(FP), BX
|
|
MOVL AX, n+16(FP)
|
|
MOVL $0, err+24(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeUnnamedResult] != 0 {
|
|
t.Fatalf("named result reference must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// An unnamed result is exactly what the ret spelling documents.
|
|
diags = lintSrc(t, `
|
|
// func f(a int) int
|
|
TEXT ·f(SB), NOSPLIT, $0-16
|
|
MOVQ a+0(FP), AX
|
|
MOVQ AX, ret+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeUnnamedResult] != 0 {
|
|
t.Fatalf("unnamed result via ret must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A result the signature itself names ret coincides with the generic
|
|
// spelling; there is nothing better to suggest.
|
|
diags = lintSrc(t, `
|
|
// func f(a int) (ret int)
|
|
TEXT ·f(SB), NOSPLIT, $0-16
|
|
MOVQ a+0(FP), AX
|
|
MOVQ AX, ret+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeUnnamedResult] != 0 {
|
|
t.Fatalf("a result literally named ret must not be flagged: %+v", diags)
|
|
}
|
|
|
|
// A parameter named ret turns the spelling into a parameter reference.
|
|
diags = lintSrc(t, `
|
|
// func f(ret int) (n int)
|
|
TEXT ·f(SB), NOSPLIT, $0-16
|
|
MOVQ ret+0(FP), AX
|
|
MOVQ AX, n+8(FP)
|
|
RET
|
|
`)
|
|
if codes(diags)[CodeUnnamedResult] != 0 {
|
|
t.Fatalf("a parameter named ret must not be flagged: %+v", diags)
|
|
}
|
|
}
|