golang.org/x/tools/gopls@v0.15.3/internal/golang/format.go (about) 1 // Copyright 2018 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 golang defines the LSP features for navigation, analysis, 6 // and refactoring of Go source code. 7 package golang 8 9 import ( 10 "bytes" 11 "context" 12 "fmt" 13 "go/ast" 14 "go/format" 15 "go/parser" 16 "go/token" 17 "strings" 18 "text/scanner" 19 20 "golang.org/x/tools/gopls/internal/cache" 21 "golang.org/x/tools/gopls/internal/file" 22 "golang.org/x/tools/gopls/internal/protocol" 23 "golang.org/x/tools/gopls/internal/util/safetoken" 24 "golang.org/x/tools/internal/diff" 25 "golang.org/x/tools/internal/event" 26 "golang.org/x/tools/internal/imports" 27 "golang.org/x/tools/internal/tokeninternal" 28 ) 29 30 // Format formats a file with a given range. 31 func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) { 32 ctx, done := event.Start(ctx, "golang.Format") 33 defer done() 34 35 // Generated files shouldn't be edited. So, don't format them 36 if IsGenerated(ctx, snapshot, fh.URI()) { 37 return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) 38 } 39 40 pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) 41 if err != nil { 42 return nil, err 43 } 44 // Even if this file has parse errors, it might still be possible to format it. 45 // Using format.Node on an AST with errors may result in code being modified. 46 // Attempt to format the source of this file instead. 47 if pgf.ParseErr != nil { 48 formatted, err := formatSource(ctx, fh) 49 if err != nil { 50 return nil, err 51 } 52 return computeTextEdits(ctx, pgf, string(formatted)) 53 } 54 55 // format.Node changes slightly from one release to another, so the version 56 // of Go used to build the LSP server will determine how it formats code. 57 // This should be acceptable for all users, who likely be prompted to rebuild 58 // the LSP server on each Go release. 59 buf := &bytes.Buffer{} 60 fset := tokeninternal.FileSetFor(pgf.Tok) 61 if err := format.Node(buf, fset, pgf.File); err != nil { 62 return nil, err 63 } 64 formatted := buf.String() 65 66 // Apply additional formatting, if any is supported. Currently, the only 67 // supported additional formatter is gofumpt. 68 if format := snapshot.Options().GofumptFormat; snapshot.Options().Gofumpt && format != nil { 69 // gofumpt can customize formatting based on language version and module 70 // path, if available. 71 // 72 // Try to derive this information, but fall-back on the default behavior. 73 // 74 // TODO: under which circumstances can we fail to find module information? 75 // Can this, for example, result in inconsistent formatting across saves, 76 // due to pending calls to packages.Load? 77 var langVersion, modulePath string 78 meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) 79 if err == nil { 80 if mi := meta.Module; mi != nil { 81 langVersion = mi.GoVersion 82 modulePath = mi.Path 83 } 84 } 85 b, err := format(ctx, langVersion, modulePath, buf.Bytes()) 86 if err != nil { 87 return nil, err 88 } 89 formatted = string(b) 90 } 91 return computeTextEdits(ctx, pgf, formatted) 92 } 93 94 func formatSource(ctx context.Context, fh file.Handle) ([]byte, error) { 95 _, done := event.Start(ctx, "golang.formatSource") 96 defer done() 97 98 data, err := fh.Content() 99 if err != nil { 100 return nil, err 101 } 102 return format.Source(data) 103 } 104 105 type importFix struct { 106 fix *imports.ImportFix 107 edits []protocol.TextEdit 108 } 109 110 // allImportsFixes formats f for each possible fix to the imports. 111 // In addition to returning the result of applying all edits, 112 // it returns a list of fixes that could be applied to the file, with the 113 // corresponding TextEdits that would be needed to apply that fix. 114 func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { 115 ctx, done := event.Start(ctx, "golang.AllImportsFixes") 116 defer done() 117 118 if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { 119 allFixEdits, editsPerFix, err = computeImportEdits(ctx, pgf, opts) 120 return err 121 }); err != nil { 122 return nil, nil, fmt.Errorf("AllImportsFixes: %v", err) 123 } 124 return allFixEdits, editsPerFix, nil 125 } 126 127 // computeImportEdits computes a set of edits that perform one or all of the 128 // necessary import fixes. 129 func computeImportEdits(ctx context.Context, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { 130 filename := pgf.URI.Path() 131 132 // Build up basic information about the original file. 133 allFixes, err := imports.FixImports(ctx, filename, pgf.Src, options) 134 if err != nil { 135 return nil, nil, err 136 } 137 138 allFixEdits, err = computeFixEdits(pgf, options, allFixes) 139 if err != nil { 140 return nil, nil, err 141 } 142 143 // Apply all of the import fixes to the file. 144 // Add the edits for each fix to the result. 145 for _, fix := range allFixes { 146 edits, err := computeFixEdits(pgf, options, []*imports.ImportFix{fix}) 147 if err != nil { 148 return nil, nil, err 149 } 150 editsPerFix = append(editsPerFix, &importFix{ 151 fix: fix, 152 edits: edits, 153 }) 154 } 155 return allFixEdits, editsPerFix, nil 156 } 157 158 // ComputeOneImportFixEdits returns text edits for a single import fix. 159 func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *ParsedGoFile, fix *imports.ImportFix) ([]protocol.TextEdit, error) { 160 options := &imports.Options{ 161 LocalPrefix: snapshot.Options().Local, 162 // Defaults. 163 AllErrors: true, 164 Comments: true, 165 Fragment: true, 166 FormatOnly: false, 167 TabIndent: true, 168 TabWidth: 8, 169 } 170 return computeFixEdits(pgf, options, []*imports.ImportFix{fix}) 171 } 172 173 func computeFixEdits(pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { 174 // trim the original data to match fixedData 175 left, err := importPrefix(pgf.Src) 176 if err != nil { 177 return nil, err 178 } 179 extra := !strings.Contains(left, "\n") // one line may have more than imports 180 if extra { 181 left = string(pgf.Src) 182 } 183 if len(left) > 0 && left[len(left)-1] != '\n' { 184 left += "\n" 185 } 186 // Apply the fixes and re-parse the file so that we can locate the 187 // new imports. 188 flags := parser.ImportsOnly 189 if extra { 190 // used all of origData above, use all of it here too 191 flags = 0 192 } 193 fixedData, err := imports.ApplyFixes(fixes, "", pgf.Src, options, flags) 194 if err != nil { 195 return nil, err 196 } 197 if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { 198 fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. 199 } 200 edits := diff.Strings(left, string(fixedData)) 201 return protocolEditsFromSource([]byte(left), edits) 202 } 203 204 // importPrefix returns the prefix of the given file content through the final 205 // import statement. If there are no imports, the prefix is the package 206 // statement and any comment groups below it. 207 func importPrefix(src []byte) (string, error) { 208 fset := token.NewFileSet() 209 // do as little parsing as possible 210 f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly|parser.ParseComments) 211 if err != nil { // This can happen if 'package' is misspelled 212 return "", fmt.Errorf("importPrefix: failed to parse: %s", err) 213 } 214 tok := fset.File(f.Pos()) 215 var importEnd int 216 for _, d := range f.Decls { 217 if x, ok := d.(*ast.GenDecl); ok && x.Tok == token.IMPORT { 218 if e, err := safetoken.Offset(tok, d.End()); err != nil { 219 return "", fmt.Errorf("importPrefix: %s", err) 220 } else if e > importEnd { 221 importEnd = e 222 } 223 } 224 } 225 226 maybeAdjustToLineEnd := func(pos token.Pos, isCommentNode bool) int { 227 offset, err := safetoken.Offset(tok, pos) 228 if err != nil { 229 return -1 230 } 231 232 // Don't go past the end of the file. 233 if offset > len(src) { 234 offset = len(src) 235 } 236 // The go/ast package does not account for different line endings, and 237 // specifically, in the text of a comment, it will strip out \r\n line 238 // endings in favor of \n. To account for these differences, we try to 239 // return a position on the next line whenever possible. 240 switch line := safetoken.Line(tok, tok.Pos(offset)); { 241 case line < tok.LineCount(): 242 nextLineOffset, err := safetoken.Offset(tok, tok.LineStart(line+1)) 243 if err != nil { 244 return -1 245 } 246 // If we found a position that is at the end of a line, move the 247 // offset to the start of the next line. 248 if offset+1 == nextLineOffset { 249 offset = nextLineOffset 250 } 251 case isCommentNode, offset+1 == tok.Size(): 252 // If the last line of the file is a comment, or we are at the end 253 // of the file, the prefix is the entire file. 254 offset = len(src) 255 } 256 return offset 257 } 258 if importEnd == 0 { 259 pkgEnd := f.Name.End() 260 importEnd = maybeAdjustToLineEnd(pkgEnd, false) 261 } 262 for _, cgroup := range f.Comments { 263 for _, c := range cgroup.List { 264 if end, err := safetoken.Offset(tok, c.End()); err != nil { 265 return "", err 266 } else if end > importEnd { 267 startLine := safetoken.Position(tok, c.Pos()).Line 268 endLine := safetoken.Position(tok, c.End()).Line 269 270 // Work around golang/go#41197 by checking if the comment might 271 // contain "\r", and if so, find the actual end position of the 272 // comment by scanning the content of the file. 273 startOffset, err := safetoken.Offset(tok, c.Pos()) 274 if err != nil { 275 return "", err 276 } 277 if startLine != endLine && bytes.Contains(src[startOffset:], []byte("\r")) { 278 if commentEnd := scanForCommentEnd(src[startOffset:]); commentEnd > 0 { 279 end = startOffset + commentEnd 280 } 281 } 282 importEnd = maybeAdjustToLineEnd(tok.Pos(end), true) 283 } 284 } 285 } 286 if importEnd > len(src) { 287 importEnd = len(src) 288 } 289 return string(src[:importEnd]), nil 290 } 291 292 // scanForCommentEnd returns the offset of the end of the multi-line comment 293 // at the start of the given byte slice. 294 func scanForCommentEnd(src []byte) int { 295 var s scanner.Scanner 296 s.Init(bytes.NewReader(src)) 297 s.Mode ^= scanner.SkipComments 298 299 t := s.Scan() 300 if t == scanner.Comment { 301 return s.Pos().Offset 302 } 303 return 0 304 } 305 306 func computeTextEdits(ctx context.Context, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) { 307 _, done := event.Start(ctx, "golang.computeTextEdits") 308 defer done() 309 310 edits := diff.Strings(string(pgf.Src), formatted) 311 return protocol.EditsFromDiffEdits(pgf.Mapper, edits) 312 } 313 314 // protocolEditsFromSource converts text edits to LSP edits using the original 315 // source. 316 func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { 317 m := protocol.NewMapper("", src) 318 var result []protocol.TextEdit 319 for _, edit := range edits { 320 rng, err := m.OffsetRange(edit.Start, edit.End) 321 if err != nil { 322 return nil, err 323 } 324 325 if rng.Start == rng.End && edit.New == "" { 326 // Degenerate case, which may result from a diff tool wanting to delete 327 // '\r' in line endings. Filter it out. 328 continue 329 } 330 result = append(result, protocol.TextEdit{ 331 Range: rng, 332 NewText: edit.New, 333 }) 334 } 335 return result, nil 336 }