feat(lint): eleven new rules over directives, data and addressing
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user