1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169
|
// 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 (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/sergi/go-diff/diffmatchpatch"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/diff"
)
// 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 (
ignoredMu sync.Mutex
ignored int // counter of diff requests on equal strings
diffStatsOnce sync.Once
diffStats *os.File // never closed
)
// save writes a JSON record of statistics about diff requests to a temporary file.
func (s *diffstat) save() {
diffStatsOnce.Do(func() {
f, err := ioutil.TempFile("", "gopls-diff-stats-*")
if err != nil {
log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full
return
}
diffStats = f
})
if diffStats == nil {
return
}
// diff is frequently called with equal strings,
// so we count repeated instances but only print every 15th.
ignoredMu.Lock()
if s.Oldedits == 0 && s.Newedits == 0 {
ignored++
if ignored < 15 {
ignoredMu.Unlock()
return
}
}
s.Ignored = ignored
ignored = 0
ignoredMu.Unlock()
// Record the name of the file in which diff was called.
// There aren't many calls, so only the base name is needed.
if _, file, line, ok := runtime.Caller(2); ok {
s.Stack = fmt.Sprintf("%s:%d", filepath.Base(file), line)
}
x, err := json.Marshal(s)
if err != nil {
log.Fatalf("internal error marshalling JSON: %v", err)
}
fmt.Fprintf(diffStats, "%s\n", x)
}
// disaster is called when the diff algorithm panics or produces a
// diff that cannot be applied. It saves the broken input in a
// new temporary file and logs the file name, which is returned.
func disaster(before, after string) string {
// We use the pid to salt the name, not os.TempFile,
// so that each process creates at most one file.
// One is sufficient for a bug report.
filename := fmt.Sprintf("%s/gopls-diff-bug-%x", os.TempDir(), os.Getpid())
// We use NUL as a separator: it should never appear in Go source.
data := before + "\x00" + after
if err := ioutil.WriteFile(filename, []byte(data), 0600); err != nil {
log.Printf("failed to write diff bug report: %v", err)
return ""
}
bug.Reportf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename)
return filename
}
// 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(before, after string) (edits []diff.Edit) {
// 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 it would 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 := ComputeEdits(before, after)
stat.Oldedits = len(oldedits)
stat.Oldtime = time.Since(now)
defer func() {
if r := recover(); r != nil {
disaster(before, after)
edits = oldedits
}
}()
now = time.Now()
newedits := diff.Strings(before, after)
stat.Newedits = len(newedits)
stat.Newtime = time.Now().Sub(now)
got, err := diff.Apply(before, newedits)
if err != nil || got != after {
stat.Msg += "FAIL"
disaster(before, after)
stat.save()
return oldedits
}
stat.save()
return newedits
}
// ComputeEdits computes a diff using the github.com/sergi/go-diff implementation.
func ComputeEdits(before, after string) (edits []diff.Edit) {
// 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 {
bug.Reportf("unable to compute edits: %s", r)
// Report one big edit for the whole file.
edits = []diff.Edit{{
Start: 0,
End: len(before),
New: after,
}}
}
}()
diffs := diffmatchpatch.New().DiffMain(before, after, true)
edits = make([]diff.Edit, 0, len(diffs))
offset := 0
for _, d := range diffs {
start := offset
switch d.Type {
case diffmatchpatch.DiffDelete:
offset += len(d.Text)
edits = append(edits, diff.Edit{Start: start, End: offset})
case diffmatchpatch.DiffEqual:
offset += len(d.Text)
case diffmatchpatch.DiffInsert:
edits = append(edits, diff.Edit{Start: start, End: start, New: d.Text})
}
}
return edits
}
|