feat(lint): eleven new rules over directives, data and addressing

Assisted-by: GLM 5.3
This commit is contained in:
2026-10-02 00:40:54 +02:00
parent f405cea5bc
commit 69dcbec8ef
12 changed files with 1431 additions and 19 deletions
+324
View File
@@ -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)
}
}