go-twig/debugging.go
semihalev cce67f94c9 Fix code style inconsistencies and update documentation
- Update documentation with negative step range workaround
- Document limitations and solutions in FINDINGS.md and SOLUTION.md
- Clean up code style and remove experimental files
- Optimize performance in core functions
- Fix whitespace and end-of-file newlines

🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-03-11 09:29:12 +03:00

292 lines
6.6 KiB
Go

package twig
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
)
// DebugLevel represents the verbosity level of debugging
type DebugLevel int
const (
// Debug levels
DebugOff DebugLevel = iota
DebugError
DebugWarning
DebugInfo
DebugVerbose
)
// Debugger provides logging and debugging tools for the Twig engine
type Debugger struct {
mu sync.Mutex
level DebugLevel
writer io.Writer
logger *log.Logger
traces []string
enabled bool
filename string
line int
}
// Global debugger instance
var debugger = &Debugger{
level: DebugOff,
writer: os.Stderr,
logger: log.New(os.Stderr, "[TWIG] ", log.LstdFlags),
enabled: false,
}
// SetDebugLevel sets the global debug level
func SetDebugLevel(level DebugLevel) {
debugger.mu.Lock()
defer debugger.mu.Unlock()
debugger.level = level
debugger.enabled = level > DebugOff
}
// SetDebugWriter sets the output writer for debug messages
func SetDebugWriter(w io.Writer) {
debugger.mu.Lock()
defer debugger.mu.Unlock()
debugger.writer = w
debugger.logger = log.New(w, "[TWIG] ", log.LstdFlags)
}
// IsDebugEnabled returns true if debugging is enabled
func IsDebugEnabled() bool {
return debugger.enabled
}
// LogError logs an error with source information
func LogError(err error, context ...string) {
if debugger.level >= DebugError {
_, file, line, _ := runtime.Caller(1)
contextStr := ""
if len(context) > 0 {
contextStr = " " + strings.Join(context, " ")
}
debugger.logger.Printf("ERROR:%s:%d:%s%s", filepath.Base(file), line, err, contextStr)
}
}
// LogWarning logs a warning with source information
func LogWarning(msg string, args ...interface{}) {
if debugger.level >= DebugWarning {
_, file, line, _ := runtime.Caller(1)
debugger.logger.Printf("WARNING:%s:%d:%s", filepath.Base(file), line, fmt.Sprintf(msg, args...))
}
}
// LogInfo logs an informational message
func LogInfo(msg string, args ...interface{}) {
if debugger.level >= DebugInfo {
debugger.logger.Printf("INFO:%s", fmt.Sprintf(msg, args...))
}
}
// LogVerbose logs detailed information for debugging
func LogVerbose(msg string, args ...interface{}) {
if debugger.level >= DebugVerbose {
_, file, line, _ := runtime.Caller(1)
debugger.logger.Printf("VERBOSE:%s:%d:%s", filepath.Base(file), line, fmt.Sprintf(msg, args...))
}
}
// LogDebug logs debugging information when debug mode is enabled
func LogDebug(msg string, args ...interface{}) {
if debugger.enabled {
debugger.logger.Printf("DEBUG:%s", fmt.Sprintf(msg, args...))
}
}
// StartTrace begins a trace of template rendering
func StartTrace(templateName string) func() {
if !debugger.enabled {
return func() {}
}
traceID := fmt.Sprintf("TRACE-%s-%d", templateName, time.Now().UnixNano())
start := time.Now()
debugger.mu.Lock()
debugger.traces = append(debugger.traces, traceID)
debugger.mu.Unlock()
LogInfo("Begin rendering template: %s", templateName)
return func() {
elapsed := time.Since(start)
LogInfo("Completed rendering template: %s (took %s)", templateName, elapsed)
debugger.mu.Lock()
defer debugger.mu.Unlock()
// Remove this trace from active traces
for i, t := range debugger.traces {
if t == traceID {
debugger.traces = append(debugger.traces[:i], debugger.traces[i+1:]...)
break
}
}
}
}
// TraceSection traces a section of template rendering
func TraceSection(name string) func() {
if !debugger.enabled {
return func() {}
}
start := time.Now()
LogVerbose("Begin section: %s", name)
return func() {
elapsed := time.Since(start)
LogVerbose("End section: %s (took %s)", name, elapsed)
}
}
// DebugRender enables detailed rendering information
func DebugRender(w io.Writer, tmpl *Template, ctx *RenderContext) error {
if !debugger.enabled {
return tmpl.RenderTo(w, ctx.context)
}
LogInfo("Rendering template %s with context containing %d variables",
tmpl.name, len(ctx.context))
// Log context variables at verbose level
if debugger.level >= DebugVerbose {
for k, v := range ctx.context {
typeName := "nil"
if v != nil {
typeName = fmt.Sprintf("%T", v)
}
LogVerbose("Context var: %s = %v (type: %s)", k, v, typeName)
}
}
// Trace full template rendering
defer StartTrace(tmpl.name)()
return tmpl.RenderTo(w, ctx.context)
}
// FormatErrorContext creates a formatted context for syntax errors
// including the source line and position indicator
func FormatErrorContext(source string, position int, line int) string {
if source == "" || position < 0 {
return ""
}
lines := strings.Split(source, "\n")
if line <= 0 || line > len(lines) {
return ""
}
// Get the problematic line
errorLine := lines[line-1]
// Calculate column position within the line
lineStartIdx := 0
for i := 0; i < line-1; i++ {
lineStartIdx += len(lines[i]) + 1 // +1 for the newline
}
colPosition := position - lineStartIdx
// Ensure column position is valid
if colPosition < 0 {
colPosition = 0
}
if colPosition > len(errorLine) {
colPosition = len(errorLine)
}
// Build the context output
context := fmt.Sprintf("Line %d: %s\n", line, errorLine)
if colPosition >= 0 {
context += strings.Repeat(" ", colPosition+8) + "^\n"
}
return context
}
// EnhancedError provides more detailed error information for debugging
type EnhancedError struct {
Err error
Template string
Line int
Column int
Source string
SourceCtx string
}
// Error implements the error interface
func (e *EnhancedError) Error() string {
if e.Err == nil {
return "unknown error"
}
location := ""
if e.Template != "" {
location = fmt.Sprintf("in template '%s' ", e.Template)
}
position := ""
if e.Line > 0 {
position = fmt.Sprintf("at line %d", e.Line)
if e.Column > 0 {
position += fmt.Sprintf(", column %d", e.Column)
}
}
context := ""
if e.SourceCtx != "" {
context = "\n" + e.SourceCtx
}
return fmt.Sprintf("Error %s%s: %s%s", location, position, e.Err.Error(), context)
}
// Unwrap returns the underlying error
func (e *EnhancedError) Unwrap() error {
return e.Err
}
// NewError creates an enhanced error with context
func NewError(err error, tmpl string, line int, col int, source string) error {
if err == nil {
return nil
}
e := &EnhancedError{
Err: err,
Template: tmpl,
Line: line,
Column: col,
Source: source,
}
if source != "" && line > 0 {
position := 0
if col > 0 {
// Calculate position from line and column
lines := strings.Split(source, "\n")
for i := 0; i < line-1 && i < len(lines); i++ {
position += len(lines[i]) + 1 // +1 for newline
}
position += col - 1
}
e.SourceCtx = FormatErrorContext(source, position, line)
}
return e
}