diff --git a/internal/jsonrpc2/handler.go b/internal/jsonrpc2/handler.go index 82079564a9..0072b72f50 100644 --- a/internal/jsonrpc2/handler.go +++ b/internal/jsonrpc2/handler.go @@ -22,7 +22,7 @@ type Handler func(context.Context, *Request) error // standard method not found response. // This should normally be the final handler in a chain. func MethodNotFound(ctx context.Context, r *Request) error { - return r.Reply(ctx, nil, NewErrorf(CodeMethodNotFound, "method %q not found", r.Method)) + return r.Reply(ctx, nil, fmt.Errorf("%w: %q", ErrMethodNotFound, r.Method)) } // MustReply creates a Handler that panics if the wrapped handler does diff --git a/internal/jsonrpc2/jsonrpc2.go b/internal/jsonrpc2/jsonrpc2.go index 3f28140a51..f483393aaa 100644 --- a/internal/jsonrpc2/jsonrpc2.go +++ b/internal/jsonrpc2/jsonrpc2.go @@ -10,6 +10,7 @@ package jsonrpc2 import ( "context" "encoding/json" + "errors" "fmt" "sync" "sync/atomic" @@ -50,15 +51,6 @@ type constError string func (e constError) Error() string { return string(e) } -// NewErrorf builds a Error struct for the supplied message and code. -// If args is not empty, message and args will be passed to Sprintf. -func NewErrorf(code int64, format string, args ...interface{}) *Error { - return &Error{ - Code: code, - Message: fmt.Sprintf(format, args...), - } -} - // NewConn creates a new connection object around the supplied stream. // You must call Run for the connection to be active. func NewConn(s Stream) *Conn { @@ -212,7 +204,13 @@ func (r *Request) Reply(ctx context.Context, result interface{}, err error) erro if callErr, ok := err.(*Error); ok { response.Error = callErr } else { - response.Error = NewErrorf(0, "%s", err) + response.Error = &Error{Message: err.Error()} + var wrapped *Error + if errors.As(err, &wrapped) { + // if we wrapped a wire error, keep the code from the wrapped error + // but the message from the outer error + response.Error.Code = wrapped.Code + } } } data, err := json.Marshal(response) diff --git a/internal/jsonrpc2/jsonrpc2_test.go b/internal/jsonrpc2/jsonrpc2_test.go index e7d77f4a76..32f5aa1516 100644 --- a/internal/jsonrpc2/jsonrpc2_test.go +++ b/internal/jsonrpc2/jsonrpc2_test.go @@ -123,25 +123,25 @@ func testHandler(log bool) jsonrpc2.Handler { switch r.Method { case "no_args": if r.Params != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params")) + return r.Reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) } return r.Reply(ctx, true, nil) case "one_string": var v string if err := json.Unmarshal(*r.Params, &v); err != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error())) + return r.Reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return r.Reply(ctx, "got:"+v, nil) case "one_number": var v int if err := json.Unmarshal(*r.Params, &v); err != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error())) + return r.Reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return r.Reply(ctx, fmt.Sprintf("got:%d", v), nil) case "join": var v []string if err := json.Unmarshal(*r.Params, &v); err != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err.Error())) + return r.Reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) } return r.Reply(ctx, path.Join(v...), nil) default: diff --git a/internal/jsonrpc2/wire.go b/internal/jsonrpc2/wire.go index bfbf981799..216e45cffb 100644 --- a/internal/jsonrpc2/wire.go +++ b/internal/jsonrpc2/wire.go @@ -13,25 +13,25 @@ import ( // this file contains the go forms of the wire specification // see http://www.jsonrpc.org/specification for details -const ( - // CodeUnknownError should be used for all non coded errors. - CodeUnknownError = -32001 - // CodeParseError is used when invalid JSON was received by the server. - CodeParseError = -32700 - //CodeInvalidRequest is used when the JSON sent is not a valid Request object. - CodeInvalidRequest = -32600 - // CodeMethodNotFound should be returned by the handler when the method does +var ( + // ErrUnknown should be used for all non coded errors. + ErrUnknown = NewError(-32001, "JSON RPC unknown error") + // ErrParse is used when invalid JSON was received by the server. + ErrParse = NewError(-32700, "JSON RPC parse error") + //ErrInvalidRequest is used when the JSON sent is not a valid Request object. + ErrInvalidRequest = NewError(-32600, "JSON RPC invalid request") + // ErrMethodNotFound should be returned by the handler when the method does // not exist / is not available. - CodeMethodNotFound = -32601 - // CodeInvalidParams should be returned by the handler when method + ErrMethodNotFound = NewError(-32601, "JSON RPC method not found") + // ErrInvalidParams should be returned by the handler when method // parameter(s) were invalid. - CodeInvalidParams = -32602 - // CodeInternalError is not currently returned but defined for completeness. - CodeInternalError = -32603 + ErrInvalidParams = NewError(-32602, "JSON RPC invalid params") + // ErrInternal is not currently returned but defined for completeness. + ErrInternal = NewError(-32603, "JSON RPC internal error") - //CodeServerOverloaded is returned when a message was refused due to a + //ErrServerOverloaded is returned when a message was refused due to a //server being temporarily unable to accept any new messages. - CodeServerOverloaded = -32000 + ErrServerOverloaded = NewError(-32000, "JSON RPC overloaded") ) // WireRequest is sent to a server to represent a Call or Notify operaton. @@ -87,10 +87,14 @@ type ID struct { Number int64 } -func (err *Error) Error() string { - if err == nil { - return "" +func NewError(code int64, message string) error { + return &Error{ + Code: code, + Message: message, } +} + +func (err *Error) Error() string { return err.Message } diff --git a/internal/lsp/general.go b/internal/lsp/general.go index f253dec96f..f82a645269 100644 --- a/internal/lsp/general.go +++ b/internal/lsp/general.go @@ -25,7 +25,7 @@ func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitializ s.stateMu.Lock() if s.state >= serverInitializing { defer s.stateMu.Unlock() - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "initialize called while server in %v state", s.state) + return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) } s.state = serverInitializing s.stateMu.Unlock() @@ -118,7 +118,7 @@ func (s *Server) initialized(ctx context.Context, params *protocol.InitializedPa s.stateMu.Lock() if s.state >= serverInitialized { defer s.stateMu.Unlock() - return jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidRequest, "initalized called while server in %v state", s.state) + return fmt.Errorf("%w: initalized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) } s.state = serverInitialized s.stateMu.Unlock() diff --git a/internal/lsp/lsprpc/lsprpc.go b/internal/lsp/lsprpc/lsprpc.go index 734b98f588..8a05a77baa 100644 --- a/internal/lsp/lsprpc/lsprpc.go +++ b/internal/lsp/lsprpc/lsprpc.go @@ -534,7 +534,7 @@ func handshaker(client *debugClient, goplsPath string, handler jsonrpc2.Handler) func sendError(ctx context.Context, req *jsonrpc2.Request, err error) { if _, ok := err.(*jsonrpc2.Error); !ok { - err = jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err) + err = fmt.Errorf("%w: %v", jsonrpc2.ErrParse, err) } if err := req.Reply(ctx, nil, err); err != nil { event.Error(ctx, "", err) diff --git a/internal/lsp/protocol/protocol.go b/internal/lsp/protocol/protocol.go index 4faa81c9e7..a5e6590ef3 100644 --- a/internal/lsp/protocol/protocol.go +++ b/internal/lsp/protocol/protocol.go @@ -14,9 +14,9 @@ import ( "golang.org/x/tools/internal/xcontext" ) -const ( +var ( // RequestCancelledError should be used when a request is cancelled early. - RequestCancelledError = -32800 + RequestCancelledError = jsonrpc2.NewError(-32800, "JSON RPC cancelled") ) // ClientDispatcher returns a Client that dispatches LSP requests across the @@ -79,7 +79,7 @@ func cancelCall(ctx context.Context, conn *jsonrpc2.Conn, id jsonrpc2.ID) { func sendParseError(ctx context.Context, req *jsonrpc2.Request, err error) error { if _, ok := err.(*jsonrpc2.Error); !ok { - err = jsonrpc2.NewErrorf(jsonrpc2.CodeParseError, "%v", err) + err = fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err) } return req.Reply(ctx, nil, err) } diff --git a/internal/lsp/protocol/tsclient.go b/internal/lsp/protocol/tsclient.go index 65abb7eaee..d27143ba7a 100644 --- a/internal/lsp/protocol/tsclient.go +++ b/internal/lsp/protocol/tsclient.go @@ -10,6 +10,7 @@ package protocol import ( "context" "encoding/json" + "fmt" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/xcontext" @@ -34,7 +35,7 @@ func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { return func(ctx context.Context, r *jsonrpc2.Request) error { if ctx.Err() != nil { ctx := xcontext.Detach(ctx) - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(RequestCancelledError, "")) + return r.Reply(ctx, nil, RequestCancelledError) } switch r.Method { case "window/showMessage": // notif @@ -74,7 +75,7 @@ func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { return r.Reply(ctx, nil, err) case "workspace/workspaceFolders": // req if r.Params != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params")) + return r.Reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) } resp, err := client.WorkspaceFolders(ctx) return r.Reply(ctx, resp, err) diff --git a/internal/lsp/protocol/tsserver.go b/internal/lsp/protocol/tsserver.go index 91db543ab0..a6481f5493 100644 --- a/internal/lsp/protocol/tsserver.go +++ b/internal/lsp/protocol/tsserver.go @@ -10,6 +10,7 @@ package protocol import ( "context" "encoding/json" + "fmt" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/xcontext" @@ -72,7 +73,7 @@ func ServerHandler(server Server, handler jsonrpc2.Handler) jsonrpc2.Handler { return func(ctx context.Context, r *jsonrpc2.Request) error { if ctx.Err() != nil { ctx := xcontext.Detach(ctx) - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(RequestCancelledError, "")) + return r.Reply(ctx, nil, RequestCancelledError) } switch r.Method { case "workspace/didChangeWorkspaceFolders": // notif @@ -219,7 +220,7 @@ func ServerHandler(server Server, handler jsonrpc2.Handler) jsonrpc2.Handler { return r.Reply(ctx, resp, err) case "shutdown": // req if r.Params != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params")) + return r.Reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)) } err := server.Shutdown(ctx) return r.Reply(ctx, nil, err) diff --git a/internal/lsp/protocol/typescript/code.ts b/internal/lsp/protocol/typescript/code.ts index 41acbbc7fc..815baf1e26 100644 --- a/internal/lsp/protocol/typescript/code.ts +++ b/internal/lsp/protocol/typescript/code.ts @@ -908,7 +908,7 @@ let server: side = { // commonly used output const notNil = `if r.Params != nil { - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInvalidParams, "Expected no params")) + return r.Reply(ctx, nil, fmt.Errorf("%w: expected no params", jsonrpc2.CodeInvalidParams)) }`; // Go code for notifications. Side is client or server, m is the request @@ -1071,6 +1071,7 @@ function output(side: side) { import ( "context" "encoding/json" + "fmt" "golang.org/x/tools/internal/jsonrpc2" "golang.org/x/tools/internal/xcontext" @@ -1084,7 +1085,7 @@ function output(side: side) { return func(ctx context.Context, r *jsonrpc2.Request) error { if ctx.Err() != nil { ctx := xcontext.Detach(ctx) - return r.Reply(ctx, nil, jsonrpc2.NewErrorf(RequestCancelledError, "")) + return r.Reply(ctx, nil, RequestCancelledError) } switch r.Method {`); side.cases.forEach((v) => {f(v)}); diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 44e4412197..65b079e4de 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -160,8 +160,8 @@ func (s *Server) clearInProgress(token string) { s.inProgressMu.Unlock() } -func notImplemented(method string) *jsonrpc2.Error { - return jsonrpc2.NewErrorf(jsonrpc2.CodeMethodNotFound, "method %q not yet implemented", method) +func notImplemented(method string) error { + return fmt.Errorf("%w: %q not yet implemented", jsonrpc2.ErrMethodNotFound, method) } //go:generate helper/helper -d protocol/tsserver.go -o server_gen.go -u . diff --git a/internal/lsp/text_synchronization.go b/internal/lsp/text_synchronization.go index 6ef7a61c4e..ee674f2e4c 100644 --- a/internal/lsp/text_synchronization.go +++ b/internal/lsp/text_synchronization.go @@ -243,7 +243,7 @@ func (s *Server) wasFirstChange(uri span.URI) bool { func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { if len(changes) == 0 { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided") + return nil, fmt.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) } // Check if the client sent the full content of the file. @@ -257,7 +257,7 @@ func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protoc func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { content, _, err := s.session.GetFile(uri).Read(ctx) if err != nil { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found (%v)", err) + return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) } for _, change := range changes { // Make sure to update column mapper along with the content. @@ -268,18 +268,18 @@ func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, chan Content: content, } if change.Range == nil { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected nil range for change") + return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) } spn, err := m.RangeSpan(*change.Range) if err != nil { return nil, err } if !spn.HasOffset() { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") + return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) } start, end := spn.Start().Offset(), spn.End().Offset() if end < start { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") + return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) } var buf bytes.Buffer buf.Write(content[:start])