github.com/nya3jp/tast@v0.0.0-20230601000426-85c8e4d83a9b/src/go.chromium.org/tast/core/cmd/tast-lint/internal/check/import_order.go (about) 1 // Copyright 2018 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package check 6 7 import ( 8 "bytes" 9 "fmt" 10 "go/ast" 11 "go/parser" 12 "go/token" 13 "os/exec" 14 "regexp" 15 16 "go.chromium.org/tast/core/cmd/tast-lint/internal/diff" 17 ) 18 19 // commentInImportRegexp matches a comment inside an import block. 20 var commentInImportRegexp = regexp.MustCompile(`import \([^)]*(//|/\*)`) 21 22 // ImportOrder checks if the order of import entries are sorted in the 23 // following order. 24 // - Import entries should be split into three groups; stdlib, third-party 25 // packages, and chromiumos packages. 26 // - In each group, the entries should be sorted in the lexicographical 27 // order. 28 // - The groups should be separated by an empty line. 29 // 30 // This order should be same as what "goimports --local chromiumos/" does. 31 func ImportOrder(path string, in []byte) []*Issue { 32 out, err := formatImports(in) 33 if err != nil { 34 panic(err.Error()) 35 } 36 37 diff, err := diff.Diff(string(in), string(out)) 38 if err != nil { 39 panic(err.Error()) 40 } 41 42 if diff != "" { 43 return []*Issue{{ 44 Pos: token.Position{Filename: path}, 45 Msg: fmt.Sprintf("Import should be grouped into standard packages, third-party packages and chromiumos packages in this order separated by empty lines.\nApply the following patch to fix:\n%s", diff), 46 Link: "https://chromium.googlesource.com/chromiumos/platform/tast/+/HEAD/docs/writing_tests.md#import", 47 Fixable: true, 48 }} 49 } 50 51 // No issue is found. 52 return nil 53 } 54 55 type importPos int 56 57 const ( 58 beforeImport importPos = iota 59 inImport 60 afterImport 61 ) 62 63 // trimImportEmptyLine removes empty lines in the import declaration. 64 func trimImportEmptyLine(in []byte) []byte { 65 var lines [][]byte 66 current := beforeImport 67 for _, line := range bytes.Split(in, []byte("\n")) { 68 trimmed := bytes.TrimSpace(line) 69 70 switch current { 71 case beforeImport: 72 if bytes.Equal(trimmed, []byte("import (")) { 73 current = inImport 74 } 75 case inImport: 76 if bytes.Equal(trimmed, []byte(")")) { 77 current = afterImport 78 } 79 } 80 81 if current == inImport && len(trimmed) == 0 { 82 // Skip empty line in import section. 83 continue 84 } 85 lines = append(lines, line) 86 } 87 return bytes.Join(lines, []byte("\n")) 88 } 89 90 // runGoimports runs "goimports --local=chromiumos/". Passed in arg will be 91 // the stdin for the subprocess. Returns the stdout. 92 func runGoimports(in []byte) ([]byte, error) { 93 _, err := exec.LookPath("goimports") 94 if err != nil { 95 panic("goimports not found. Please install. If already installed, check that GOPATH[0]/bin is in your PATH.") 96 } 97 98 cmd := exec.Command("goimports", "--local=chromiumos/,go.chromium.org/tast") 99 cmd.Stdin = bytes.NewBuffer(in) 100 out, err := cmd.Output() 101 if err != nil { 102 return nil, err 103 } 104 return out, nil 105 } 106 107 func formatImports(in []byte) ([]byte, error) { 108 if !goimportApplicable(in) { 109 return in, nil 110 } 111 112 // goimports preserves import blocks separated by empty lines. To avoid 113 // unexpected sorting, remove all empty lines here in import 114 // declaration. 115 trimmed := trimImportEmptyLine(in) 116 117 // This may potentially raise a false alarm. goimports actually adds 118 // or removes some entries in import(), which depends on GOPATH. 119 // However, this lint check is running outside of the chroot, unlike 120 // actual build, so the GOPATH value and directory structure can be 121 // different. 122 return runGoimports(trimmed) 123 } 124 125 // ImportOrderAutoFix returns ast.File node whose import was fixed from given node correctly. 126 func ImportOrderAutoFix(fs *token.FileSet, f *ast.File) (*ast.File, error) { 127 // Format ast.File to buffer. 128 in, err := formatASTNode(fs, f) 129 if err != nil { 130 return nil, err 131 } 132 out, err := formatImports(in) 133 if err != nil { 134 return nil, err 135 } 136 // Parse again. 137 path := fs.Position(f.Pos()).Filename 138 newf, err := parser.ParseFile(fs, path, out, parser.ParseComments) 139 if err != nil { 140 return nil, err 141 } 142 return newf, nil 143 } 144 145 // goimportApplicable returns true if there is no comment inside an import block 146 // since we can't handle it correctly now. 147 // TODO(crbug.com/900131): Handle it correctly and remove this check. 148 func goimportApplicable(in []byte) bool { 149 return !commentInImportRegexp.Match(in) 150 }