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

Reply via email to