feat(fmt): go-fmt-style recursive formatting and canonical blank-line layout
Assisted-by: Qwen 3.8 Max Preview
This commit is contained in:
+64
-10
@@ -11,7 +11,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
||||
@@ -26,7 +28,7 @@ import (
|
||||
|
||||
// version is the release version, stamped at build time via
|
||||
// -ldflags "-X main.version=…" (defaulting to the current release).
|
||||
var version = "0.6.0"
|
||||
var version = "0.7.0"
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
@@ -63,7 +65,8 @@ func usage(w io.Writer) {
|
||||
Usage:
|
||||
gasm tokens <file> print the lexical token stream
|
||||
gasm parse <file> parse and report syntax errors
|
||||
gasm fmt [-w] <file...> canonicalise formatting (-w writes in place)
|
||||
gasm fmt [-w] [path...] canonicalise formatting (no path or a directory:
|
||||
reformat every .s below it in place, like go fmt)
|
||||
gasm lint <file...> run static checks
|
||||
gasm asm [-o out.bin] <file> assemble to machine code (amd64, Phase 2)
|
||||
gasm lsp run the language server over stdio
|
||||
@@ -130,15 +133,39 @@ func cmdParse(args []string) int {
|
||||
}
|
||||
|
||||
func cmdFmt(args []string) int {
|
||||
fs := flag.NewFlagSet("fmt", flag.ExitOnError)
|
||||
write := fs.Bool("w", false, "write result to the source file")
|
||||
fs.Parse(args)
|
||||
if fs.NArg() == 0 {
|
||||
fmt.Fprintln(os.Stderr, "usage: gasm fmt [-w] <file...>")
|
||||
return 2
|
||||
fs_ := flag.NewFlagSet("fmt", flag.ExitOnError)
|
||||
write := fs_.Bool("w", false, "write result to the source file")
|
||||
fs_.Parse(args)
|
||||
// Like go fmt: with no arguments, or with a directory argument, every .s
|
||||
// file below the directory is formatted in place and the names of the
|
||||
// changed files are listed; explicit file arguments keep the -w / stdout
|
||||
// behaviour.
|
||||
paths := fs_.Args()
|
||||
dirMode := len(paths) == 0
|
||||
if dirMode {
|
||||
paths = []string{"."}
|
||||
}
|
||||
var files []string
|
||||
for _, p := range paths {
|
||||
info, err := os.Stat(p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
return 1
|
||||
}
|
||||
if info.IsDir() {
|
||||
dirMode = true
|
||||
found, err := asmFiles(p)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
return 1
|
||||
}
|
||||
files = append(files, found...)
|
||||
continue
|
||||
}
|
||||
files = append(files, p)
|
||||
}
|
||||
rc := 0
|
||||
for _, path := range fs.Args() {
|
||||
for _, path := range files {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
@@ -146,11 +173,15 @@ func cmdFmt(args []string) int {
|
||||
continue
|
||||
}
|
||||
out := format.Source(path, src)
|
||||
if *write {
|
||||
if dirMode || *write {
|
||||
if out != src {
|
||||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gasm:", err)
|
||||
rc = 1
|
||||
continue
|
||||
}
|
||||
if dirMode {
|
||||
fmt.Println(path)
|
||||
}
|
||||
}
|
||||
continue
|
||||
@@ -160,6 +191,29 @@ func cmdFmt(args []string) int {
|
||||
return rc
|
||||
}
|
||||
|
||||
// asmFiles collects the .s files below dir, skipping directories whose name
|
||||
// starts with "." or "_" — as the go tooling does, which keeps .git and
|
||||
// scratch or reference trees (e.g. _refs) untouched.
|
||||
func asmFiles(dir string) ([]string, error) {
|
||||
var out []string
|
||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
if path != dir && (strings.HasPrefix(d.Name(), ".") || strings.HasPrefix(d.Name(), "_")) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if strings.HasSuffix(d.Name(), ".s") {
|
||||
out = append(out, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
func cmdLint(args []string) int {
|
||||
fs := flag.NewFlagSet("lint", flag.ExitOnError)
|
||||
disable := fs.String("disable", "", "comma-separated rule codes to disable")
|
||||
|
||||
+52
-3
@@ -52,6 +52,54 @@ func capture(fn func() int) (stdout, stderr string, code int) {
|
||||
return string(ob), string(eb), code
|
||||
}
|
||||
|
||||
// TestCmdFmtRecursive checks the go-fmt-style directory mode: with no
|
||||
// arguments every .s file below the working directory is formatted in place
|
||||
// ("." and "_" directories skipped), changed files are listed, and a second
|
||||
// run is a no-op.
|
||||
func TestCmdFmtRecursive(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Chdir(tmp)
|
||||
unformatted := []byte("TEXT ·f(SB),NOSPLIT,$0\nRET\n")
|
||||
write := func(path string) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, unformatted, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("a_amd64.s")
|
||||
write(filepath.Join("sub", "b_amd64.s"))
|
||||
write(filepath.Join("_refs", "c_amd64.s"))
|
||||
write(filepath.Join(".git", "d_amd64.s"))
|
||||
|
||||
out, errOut, code := capture(func() int { return cmdFmt(nil) })
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d (%s)", code, errOut)
|
||||
}
|
||||
if out != "a_amd64.s\n"+filepath.Join("sub", "b_amd64.s")+"\n" {
|
||||
t.Errorf("listed files unexpected:\n%s", out)
|
||||
}
|
||||
for _, p := range []string{"a_amd64.s", filepath.Join("sub", "b_amd64.s")} {
|
||||
b, _ := os.ReadFile(p)
|
||||
if !strings.Contains(string(b), "\tRET") {
|
||||
t.Errorf("%s not formatted in place:\n%s", p, b)
|
||||
}
|
||||
}
|
||||
for _, p := range []string{filepath.Join("_refs", "c_amd64.s"), filepath.Join(".git", "d_amd64.s")} {
|
||||
b, _ := os.ReadFile(p)
|
||||
if string(b) != string(unformatted) {
|
||||
t.Errorf("%s must not be touched:\n%s", p, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: everything is canonical, nothing is listed.
|
||||
out, _, code = capture(func() int { return cmdFmt(nil) })
|
||||
if code != 0 || out != "" {
|
||||
t.Errorf("second pass: code=%d out=%q, want a no-op", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCmdTokens(t *testing.T) {
|
||||
path := writeTemp(t, "f_amd64.s", clean)
|
||||
out, _, code := capture(func() int { return cmdTokens([]string{path}) })
|
||||
@@ -158,9 +206,10 @@ func TestUsage(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCmdArgErrors(t *testing.T) {
|
||||
// Missing file arguments produce a usage error (code 2).
|
||||
if _, _, code := capture(func() int { return cmdFmt(nil) }); code != 2 {
|
||||
t.Errorf("cmdFmt() code = %d, want 2", code)
|
||||
// A missing path is an error (code 1); cmdFmt with no arguments is the
|
||||
// recursive mode now, covered by TestCmdFmtRecursive.
|
||||
if _, _, code := capture(func() int { return cmdFmt([]string{"no/such/path"}) }); code != 1 {
|
||||
t.Errorf("cmdFmt(missing path) code = %d, want 1", code)
|
||||
}
|
||||
if _, _, code := capture(func() int { return cmdLint(nil) }); code != 2 {
|
||||
t.Errorf("cmdLint() code = %d, want 2", code)
|
||||
|
||||
Reference in New Issue
Block a user