...

Source file src/k8s.io/kubernetes/cmd/preferredimports/preferredimports.go

Documentation: k8s.io/kubernetes/cmd/preferredimports

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // verify that all the imports have our preferred alias(es).
    18  package main
    19  
    20  import (
    21  	"bytes"
    22  	"encoding/json"
    23  	"flag"
    24  	"fmt"
    25  	"go/ast"
    26  	"go/build"
    27  	"go/format"
    28  	"go/parser"
    29  	"go/token"
    30  	"log"
    31  	"os"
    32  	"path/filepath"
    33  	"regexp"
    34  	"sort"
    35  	"strings"
    36  
    37  	"golang.org/x/term"
    38  )
    39  
    40  var (
    41  	importAliases = flag.String("import-aliases", "hack/.import-aliases", "json file with import aliases")
    42  	confirm       = flag.Bool("confirm", false, "update file with the preferred aliases for imports")
    43  	regex         = flag.String("include-path", "(test/e2e/|test/e2e_node)", "only files with paths matching this regex is touched")
    44  	isTerminal    = term.IsTerminal(int(os.Stdout.Fd()))
    45  	logPrefix     = ""
    46  	aliases       = map[*regexp.Regexp]string{}
    47  )
    48  
    49  type analyzer struct {
    50  	fset      *token.FileSet // positions are relative to fset
    51  	ctx       build.Context
    52  	failed    bool
    53  	donePaths map[string]interface{}
    54  }
    55  
    56  func newAnalyzer() *analyzer {
    57  	ctx := build.Default
    58  	ctx.CgoEnabled = true
    59  
    60  	a := &analyzer{
    61  		fset:      token.NewFileSet(),
    62  		ctx:       ctx,
    63  		donePaths: make(map[string]interface{}),
    64  	}
    65  
    66  	return a
    67  }
    68  
    69  // collect extracts test metadata from a file.
    70  func (a *analyzer) collect(dir string) {
    71  	if _, ok := a.donePaths[dir]; ok {
    72  		return
    73  	}
    74  	a.donePaths[dir] = nil
    75  
    76  	// Create the AST by parsing src.
    77  	fs, err := parser.ParseDir(a.fset, dir, nil, parser.AllErrors|parser.ParseComments)
    78  
    79  	if err != nil {
    80  		fmt.Fprintln(os.Stderr, "ERROR(syntax)", logPrefix, err)
    81  		a.failed = true
    82  		return
    83  	}
    84  
    85  	for _, p := range fs {
    86  		// returns first error, but a.handleError deals with it
    87  		files := a.filterFiles(p.Files)
    88  		for _, file := range files {
    89  			replacements := make(map[string]string)
    90  			pathToFile := a.fset.File(file.Pos()).Name()
    91  			for _, imp := range file.Imports {
    92  				importPath := strings.Replace(imp.Path.Value, "\"", "", -1)
    93  				pathSegments := strings.Split(importPath, "/")
    94  				importName := pathSegments[len(pathSegments)-1]
    95  				if imp.Name != nil {
    96  					importName = imp.Name.Name
    97  				}
    98  				for re, template := range aliases {
    99  					match := re.FindStringSubmatchIndex(importPath)
   100  					if match == nil {
   101  						// No match.
   102  						continue
   103  					}
   104  					if match[0] > 0 || match[1] < len(importPath) {
   105  						// Not a full match.
   106  						continue
   107  					}
   108  					alias := string(re.ExpandString(nil, template, importPath, match))
   109  					if alias != importName {
   110  						if !*confirm {
   111  							fmt.Fprintf(os.Stderr, "%sERROR wrong alias for import \"%s\" should be %s in file %s\n", logPrefix, importPath, alias, pathToFile)
   112  							a.failed = true
   113  						}
   114  						replacements[importName] = alias
   115  						if imp.Name != nil {
   116  							imp.Name.Name = alias
   117  						} else {
   118  							imp.Name = ast.NewIdent(alias)
   119  						}
   120  					}
   121  					break
   122  				}
   123  			}
   124  
   125  			if len(replacements) > 0 {
   126  				if *confirm {
   127  					fmt.Printf("%sReplacing imports with aliases in file %s\n", logPrefix, pathToFile)
   128  					for key, value := range replacements {
   129  						renameImportUsages(file, key, value)
   130  					}
   131  					ast.SortImports(a.fset, file)
   132  					var buffer bytes.Buffer
   133  					if err = format.Node(&buffer, a.fset, file); err != nil {
   134  						panic(fmt.Sprintf("Error formatting ast node after rewriting import.\n%s\n", err.Error()))
   135  					}
   136  
   137  					fileInfo, err := os.Stat(pathToFile)
   138  					if err != nil {
   139  						panic(fmt.Sprintf("Error stat'ing file: %s\n%s\n", pathToFile, err.Error()))
   140  					}
   141  
   142  					err = os.WriteFile(pathToFile, buffer.Bytes(), fileInfo.Mode())
   143  					if err != nil {
   144  						panic(fmt.Sprintf("Error writing file: %s\n%s\n", pathToFile, err.Error()))
   145  					}
   146  				}
   147  			}
   148  		}
   149  	}
   150  }
   151  
   152  func renameImportUsages(f *ast.File, old, new string) {
   153  	// use this to avoid renaming the package declaration, eg:
   154  	//   given: package foo; import foo "bar"; foo.Baz, rename foo->qux
   155  	//   yield: package foo; import qux "bar"; qux.Baz
   156  	var pkg *ast.Ident
   157  
   158  	// Rename top-level old to new, both unresolved names
   159  	// (probably defined in another file) and names that resolve
   160  	// to a declaration we renamed.
   161  	ast.Inspect(f, func(node ast.Node) bool {
   162  		if node == nil {
   163  			return false
   164  		}
   165  		switch id := node.(type) {
   166  		case *ast.File:
   167  			pkg = id.Name
   168  		case *ast.Ident:
   169  			if pkg != nil && id == pkg {
   170  				return false
   171  			}
   172  			if id.Name == old {
   173  				id.Name = new
   174  			}
   175  		}
   176  		return true
   177  	})
   178  }
   179  
   180  func (a *analyzer) filterFiles(fs map[string]*ast.File) []*ast.File {
   181  	var files []*ast.File
   182  	for _, f := range fs {
   183  		files = append(files, f)
   184  	}
   185  	return files
   186  }
   187  
   188  type collector struct {
   189  	dirs  []string
   190  	regex *regexp.Regexp
   191  }
   192  
   193  // handlePath walks the filesystem recursively, collecting directories,
   194  // ignoring some unneeded directories (hidden/vendored) that are handled
   195  // specially later.
   196  func (c *collector) handlePath(path string, info os.FileInfo, err error) error {
   197  	if err != nil {
   198  		return err
   199  	}
   200  	if info.IsDir() {
   201  		// Ignore hidden directories (.git, .cache, etc)
   202  		if len(path) > 1 && path[0] == '.' ||
   203  			// OS-specific vendor code tends to be imported by OS-specific
   204  			// packages. We recursively typecheck imported vendored packages for
   205  			// each OS, but don't typecheck everything for every OS.
   206  			path == "vendor" ||
   207  			path == "_output" ||
   208  			// This is a weird one. /testdata/ is *mostly* ignored by Go,
   209  			// and this translates to kubernetes/vendor not working.
   210  			// edit/record.go doesn't compile without gopkg.in/yaml.v2
   211  			// in $GOSRC/$GOROOT (both typecheck and the shell script).
   212  			path == "pkg/kubectl/cmd/testdata/edit" {
   213  			return filepath.SkipDir
   214  		}
   215  		if c.regex.MatchString(path) {
   216  			c.dirs = append(c.dirs, path)
   217  		}
   218  	}
   219  	return nil
   220  }
   221  
   222  func main() {
   223  	flag.Parse()
   224  	args := flag.Args()
   225  
   226  	if len(args) == 0 {
   227  		args = append(args, ".")
   228  	}
   229  
   230  	regex, err := regexp.Compile(*regex)
   231  	if err != nil {
   232  		log.Fatalf("Error compiling regex: %v", err)
   233  	}
   234  	c := collector{regex: regex}
   235  	for _, arg := range args {
   236  		err := filepath.Walk(arg, c.handlePath)
   237  		if err != nil {
   238  			log.Fatalf("Error walking: %v", err)
   239  		}
   240  	}
   241  	sort.Strings(c.dirs)
   242  
   243  	if len(*importAliases) > 0 {
   244  		bytes, err := os.ReadFile(*importAliases)
   245  		if err != nil {
   246  			log.Fatalf("Error reading import aliases: %v", err)
   247  		}
   248  		var stringAliases map[string]string
   249  		err = json.Unmarshal(bytes, &stringAliases)
   250  		if err != nil {
   251  			log.Fatalf("Error loading aliases: %v", err)
   252  		}
   253  		for pattern, name := range stringAliases {
   254  			re, err := regexp.Compile(pattern)
   255  			if err != nil {
   256  				log.Fatalf("Error parsing import path pattern %q as regular expression: %v", pattern, err)
   257  			}
   258  			aliases[re] = name
   259  		}
   260  	}
   261  	if isTerminal {
   262  		logPrefix = "\r" // clear status bar when printing
   263  	}
   264  	fmt.Println("checking-imports: ")
   265  
   266  	a := newAnalyzer()
   267  	for _, dir := range c.dirs {
   268  		if isTerminal {
   269  			fmt.Printf("\r\033[0m %-80s", dir)
   270  		}
   271  		a.collect(dir)
   272  	}
   273  	fmt.Println()
   274  	if a.failed {
   275  		os.Exit(1)
   276  	}
   277  }
   278  

View as plain text