// Copyright 2018 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 provides core features for use by Go editors and tools. package source import ( "bytes" "context" "fmt" "go/ast" "go/format" "go/parser" "go/scanner" "go/token" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/lsp/diff" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/trace" errors "golang.org/x/xerrors" ) // Format formats a file with a given range. func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.TextEdit, error) { ctx, done := trace.StartSpan(ctx, "source.Format") defer done() pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestCheckPackageHandle) if err != nil { return nil, fmt.Errorf("getting file for Format: %v", err) } // Be extra careful that the file's ParseMode is correct, // otherwise we might replace the user's code with a trimmed AST. if pgh.Mode() != ParseFull { return nil, errors.Errorf("%s was parsed in the incorrect mode", pgh.File().Identity().URI) } file, m, _, err := pgh.Parse(ctx) if err != nil { return nil, err } if hasListErrors(pkg) || hasParseErrors(pkg, fh.Identity().URI) { // Even if this package has list or parse errors, this file may not // have any parse errors and can still be formatted. Using format.Node // on an ast with errors may result in code being added or removed. // Attempt to format the source of this file instead. formatted, err := formatSource(ctx, snapshot, fh) if err != nil { return nil, err } return computeTextEdits(ctx, snapshot.View(), pgh.File(), m, string(formatted)) } fset := snapshot.View().Session().Cache().FileSet() buf := &bytes.Buffer{} // format.Node changes slightly from one release to another, so the version // of Go used to build the LSP server will determine how it formats code. // This should be acceptable for all users, who likely be prompted to rebuild // the LSP server on each Go release. if err := format.Node(buf, fset, file); err != nil { return nil, err } return computeTextEdits(ctx, snapshot.View(), pgh.File(), m, buf.String()) } func formatSource(ctx context.Context, s Snapshot, fh FileHandle) ([]byte, error) { ctx, done := trace.StartSpan(ctx, "source.formatSource") defer done() data, _, err := fh.Read(ctx) if err != nil { return nil, err } return format.Source(data) } type ImportFix struct { Fix *imports.ImportFix Edits []protocol.TextEdit } // AllImportsFixes formats f for each possible fix to the imports. // In addition to returning the result of applying all edits, // it returns a list of fixes that could be applied to the file, with the // corresponding TextEdits that would be needed to apply that fix. func AllImportsFixes(ctx context.Context, snapshot Snapshot, fh FileHandle) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { ctx, done := trace.StartSpan(ctx, "source.AllImportsFixes") defer done() pkg, pgh, err := getParsedFile(ctx, snapshot, fh, NarrowestCheckPackageHandle) if err != nil { return nil, nil, errors.Errorf("getting file for AllImportsFixes: %v", err) } if hasListErrors(pkg) { return nil, nil, errors.Errorf("%s has list errors, not running goimports", fh.Identity().URI) } err = snapshot.View().RunProcessEnvFunc(ctx, func(opts *imports.Options) error { allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot.View(), pgh, opts) return err }) if err != nil { return nil, nil, errors.Errorf("computing fix edits: %v", err) } return allFixEdits, editsPerFix, nil } // computeImportEdits computes a set of edits that perform one or all of the // necessary import fixes. func computeImportEdits(ctx context.Context, view View, ph ParseGoHandle, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { filename := ph.File().Identity().URI.Filename() // Build up basic information about the original file. origData, _, err := ph.File().Read(ctx) if err != nil { return nil, nil, err } origAST, origMapper, _, err := ph.Parse(ctx) if err != nil { return nil, nil, err } allFixes, err := imports.FixImports(filename, origData, options) if err != nil { return nil, nil, err } origImports, origImportOffset := trimToImports(view.Session().Cache().FileSet(), origAST, origData) allFixEdits, err = computeFixEdits(view, ph, options, origData, origAST, origMapper, origImports, origImportOffset, allFixes) if err != nil { return nil, nil, err } // Apply all of the import fixes to the file. // Add the edits for each fix to the result. for _, fix := range allFixes { edits, err := computeFixEdits(view, ph, options, origData, origAST, origMapper, origImports, origImportOffset, []*imports.ImportFix{fix}) if err != nil { return nil, nil, err } editsPerFix = append(editsPerFix, &ImportFix{ Fix: fix, Edits: edits, }) } return allFixEdits, editsPerFix, nil } func computeOneImportFixEdits(ctx context.Context, view View, ph ParseGoHandle, fix *imports.ImportFix) ([]protocol.TextEdit, error) { origData, _, err := ph.File().Read(ctx) if err != nil { return nil, err } origAST, origMapper, _, err := ph.Parse(ctx) if err != nil { return nil, err } origImports, origImportOffset := trimToImports(view.Session().Cache().FileSet(), origAST, origData) options := &imports.Options{ // Defaults. AllErrors: true, Comments: true, Fragment: true, FormatOnly: false, TabIndent: true, TabWidth: 8, } return computeFixEdits(view, ph, options, origData, origAST, origMapper, origImports, origImportOffset, []*imports.ImportFix{fix}) } func computeFixEdits(view View, ph ParseGoHandle, options *imports.Options, origData []byte, origAST *ast.File, origMapper *protocol.ColumnMapper, origImports []byte, origImportOffset int, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { filename := ph.File().Identity().URI.Filename() // Apply the fixes and re-parse the file so that we can locate the // new imports. fixedData, err := imports.ApplyFixes(fixes, filename, origData, options, parser.ImportsOnly) fixedData = append(fixedData, '\n') // ApplyFixes comes out missing the newline, go figure. if err != nil { return nil, err } fixedFset := token.NewFileSet() fixedAST, err := parser.ParseFile(fixedFset, filename, fixedData, parser.ImportsOnly) // Any error here prevents us from computing the edits. if err != nil { return nil, err } fixedImports, fixedImportsOffset := trimToImports(fixedFset, fixedAST, fixedData) // Prepare the diff. If both sides had import statements, we can diff // just those sections against each other, then shift the resulting // edits to the right lines in the original file. left, right := origImports, fixedImports converter := span.NewContentConverter(filename, origImports) offset := origImportOffset // If one side or the other has no imports, we won't know where to // anchor the diffs. Instead, use the beginning of the file, up to its // first non-imports decl. We know the imports code will insert // somewhere before that. if origImportOffset == 0 || fixedImportsOffset == 0 { left, _ = trimToFirstNonImport(view.Session().Cache().FileSet(), origAST, origData, nil) fixedData, err = imports.ApplyFixes(fixes, filename, origData, options, 0) if err != nil { return nil, err } // We need the whole file here, not just the ImportsOnly versions we made above. fixedAST, err = parser.ParseFile(fixedFset, filename, fixedData, 0) if fixedAST == nil { return nil, err } var ok bool right, ok = trimToFirstNonImport(fixedFset, fixedAST, fixedData, err) if !ok { return nil, errors.Errorf("error %v detected in the import block", err) } // We're now working with a prefix of the original file, so we can // use the original converter, and there is no offset on the edits. converter = origMapper.Converter offset = 0 } // Perform the diff and adjust the results for the trimming, if any. edits := view.Options().ComputeEdits(ph.File().Identity().URI, string(left), string(right)) for i := range edits { s, err := edits[i].Span.WithPosition(converter) if err != nil { return nil, err } start := span.NewPoint(s.Start().Line()+offset, s.Start().Column(), -1) end := span.NewPoint(s.End().Line()+offset, s.End().Column(), -1) edits[i].Span = span.New(s.URI(), start, end) } return ToProtocolEdits(origMapper, edits) } // trimToImports returns a section of the source file that covers all of the // import declarations, and the line offset into the file that section starts at. func trimToImports(fset *token.FileSet, f *ast.File, src []byte) ([]byte, int) { var firstImport, lastImport ast.Decl for _, decl := range f.Decls { if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT { if firstImport == nil { firstImport = decl } lastImport = decl } } if firstImport == nil { return nil, 0 } tok := fset.File(f.Pos()) start := firstImport.Pos() end := lastImport.End() if tok.LineCount() > fset.Position(end).Line { end = fset.File(f.Pos()).LineStart(fset.Position(lastImport.End()).Line + 1) } startLineOffset := fset.Position(start).Line - 1 // lines are 1-indexed. return src[fset.Position(firstImport.Pos()).Offset:fset.Position(end).Offset], startLineOffset } // trimToFirstNonImport returns src from the beginning to the first non-import // declaration, or the end of the file if there is no such decl. func trimToFirstNonImport(fset *token.FileSet, f *ast.File, src []byte, err error) ([]byte, bool) { var firstDecl ast.Decl for _, decl := range f.Decls { if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.IMPORT { continue } firstDecl = decl break } tok := fset.File(f.Pos()) if tok == nil { return nil, false } end := f.End() if firstDecl != nil { if firstDeclLine := fset.Position(firstDecl.Pos()).Line; firstDeclLine > 1 { end = tok.LineStart(firstDeclLine - 1) } } // Any errors in the file must be after the part of the file that we care about. switch err := err.(type) { case *scanner.Error: pos := tok.Pos(err.Pos.Offset) if pos <= end { return nil, false } case scanner.ErrorList: if err.Len() > 0 { pos := tok.Pos(err[0].Pos.Offset) if pos <= end { return nil, false } } } return src[0:fset.Position(end).Offset], true } // hasParseErrors returns true if the given file has parse errors. func hasParseErrors(pkg Package, uri span.URI) bool { for _, e := range pkg.GetErrors() { if e.File.URI == uri && e.Kind == ParseError { return true } } return false } func hasListErrors(pkg Package) bool { for _, err := range pkg.GetErrors() { if err.Kind == ListError { return true } } return false } func computeTextEdits(ctx context.Context, view View, fh FileHandle, m *protocol.ColumnMapper, formatted string) ([]protocol.TextEdit, error) { ctx, done := trace.StartSpan(ctx, "source.computeTextEdits") defer done() data, _, err := fh.Read(ctx) if err != nil { return nil, err } edits := view.Options().ComputeEdits(fh.Identity().URI, string(data), formatted) return ToProtocolEdits(m, edits) } func ToProtocolEdits(m *protocol.ColumnMapper, edits []diff.TextEdit) ([]protocol.TextEdit, error) { if edits == nil { return nil, nil } result := make([]protocol.TextEdit, len(edits)) for i, edit := range edits { rng, err := m.Range(edit.Span) if err != nil { return nil, err } result[i] = protocol.TextEdit{ Range: rng, NewText: edit.NewText, } } return result, nil } func FromProtocolEdits(m *protocol.ColumnMapper, edits []protocol.TextEdit) ([]diff.TextEdit, error) { if edits == nil { return nil, nil } result := make([]diff.TextEdit, len(edits)) for i, edit := range edits { spn, err := m.RangeSpan(edit.Range) if err != nil { return nil, err } result[i] = diff.TextEdit{ Span: spn, NewText: edit.NewText, } } return result, nil }