Files
filepuff-mcp/pkg/errors/errors.go
T
Lukasz Raczylo 185e73da47 Ho hum.
2026-01-18 18:40:26 +00:00

290 lines
9.0 KiB
Go

// Package errors provides structured error handling with error codes and context.
package errors
import (
"fmt"
"runtime"
"strings"
)
// ErrorCode represents a specific error condition.
type ErrorCode int
// Error codes organized by category
const (
// Search errors (1000-1099)
ErrRipgrepNotFound ErrorCode = 1001
ErrRipgrepTimeout ErrorCode = 1002
ErrInvalidPattern ErrorCode = 1003
ErrSearchFailed ErrorCode = 1004
ErrNoResults ErrorCode = 1005
// Parser errors (1100-1199)
ErrParserNotFound ErrorCode = 1101
ErrParseFailed ErrorCode = 1102
ErrInvalidLanguage ErrorCode = 1103
ErrFileTooBig ErrorCode = 1104
ErrInvalidSyntax ErrorCode = 1105
// LSP errors (1200-1299)
ErrLSPServerNotFound ErrorCode = 1201
ErrLSPInitFailed ErrorCode = 1202
ErrLSPTimeout ErrorCode = 1203
ErrLSPCommunication ErrorCode = 1204
ErrNoHoverInfo ErrorCode = 1205
ErrNoDefinition ErrorCode = 1206
ErrNoReferences ErrorCode = 1207
// Edit errors (1300-1399)
ErrEditFailed ErrorCode = 1301
ErrInvalidEdit ErrorCode = 1302
ErrFileNotFound ErrorCode = 1303
ErrFileNotReadable ErrorCode = 1304
ErrFileNotWritable ErrorCode = 1305
ErrNodeNotFound ErrorCode = 1306
ErrValidationFailed ErrorCode = 1307
ErrInvalidSelection ErrorCode = 1308
// Query errors (1400-1499)
ErrInvalidQuery ErrorCode = 1401
ErrQueryTimeout ErrorCode = 1402
ErrNoMatches ErrorCode = 1403
ErrQueryCompile ErrorCode = 1404
// Config errors (1500-1599)
ErrInvalidConfig ErrorCode = 1501
ErrPathNotAllowed ErrorCode = 1502
ErrWorkspaceNotSet ErrorCode = 1503
// Internal errors (1900-1999)
ErrInternal ErrorCode = 1900
ErrCacheFailed ErrorCode = 1901
ErrConcurrency ErrorCode = 1902
)
// StructuredError represents an error with rich context and remediation info.
type StructuredError struct {
Cause error
Context map[string]any
Message string
Remediation string
Stack string
Code ErrorCode
}
// Error implements the error interface.
func (e *StructuredError) Error() string {
var sb strings.Builder
// Error code and message
sb.WriteString(fmt.Sprintf("[%d] %s", e.Code, e.Message))
// Context if available
if len(e.Context) > 0 {
sb.WriteString("\nContext:")
for k, v := range e.Context {
sb.WriteString(fmt.Sprintf("\n %s: %v", k, v))
}
}
// Remediation if available
if e.Remediation != "" {
sb.WriteString(fmt.Sprintf("\nHow to fix: %s", e.Remediation))
}
// Underlying cause if available
if e.Cause != nil {
sb.WriteString(fmt.Sprintf("\nCaused by: %v", e.Cause))
}
return sb.String()
}
// Unwrap returns the underlying cause for error chain support.
func (e *StructuredError) Unwrap() error {
return e.Cause
}
// WithContext adds context to the error.
func (e *StructuredError) WithContext(key string, value any) *StructuredError {
if e.Context == nil {
e.Context = make(map[string]any)
}
e.Context[key] = value
return e
}
// WithRemediation sets the remediation message.
func (e *StructuredError) WithRemediation(msg string) *StructuredError {
e.Remediation = msg
return e
}
// New creates a new structured error with stack trace.
func New(code ErrorCode, message string) *StructuredError {
return &StructuredError{
Code: code,
Message: message,
Context: make(map[string]interface{}),
Stack: captureStack(2),
}
}
// Wrap wraps an existing error with structured error information.
func Wrap(code ErrorCode, message string, cause error) *StructuredError {
return &StructuredError{
Code: code,
Message: message,
Context: make(map[string]interface{}),
Cause: cause,
Stack: captureStack(2),
}
}
// Is checks if an error matches the given error code.
func Is(err error, code ErrorCode) bool {
if err == nil {
return false
}
if se, ok := err.(*StructuredError); ok {
return se.Code == code
}
return false
}
// GetCode extracts the error code from an error, or returns 0 if not a structured error.
func GetCode(err error) ErrorCode {
if err == nil {
return 0
}
if se, ok := err.(*StructuredError); ok {
return se.Code
}
return 0
}
// captureStack captures the stack trace.
func captureStack(skip int) string {
const depth = 16
var pcs [depth]uintptr
n := runtime.Callers(skip+1, pcs[:])
var sb strings.Builder
frames := runtime.CallersFrames(pcs[:n])
for {
frame, more := frames.Next()
if !strings.Contains(frame.File, "runtime/") {
sb.WriteString(fmt.Sprintf("\n %s:%d %s", frame.File, frame.Line, frame.Function))
}
if !more {
break
}
}
return sb.String()
}
// Common error constructors for convenience
// NewRipgrepNotFound creates an error for missing ripgrep binary.
func NewRipgrepNotFound() *StructuredError {
os := runtime.GOOS
install := "brew install ripgrep"
switch os {
case "linux":
install = "apt-get install ripgrep (Debian/Ubuntu) or yum install ripgrep (RHEL/CentOS)"
case "windows":
install = "choco install ripgrep or scoop install ripgrep"
}
return New(ErrRipgrepNotFound, "ripgrep (rg) binary not found in system PATH").
WithContext("os", os).
WithRemediation(fmt.Sprintf("Install ripgrep: %s", install))
}
// NewLSPServerNotFound creates an error for missing LSP server.
func NewLSPServerNotFound(language, serverName string) *StructuredError {
return New(ErrLSPServerNotFound, fmt.Sprintf("LSP server '%s' not found for language %s", serverName, language)).
WithContext("language", language).
WithContext("server", serverName).
WithRemediation(fmt.Sprintf("Install the %s LSP server to enable IDE features for %s", serverName, language))
}
// NewFileTooLarge creates an error for files exceeding size limit.
func NewFileTooLarge(path string, size, limit int64) *StructuredError {
return New(ErrFileTooBig, "file exceeds maximum size limit").
WithContext("file", path).
WithContext("size_bytes", size).
WithContext("limit_bytes", limit).
WithRemediation(fmt.Sprintf("File size (%d bytes) exceeds limit (%d bytes). Consider processing smaller files or increasing the limit.", size, limit))
}
// NewParseError creates an error for parsing failures.
func NewParseError(language, file string, cause error) *StructuredError {
return Wrap(ErrParseFailed, fmt.Sprintf("failed to parse %s file", language), cause).
WithContext("language", language).
WithContext("file", file).
WithRemediation("Check file syntax and ensure it's valid source code for the specified language")
}
// NewSearchTimeout creates an error for search timeouts.
func NewSearchTimeout(pattern string, duration string) *StructuredError {
return New(ErrRipgrepTimeout, "search operation timed out").
WithContext("pattern", pattern).
WithContext("duration", duration).
WithRemediation("Try narrowing the search scope, using more specific patterns, or increasing the timeout limit")
}
// NewEditValidationError creates an error for edit validation failures.
func NewEditValidationError(file string, cause error) *StructuredError {
return Wrap(ErrValidationFailed, "edit validation failed - syntax errors detected", cause).
WithContext("file", file).
WithRemediation("Review the edit operation and ensure it produces valid syntax. The file was not modified.")
}
// NewFileNotFoundError creates an error for missing files.
func NewFileNotFoundError(file string) *StructuredError {
return New(ErrFileNotFound, fmt.Sprintf("file not found: %s", file)).
WithContext("file", file).
WithRemediation("Verify the file path is correct and the file exists")
}
// NewFileNotReadableError creates an error for unreadable files.
func NewFileNotReadableError(file string, cause error) *StructuredError {
return Wrap(ErrFileNotReadable, fmt.Sprintf("cannot read file: %s", file), cause).
WithContext("file", file).
WithRemediation("Check file permissions and ensure the file is not locked by another process")
}
// NewFileNotWritableError creates an error for write failures.
func NewFileNotWritableError(file string, cause error) *StructuredError {
return Wrap(ErrFileNotWritable, fmt.Sprintf("cannot write to file: %s", file), cause).
WithContext("file", file).
WithRemediation("Check file permissions, disk space, and ensure the file is not locked by another process")
}
// NewNodeNotFoundError creates an error when AST node selector finds no matches.
func NewNodeNotFoundError(selector string) *StructuredError {
return New(ErrNodeNotFound, "no AST nodes match the selector criteria").
WithContext("selector", selector).
WithRemediation("Verify the selector criteria (kind, name, pattern, line) match an existing code structure")
}
// NewInvalidSelectionError creates an error for ambiguous or invalid selectors.
func NewInvalidSelectionError(message string) *StructuredError {
return New(ErrInvalidSelection, message).
WithRemediation("Refine the selector to be more specific or provide a selector_index to choose between multiple matches")
}
// NewInvalidEditError creates an error for invalid edit operations.
func NewInvalidEditError(message string) *StructuredError {
return New(ErrInvalidEdit, message).
WithRemediation("Review the edit request and ensure all required fields are provided with valid values")
}