feat(fmt): go-fmt-style recursive formatting and canonical blank-line layout
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
+82
-13
@@ -27,14 +27,6 @@ func Source(path, src string) string {
|
||||
mnemLen int
|
||||
funcID int
|
||||
}
|
||||
const (
|
||||
kBlank = iota
|
||||
kComment
|
||||
kPreproc
|
||||
kDirective
|
||||
kLabel
|
||||
kInstr
|
||||
)
|
||||
|
||||
infos := make([]info, len(lines))
|
||||
funcID := -1
|
||||
@@ -70,8 +62,8 @@ func Source(path, src string) string {
|
||||
infos[i] = inf
|
||||
}
|
||||
|
||||
// Second pass: render.
|
||||
var b strings.Builder
|
||||
// Second pass: render each line.
|
||||
outs := make([]outLine, 0, len(lines))
|
||||
inBody := false
|
||||
for i, line := range lines {
|
||||
inf := infos[i]
|
||||
@@ -106,10 +98,87 @@ func Source(path, src string) string {
|
||||
inBody = false
|
||||
}
|
||||
}
|
||||
b.WriteString(strings.TrimRight(out, " \t"))
|
||||
b.WriteByte('\n')
|
||||
outs = append(outs, outLine{kind: inf.kind, text: strings.TrimRight(out, " \t")})
|
||||
}
|
||||
return b.String()
|
||||
return normalizeSpacing(outs)
|
||||
}
|
||||
|
||||
// Line classification, shared by the formatting passes.
|
||||
const (
|
||||
kBlank = iota
|
||||
kComment
|
||||
kPreproc
|
||||
kDirective
|
||||
kLabel
|
||||
kInstr
|
||||
)
|
||||
|
||||
// outLine is one rendered line together with its classification.
|
||||
type outLine struct {
|
||||
kind int
|
||||
text string
|
||||
}
|
||||
|
||||
// normalizeSpacing enforces the canonical blank-line layout: runs of blank
|
||||
// lines collapse to one, and a new block — a label, or a TEXT or GLOBL
|
||||
// directive — is preceded by exactly one blank line. Comments immediately
|
||||
// above a block belong to it, so the blank line is inserted before them. No
|
||||
// blank line is forced at the top of the file, right after a TEXT (the
|
||||
// function's first label), or between stacked labels that share an address.
|
||||
func normalizeSpacing(outs []outLine) string {
|
||||
blockStart := func(ol outLine) bool {
|
||||
switch ol.kind {
|
||||
case kLabel:
|
||||
return true
|
||||
case kDirective:
|
||||
// TEXT and GLOBL open a block; DATA continues a GLOBL block.
|
||||
return strings.HasPrefix(ol.text, "TEXT") || strings.HasPrefix(ol.text, "GLOBL")
|
||||
}
|
||||
return false
|
||||
}
|
||||
insert := make([]bool, len(outs))
|
||||
for i, ol := range outs {
|
||||
if !blockStart(ol) {
|
||||
continue
|
||||
}
|
||||
j := i
|
||||
for j > 0 && outs[j-1].kind == kComment {
|
||||
j--
|
||||
}
|
||||
if j == 0 {
|
||||
continue // top of file
|
||||
}
|
||||
switch prev := outs[j-1]; {
|
||||
case prev.kind == kBlank, prev.kind == kLabel:
|
||||
continue // already separated, or stacked labels
|
||||
case prev.kind == kDirective && strings.HasPrefix(prev.text, "TEXT"):
|
||||
continue // the function's first label
|
||||
}
|
||||
insert[j] = true
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
prevBlank := true // also suppresses leading blanks
|
||||
for i, ol := range outs {
|
||||
if insert[i] && !prevBlank {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if ol.kind == kBlank {
|
||||
if !prevBlank {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
prevBlank = true
|
||||
continue
|
||||
}
|
||||
b.WriteString(ol.text)
|
||||
b.WriteByte('\n')
|
||||
prevBlank = false
|
||||
}
|
||||
out := strings.TrimRight(b.String(), "\n")
|
||||
if out == "" {
|
||||
return ""
|
||||
}
|
||||
return out + "\n"
|
||||
}
|
||||
|
||||
// renderInstr renders an instruction line: a tab, the mnemonic padded to the
|
||||
|
||||
@@ -77,6 +77,64 @@ func TestDocCommentIndent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBlankLines checks the blank-line canonicalisation: exactly one blank
|
||||
// line before a new block (a label, or TEXT/GLOBL), runs of blanks collapsed
|
||||
// to one, and no blank forced after TEXT, between stacked labels, or at the
|
||||
// top of the file. Leading comments belong to the block they precede.
|
||||
func TestBlankLines(t *testing.T) {
|
||||
in := "#include \"textflag.h\"\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||
"first:\n" + // first label: no blank after TEXT
|
||||
"XORQ AX, AX\n" +
|
||||
"JMP next\n" + // unlabeled glue: fmt inserts a blank before next:
|
||||
"next:\n" +
|
||||
"stacked:\n" + // stacked labels share an address: no blank between
|
||||
"INCQ AX\n" +
|
||||
"\n" +
|
||||
"\n" + // two blanks collapse to one
|
||||
"// separated block\n" + // comment belongs to the label below
|
||||
"later:\n" +
|
||||
"RET\n" +
|
||||
"// func g()\n" + // doc comment: blank goes before it
|
||||
"TEXT ·g(SB), NOSPLIT, $0\n" +
|
||||
"RET\n" +
|
||||
"GLOBL ·mask(SB), RODATA, $8\n" + // blank before GLOBL…
|
||||
"DATA ·mask+0(SB)/4, $1\n" + // …but not before DATA
|
||||
"\n" +
|
||||
"\n" +
|
||||
"\n" // trailing blanks dropped
|
||||
|
||||
want := "#include \"textflag.h\"\n" +
|
||||
"\n" +
|
||||
"TEXT ·f(SB), NOSPLIT, $0\n" +
|
||||
"first:\n" +
|
||||
"\tXORQ AX, AX\n" +
|
||||
"\tJMP next\n" +
|
||||
"\n" +
|
||||
"next:\n" +
|
||||
"stacked:\n" +
|
||||
"\tINCQ AX\n" +
|
||||
"\n" +
|
||||
"\t// separated block\n" + // body comment before a label stays indented
|
||||
"later:\n" +
|
||||
"\tRET\n" +
|
||||
"\n" +
|
||||
"// func g()\n" +
|
||||
"TEXT ·g(SB), NOSPLIT, $0\n" +
|
||||
"\tRET\n" +
|
||||
"\n" +
|
||||
"GLOBL ·mask(SB), RODATA, $8\n" +
|
||||
"DATA ·mask+0(SB)/4, $1\n"
|
||||
|
||||
got := Source("b_amd64.s", in)
|
||||
if got != want {
|
||||
t.Fatalf("formatting mismatch:\n--- got ---\n%q\n--- want ---\n%q", got, want)
|
||||
}
|
||||
if again := Source("b_amd64.s", got); again != got {
|
||||
t.Fatalf("not idempotent:\n%q", again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperandSpacing(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"4(SI)": "4(SI)",
|
||||
|
||||
Reference in New Issue
Block a user