| // 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" |
| "log" |
| "os" |
| "path/filepath" |
| "runtime" |
| "sync" |
| "time" |
| |
| "github.com/sergi/go-diff/diffmatchpatch" |
| "golang.org/x/tools/gopls/internal/util/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 := os.CreateTemp("", "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 := os.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 |
| } |