imports.go (10940B)
1 // Copyright 2013 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package imports implements a Go pretty-printer (like package "go/format") 6 // that also adds or removes import statements as necessary. 7 package imports 8 9 import ( 10 "bufio" 11 "bytes" 12 "context" 13 "fmt" 14 "go/ast" 15 "go/format" 16 "go/parser" 17 "go/printer" 18 "go/token" 19 "io" 20 "regexp" 21 "strconv" 22 "strings" 23 24 "golang.org/x/tools/go/ast/astutil" 25 "golang.org/x/tools/internal/event" 26 ) 27 28 // Options is golang.org/x/tools/imports.Options with extra internal-only options. 29 type Options struct { 30 Env *ProcessEnv // The environment to use. Note: this contains the cached module and filesystem state. 31 32 // LocalPrefix is a comma-separated string of import path prefixes, which, if 33 // set, instructs Process to sort the import paths with the given prefixes 34 // into another group after 3rd-party packages. 35 LocalPrefix string 36 37 Fragment bool // Accept fragment of a source file (no package statement) 38 AllErrors bool // Report all errors (not just the first 10 on different lines) 39 40 Comments bool // Print comments (true if nil *Options provided) 41 TabIndent bool // Use tabs for indent (true if nil *Options provided) 42 TabWidth int // Tab width (8 if nil *Options provided) 43 44 FormatOnly bool // Disable the insertion and deletion of imports 45 } 46 47 // Process implements golang.org/x/tools/imports.Process with explicit context in opt.Env. 48 func Process(filename string, src []byte, opt *Options) (formatted []byte, err error) { 49 fileSet := token.NewFileSet() 50 var parserMode parser.Mode 51 if opt.Comments { 52 parserMode |= parser.ParseComments 53 } 54 if opt.AllErrors { 55 parserMode |= parser.AllErrors 56 } 57 file, adjust, err := parse(fileSet, filename, src, parserMode, opt.Fragment) 58 if err != nil { 59 return nil, err 60 } 61 62 if !opt.FormatOnly { 63 if err := fixImports(fileSet, file, filename, opt.Env); err != nil { 64 return nil, err 65 } 66 } 67 return formatFile(fileSet, file, src, adjust, opt) 68 } 69 70 // FixImports returns a list of fixes to the imports that, when applied, 71 // will leave the imports in the same state as Process. src and opt must 72 // be specified. 73 // 74 // Note that filename's directory influences which imports can be chosen, 75 // so it is important that filename be accurate. 76 func FixImports(ctx context.Context, filename string, src []byte, goroot string, logf func(string, ...any), source Source) (fixes []*ImportFix, err error) { 77 if source == nil { 78 // In case someone adds a defective call from a new place 79 panic("source is nil") 80 } 81 ctx, done := event.Start(ctx, "imports.FixImports") 82 defer done() 83 84 fileSet := token.NewFileSet() 85 // TODO(rfindley): these default values for ParseComments and AllErrors were 86 // extracted from gopls, but are they even needed? 87 file, _, err := parse(fileSet, filename, src, parser.ParseComments|parser.AllErrors, true) 88 if err != nil { 89 return nil, err 90 } 91 92 return getFixesWithSource(ctx, fileSet, file, filename, goroot, logf, source) 93 } 94 95 // ApplyFixes applies all of the fixes to the file and formats it. extraMode 96 // is added in when parsing the file. src and opts must be specified, but no 97 // env is needed. 98 func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options, extraMode parser.Mode) (formatted []byte, err error) { 99 // Don't use parse() -- we don't care about fragments or statement lists 100 // here, and we need to work with unparsable files. 101 fileSet := token.NewFileSet() 102 parserMode := parser.SkipObjectResolution 103 if opt.Comments { 104 parserMode |= parser.ParseComments 105 } 106 if opt.AllErrors { 107 parserMode |= parser.AllErrors 108 } 109 parserMode |= extraMode 110 111 file, err := parser.ParseFile(fileSet, filename, src, parserMode) 112 if file == nil { 113 return nil, err 114 } 115 116 // Apply the fixes to the file. 117 apply(fileSet, file, fixes) 118 119 return formatFile(fileSet, file, src, nil, opt) 120 } 121 122 // formatFile formats the file syntax tree. 123 // It may mutate the token.FileSet and the ast.File. 124 // 125 // If an adjust function is provided, it is called after formatting 126 // with the original source (formatFile's src parameter) and the 127 // formatted file, and returns the postpocessed result. 128 func formatFile(fset *token.FileSet, file *ast.File, src []byte, adjust func(orig []byte, src []byte) []byte, opt *Options) ([]byte, error) { 129 mergeImports(file) 130 sortImports(opt.LocalPrefix, fset.File(file.FileStart), file) 131 var spacesBefore []string // import paths we need spaces before 132 for _, impSection := range astutil.Imports(fset, file) { 133 // Within each block of contiguous imports, see if any 134 // import lines are in different group numbers. If so, 135 // we'll need to put a space between them so it's 136 // compatible with gofmt. 137 lastGroup := -1 138 for _, importSpec := range impSection { 139 importPath, _ := strconv.Unquote(importSpec.Path.Value) 140 groupNum := importGroup(opt.LocalPrefix, importPath) 141 if groupNum != lastGroup && lastGroup != -1 { 142 spacesBefore = append(spacesBefore, importPath) 143 } 144 lastGroup = groupNum 145 } 146 147 } 148 149 printerMode := printer.UseSpaces 150 if opt.TabIndent { 151 printerMode |= printer.TabIndent 152 } 153 printConfig := &printer.Config{Mode: printerMode, Tabwidth: opt.TabWidth} 154 155 var buf bytes.Buffer 156 err := printConfig.Fprint(&buf, fset, file) 157 if err != nil { 158 return nil, err 159 } 160 out := buf.Bytes() 161 if adjust != nil { 162 out = adjust(src, out) 163 } 164 if len(spacesBefore) > 0 { 165 out, err = addImportSpaces(bytes.NewReader(out), spacesBefore) 166 if err != nil { 167 return nil, err 168 } 169 } 170 171 out, err = format.Source(out) 172 if err != nil { 173 return nil, err 174 } 175 return out, nil 176 } 177 178 // parse parses src, which was read from filename, 179 // as a Go source file or statement list. 180 func parse(fset *token.FileSet, filename string, src []byte, parserMode parser.Mode, fragment bool) (*ast.File, func(orig, src []byte) []byte, error) { 181 if parserMode&parser.SkipObjectResolution != 0 { 182 panic("legacy ast.Object resolution is required") 183 } 184 185 // Try as whole source file. 186 file, err := parser.ParseFile(fset, filename, src, parserMode) 187 if err == nil { 188 return file, nil, nil 189 } 190 // If the error is that the source file didn't begin with a 191 // package line and we accept fragmented input, fall through to 192 // try as a source fragment. Stop and return on any other error. 193 if !fragment || !strings.Contains(err.Error(), "expected 'package'") { 194 return nil, nil, err 195 } 196 197 // If this is a declaration list, make it a source file 198 // by inserting a package clause. 199 // Insert using a ;, not a newline, so that parse errors are on 200 // the correct line. 201 const prefix = "package main;" 202 psrc := append([]byte(prefix), src...) 203 file, err = parser.ParseFile(fset, filename, psrc, parserMode) 204 if err == nil { 205 // Gofmt will turn the ; into a \n. 206 // Do that ourselves now and update the file contents, 207 // so that positions and line numbers are correct going forward. 208 psrc[len(prefix)-1] = '\n' 209 fset.File(file.Package).SetLinesForContent(psrc) 210 211 // If a main function exists, we will assume this is a main 212 // package and leave the file. 213 if containsMainFunc(file) { 214 return file, nil, nil 215 } 216 217 adjust := func(orig, src []byte) []byte { 218 // Remove the package clause. 219 src = src[len(prefix):] 220 return matchSpace(orig, src) 221 } 222 return file, adjust, nil 223 } 224 // If the error is that the source file didn't begin with a 225 // declaration, fall through to try as a statement list. 226 // Stop and return on any other error. 227 if !strings.Contains(err.Error(), "expected declaration") { 228 return nil, nil, err 229 } 230 231 // If this is a statement list, make it a source file 232 // by inserting a package clause and turning the list 233 // into a function body. This handles expressions too. 234 // Insert using a ;, not a newline, so that the line numbers 235 // in fsrc match the ones in src. 236 fsrc := append(append([]byte("package p; func _() {"), src...), '}') 237 file, err = parser.ParseFile(fset, filename, fsrc, parserMode) 238 if err == nil { 239 adjust := func(orig, src []byte) []byte { 240 // Remove the wrapping. 241 // Gofmt has turned the ; into a \n\n. 242 src = src[len("package p\n\nfunc _() {"):] 243 src = src[:len(src)-len("}\n")] 244 // Gofmt has also indented the function body one level. 245 // Remove that indent. 246 src = bytes.ReplaceAll(src, []byte("\n\t"), []byte("\n")) 247 return matchSpace(orig, src) 248 } 249 return file, adjust, nil 250 } 251 252 // Failed, and out of options. 253 return nil, nil, err 254 } 255 256 // containsMainFunc checks if a file contains a function declaration with the 257 // function signature 'func main()' 258 func containsMainFunc(file *ast.File) bool { 259 for _, decl := range file.Decls { 260 if f, ok := decl.(*ast.FuncDecl); ok { 261 if f.Name.Name != "main" { 262 continue 263 } 264 265 if len(f.Type.Params.List) != 0 { 266 continue 267 } 268 269 if f.Type.Results != nil && len(f.Type.Results.List) != 0 { 270 continue 271 } 272 273 return true 274 } 275 } 276 277 return false 278 } 279 280 func cutSpace(b []byte) (before, middle, after []byte) { 281 i := 0 282 for i < len(b) && (b[i] == ' ' || b[i] == '\t' || b[i] == '\n') { 283 i++ 284 } 285 j := len(b) 286 for j > 0 && (b[j-1] == ' ' || b[j-1] == '\t' || b[j-1] == '\n') { 287 j-- 288 } 289 if i <= j { 290 return b[:i], b[i:j], b[j:] 291 } 292 return nil, nil, b[j:] 293 } 294 295 // matchSpace reformats src to use the same space context as orig. 296 // 1. If orig begins with blank lines, matchSpace inserts them at the beginning of src. 297 // 2. matchSpace copies the indentation of the first non-blank line in orig 298 // to every non-blank line in src. 299 // 3. matchSpace copies the trailing space from orig and uses it in place 300 // of src's trailing space. 301 func matchSpace(orig []byte, src []byte) []byte { 302 before, _, after := cutSpace(orig) 303 i := bytes.LastIndex(before, []byte{'\n'}) 304 before, indent := before[:i+1], before[i+1:] 305 306 _, src, _ = cutSpace(src) 307 308 var b bytes.Buffer 309 b.Write(before) 310 for len(src) > 0 { 311 line := src 312 if i := bytes.IndexByte(line, '\n'); i >= 0 { 313 line, src = line[:i+1], line[i+1:] 314 } else { 315 src = nil 316 } 317 if len(line) > 0 && line[0] != '\n' { // not blank 318 b.Write(indent) 319 } 320 b.Write(line) 321 } 322 b.Write(after) 323 return b.Bytes() 324 } 325 326 var impLine = regexp.MustCompile(`^\s+(?:[\w\.]+\s+)?"(.+?)"`) 327 328 func addImportSpaces(r io.Reader, breaks []string) ([]byte, error) { 329 var out bytes.Buffer 330 in := bufio.NewReader(r) 331 inImports := false 332 done := false 333 for { 334 s, err := in.ReadString('\n') 335 if err == io.EOF { 336 break 337 } else if err != nil { 338 return nil, err 339 } 340 341 if !inImports && !done && strings.HasPrefix(s, "import") { 342 inImports = true 343 } 344 if inImports && (strings.HasPrefix(s, "var") || 345 strings.HasPrefix(s, "func") || 346 strings.HasPrefix(s, "const") || 347 strings.HasPrefix(s, "type")) { 348 done = true 349 inImports = false 350 } 351 if inImports && len(breaks) > 0 { 352 if m := impLine.FindStringSubmatch(s); m != nil { 353 if m[1] == breaks[0] { 354 out.WriteByte('\n') 355 breaks = breaks[1:] 356 } 357 } 358 } 359 360 fmt.Fprint(&out, s) 361 } 362 return out.Bytes(), nil 363 }