src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }