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  }