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