blob: e0461a152bb85dabd5040d1723186fd1f4914aeb [file] [log] [blame] [edit]
// Copyright 2019 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 hooks
import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
"log"
"math/big"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"unicode"
"github.com/sergi/go-diff/diffmatchpatch"
"golang.org/x/tools/internal/lsp/diff"
"golang.org/x/tools/internal/span"
)
// structure for saving information about diffs
// while the new code is being rolled out
type diffstat struct {
Before, After int
Oldedits, Newedits int
Oldtime, Newtime time.Duration
Stack string
Msg string `json:",omitempty"` // for errors
Ignored int `json:",omitempty"` // numbr of skipped records with 0 edits
}
var (
mu sync.Mutex // serializes writes and protects ignored
difffd io.Writer
ignored int // lots of the diff calls have 0 diffs
)
var fileonce sync.Once
func (s *diffstat) save() {
// save log records in a file in os.TempDir().
// diff is frequently called with identical strings, so
// these are somewhat compressed out
fileonce.Do(func() {
fname := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-diff-%x", os.Getpid()))
fd, err := os.Create(fname)
if err != nil {
// now what?
}
difffd = fd
})
mu.Lock()
defer mu.Unlock()
if s.Oldedits == 0 && s.Newedits == 0 {
if ignored < 15 {
// keep track of repeated instances of no diffs
// but only print every 15th
ignored++
return
}
s.Ignored = ignored + 1
} else {
s.Ignored = ignored
}
ignored = 0
// it would be really nice to see why diff was called
_, f, l, ok := runtime.Caller(2)
if ok {
var fname string
fname = filepath.Base(f) // diff is only called from a few places
s.Stack = fmt.Sprintf("%s:%d", fname, l)
}
x, err := json.Marshal(s)
if err != nil {
log.Print(err) // failure to print statistics should not stop gopls
}
fmt.Fprintf(difffd, "%s\n", x)
}
// save encrypted versions of the broken input and return the file name
// (the saved strings will have the same diff behavior as the user's strings)
func disaster(before, after string) string {
// encrypt before and after for privacy. (randomized monoalphabetic cipher)
// got will contain the substitution cipher
// for the runes in before and after
got := map[rune]rune{}
for _, r := range before {
got[r] = ' ' // value doesn't matter
}
for _, r := range after {
got[r] = ' '
}
repl := initrepl(len(got))
i := 0
for k := range got { // randomized
got[k] = repl[i]
i++
}
// use got to encrypt before and after
subst := func(r rune) rune { return got[r] }
first := strings.Map(subst, before)
second := strings.Map(subst, after)
// one failure per session is enough, and more private.
// this saves the last one.
fname := fmt.Sprintf("%s/gopls-failed-%x", os.TempDir(), os.Getpid())
fd, err := os.Create(fname)
defer fd.Close()
_, err = fd.Write([]byte(fmt.Sprintf("%s\n%s\n", string(first), string(second))))
if err != nil {
// what do we tell the user?
return ""
}
// ask the user to send us the file, somehow
return fname
}
func initrepl(n int) []rune {
repl := make([]rune, 0, n)
for r := rune(0); len(repl) < n; r++ {
if unicode.IsLetter(r) || unicode.IsNumber(r) {
repl = append(repl, r)
}
}
// randomize repl
rdr := rand.Reader
lim := big.NewInt(int64(len(repl)))
for i := 1; i < n; i++ {
v, _ := rand.Int(rdr, lim)
k := v.Int64()
repl[i], repl[k] = repl[k], repl[i]
}
return repl
}
// BothDiffs edits calls both the new and old diffs, checks that the new diffs
// change before into after, and attempts to preserve some statistics.
func BothDiffs(uri span.URI, before, after string) (edits []diff.TextEdit, err error) {
// The new diff code contains a lot of internal checks that panic when they
// fail. This code catches the panics, or other failures, tries to save
// the failing example (and ut wiykd ask the user to send it back to us, and
// changes options.newDiff to 'old', if only we could figure out how.)
stat := diffstat{Before: len(before), After: len(after)}
now := time.Now()
Oldedits, oerr := ComputeEdits(uri, before, after)
if oerr != nil {
stat.Msg += fmt.Sprintf("old:%v", oerr)
}
stat.Oldedits = len(Oldedits)
stat.Oldtime = time.Since(now)
defer func() {
if r := recover(); r != nil {
disaster(before, after)
edits, err = Oldedits, oerr
}
}()
now = time.Now()
Newedits, rerr := diff.NComputeEdits(uri, before, after)
stat.Newedits = len(Newedits)
stat.Newtime = time.Now().Sub(now)
got := diff.ApplyEdits(before, Newedits)
if got != after {
stat.Msg += "FAIL"
disaster(before, after)
stat.save()
return Oldedits, oerr
}
stat.save()
return Newedits, rerr
}
func ComputeEdits(uri span.URI, before, after string) (edits []diff.TextEdit, err error) {
// The go-diff library has an unresolved panic (see golang/go#278774).
// TODO(rstambler): Remove the recover once the issue has been fixed
// upstream.
defer func() {
if r := recover(); r != nil {
edits = nil
err = fmt.Errorf("unable to compute edits for %s: %s", uri.Filename(), r)
}
}()
diffs := diffmatchpatch.New().DiffMain(before, after, true)
edits = make([]diff.TextEdit, 0, len(diffs))
offset := 0
for _, d := range diffs {
start := span.NewPoint(0, 0, offset)
switch d.Type {
case diffmatchpatch.DiffDelete:
offset += len(d.Text)
edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.NewPoint(0, 0, offset))})
case diffmatchpatch.DiffEqual:
offset += len(d.Text)
case diffmatchpatch.DiffInsert:
edits = append(edits, diff.TextEdit{Span: span.New(uri, start, span.Point{}), NewText: d.Text})
}
}
return edits, nil
}