diff --git a/lint/lint.go b/lint/lint.go index 3e8fe44..5f3656a 100644 --- a/lint/lint.go +++ b/lint/lint.go @@ -74,8 +74,17 @@ const ( CodeNosplitFrame = "nosplit-frame" CodeRegisterClobber = "register-clobber" CodeFuncdata = "funcdata-pcdata" + CodeUnusedLabel = "unused-label" + CodeInvalidFlag = "invalid-textflag" ) +// knownTextFlags are the flags recognised by the Go assembler's textflag.h. +var knownTextFlags = map[string]bool{ + "NOSPLIT": true, "DUPOK": true, "RODATA": true, "NOPROF": true, + "WRIT": true, "TLSBSS": true, "NOFRAME": true, "REFLECTED": true, + "TOPFRAME": true, "ABIWRAPPER": true, +} + // pseudoOps are assembler pseudo-operations that are valid instruction-position // tokens but are not machine instructions and so absent from the arch tables. var pseudoOps = map[string]bool{ @@ -141,6 +150,38 @@ func File(f *ast.File, cfg Config) []Diagnostic { }) } + // Invalid TEXT/GLOBL flags. + if !cfg.Disable[CodeInvalidFlag] { + for _, d := range f.Decls { + var flags []string + var pos token.Position + switch dd := d.(type) { + case *ast.Text: + flags = dd.Flags + pos = dd.Keyword.Pos + case *ast.Globl: + flags = dd.Flags + if dd.Name != nil { + pos = dd.Name.Pos + } + } + for _, fl := range flags { + // Numeric flags (1, 8, 9) are legacy Go toolchain constants. + if fl >= "0" && fl <= "9" { + continue + } + if !knownTextFlags[fl] { + out = append(out, Diagnostic{ + Pos: pos, + Severity: Warning, + Code: CodeInvalidFlag, + Message: fmt.Sprintf("unknown TEXT/GLOBL flag %q", fl), + }) + } + } + } + } + sortDiagnostics(out) return out } @@ -282,6 +323,22 @@ func lintText(t *ast.Text, tab *arch.Table, archKnown bool, cfg Config, macros m } } + // Unused labels: defined but never referenced. Suppressed when macros + // or indirect branches are present (the reference may be invisible). + if doLabelChecks && !cfg.Disable[CodeUnusedLabel] && !hasIndirect { + for name, pos := range defined { + if _, ok := referenced[name]; !ok { + out = append(out, Diagnostic{ + Pos: pos, + End: token.Position{Line: pos.Line, Column: pos.Column + len(name)}, + Severity: Hint, + Code: CodeUnusedLabel, + Message: fmt.Sprintf("label %q is defined but never referenced", name), + }) + } + } + } + // Missing RET heuristic. Functions that invoke a macro are skipped: the // macro body (opaque to us) may supply the RET. if doLabelChecks && !cfg.Disable[CodeMissingRet] && instrCount > 0 && !hasRet && !lastTerminal && !hasMacro { diff --git a/lint/lint_test.go b/lint/lint_test.go index 3a14420..a816546 100644 --- a/lint/lint_test.go +++ b/lint/lint_test.go @@ -233,3 +233,52 @@ TEXT ·f(SB), NOSPLIT, $0 t.Fatalf("label rules should be suppressed in macro files: %+v", diags) } } + +func TestUnusedLabel(t *testing.T) { + diags := lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 +unused: + ADDQ $1, AX + RET +`) + if codes(diags)[CodeUnusedLabel] != 1 { + t.Fatalf("want one unused-label, got %+v", diags) + } +} + +func TestUsedLabelNotFlagged(t *testing.T) { + diags := lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT, $0 +loop: + ADDQ $1, AX + JMP loop + RET +`) + if codes(diags)[CodeUnusedLabel] != 0 { + t.Fatalf("used label must not be flagged: %+v", diags) + } +} + +func TestInvalidTextflag(t *testing.T) { + diags := lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT|BOGUS, $0 + RET +`) + if codes(diags)[CodeInvalidFlag] != 1 { + t.Fatalf("want one invalid-textflag, got %+v", diags) + } +} + +func TestValidTextflags(t *testing.T) { + diags := lintSrc(t, ` +#include "textflag.h" +TEXT ·f(SB), NOSPLIT|NOFRAME|DUPOK, $0 + RET +`) + if codes(diags)[CodeInvalidFlag] != 0 { + t.Fatalf("valid flags must not be flagged: %+v", diags) + } +}