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  }