golang.org/x/tools/gopls@v0.15.3/internal/golang/completion/package.go (about) 1 // Copyright 2020 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 completion 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "go/ast" 13 "go/parser" 14 "go/scanner" 15 "go/token" 16 "go/types" 17 "path/filepath" 18 "strings" 19 "unicode" 20 21 "golang.org/x/tools/gopls/internal/cache" 22 "golang.org/x/tools/gopls/internal/file" 23 "golang.org/x/tools/gopls/internal/golang" 24 "golang.org/x/tools/gopls/internal/protocol" 25 "golang.org/x/tools/gopls/internal/util/safetoken" 26 "golang.org/x/tools/internal/fuzzy" 27 ) 28 29 // packageClauseCompletions offers completions for a package declaration when 30 // one is not present in the given file. 31 func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]CompletionItem, *Selection, error) { 32 // We know that the AST for this file will be empty due to the missing 33 // package declaration, but parse it anyway to get a mapper. 34 // TODO(adonovan): opt: there's no need to parse just to get a mapper. 35 pgf, err := snapshot.ParseGo(ctx, fh, golang.ParseFull) 36 if err != nil { 37 return nil, nil, err 38 } 39 40 offset, err := pgf.Mapper.PositionOffset(position) 41 if err != nil { 42 return nil, nil, err 43 } 44 surrounding, err := packageCompletionSurrounding(pgf, offset) 45 if err != nil { 46 return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) 47 } 48 49 packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "") 50 if err != nil { 51 return nil, nil, err 52 } 53 54 var items []CompletionItem 55 for _, pkg := range packageSuggestions { 56 insertText := fmt.Sprintf("package %s", pkg.name) 57 items = append(items, CompletionItem{ 58 Label: insertText, 59 Kind: protocol.ModuleCompletion, 60 InsertText: insertText, 61 Score: pkg.score, 62 }) 63 } 64 65 return items, surrounding, nil 66 } 67 68 // packageCompletionSurrounding returns surrounding for package completion if a 69 // package completions can be suggested at a given cursor offset. A valid location 70 // for package completion is above any declarations or import statements. 71 func packageCompletionSurrounding(pgf *golang.ParsedGoFile, offset int) (*Selection, error) { 72 m := pgf.Mapper 73 // If the file lacks a package declaration, the parser will return an empty 74 // AST. As a work-around, try to parse an expression from the file contents. 75 fset := token.NewFileSet() 76 expr, _ := parser.ParseExprFrom(fset, m.URI.Path(), pgf.Src, parser.Mode(0)) 77 if expr == nil { 78 return nil, fmt.Errorf("unparseable file (%s)", m.URI) 79 } 80 tok := fset.File(expr.Pos()) 81 cursor := tok.Pos(offset) 82 83 // If we were able to parse out an identifier as the first expression from 84 // the file, it may be the beginning of a package declaration ("pack "). 85 // We can offer package completions if the cursor is in the identifier. 86 if name, ok := expr.(*ast.Ident); ok { 87 if cursor >= name.Pos() && cursor <= name.End() { 88 if !strings.HasPrefix(PACKAGE, name.Name) { 89 return nil, fmt.Errorf("cursor in non-matching ident") 90 } 91 return &Selection{ 92 content: name.Name, 93 cursor: cursor, 94 tokFile: tok, 95 start: name.Pos(), 96 end: name.End(), 97 mapper: m, 98 }, nil 99 } 100 } 101 102 // The file is invalid, but it contains an expression that we were able to 103 // parse. We will use this expression to construct the cursor's 104 // "surrounding". 105 106 // First, consider the possibility that we have a valid "package" keyword 107 // with an empty package name ("package "). "package" is parsed as an 108 // *ast.BadDecl since it is a keyword. This logic would allow "package" to 109 // appear on any line of the file as long as it's the first code expression 110 // in the file. 111 lines := strings.Split(string(pgf.Src), "\n") 112 cursorLine := safetoken.Line(tok, cursor) 113 if cursorLine <= 0 || cursorLine > len(lines) { 114 return nil, fmt.Errorf("invalid line number") 115 } 116 if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine { 117 words := strings.Fields(lines[cursorLine-1]) 118 if len(words) > 0 && words[0] == PACKAGE { 119 content := PACKAGE 120 // Account for spaces if there are any. 121 if len(words) > 1 { 122 content += " " 123 } 124 125 start := expr.Pos() 126 end := token.Pos(int(expr.Pos()) + len(content) + 1) 127 // We have verified that we have a valid 'package' keyword as our 128 // first expression. Ensure that cursor is in this keyword or 129 // otherwise fallback to the general case. 130 if cursor >= start && cursor <= end { 131 return &Selection{ 132 content: content, 133 cursor: cursor, 134 tokFile: tok, 135 start: start, 136 end: end, 137 mapper: m, 138 }, nil 139 } 140 } 141 } 142 143 // If the cursor is after the start of the expression, no package 144 // declaration will be valid. 145 if cursor > expr.Pos() { 146 return nil, fmt.Errorf("cursor after expression") 147 } 148 149 // If the cursor is in a comment, don't offer any completions. 150 if cursorInComment(tok, cursor, m.Content) { 151 return nil, fmt.Errorf("cursor in comment") 152 } 153 154 // The surrounding range in this case is the cursor. 155 return &Selection{ 156 content: "", 157 tokFile: tok, 158 start: cursor, 159 end: cursor, 160 cursor: cursor, 161 mapper: m, 162 }, nil 163 } 164 165 func cursorInComment(file *token.File, cursor token.Pos, src []byte) bool { 166 var s scanner.Scanner 167 s.Init(file, src, func(_ token.Position, _ string) {}, scanner.ScanComments) 168 for { 169 pos, tok, lit := s.Scan() 170 if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) { 171 return tok == token.COMMENT 172 } 173 if tok == token.EOF { 174 break 175 } 176 } 177 return false 178 } 179 180 // packageNameCompletions returns name completions for a package clause using 181 // the current name as prefix. 182 func (c *completer) packageNameCompletions(ctx context.Context, fileURI protocol.DocumentURI, name *ast.Ident) error { 183 cursor := int(c.pos - name.NamePos) 184 if cursor < 0 || cursor > len(name.Name) { 185 return errors.New("cursor is not in package name identifier") 186 } 187 188 c.completionContext.packageCompletion = true 189 190 prefix := name.Name[:cursor] 191 packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix) 192 if err != nil { 193 return err 194 } 195 196 for _, pkg := range packageSuggestions { 197 c.deepState.enqueue(pkg) 198 } 199 return nil 200 } 201 202 // packageSuggestions returns a list of packages from workspace packages that 203 // have the given prefix and are used in the same directory as the given 204 // file. This also includes test packages for these packages (<pkg>_test) and 205 // the directory name itself. 206 func packageSuggestions(ctx context.Context, snapshot *cache.Snapshot, fileURI protocol.DocumentURI, prefix string) (packages []candidate, err error) { 207 active, err := snapshot.WorkspaceMetadata(ctx) 208 if err != nil { 209 return nil, err 210 } 211 212 toCandidate := func(name string, score float64) candidate { 213 obj := types.NewPkgName(0, nil, name, types.NewPackage("", name)) 214 return candidate{obj: obj, name: name, detail: name, score: score} 215 } 216 217 matcher := fuzzy.NewMatcher(prefix) 218 219 // Always try to suggest a main package 220 defer func() { 221 if score := float64(matcher.Score("main")); score > 0 { 222 packages = append(packages, toCandidate("main", score*lowScore)) 223 } 224 }() 225 226 dirPath := filepath.Dir(fileURI.Path()) 227 dirName := filepath.Base(dirPath) 228 if !isValidDirName(dirName) { 229 return packages, nil 230 } 231 pkgName := convertDirNameToPkgName(dirName) 232 233 seenPkgs := make(map[golang.PackageName]struct{}) 234 235 // The `go` command by default only allows one package per directory but we 236 // support multiple package suggestions since gopls is build system agnostic. 237 for _, mp := range active { 238 if mp.Name == "main" || mp.Name == "" { 239 continue 240 } 241 if _, ok := seenPkgs[mp.Name]; ok { 242 continue 243 } 244 245 // Only add packages that are previously used in the current directory. 246 var relevantPkg bool 247 for _, uri := range mp.CompiledGoFiles { 248 if filepath.Dir(uri.Path()) == dirPath { 249 relevantPkg = true 250 break 251 } 252 } 253 if !relevantPkg { 254 continue 255 } 256 257 // Add a found package used in current directory as a high relevance 258 // suggestion and the test package for it as a medium relevance 259 // suggestion. 260 if score := float64(matcher.Score(string(mp.Name))); score > 0 { 261 packages = append(packages, toCandidate(string(mp.Name), score*highScore)) 262 } 263 seenPkgs[mp.Name] = struct{}{} 264 265 testPkgName := mp.Name + "_test" 266 if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(mp.Name), "_test") { 267 continue 268 } 269 if score := float64(matcher.Score(string(testPkgName))); score > 0 { 270 packages = append(packages, toCandidate(string(testPkgName), score*stdScore)) 271 } 272 seenPkgs[testPkgName] = struct{}{} 273 } 274 275 // Add current directory name as a low relevance suggestion. 276 if _, ok := seenPkgs[pkgName]; !ok { 277 if score := float64(matcher.Score(string(pkgName))); score > 0 { 278 packages = append(packages, toCandidate(string(pkgName), score*lowScore)) 279 } 280 281 testPkgName := pkgName + "_test" 282 if score := float64(matcher.Score(string(testPkgName))); score > 0 { 283 packages = append(packages, toCandidate(string(testPkgName), score*lowScore)) 284 } 285 } 286 287 return packages, nil 288 } 289 290 // isValidDirName checks whether the passed directory name can be used in 291 // a package path. Requirements for a package path can be found here: 292 // https://golang.org/ref/mod#go-mod-file-ident. 293 func isValidDirName(dirName string) bool { 294 if dirName == "" { 295 return false 296 } 297 298 for i, ch := range dirName { 299 if isLetter(ch) || isDigit(ch) { 300 continue 301 } 302 if i == 0 { 303 // Directory name can start only with '_'. '.' is not allowed in module paths. 304 // '-' and '~' are not allowed because elements of package paths must be 305 // safe command-line arguments. 306 if ch == '_' { 307 continue 308 } 309 } else { 310 // Modules path elements can't end with '.' 311 if isAllowedPunctuation(ch) && (i != len(dirName)-1 || ch != '.') { 312 continue 313 } 314 } 315 316 return false 317 } 318 return true 319 } 320 321 // convertDirNameToPkgName converts a valid directory name to a valid package name. 322 // It leaves only letters and digits. All letters are mapped to lower case. 323 func convertDirNameToPkgName(dirName string) golang.PackageName { 324 var buf bytes.Buffer 325 for _, ch := range dirName { 326 switch { 327 case isLetter(ch): 328 buf.WriteRune(unicode.ToLower(ch)) 329 330 case buf.Len() != 0 && isDigit(ch): 331 buf.WriteRune(ch) 332 } 333 } 334 return golang.PackageName(buf.String()) 335 } 336 337 // isLetter and isDigit allow only ASCII characters because 338 // "Each path element is a non-empty string made of up ASCII letters, 339 // ASCII digits, and limited ASCII punctuation" 340 // (see https://golang.org/ref/mod#go-mod-file-ident). 341 342 func isLetter(ch rune) bool { 343 return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' 344 } 345 346 func isDigit(ch rune) bool { 347 return '0' <= ch && ch <= '9' 348 } 349 350 func isAllowedPunctuation(ch rune) bool { 351 return ch == '_' || ch == '-' || ch == '~' || ch == '.' 352 }