+++ /dev/null
-// 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.Rest // save 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:], " ")
- msg.Rest = `"` + msg.Rest + `"`
- 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)
-}