mirror of
https://github.com/golang/go
synced 2024-11-18 12:54:44 -07:00
46bd65c853
This change attempts to fix a concurrency error that would cause textDocument/CodeLens, textDocument/Formatting, textDocument/DocumentLink, and textDocument/Hover from failing on go.mod files. The issue was that the go command would return a potential concurrency error since the ModHandle and the ModTidyHandle are both using the temporary go.mod file. Updates golang/go#37824 Change-Id: I6cd63c1f75817c7308e033aec473966536a2a3bd Reviewed-on: https://go-review.googlesource.com/c/tools/+/224917 Reviewed-by: Heschi Kreinick <heschi@google.com> Run-TryBot: Rebecca Stambler <rstambler@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org>
414 lines
12 KiB
Go
414 lines
12 KiB
Go
// Copyright 2013 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:generate go run mkstdlib.go
|
|
|
|
// Package imports implements a Go pretty-printer (like package "go/format")
|
|
// that also adds or removes import statements as necessary.
|
|
package imports
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/format"
|
|
"go/parser"
|
|
"go/printer"
|
|
"go/token"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/ast/astutil"
|
|
"golang.org/x/tools/internal/gocommand"
|
|
)
|
|
|
|
// Options is golang.org/x/tools/imports.Options with extra internal-only options.
|
|
type Options struct {
|
|
Env *ProcessEnv // The environment to use. Note: this contains the cached module and filesystem state.
|
|
|
|
Fragment bool // Accept fragment of a source file (no package statement)
|
|
AllErrors bool // Report all errors (not just the first 10 on different lines)
|
|
|
|
Comments bool // Print comments (true if nil *Options provided)
|
|
TabIndent bool // Use tabs for indent (true if nil *Options provided)
|
|
TabWidth int // Tab width (8 if nil *Options provided)
|
|
|
|
FormatOnly bool // Disable the insertion and deletion of imports
|
|
}
|
|
|
|
// Process implements golang.org/x/tools/imports.Process with explicit context in env.
|
|
func Process(filename string, src []byte, opt *Options) (formatted []byte, err error) {
|
|
src, opt, err = initialize(filename, src, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileSet := token.NewFileSet()
|
|
file, adjust, err := parse(fileSet, filename, src, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !opt.FormatOnly {
|
|
if err := fixImports(fileSet, file, filename, opt.Env); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return formatFile(fileSet, file, src, adjust, opt)
|
|
}
|
|
|
|
// FixImports returns a list of fixes to the imports that, when applied,
|
|
// will leave the imports in the same state as Process.
|
|
//
|
|
// Note that filename's directory influences which imports can be chosen,
|
|
// so it is important that filename be accurate.
|
|
func FixImports(filename string, src []byte, opt *Options) (fixes []*ImportFix, err error) {
|
|
src, opt, err = initialize(filename, src, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fileSet := token.NewFileSet()
|
|
file, _, err := parse(fileSet, filename, src, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return getFixes(fileSet, file, filename, opt.Env)
|
|
}
|
|
|
|
// ApplyFixes applies all of the fixes to the file and formats it. extraMode
|
|
// is added in when parsing the file.
|
|
func ApplyFixes(fixes []*ImportFix, filename string, src []byte, opt *Options, extraMode parser.Mode) (formatted []byte, err error) {
|
|
src, opt, err = initialize(filename, src, opt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Don't use parse() -- we don't care about fragments or statement lists
|
|
// here, and we need to work with unparseable files.
|
|
fileSet := token.NewFileSet()
|
|
parserMode := parser.Mode(0)
|
|
if opt.Comments {
|
|
parserMode |= parser.ParseComments
|
|
}
|
|
if opt.AllErrors {
|
|
parserMode |= parser.AllErrors
|
|
}
|
|
parserMode |= extraMode
|
|
|
|
file, err := parser.ParseFile(fileSet, filename, src, parserMode)
|
|
if file == nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Apply the fixes to the file.
|
|
apply(fileSet, file, fixes)
|
|
|
|
return formatFile(fileSet, file, src, nil, opt)
|
|
}
|
|
|
|
// GetAllCandidates gets all of the packages starting with prefix that can be
|
|
// imported by filename, sorted by import path.
|
|
func GetAllCandidates(ctx context.Context, callback func(ImportFix), searchPrefix, filename, filePkg string, opt *Options) error {
|
|
_, opt, err := initialize(filename, []byte{}, opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return getAllCandidates(ctx, callback, searchPrefix, filename, filePkg, opt.Env)
|
|
}
|
|
|
|
// GetPackageExports returns all known packages with name pkg and their exports.
|
|
func GetPackageExports(ctx context.Context, callback func(PackageExport), searchPkg, filename, filePkg string, opt *Options) error {
|
|
_, opt, err := initialize(filename, []byte{}, opt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return getPackageExports(ctx, callback, searchPkg, filename, filePkg, opt.Env)
|
|
}
|
|
|
|
// initialize sets the values for opt and src.
|
|
// If they are provided, they are not changed. Otherwise opt is set to the
|
|
// default values and src is read from the file system.
|
|
func initialize(filename string, src []byte, opt *Options) ([]byte, *Options, error) {
|
|
// Use defaults if opt is nil.
|
|
if opt == nil {
|
|
opt = &Options{Comments: true, TabIndent: true, TabWidth: 8}
|
|
}
|
|
|
|
// Set the env if the user has not provided it.
|
|
if opt.Env == nil {
|
|
opt.Env = &ProcessEnv{
|
|
GOPATH: build.Default.GOPATH,
|
|
GOROOT: build.Default.GOROOT,
|
|
GOFLAGS: os.Getenv("GOFLAGS"),
|
|
GO111MODULE: os.Getenv("GO111MODULE"),
|
|
GOPROXY: os.Getenv("GOPROXY"),
|
|
GOSUMDB: os.Getenv("GOSUMDB"),
|
|
}
|
|
}
|
|
// Set the gocmdRunner if the user has not provided it.
|
|
if opt.Env.GocmdRunner == nil {
|
|
opt.Env.GocmdRunner = &gocommand.Runner{}
|
|
}
|
|
if src == nil {
|
|
b, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
src = b
|
|
}
|
|
|
|
return src, opt, nil
|
|
}
|
|
|
|
func formatFile(fileSet *token.FileSet, file *ast.File, src []byte, adjust func(orig []byte, src []byte) []byte, opt *Options) ([]byte, error) {
|
|
mergeImports(opt.Env, fileSet, file)
|
|
sortImports(opt.Env, fileSet, file)
|
|
imps := astutil.Imports(fileSet, file)
|
|
var spacesBefore []string // import paths we need spaces before
|
|
for _, impSection := range imps {
|
|
// Within each block of contiguous imports, see if any
|
|
// import lines are in different group numbers. If so,
|
|
// we'll need to put a space between them so it's
|
|
// compatible with gofmt.
|
|
lastGroup := -1
|
|
for _, importSpec := range impSection {
|
|
importPath, _ := strconv.Unquote(importSpec.Path.Value)
|
|
groupNum := importGroup(opt.Env, importPath)
|
|
if groupNum != lastGroup && lastGroup != -1 {
|
|
spacesBefore = append(spacesBefore, importPath)
|
|
}
|
|
lastGroup = groupNum
|
|
}
|
|
|
|
}
|
|
|
|
printerMode := printer.UseSpaces
|
|
if opt.TabIndent {
|
|
printerMode |= printer.TabIndent
|
|
}
|
|
printConfig := &printer.Config{Mode: printerMode, Tabwidth: opt.TabWidth}
|
|
|
|
var buf bytes.Buffer
|
|
err := printConfig.Fprint(&buf, fileSet, file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := buf.Bytes()
|
|
if adjust != nil {
|
|
out = adjust(src, out)
|
|
}
|
|
if len(spacesBefore) > 0 {
|
|
out, err = addImportSpaces(bytes.NewReader(out), spacesBefore)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
out, err = format.Source(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// parse parses src, which was read from filename,
|
|
// as a Go source file or statement list.
|
|
func parse(fset *token.FileSet, filename string, src []byte, opt *Options) (*ast.File, func(orig, src []byte) []byte, error) {
|
|
parserMode := parser.Mode(0)
|
|
if opt.Comments {
|
|
parserMode |= parser.ParseComments
|
|
}
|
|
if opt.AllErrors {
|
|
parserMode |= parser.AllErrors
|
|
}
|
|
|
|
// Try as whole source file.
|
|
file, err := parser.ParseFile(fset, filename, src, parserMode)
|
|
if err == nil {
|
|
return file, nil, nil
|
|
}
|
|
// If the error is that the source file didn't begin with a
|
|
// package line and we accept fragmented input, fall through to
|
|
// try as a source fragment. Stop and return on any other error.
|
|
if !opt.Fragment || !strings.Contains(err.Error(), "expected 'package'") {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// If this is a declaration list, make it a source file
|
|
// by inserting a package clause.
|
|
// Insert using a ;, not a newline, so that parse errors are on
|
|
// the correct line.
|
|
const prefix = "package main;"
|
|
psrc := append([]byte(prefix), src...)
|
|
file, err = parser.ParseFile(fset, filename, psrc, parserMode)
|
|
if err == nil {
|
|
// Gofmt will turn the ; into a \n.
|
|
// Do that ourselves now and update the file contents,
|
|
// so that positions and line numbers are correct going forward.
|
|
psrc[len(prefix)-1] = '\n'
|
|
fset.File(file.Package).SetLinesForContent(psrc)
|
|
|
|
// If a main function exists, we will assume this is a main
|
|
// package and leave the file.
|
|
if containsMainFunc(file) {
|
|
return file, nil, nil
|
|
}
|
|
|
|
adjust := func(orig, src []byte) []byte {
|
|
// Remove the package clause.
|
|
src = src[len(prefix):]
|
|
return matchSpace(orig, src)
|
|
}
|
|
return file, adjust, nil
|
|
}
|
|
// If the error is that the source file didn't begin with a
|
|
// declaration, fall through to try as a statement list.
|
|
// Stop and return on any other error.
|
|
if !strings.Contains(err.Error(), "expected declaration") {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// If this is a statement list, make it a source file
|
|
// by inserting a package clause and turning the list
|
|
// into a function body. This handles expressions too.
|
|
// Insert using a ;, not a newline, so that the line numbers
|
|
// in fsrc match the ones in src.
|
|
fsrc := append(append([]byte("package p; func _() {"), src...), '}')
|
|
file, err = parser.ParseFile(fset, filename, fsrc, parserMode)
|
|
if err == nil {
|
|
adjust := func(orig, src []byte) []byte {
|
|
// Remove the wrapping.
|
|
// Gofmt has turned the ; into a \n\n.
|
|
src = src[len("package p\n\nfunc _() {"):]
|
|
src = src[:len(src)-len("}\n")]
|
|
// Gofmt has also indented the function body one level.
|
|
// Remove that indent.
|
|
src = bytes.Replace(src, []byte("\n\t"), []byte("\n"), -1)
|
|
return matchSpace(orig, src)
|
|
}
|
|
return file, adjust, nil
|
|
}
|
|
|
|
// Failed, and out of options.
|
|
return nil, nil, err
|
|
}
|
|
|
|
// containsMainFunc checks if a file contains a function declaration with the
|
|
// function signature 'func main()'
|
|
func containsMainFunc(file *ast.File) bool {
|
|
for _, decl := range file.Decls {
|
|
if f, ok := decl.(*ast.FuncDecl); ok {
|
|
if f.Name.Name != "main" {
|
|
continue
|
|
}
|
|
|
|
if len(f.Type.Params.List) != 0 {
|
|
continue
|
|
}
|
|
|
|
if f.Type.Results != nil && len(f.Type.Results.List) != 0 {
|
|
continue
|
|
}
|
|
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func cutSpace(b []byte) (before, middle, after []byte) {
|
|
i := 0
|
|
for i < len(b) && (b[i] == ' ' || b[i] == '\t' || b[i] == '\n') {
|
|
i++
|
|
}
|
|
j := len(b)
|
|
for j > 0 && (b[j-1] == ' ' || b[j-1] == '\t' || b[j-1] == '\n') {
|
|
j--
|
|
}
|
|
if i <= j {
|
|
return b[:i], b[i:j], b[j:]
|
|
}
|
|
return nil, nil, b[j:]
|
|
}
|
|
|
|
// matchSpace reformats src to use the same space context as orig.
|
|
// 1) If orig begins with blank lines, matchSpace inserts them at the beginning of src.
|
|
// 2) matchSpace copies the indentation of the first non-blank line in orig
|
|
// to every non-blank line in src.
|
|
// 3) matchSpace copies the trailing space from orig and uses it in place
|
|
// of src's trailing space.
|
|
func matchSpace(orig []byte, src []byte) []byte {
|
|
before, _, after := cutSpace(orig)
|
|
i := bytes.LastIndex(before, []byte{'\n'})
|
|
before, indent := before[:i+1], before[i+1:]
|
|
|
|
_, src, _ = cutSpace(src)
|
|
|
|
var b bytes.Buffer
|
|
b.Write(before)
|
|
for len(src) > 0 {
|
|
line := src
|
|
if i := bytes.IndexByte(line, '\n'); i >= 0 {
|
|
line, src = line[:i+1], line[i+1:]
|
|
} else {
|
|
src = nil
|
|
}
|
|
if len(line) > 0 && line[0] != '\n' { // not blank
|
|
b.Write(indent)
|
|
}
|
|
b.Write(line)
|
|
}
|
|
b.Write(after)
|
|
return b.Bytes()
|
|
}
|
|
|
|
var impLine = regexp.MustCompile(`^\s+(?:[\w\.]+\s+)?"(.+)"`)
|
|
|
|
func addImportSpaces(r io.Reader, breaks []string) ([]byte, error) {
|
|
var out bytes.Buffer
|
|
in := bufio.NewReader(r)
|
|
inImports := false
|
|
done := false
|
|
for {
|
|
s, err := in.ReadString('\n')
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !inImports && !done && strings.HasPrefix(s, "import") {
|
|
inImports = true
|
|
}
|
|
if inImports && (strings.HasPrefix(s, "var") ||
|
|
strings.HasPrefix(s, "func") ||
|
|
strings.HasPrefix(s, "const") ||
|
|
strings.HasPrefix(s, "type")) {
|
|
done = true
|
|
inImports = false
|
|
}
|
|
if inImports && len(breaks) > 0 {
|
|
if m := impLine.FindStringSubmatch(s); m != nil {
|
|
if m[1] == breaks[0] {
|
|
out.WriteByte('\n')
|
|
breaks = breaks[1:]
|
|
}
|
|
}
|
|
}
|
|
|
|
fmt.Fprint(&out, s)
|
|
}
|
|
return out.Bytes(), nil
|
|
}
|