1
0
mirror of https://github.com/golang/go synced 2024-11-19 06:04:39 -07:00
go/gopls/integration/parse/parse.go
Rob Findley 9497f49d57 gopls/integration: cosmetic/ergonomic updates
Some ergonomic and cosmetic updates are made to the integration/replay
and integration/parse packages, following a pass through the code:
 + In the README, a some typos are corrected.
 + A few symbols are renamed:
   - Direction->MsgType, since this type is more than a client-server
     direction
   - logRec->scanLogs, to be more consistent with the naming convention
     SplitFuncs in the bufio package
   - CamelCase is applied in a few places.
 + Context is plumbed through, rather than use a package local ctx.
 + In a few cases, returning an error is preferred over log.Fatal.
 + Some duplicated code is deleted, and types from the parse package are
   reused. (this was probably cruft from the repo migration)
 + The logfile is made an argument rather than a flag to the replay
   tool.

Change-Id: Ie72e6e8a4d7020d8cf708f6791353897791bcc86
Reviewed-on: https://go-review.googlesource.com/c/tools/+/211057
Reviewed-by: Peter Weinberger <pjw@google.com>
2020-01-02 14:09:08 +00:00

230 lines
6.0 KiB
Go

// 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 parse provides functions to parse LSP logs.
// Fully processed logs are returned by ToRLog().
package parse
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"regexp"
"strings"
)
// MsgType is the type of message.
type MsgType int
const (
// ClRequest from client to server has method and id
ClRequest MsgType = iota
// ClResponse from server to client
ClResponse
// SvRequest from server to client, has method and id
SvRequest
// SvResponse from client to server
SvResponse
// ToServer notification has method, but no id
ToServer
// ToClient notification
ToClient
// ReportErr is an error message
ReportErr // errors have method and id
)
// Logmsg is the type of a parsed log entry.
type Logmsg struct {
Type MsgType
Method string
ID string // for requests/responses. Client and server request ids overlap
Elapsed string // for responses
Hdr string // header. do we need to keep all these strings?
Rest string // the unparsed result, with newlines or not
Body interface{} // the parsed result
}
// ReadLogs from a file. Most users should use ToRlog().
func ReadLogs(fname string) ([]*Logmsg, error) {
byid := make(map[string]int)
msgs := []*Logmsg{}
fd, err := os.Open(fname)
if err != nil {
return nil, err
}
defer fd.Close()
logrdr := bufio.NewScanner(fd)
logrdr.Buffer(nil, 1<<25) // a large buffer, for safety
logrdr.Split(scanLogs)
for i := 0; logrdr.Scan(); i++ {
flds := strings.SplitN(logrdr.Text(), "\n", 2)
if len(flds) == 1 {
flds = append(flds, "") // for Errors
}
msg, err := parselog(flds[0], flds[1])
if err != nil {
return nil, fmt.Errorf("failed to parse %q: %v", logrdr.Text(), err)
}
switch msg.Type {
case ClRequest, SvRequest:
v, err := msg.unmarshal(Requests(msg.Method))
if err != nil {
return nil, fmt.Errorf("%v for %s, %T", err, msg.Method, Requests(msg.Method))
}
msg.Body = v
case ClResponse, SvResponse:
v, err := msg.doresponse()
if err != nil {
return nil, fmt.Errorf("%v %s", err, msg.Method)
}
msg.Body = v
case ToServer, ToClient:
v, err := msg.unmarshal(Notifs(msg.Method))
if err != nil && Notifs(msg.Method) != nil {
return nil, fmt.Errorf("%s/%T: %v", msg.Method, Notifs(msg.Method), err)
}
msg.Body = v
case ReportErr:
msg.Body = msg.ID // cause?
}
byid[msg.ID]++
msgs = append(msgs, msg)
}
if err = logrdr.Err(); err != nil {
return msgs, err
}
return msgs, nil
}
// parse a single log message, given first line, and the rest
func parselog(first, rest string) (*Logmsg, error) {
if strings.HasPrefix(rest, "Params: ") {
rest = rest[8:]
} else if strings.HasPrefix(rest, "Result: ") {
rest = rest[8:]
}
msg := &Logmsg{Hdr: first, Rest: rest}
fixid := func(s string) string {
// emacs does (n)., gopls does (n)'.
s = strings.Trim(s, "()'.{)")
return s
}
flds := strings.Fields(first)
chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
// gopls and emacs differ in how they report elapsed time
switch {
case chk("Sending request", 9):
msg.Type = ClRequest
msg.Method = flds[6][1:]
msg.ID = fixid(flds[8][:len(flds[8])-2])
case chk("Received response", 11):
msg.Type = ClResponse
msg.Method = flds[6][1:]
msg.ID = fixid(flds[8])
msg.Elapsed = flds[10]
case chk("Received request", 9):
msg.Type = SvRequest
msg.Method = flds[6][1:]
msg.ID = fixid(flds[8])
case chk("Sending response", 11), // gopls
chk("Sending response", 13): // emacs
msg.Type = SvResponse
msg.Method = flds[6][1:]
msg.ID = fixid(flds[8][:len(flds[8])-1])
msg.Elapsed = flds[10]
case chk("Sending notification", 7):
msg.Type = ToServer
msg.Method = strings.Trim(flds[6], ".'")
if len(flds) == 9 {
log.Printf("len=%d method=%s %q", len(flds), msg.Method, first)
}
case chk("Received notification", 7):
msg.Type = ToClient
msg.Method = flds[6][1 : len(flds[6])-2]
case strings.HasPrefix(first, "[Error - "):
msg.Type = ReportErr
both := flds[5]
idx := strings.Index(both, "#") // relies on ID.Number
msg.Method = both[:idx]
msg.ID = fixid(both[idx+1:])
msg.Rest = strings.Join(flds[6:], " ")
default:
return nil, fmt.Errorf("surprise, first=%q with %d flds", first, len(flds))
}
return msg, nil
}
// unmarshal into a proposed type
func (l *Logmsg) unmarshal(p interface{}) (interface{}, error) {
r := []byte(l.Rest)
if err := json.Unmarshal(r, p); err != nil {
// need general alternatives, but for now
// if p is *[]foo and rest is {}, return an empty p (or *p?)
// or, cheat:
if l.Rest == "{}" {
return nil, nil
}
return nil, err
}
return p, nil
}
func (l *Logmsg) doresponse() (interface{}, error) {
for _, x := range Responses(l.Method) {
v, err := l.unmarshal(x)
if err == nil {
return v, nil
}
if x == nil {
return new(interface{}), nil
}
}
// failure!
rr := Responses(l.Method)
for _, x := range rr {
log.Printf("tried %T", x)
}
log.Fatalf("(%d) doresponse failed for %s %q", len(rr), l.Method, l.Rest)
return nil, nil
}
// be a little forgiving in separating log records
var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
// return offset of start of next record, contents of record, error
func scanLogs(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
got := recSep.FindIndex(b)
if got == nil {
if atEOF && len(b) > 0 {
return 0, nil, errors.New("malformed log: all logs should end with a separator")
}
return 0, nil, nil
}
return got[1], b[:got[0]], nil
}
// String returns a user-useful versin of a Direction
func (d MsgType) String() string {
switch d {
case ClRequest:
return "clrequest"
case ClResponse:
return "clresponse"
case SvRequest:
return "svrequest"
case SvResponse:
return "svresponse"
case ToServer:
return "toserver"
case ToClient:
return "toclient"
case ReportErr:
return "reporterr"
}
return fmt.Sprintf("dirname: %d unknown", d)
}