This is an automated email from the ASF dual-hosted git repository.
alexstocks pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git
The following commit(s) were added to refs/heads/develop by this push:
new a4456d34a feat(tools): add imports formatter (#3019)
a4456d34a is described below
commit a4456d34a2c1f2c6d57b288a65b4c04a183fc75e
Author: marsevilspirit <[email protected]>
AuthorDate: Wed Sep 10 09:16:31 2025 +0800
feat(tools): add imports formatter (#3019)
* feat(tools): add imports formatter
* feat(makefile): update makefile
* style(comment): change to english comments
* fix(license): add license header
* fix(ci): fix sonar ai error
---
Makefile | 3 +-
tools/imports-formatter/constant/version.go | 20 +
tools/imports-formatter/go.mod | 8 +
tools/imports-formatter/go.sum | 4 +
tools/imports-formatter/main.go | 534 +++++++++++++++++++++
tools/imports-formatter/main_test.go | 42 ++
tools/imports-formatter/test-files/test-imports.go | 29 ++
7 files changed, 639 insertions(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index a81503ea6..ad4e56969 100644
--- a/Makefile
+++ b/Makefile
@@ -24,6 +24,7 @@ MAKEFLAGS += --no-builtin-rules
MAKEFLAGS += --no-print-directory
CLI_DIR = tools/dubbogo-cli
+IMPORTS_FORMATTER_DIR = tools/imports-formatter
.PHONY: help test fmt clean lint
@@ -58,4 +59,4 @@ install-golangci-lint:
go install github.com/golangci/golangci-lint/v2/cmd/[email protected]
install-imports-formatter:
- go install github.com/dubbogo/tools/cmd/imports-formatter@latest
+ cd $(IMPORTS_FORMATTER_DIR) && go install
diff --git a/tools/imports-formatter/constant/version.go
b/tools/imports-formatter/constant/version.go
new file mode 100644
index 000000000..9a0659677
--- /dev/null
+++ b/tools/imports-formatter/constant/version.go
@@ -0,0 +1,20 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 constant
+
+const (
+ Version = "1.0.10-dev"
+)
diff --git a/tools/imports-formatter/go.mod b/tools/imports-formatter/go.mod
new file mode 100644
index 000000000..570835639
--- /dev/null
+++ b/tools/imports-formatter/go.mod
@@ -0,0 +1,8 @@
+module dubbo.apache.org/dubbo-go/v3/tools/imports-formatter
+
+go 1.25.0
+
+require (
+ github.com/magiconair/properties v1.8.10
+ github.com/pkg/errors v0.9.1
+)
diff --git a/tools/imports-formatter/go.sum b/tools/imports-formatter/go.sum
new file mode 100644
index 000000000..5ec2a9576
--- /dev/null
+++ b/tools/imports-formatter/go.sum
@@ -0,0 +1,4 @@
+github.com/magiconair/properties v1.8.10
h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
+github.com/magiconair/properties v1.8.10/go.mod
h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod
h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
diff --git a/tools/imports-formatter/main.go b/tools/imports-formatter/main.go
new file mode 100644
index 000000000..5a1522e3f
--- /dev/null
+++ b/tools/imports-formatter/main.go
@@ -0,0 +1,534 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 main
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "sort"
+ "strings"
+)
+
+import (
+ "github.com/pkg/errors"
+)
+
+import (
+ "dubbo.apache.org/dubbo-go/v3/tools/imports-formatter/constant"
+)
+
+const (
+ GO_FILE_SUFFIX = ".go"
+ GO_ROOT = "GOROOT"
+ QUOTATION_MARK = "\""
+ PATH_SEPARATOR = "/"
+ IMPORT = "import"
+ GO_MOD = "go.mod"
+)
+
+var (
+ blankLine bool
+ currentWorkDir, _ = os.Getwd()
+ goRoot = os.Getenv(GO_ROOT) + "/src"
+ endBlocks = []string{"var", "const", "type", "func"}
+ projectRootPath string
+ projectName string
+ goPkgMap = make(map[string]struct{})
+ outerComments = make([]string, 0)
+ // record comments between importBlocks and endBlocks
+ innerComments = make([]string, 0)
+ ignorePath = []string{".git", ".idea", ".github", ".vscode",
"vendor", "swagger", "docs"}
+ newLine = false
+ blockCount = 0
+)
+
+func init() {
+ flag.StringVar(&projectRootPath, "path", currentWorkDir, "the path need
to be reformatted")
+ flag.StringVar(&projectName, "module", "", "project name, namely module
name in the go.mod")
+ flag.BoolVar(&blankLine, "bl", true, "if true, it will split different
import modules with a blank line")
+}
+
+func main() {
+ fmt.Println("imports-formatter:", constant.Version)
+ flag.Parse()
+ var err error
+ projectName, err = getProjectName(projectRootPath)
+ if err != nil {
+ panic(err)
+ }
+
+ if os.Getenv(GO_ROOT) == "" {
+ goRoot = generateGoRoot()
+ }
+
+ err = preProcess(goRoot, goPkgMap)
+ if err != nil {
+ panic(err)
+ }
+
+ err = reformatImports(projectRootPath)
+ if err != nil {
+ panic(err)
+ }
+}
+
+func getProjectName(path string) (string, error) {
+ if projectName != "" {
+ return projectName, nil
+ }
+
+ fileInfos, err := ioutil.ReadDir(path)
+ if err != nil {
+ return "", err
+ }
+
+ for _, fileInfo := range fileInfos {
+ if fileInfo.Name() == GO_MOD {
+ f, err :=
os.OpenFile(path+PATH_SEPARATOR+fileInfo.Name(), os.O_RDONLY, 0644)
+ if err != nil {
+ return "", err
+ }
+
+ reader := bufio.NewReader(f)
+ for {
+ line, _, err := reader.ReadLine()
+ if err != nil {
+ return "", err
+ }
+ lineStr := strings.TrimSpace(string(line))
+ if strings.HasPrefix(lineStr, "module") {
+ return strings.Split(lineStr, " ")[1],
nil
+ }
+ }
+ }
+ }
+
+ return "", err
+}
+
+func preProcess(path string, goPkgMap map[string]struct{}) error {
+ fileInfos, err := ioutil.ReadDir(path)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ dirs := make([]os.FileInfo, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() {
+ dirs = append(dirs, fileInfo)
+ } else if strings.HasSuffix(fileInfo.Name(), GO_FILE_SUFFIX) {
+ goPkgMap[strings.TrimPrefix(path, goRoot+"/")] =
struct{}{}
+ }
+ }
+
+ for _, dir := range dirs {
+ err := preProcess(path+PATH_SEPARATOR+dir.Name(), goPkgMap)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ }
+
+ return nil
+}
+
+func reformatImports(path string) error {
+ fileInfos, err := ioutil.ReadDir(path)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ dirs := make([]os.FileInfo, 0)
+ for _, fileInfo := range fileInfos {
+ if fileInfo.IsDir() && !ignore(fileInfo.Name()) {
+ dirs = append(dirs, fileInfo)
+ } else if strings.HasSuffix(fileInfo.Name(), GO_FILE_SUFFIX) {
+ clearData()
+ newLine = false
+ err = doReformat(path + PATH_SEPARATOR +
fileInfo.Name())
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ }
+ }
+
+ for _, dir := range dirs {
+ err := reformatImports(path + "/" + dir.Name())
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ }
+
+ return nil
+}
+
+func doReformat(filePath string) error {
+ f, err := os.OpenFile(filePath, os.O_RDONLY, 0644)
+ defer func(f *os.File) {
+ err := f.Close()
+ if err != nil {
+ panic(errors.New(filePath + "encounter error:" +
err.Error()))
+ }
+ }(f)
+ if err != nil {
+ return errors.New("open " + filePath + " encounter error:" +
err.Error())
+ }
+
+ // Check if this is a generated file (proto, etc.) and skip it
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.Contains(line, "Code generated by") {
+ // This is a generated file, skip formatting
+ return nil
+ }
+ // Only check first few lines to avoid reading entire file
+ if strings.Contains(line, "package ") {
+ break
+ }
+ }
+
+ // Reset file pointer to beginning
+ if _, err := f.Seek(0, 0); err != nil {
+ return errors.New("seek to beginning of " + filePath + "
encounter error:" + err.Error())
+ }
+
+ reader := bufio.NewReader(f)
+ beginImports := false
+ endImport := false
+ output := make([]byte, 0)
+
+ // processed import(organization) -> original import packages
+ rootImports := make(map[string][]string)
+ internalImports := make(map[string][]string)
+ thirdImports := make(map[string][]string)
+
+ for {
+ if len(outerComments) > 0 {
+ for _, c := range outerComments {
+ output = append(output, []byte(c+"\n")...)
+ }
+ outerComments = make([]string, 0)
+ }
+
+ line, _, err := reader.ReadLine()
+ if err != nil {
+ if err == io.EOF {
+ root := len(rootImports)
+ internal := len(internalImports)
+ third := len(thirdImports)
+ if root > 0 && internal > 0 && third > 0 {
+ blockCount = 3
+ } else if (root > 0 && internal > 0) || (root >
0 && third > 0) || (internal > 0 && third > 0) {
+ blockCount = 2
+ } else if root > 0 || internal > 0 || third > 0
{
+ blockCount = 1
+ }
+ break
+ }
+ return errors.New("read line of " + filePath + "
encounter error:" + err.Error())
+ }
+
+ if endImport {
+ output = append(output, line...)
+ output = append(output, []byte("\n")...)
+ continue
+ }
+
+ // if import blocks end
+ for _, block := range endBlocks {
+ if strings.HasPrefix(string(line), block) {
+ endImport = true
+ beginImports = false
+ newLine = true
+ output = refreshImports(output,
mergeImports(rootImports), false)
+ output = refreshImports(output,
mergeImports(thirdImports), blankLine)
+ output = refreshImports(output,
mergeImports(internalImports), false)
+ if len(innerComments) > 0 {
+ for _, c := range innerComments {
+ output = append(output,
[]byte(c+"\n")...)
+ }
+ }
+ break
+ }
+ }
+
+ lineStr := string(line)
+ if strings.HasPrefix(lineStr, IMPORT) {
+ beginImports = true
+ }
+
+ orgImportPkg := strings.TrimSpace(lineStr)
+ // single line comment
+ if strings.HasPrefix(orgImportPkg, "//") ||
(strings.HasPrefix(orgImportPkg, "/*") && strings.HasSuffix(orgImportPkg,
"*/")) {
+ if beginImports {
+ innerComments = append(innerComments, lineStr)
+ } else {
+ outerComments = append(outerComments, lineStr)
+ }
+ continue
+ }
+ // multiple lines comment
+ if strings.HasPrefix(orgImportPkg, "/*") {
+ if beginImports {
+ innerComments = append(innerComments, lineStr)
+ commentLine, _, err := reader.ReadLine()
+ commentLineStr := string(commentLine)
+ for err == nil &&
!strings.HasSuffix(strings.TrimSpace(commentLineStr), "*/") {
+ innerComments = append(innerComments,
commentLineStr)
+ commentLine, _, err = reader.ReadLine()
+ commentLineStr = string(commentLine)
+ }
+ if err == nil {
+ innerComments = append(innerComments,
commentLineStr)
+ } else {
+ return errors.New("read line of " +
filePath + " encounter error:" + err.Error())
+ }
+ } else {
+ outerComments = append(outerComments, lineStr)
+ commentLine, _, err := reader.ReadLine()
+ commentLineStr := string(commentLine)
+ for err == nil &&
!strings.HasSuffix(strings.TrimSpace(commentLineStr), "*/") {
+ outerComments = append(outerComments,
commentLineStr)
+ commentLine, _, err = reader.ReadLine()
+ commentLineStr = string(commentLine)
+ }
+ if err == nil {
+ outerComments = append(outerComments,
commentLineStr)
+ } else {
+ return errors.New("read line of " +
filePath + " encounter error:" + err.Error())
+ }
+ }
+ continue
+ }
+
+ // collect imports
+ if beginImports && strings.Contains(orgImportPkg,
QUOTATION_MARK) {
+ innerComments = innerComments[:0]
+ // single line import
+ if strings.HasPrefix(orgImportPkg, IMPORT+" ") {
+ orgImportPkg = strings.TrimPrefix(orgImportPkg,
IMPORT+" ")
+ }
+ importKey := orgImportPkg
+ // process those imports that has alias
+ importKey = unwrapImport(importKey)
+
+ if _, ok := goPkgMap[importKey]; ok {
+ // go root import block
+ cacheImports(rootImports, importKey,
[]string{orgImportPkg})
+ } else if importKey == projectName ||
(strings.HasPrefix(importKey, projectName) && len(importKey) > len(projectName)
&& importKey[len(projectName)] == '/') {
+ /**
+ for project a
+ ****************************
+ import (
+ a
+ a/b
+ aa
+ )
+ ****************************
+ we need to combine a&a/b, and exclude aa.
+ importKey == projectName is for a
+ strings.HasPrefix(importKey, projectName) &&
len(importKey) > len(projectName) && importKey[len(projectName)] == '/' is for
a/b
+ if we simply use strings.HasPrefix(importKey,
projectName), it will recognize a&aa as the same project.
+ */
+ // internal imports of the project
+ cacheImports(internalImports, importKey,
[]string{orgImportPkg})
+ } else {
+ // imports of the third projects
+ project, importsSegment := "",
strings.Split(importKey, "/")
+
+ // like google.golang.org/grpc etc.
+ if len(importsSegment) == 2 {
+ project =
strings.Join(importsSegment[:2], "/")
+ } else if len(importsSegment) > 2 {
+ project =
strings.Join(importsSegment[:3], "/")
+ } else {
+ return errors.New("unexpected import
format: " + orgImportPkg + " in file " + filePath)
+ }
+ cacheImports(thirdImports, project,
[]string{orgImportPkg})
+ }
+ continue
+ }
+
+ // to process `import (`
+ if beginImports {
+ continue
+ }
+
+ output = append(output, line...)
+ output = append(output, []byte("\n")...)
+ }
+
+ if !endImport {
+ output = refreshImports(output, mergeImports(rootImports),
false)
+ output = refreshImports(output, mergeImports(thirdImports),
blankLine)
+ output = refreshImports(output, mergeImports(internalImports),
false)
+ if len(innerComments) > 0 {
+ for _, c := range innerComments {
+ output = append(output, []byte(c+"\n")...)
+ }
+ }
+ }
+
+ outF, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC,
0644)
+ if err != nil {
+ return errors.New("open/create" + filePath + " encounter
error:" + err.Error())
+ }
+ defer func(outF *os.File) {
+ err := outF.Close()
+ if err != nil {
+
+ }
+ }(outF)
+ writer := bufio.NewWriter(outF)
+ _, err = writer.Write(output)
+ if err != nil {
+ return errors.New("write " + filePath + " encounter error:" +
err.Error())
+ }
+ err = writer.Flush()
+ if err != nil {
+ return errors.New("flush " + filePath + " encounter error:" +
err.Error())
+ }
+ return nil
+}
+
+func unwrapImport(importStr string) string {
+ // exists alias
+ if !strings.HasPrefix(importStr, QUOTATION_MARK) {
+ importStr = strings.Fields(importStr)[1]
+ }
+ return strings.Trim(importStr, QUOTATION_MARK)
+}
+
+func cacheImports(m map[string][]string, key string, values []string) {
+ if oldValues, ok := m[key]; ok {
+ oldValues = append(oldValues, values...)
+ m[key] = oldValues
+ } else {
+ m[key] = values
+ }
+}
+
+func mergeImports(m map[string][]string) map[string][]string {
+ mergedMap := make(map[string][]string)
+ for key := range m {
+ mergedKeys := make([]string, len(m))
+ newMergedMap := make(map[string][]string)
+ mergedValues := make([]string, 0)
+ mergedValues = append(mergedValues, m[key]...)
+ rootKey := key
+
+ for mKey := range mergedMap {
+ if strings.HasPrefix(rootKey, mKey) {
+ rootKey = mKey
+ }
+ if strings.HasPrefix(key, mKey) ||
strings.HasPrefix(mKey, key) {
+ // mKey is a sub package of the module key ||
key is a sub package of the module mKey
+ mergedKeys = append(mergedKeys, mKey)
+ }
+ }
+
+ for mKey, value := range mergedMap {
+ target := false
+ for _, mKey1 := range mergedKeys {
+ if mKey == mKey1 {
+ target = true
+ mergedValues = append(mergedValues,
mergedMap[mKey]...)
+ break
+ }
+ }
+
+ if !target {
+ cacheImports(newMergedMap, mKey, value)
+ }
+ }
+
+ cacheImports(newMergedMap, rootKey, mergedValues)
+ mergedMap = newMergedMap
+ }
+
+ return mergedMap
+}
+
+func refreshImports(content []byte, importsMap map[string][]string, blankLine
bool) []byte {
+ if len(importsMap) <= 0 {
+ return content
+ }
+ blockCount--
+ content = append(content, []byte("import (\n")...)
+ sortedKeys := make([]string, 0, len(importsMap))
+ for key := range importsMap {
+ sortedKeys = append(sortedKeys, key)
+ }
+ sort.Strings(sortedKeys)
+
+ for idx, key := range sortedKeys {
+ value := importsMap[key]
+ content = doRefreshImports(content, value)
+ if blankLine && idx < len(sortedKeys)-1 {
+ content = append(content, []byte("\n")...)
+ }
+ }
+ if !newLine && blockCount == 0 && len(innerComments) <= 0 {
+ content = append(content, []byte(")\n")...)
+ } else {
+ content = append(content, []byte(")\n\n")...)
+ }
+ return content
+}
+
+func doRefreshImports(content []byte, imports []string) []byte {
+ sort.SliceStable(imports, func(i, j int) bool {
+ v1 := unwrapImport(imports[i])
+ v2 := unwrapImport(imports[j])
+ return v1 < v2
+ })
+ for _, rImport := range imports {
+ content = append(content, []byte("\t"+rImport+"\n")...)
+ }
+ return content
+}
+
+func clearData() {
+ innerComments = innerComments[:0]
+ outerComments = outerComments[:0]
+}
+
+func ignore(path string) bool {
+ for _, name := range ignorePath {
+ if name == path {
+ return true
+ }
+ }
+ return false
+}
+
+func generateGoRoot() string {
+ cmd := exec.Command("go", "env", "GOROOT") //NOSONAR
+ output, err := cmd.Output()
+ if err != nil {
+ panic(err)
+ }
+ goRootPath := strings.TrimSpace(string(output))
+
+ return goRootPath + "/src"
+}
diff --git a/tools/imports-formatter/main_test.go
b/tools/imports-formatter/main_test.go
new file mode 100644
index 000000000..38731b055
--- /dev/null
+++ b/tools/imports-formatter/main_test.go
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 main
+
+import (
+ "testing"
+)
+
+import (
+ "github.com/magiconair/properties/assert"
+)
+
+func TestMergeImports(t *testing.T) {
+ imports := make(map[string][]string)
+ imports["common"] = []string{"common"}
+ imports["common/constant"] = []string{"common/constant"}
+ imports["common/extension"] = []string{"common/extension"}
+ imports["common/logger"] = []string{"common/logger"}
+ imports["registry"] = []string{"registry"}
+
+ mImports := mergeImports(imports)
+ for i := 0; i < 100; i++ {
+ assert.Equal(t, 2, len(mImports))
+ commonImports := mImports["common"]
+ if len(commonImports) != 4 {
+ t.Error(mImports)
+ }
+ }
+}
diff --git a/tools/imports-formatter/test-files/test-imports.go
b/tools/imports-formatter/test-files/test-imports.go
new file mode 100644
index 000000000..9ea0e1536
--- /dev/null
+++ b/tools/imports-formatter/test-files/test-imports.go
@@ -0,0 +1,29 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 test_files
+
+// test outside comment
+import (
+ _ "io"
+ _ "io/ioutil"
+ _ "os"
+)
+
+import (
+ _ "github.com/pkg/errors"
+)
+
+// test mid comment