2019-08-14 15:25:47 -06:00
|
|
|
// Copyright 2019 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.
|
|
|
|
|
|
|
|
package source
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2019-08-16 13:02:11 -06:00
|
|
|
"fmt"
|
2019-08-14 15:25:47 -06:00
|
|
|
"go/ast"
|
|
|
|
"go/format"
|
|
|
|
"go/token"
|
|
|
|
"strconv"
|
2019-08-16 13:02:11 -06:00
|
|
|
"strings"
|
2019-08-14 15:25:47 -06:00
|
|
|
|
2019-08-19 17:28:08 -06:00
|
|
|
"golang.org/x/tools/internal/lsp/diff"
|
2019-08-14 15:25:47 -06:00
|
|
|
"golang.org/x/tools/internal/span"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Taken and then modified from golang.org/x/tools/go/ast/astutil.
|
|
|
|
//
|
|
|
|
// We currently choose to create our own version of AddNamedImport for the following reasons:
|
|
|
|
// 1. We do not want to edit the current ast. This is so that we can use the same ast
|
|
|
|
// to get the changes from multiple distinct modifications.
|
|
|
|
// 2. We need the changes that *only* affect the import declarations, because the edits
|
|
|
|
// are not allowed to overlap with the position in the source that is being edited.
|
|
|
|
// astutil.AddNamedImport makes it hard to determine what changes need to be made
|
|
|
|
// to the source document from the ast, as astutil.AddNamedImport includes a merging pass.
|
|
|
|
|
2019-08-16 13:02:11 -06:00
|
|
|
// addNamedImport adds the import with the given name and path to the file f, if absent.
|
2019-08-14 15:25:47 -06:00
|
|
|
// If name is not empty, it is used to rename the import.
|
|
|
|
//
|
|
|
|
// For example, calling
|
2019-08-16 13:02:11 -06:00
|
|
|
// addNamedImport(fset, f, "pathpkg", "path")
|
2019-08-14 15:25:47 -06:00
|
|
|
// adds
|
|
|
|
// import pathpkg "path"
|
|
|
|
//
|
2019-08-16 13:02:11 -06:00
|
|
|
// addNamedImport only returns edits that affect the import declarations.
|
2019-08-19 17:28:08 -06:00
|
|
|
func addNamedImport(fset *token.FileSet, f *ast.File, name, path string) (edits []diff.TextEdit, err error) {
|
2019-08-14 15:25:47 -06:00
|
|
|
if alreadyImports(f, name, path) {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
newImport := &ast.ImportSpec{
|
|
|
|
Path: &ast.BasicLit{
|
|
|
|
Kind: token.STRING,
|
|
|
|
Value: strconv.Quote(path),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
if name != "" {
|
|
|
|
newImport.Name = &ast.Ident{Name: name}
|
|
|
|
}
|
|
|
|
|
2019-08-16 13:02:11 -06:00
|
|
|
// Find an import decl to add to.
|
|
|
|
// The goal is to find an existing import
|
|
|
|
// whose import path has the longest shared
|
|
|
|
// prefix with path.
|
|
|
|
var (
|
|
|
|
bestMatch = -1 // length of longest shared prefix
|
|
|
|
lastImport = -1 // index in f.Decls of the file's final import decl
|
|
|
|
impDecl *ast.GenDecl // import decl containing the best match
|
|
|
|
impIndex = -1 // spec index in impDecl containing the best match
|
|
|
|
|
|
|
|
isThirdPartyPath = isThirdParty(path)
|
|
|
|
)
|
2019-08-14 15:25:47 -06:00
|
|
|
for i, decl := range f.Decls {
|
|
|
|
gen, ok := decl.(*ast.GenDecl)
|
|
|
|
if ok && gen.Tok == token.IMPORT {
|
|
|
|
lastImport = i
|
2019-08-16 13:02:11 -06:00
|
|
|
// Do not add to import "C", to avoid disrupting the
|
|
|
|
// association with its doc comment, breaking cgo.
|
|
|
|
if declImports(gen, "C") {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do not add to single imports.
|
|
|
|
if !gen.Lparen.IsValid() {
|
|
|
|
continue
|
|
|
|
}
|
2019-08-14 15:25:47 -06:00
|
|
|
|
2019-08-16 13:02:11 -06:00
|
|
|
// Match an empty import decl if that's all that is available.
|
|
|
|
if len(gen.Specs) == 0 && bestMatch == -1 {
|
|
|
|
impDecl = gen
|
|
|
|
}
|
|
|
|
|
|
|
|
// Compute longest shared prefix with imports in this group and find best
|
|
|
|
// matched import spec.
|
|
|
|
// 1. Always prefer import spec with longest shared prefix.
|
|
|
|
// 2. While match length is 0,
|
|
|
|
// - for stdlib package: prefer first import spec.
|
|
|
|
// - for third party package: prefer first third party import spec.
|
|
|
|
// We cannot use last import spec as best match for third party package
|
|
|
|
// because grouped imports are usually placed last by goimports -local
|
|
|
|
// flag.
|
|
|
|
// See issue #19190.
|
|
|
|
seenAnyThirdParty := false
|
|
|
|
for j, spec := range gen.Specs {
|
|
|
|
impspec := spec.(*ast.ImportSpec)
|
|
|
|
p := importPath(impspec)
|
|
|
|
n := matchLen(p, path)
|
|
|
|
if n > bestMatch || (bestMatch == 0 && !seenAnyThirdParty && isThirdPartyPath) {
|
|
|
|
bestMatch = n
|
|
|
|
impDecl = gen
|
|
|
|
impIndex = j
|
|
|
|
}
|
|
|
|
seenAnyThirdParty = seenAnyThirdParty || isThirdParty(p)
|
|
|
|
}
|
|
|
|
}
|
2019-08-14 15:25:47 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
var insertPos token.Pos
|
2019-08-16 13:02:11 -06:00
|
|
|
var newText string
|
|
|
|
// If no import decl found, add one after the last import.
|
|
|
|
if impDecl == nil {
|
|
|
|
// Add an import decl after the last import.
|
|
|
|
impDecl = &ast.GenDecl{
|
|
|
|
Tok: token.IMPORT,
|
|
|
|
}
|
|
|
|
impDecl.Specs = append(impDecl.Specs, newImport)
|
|
|
|
|
|
|
|
if lastImport >= 0 {
|
|
|
|
insertPos = f.Decls[lastImport].End()
|
|
|
|
} else {
|
|
|
|
// There are no existing imports.
|
|
|
|
// Our new import goes after the package declaration.
|
|
|
|
insertPos = f.Name.End()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Print the whole import declaration.
|
|
|
|
newText = fmt.Sprintf("\n\nimport %s", printImportSpec(fset, newImport))
|
2019-08-14 15:25:47 -06:00
|
|
|
} else {
|
2019-08-16 13:02:11 -06:00
|
|
|
// Insert new import at insertAt.
|
|
|
|
insertAt := 0
|
|
|
|
if impIndex >= 0 {
|
|
|
|
// insert after the found import
|
|
|
|
insertAt = impIndex + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
insertPos = impDecl.Lparen + 1 // insert after the parenthesis
|
|
|
|
if len(impDecl.Specs) > 0 {
|
|
|
|
insertPos = impDecl.Specs[0].Pos() // insert at the beginning
|
|
|
|
}
|
|
|
|
if insertAt > 0 {
|
|
|
|
// If there is a comment after an existing import, preserve the comment
|
|
|
|
// position by adding the new import after the comment.
|
|
|
|
if spec, ok := impDecl.Specs[insertAt-1].(*ast.ImportSpec); ok && spec.Comment != nil {
|
|
|
|
insertPos = spec.Comment.End()
|
|
|
|
} else {
|
|
|
|
// Assign same position as the previous import,
|
|
|
|
// so that the sorter sees it as being in the same block.
|
|
|
|
insertPos = impDecl.Specs[insertAt-1].End()
|
2019-08-14 15:25:47 -06:00
|
|
|
}
|
|
|
|
}
|
2019-08-16 13:02:11 -06:00
|
|
|
|
|
|
|
// Print this import declaration.
|
|
|
|
newText = fmt.Sprintf("\n\t%s", printImportSpec(fset, newImport))
|
2019-08-14 15:25:47 -06:00
|
|
|
}
|
|
|
|
|
2019-08-16 13:02:11 -06:00
|
|
|
// If we didn't find a valid insert position, return no edits.
|
|
|
|
if !insertPos.IsValid() {
|
|
|
|
return edits, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure that we are printed after any comments that start on the same line.
|
|
|
|
file := fset.File(insertPos)
|
|
|
|
pkgLine := file.Line(insertPos)
|
|
|
|
for _, c := range f.Comments {
|
|
|
|
if file.Line(c.Pos()) > pkgLine {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if c.End() > insertPos {
|
|
|
|
insertPos = c.End()
|
|
|
|
}
|
|
|
|
}
|
2019-08-14 15:25:47 -06:00
|
|
|
|
|
|
|
rng := span.NewRange(fset, insertPos, insertPos)
|
|
|
|
spn, err := rng.Span()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-08-19 17:28:08 -06:00
|
|
|
edits = append(edits, diff.TextEdit{
|
2019-08-14 15:25:47 -06:00
|
|
|
Span: spn,
|
|
|
|
NewText: newText,
|
|
|
|
})
|
|
|
|
return edits, nil
|
|
|
|
}
|
|
|
|
|
2019-08-16 13:02:11 -06:00
|
|
|
func printImportSpec(fset *token.FileSet, spec *ast.ImportSpec) string {
|
|
|
|
var buf bytes.Buffer
|
|
|
|
format.Node(&buf, fset, spec)
|
|
|
|
return buf.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
func isThirdParty(importPath string) bool {
|
|
|
|
// Third party package import path usually contains "." (".com", ".org", ...)
|
|
|
|
// This logic is taken from golang.org/x/tools/imports package.
|
|
|
|
return strings.Contains(importPath, ".")
|
|
|
|
}
|
|
|
|
|
2019-08-14 15:25:47 -06:00
|
|
|
// alreadyImports reports whether f has an import with the specified name and path.
|
|
|
|
func alreadyImports(f *ast.File, name, path string) bool {
|
|
|
|
for _, s := range f.Imports {
|
|
|
|
if importName(s) == name && importPath(s) == path {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// importName returns the name of s,
|
|
|
|
// or "" if the import is not named.
|
|
|
|
func importName(s *ast.ImportSpec) string {
|
|
|
|
if s.Name == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return s.Name.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
// importPath returns the unquoted import path of s,
|
|
|
|
// or "" if the path is not properly quoted.
|
|
|
|
func importPath(s *ast.ImportSpec) string {
|
|
|
|
t, err := strconv.Unquote(s.Path.Value)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return t
|
|
|
|
}
|
2019-08-16 13:02:11 -06:00
|
|
|
|
|
|
|
// declImports reports whether gen contains an import of path.
|
|
|
|
func declImports(gen *ast.GenDecl, path string) bool {
|
|
|
|
if gen.Tok != token.IMPORT {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for _, spec := range gen.Specs {
|
|
|
|
impspec := spec.(*ast.ImportSpec)
|
|
|
|
if importPath(impspec) == path {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
// matchLen returns the length of the longest path segment prefix shared by x and y.
|
|
|
|
func matchLen(x, y string) int {
|
|
|
|
n := 0
|
|
|
|
for i := 0; i < len(x) && i < len(y) && x[i] == y[i]; i++ {
|
|
|
|
if x[i] == '/' {
|
|
|
|
n++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|