Repository: goplus/gop
Pull request #2070 (2025-01-11 18:39:58)

gop/format/formatutil: RearrangeFuncs if necessary


Patch:
diff --git a/format/formatutil/format_gop.go b/format/formatutil/format_gop.go
new file mode 100644
index 00000000..7bcb69a1
--- /dev/null
+++ b/format/formatutil/format_gop.go
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package formatutil
+
+import (
+	"github.com/goplus/gop/format"
+	"github.com/goplus/gop/scanner"
+	"github.com/goplus/gop/token"
+)
+
+// RearrangeFuncs rearranges functions in src.
+func RearrangeFuncs(src []byte, filename ...string) ([]byte, error) {
+	var fname string
+	if filename != nil {
+		fname = filename[0]
+	}
+
+	fset := token.NewFileSet()
+	base := fset.Base()
+	f := fset.AddFile(fname, base, len(src))
+
+	var s scanner.Scanner
+	s.Init(f, src, nil, scanner.ScanComments)
+	stmts := splitStmts(&s)
+	first := firstNonDecl(stmts)
+	if first < 0 { // no non-decl stmt
+		return src, nil
+	}
+
+	ret := make([]byte, 0, len(src))
+	off := int(stmts[first].words[0].pos) - base
+	ret = append(ret, src[:off]...)
+
+	rest := stmts[first:]
+	for i, s := range rest {
+		if s.isFuncDecl() {
+			ret = append(ret, codeOf(src, base, i, rest)...)
+		}
+	}
+	for i, s := range rest {
+		if !s.isFuncDecl() {
+			ret = append(ret, codeOf(src, base, i, rest)...)
+		}
+	}
+	return ret, nil
+}
+
+func codeOf(src []byte, base, i int, rest []aStmt) []byte {
+	from := int(rest[i].words[0].pos) - base
+	to := 0
+	if i == len(rest)-1 {
+		to = len(src)
+	} else {
+		to = int(rest[i+1].words[0].pos) - base
+	}
+	return src[from:to]
+}
+
+func firstNonDecl(stmts []aStmt) int {
+	for i, stmt := range stmts {
+		if !stmt.isDecl() {
+			return i
+		}
+	}
+	return -1
+}
+
+func splitStmts(s *scanner.Scanner) (stmts []aStmt) {
+	var level int
+	var stmt aStmt
+	for {
+		pos, tok, _ := s.Scan()
+		if tok == token.EOF {
+			return
+		}
+		stmt.words = append(stmt.words, aWord{pos, tok})
+		switch tok {
+		case token.LBRACE:
+			level++
+		case token.RBRACE:
+			level--
+		}
+		if tok == token.SEMICOLON && level == 0 {
+			stmt.tok, stmt.at = tokOf(stmt.words)
+			stmts = append(stmts, stmt)
+			stmt = aStmt{}
+			continue
+		}
+	}
+}
+
+type aWord struct {
+	pos token.Pos
+	tok token.Token
+}
+
+type aStmt struct {
+	words []aWord
+	tok   token.Token
+	at    int
+}
+
+func (s aStmt) isFuncDecl() bool {
+	return s.tok == token.FUNC && isFuncDecl(s.words[s.at+1:])
+}
+
+func (s aStmt) isDecl() bool {
+	switch s.tok {
+	case token.CONST, token.TYPE, token.VAR:
+		return true
+	case token.FUNC:
+		return isFuncDecl(s.words[s.at+1:])
+	}
+	return false
+}
+
+func tokOf(words []aWord) (tok token.Token, at int) {
+	for i, w := range words {
+		if w.tok != token.COMMENT {
+			return w.tok, i
+		}
+	}
+	return words[0].tok, 0
+}
+
+func isFuncDecl(words []aWord) bool {
+	if startWith(words, token.LPAREN) { // func (
+		words = seekAfter(words[1:], token.RPAREN, token.LPAREN) // func (...)
+		if startWith(words, token.LBRACE) {                      // func (...) {
+			return false
+		}
+	}
+	return true
+}
+
+func seekAfter(words []aWord, tokR, tokL token.Token) []aWord {
+	level := 0
+	for i, w := range words {
+		switch w.tok {
+		case tokR:
+			if level == 0 {
+				return words[i+1:]
+			}
+			level--
+		case tokL:
+			level++
+		}
+	}
+	return nil
+}
+
+func startWith(words []aWord, tok token.Token) bool {
+	for _, w := range words {
+		switch w.tok {
+		case token.COMMENT:
+			continue
+		case tok:
+			return true
+		}
+		break
+	}
+	return false
+}
+
+// SourceEx formats src in canonical gopfmt style and returns the result
+// or an (I/O or syntax) error. src is expected to be a syntactically
+// correct Go+ source file, or a list of Go+ declarations or statements.
+//
+// If src is a partial source file, the leading and trailing space of src
+// is applied to the result (such that it has the same leading and trailing
+// space as src), and the result is indented by the same amount as the first
+// line of src containing code. Imports are not sorted for partial source files.
+func SourceEx(src []byte, class bool, filename ...string) (formatted []byte, err error) {
+	formatted, err = format.Source(src, class, filename...)
+	if err == nil {
+		return
+	}
+	src, err = RearrangeFuncs(src, filename...)
+	if err == nil {
+		formatted, err = format.Source(src, class, filename...)
+	}
+	return
+}
diff --git a/format/internal.go b/format/internal.go
index ded55ece..37584a85 100644
--- a/format/internal.go
+++ b/format/internal.go
@@ -32,7 +32,7 @@ import (
 
 // parse parses src, which was read from the named file,
 // as a Go source file, declaration, or statement list.
-func parse(fset *token.FileSet, filename string, src []byte, class, fragmentOk bool) (
+func parse(fset *token.FileSet, filename string, src []byte, class, _ /* fragmentOk */ bool) (
 	file *ast.File,
 	sourceAdj func(src []byte, indent int) []byte,
 	indentAdj int,


Test patch:
diff --git a/format/formatutil/_testdata/format/basic/in.data b/format/formatutil/_testdata/format/basic/in.data
new file mode 100644
index 00000000..7f3e2a0e
--- /dev/null
+++ b/format/formatutil/_testdata/format/basic/in.data
@@ -0,0 +1,20 @@
+goto "a"
+
+// this is a comment
+
+// x comment 1
+// x comment 2
+// x comment 3
+func x() {
+}
+
+// call
+func(a func()) {}(nil)
+
+/*
+ y comment
+*/
+func y() {
+}
+
+echo "Hi"
diff --git a/format/formatutil/_testdata/format/basic/out.expect b/format/formatutil/_testdata/format/basic/out.expect
new file mode 100644
index 00000000..b5b0bc36
--- /dev/null
+++ b/format/formatutil/_testdata/format/basic/out.expect
@@ -0,0 +1,20 @@
+// this is a comment
+
+// x comment 1
+// x comment 2
+// x comment 3
+func x() {
+}
+
+/*
+ y comment
+*/
+func y() {
+}
+
+goto "a"
+
+// call
+func(a func()) {}(nil)
+
+echo "Hi"
diff --git a/format/formatutil/_testdata/format/nondecl/in.data b/format/formatutil/_testdata/format/nondecl/in.data
new file mode 100644
index 00000000..2b7225e6
--- /dev/null
+++ b/format/formatutil/_testdata/format/nondecl/in.data
@@ -0,0 +1,1 @@
+const a = 0
diff --git a/format/formatutil/_testdata/format/nondecl/out.expect b/format/formatutil/_testdata/format/nondecl/out.expect
new file mode 100644
index 00000000..2b7225e6
--- /dev/null
+++ b/format/formatutil/_testdata/format/nondecl/out.expect
@@ -0,0 +1,1 @@
+const a = 0
diff --git a/format/formatutil/_testdata/rearrange/noeol/in.data b/format/formatutil/_testdata/rearrange/noeol/in.data
new file mode 100644
index 00000000..f6c8d2be
--- /dev/null
+++ b/format/formatutil/_testdata/rearrange/noeol/in.data
@@ -0,0 +1,1 @@
+var a int
\ No newline at end of file
diff --git a/format/formatutil/_testdata/rearrange/noeol/out.expect b/format/formatutil/_testdata/rearrange/noeol/out.expect
new file mode 100644
index 00000000..f6c8d2be
--- /dev/null
+++ b/format/formatutil/_testdata/rearrange/noeol/out.expect
@@ -0,0 +1,1 @@
+var a int
\ No newline at end of file
diff --git a/format/formatutil/_testdata/rearrange/nondecl/in.data b/format/formatutil/_testdata/rearrange/nondecl/in.data
new file mode 100644
index 00000000..a333d186
--- /dev/null
+++ b/format/formatutil/_testdata/rearrange/nondecl/in.data
@@ -0,0 +1,12 @@
+// this is a comment
+
+// x comment 1
+// x comment 2
+// x comment 3
+func x() {
+}
+
+/*
+ y comment
+*/
+var y int
diff --git a/format/formatutil/_testdata/rearrange/nondecl/out.expect b/format/formatutil/_testdata/rearrange/nondecl/out.expect
new file mode 100644
index 00000000..a333d186
--- /dev/null
+++ b/format/formatutil/_testdata/rearrange/nondecl/out.expect
@@ -0,0 +1,12 @@
+// this is a comment
+
+// x comment 1
+// x comment 2
+// x comment 3
+func x() {
+}
+
+/*
+ y comment
+*/
+var y int
diff --git a/format/formatutil/_testdata/splitstmts/basic/in.data b/format/formatutil/_testdata/splitstmts/basic/in.data
new file mode 100644
index 00000000..7f3e2a0e
--- /dev/null
+++ b/format/formatutil/_testdata/splitstmts/basic/in.data
@@ -0,0 +1,20 @@
+goto "a"
+
+// this is a comment
+
+// x comment 1
+// x comment 2
+// x comment 3
+func x() {
+}
+
+// call
+func(a func()) {}(nil)
+
+/*
+ y comment
+*/
+func y() {
+}
+
+echo "Hi"
diff --git a/format/formatutil/_testdata/splitstmts/basic/out.expect b/format/formatutil/_testdata/splitstmts/basic/out.expect
new file mode 100644
index 00000000..6ef6ad46
--- /dev/null
+++ b/format/formatutil/_testdata/splitstmts/basic/out.expect
@@ -0,0 +1,5 @@
+goto
+FUNC
+FNCALL
+FUNC
+IDENT
diff --git a/format/formatutil/format_test.go b/format/formatutil/format_test.go
new file mode 100644
index 00000000..7765322c
--- /dev/null
+++ b/format/formatutil/format_test.go
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package formatutil
+
+import (
+	"bytes"
+	"log"
+	"os"
+	"path"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/goplus/gop/scanner"
+	"github.com/goplus/gop/token"
+)
+
+func TestSeekAfter(t *testing.T) {
+	if seekAfter(nil, 0, 0) != nil {
+		t.Fatal("seekAfter failed")
+	}
+}
+
+func TestTokOf(t *testing.T) {
+	words := []aWord{{tok: token.COMMENT}}
+	if tok, _ := tokOf(words); token.COMMENT != tok {
+		t.Fatal("tokOf failed:", tok)
+	}
+	if startWith(words, token.VAR) {
+		t.Fatal("startWith failed")
+	}
+}
+
+func doSplitStmts(src []byte) (ret []string) {
+	fset := token.NewFileSet()
+	base := fset.Base()
+	f := fset.AddFile("", base, len(src))
+
+	var s scanner.Scanner
+	s.Init(f, src, nil, scanner.ScanComments)
+	stmts := splitStmts(&s)
+
+	ret = make([]string, len(stmts))
+	for i, stmt := range stmts {
+		ret[i] = stmtKind(stmt)
+	}
+	return
+}
+
+func stmtKind(s aStmt) string {
+	tok := s.tok
+	if tok == token.FUNC {
+		if isFuncDecl(s.words[s.at+1:]) {
+			return "FUNC"
+		}
+		return "FNCALL"
+	}
+	return tok.String()
+}
+
+func testFrom(t *testing.T, pkgDir, sel string, doIt func(in []byte) ([]byte, error)) {
+	if sel != "" && !strings.Contains(pkgDir, sel) {
+		return
+	}
+	t.Helper()
+	log.Println("Parsing", pkgDir)
+	in, err := os.ReadFile(pkgDir + "/in.data")
+	if err != nil {
+		t.Fatal("Parsing", pkgDir, "-", err)
+	}
+	out, err := os.ReadFile(pkgDir + "/out.expect")
+	if err != nil {
+		t.Fatal("Parsing", pkgDir, "-", err)
+	}
+	ret, err := doIt(in)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if !bytes.Equal(ret, out) {
+		t.Fatal("Parsing", pkgDir, "- failed:\n"+string(ret))
+	}
+}
+
+func testFromDir(t *testing.T, sel, relDir string, doIt func(in []byte) ([]byte, error)) {
+	dir, err := os.Getwd()
+	if err != nil {
+		t.Fatal("Getwd failed:", err)
+	}
+	dir = path.Join(dir, relDir)
+	fis, err := os.ReadDir(dir)
+	if err != nil {
+		t.Fatal("ReadDir failed:", err)
+	}
+	for _, fi := range fis {
+		name := fi.Name()
+		if strings.HasPrefix(name, "_") {
+			continue
+		}
+		t.Run(name, func(t *testing.T) {
+			testFrom(t, dir+"/"+name, sel, doIt)
+		})
+	}
+}
+
+func TestSplitStmts(t *testing.T) {
+	testFromDir(t, "", "./_testdata/splitstmts", func(in []byte) ([]byte, error) {
+		ret := strings.Join(doSplitStmts(in), "\n") + "\n"
+		return []byte(ret), nil
+	})
+}
+
+func TestRearrangeFuncs(t *testing.T) {
+	if runtime.GOOS == "windows" { // skip temporarily
+		return
+	}
+	testFromDir(t, "", "./_testdata/rearrange", func(in []byte) ([]byte, error) {
+		return RearrangeFuncs(in)
+	})
+}
+
+func TestFormat(t *testing.T) {
+	testFromDir(t, "", "./_testdata/format", func(in []byte) ([]byte, error) {
+		return SourceEx(in, false, "")
+	})
+}