errors.go raw

   1  package toml
   2  
   3  import (
   4  	"fmt"
   5  	"strconv"
   6  	"strings"
   7  
   8  	"github.com/pelletier/go-toml/v2/internal/danger"
   9  	"github.com/pelletier/go-toml/v2/unstable"
  10  )
  11  
  12  // DecodeError represents an error encountered during the parsing or decoding
  13  // of a TOML document.
  14  //
  15  // In addition to the error message, it contains the position in the document
  16  // where it happened, as well as a human-readable representation that shows
  17  // where the error occurred in the document.
  18  type DecodeError struct {
  19  	message string
  20  	line    int
  21  	column  int
  22  	key     Key
  23  
  24  	human string
  25  }
  26  
  27  // StrictMissingError occurs in a TOML document that does not have a
  28  // corresponding field in the target value. It contains all the missing fields
  29  // in Errors.
  30  //
  31  // Emitted by Decoder when DisallowUnknownFields() was called.
  32  type StrictMissingError struct {
  33  	// One error per field that could not be found.
  34  	Errors []DecodeError
  35  }
  36  
  37  // Error returns the canonical string for this error.
  38  func (s *StrictMissingError) Error() string {
  39  	return "strict mode: fields in the document are missing in the target struct"
  40  }
  41  
  42  // String returns a human readable description of all errors.
  43  func (s *StrictMissingError) String() string {
  44  	var buf strings.Builder
  45  
  46  	for i, e := range s.Errors {
  47  		if i > 0 {
  48  			buf.WriteString("\n---\n")
  49  		}
  50  
  51  		buf.WriteString(e.String())
  52  	}
  53  
  54  	return buf.String()
  55  }
  56  
  57  type Key []string
  58  
  59  // Error returns the error message contained in the DecodeError.
  60  func (e *DecodeError) Error() string {
  61  	return "toml: " + e.message
  62  }
  63  
  64  // String returns the human-readable contextualized error. This string is multi-line.
  65  func (e *DecodeError) String() string {
  66  	return e.human
  67  }
  68  
  69  // Position returns the (line, column) pair indicating where the error
  70  // occurred in the document. Positions are 1-indexed.
  71  func (e *DecodeError) Position() (row int, column int) {
  72  	return e.line, e.column
  73  }
  74  
  75  // Key that was being processed when the error occurred. The key is present only
  76  // if this DecodeError is part of a StrictMissingError.
  77  func (e *DecodeError) Key() Key {
  78  	return e.key
  79  }
  80  
  81  // decodeErrorFromHighlight creates a DecodeError referencing a highlighted
  82  // range of bytes from document.
  83  //
  84  // highlight needs to be a sub-slice of document, or this function panics.
  85  //
  86  // The function copies all bytes used in DecodeError, so that document and
  87  // highlight can be freely deallocated.
  88  //
  89  //nolint:funlen
  90  func wrapDecodeError(document []byte, de *unstable.ParserError) *DecodeError {
  91  	offset := danger.SubsliceOffset(document, de.Highlight)
  92  
  93  	errMessage := de.Error()
  94  	errLine, errColumn := positionAtEnd(document[:offset])
  95  	before, after := linesOfContext(document, de.Highlight, offset, 3)
  96  
  97  	var buf strings.Builder
  98  
  99  	maxLine := errLine + len(after) - 1
 100  	lineColumnWidth := len(strconv.Itoa(maxLine))
 101  
 102  	// Write the lines of context strictly before the error.
 103  	for i := len(before) - 1; i > 0; i-- {
 104  		line := errLine - i
 105  		buf.WriteString(formatLineNumber(line, lineColumnWidth))
 106  		buf.WriteString("|")
 107  
 108  		if len(before[i]) > 0 {
 109  			buf.WriteString(" ")
 110  			buf.Write(before[i])
 111  		}
 112  
 113  		buf.WriteRune('\n')
 114  	}
 115  
 116  	// Write the document line that contains the error.
 117  
 118  	buf.WriteString(formatLineNumber(errLine, lineColumnWidth))
 119  	buf.WriteString("| ")
 120  
 121  	if len(before) > 0 {
 122  		buf.Write(before[0])
 123  	}
 124  
 125  	buf.Write(de.Highlight)
 126  
 127  	if len(after) > 0 {
 128  		buf.Write(after[0])
 129  	}
 130  
 131  	buf.WriteRune('\n')
 132  
 133  	// Write the line with the error message itself (so it does not have a line
 134  	// number).
 135  
 136  	buf.WriteString(strings.Repeat(" ", lineColumnWidth))
 137  	buf.WriteString("| ")
 138  
 139  	if len(before) > 0 {
 140  		buf.WriteString(strings.Repeat(" ", len(before[0])))
 141  	}
 142  
 143  	buf.WriteString(strings.Repeat("~", len(de.Highlight)))
 144  
 145  	if len(errMessage) > 0 {
 146  		buf.WriteString(" ")
 147  		buf.WriteString(errMessage)
 148  	}
 149  
 150  	// Write the lines of context strictly after the error.
 151  
 152  	for i := 1; i < len(after); i++ {
 153  		buf.WriteRune('\n')
 154  		line := errLine + i
 155  		buf.WriteString(formatLineNumber(line, lineColumnWidth))
 156  		buf.WriteString("|")
 157  
 158  		if len(after[i]) > 0 {
 159  			buf.WriteString(" ")
 160  			buf.Write(after[i])
 161  		}
 162  	}
 163  
 164  	return &DecodeError{
 165  		message: errMessage,
 166  		line:    errLine,
 167  		column:  errColumn,
 168  		key:     de.Key,
 169  		human:   buf.String(),
 170  	}
 171  }
 172  
 173  func formatLineNumber(line int, width int) string {
 174  	format := "%" + strconv.Itoa(width) + "d"
 175  
 176  	return fmt.Sprintf(format, line)
 177  }
 178  
 179  func linesOfContext(document []byte, highlight []byte, offset int, linesAround int) ([][]byte, [][]byte) {
 180  	return beforeLines(document, offset, linesAround), afterLines(document, highlight, offset, linesAround)
 181  }
 182  
 183  func beforeLines(document []byte, offset int, linesAround int) [][]byte {
 184  	var beforeLines [][]byte
 185  
 186  	// Walk the document backward from the highlight to find previous lines
 187  	// of context.
 188  	rest := document[:offset]
 189  backward:
 190  	for o := len(rest) - 1; o >= 0 && len(beforeLines) <= linesAround && len(rest) > 0; {
 191  		switch {
 192  		case rest[o] == '\n':
 193  			// handle individual lines
 194  			beforeLines = append(beforeLines, rest[o+1:])
 195  			rest = rest[:o]
 196  			o = len(rest) - 1
 197  		case o == 0:
 198  			// add the first line only if it's non-empty
 199  			beforeLines = append(beforeLines, rest)
 200  
 201  			break backward
 202  		default:
 203  			o--
 204  		}
 205  	}
 206  
 207  	return beforeLines
 208  }
 209  
 210  func afterLines(document []byte, highlight []byte, offset int, linesAround int) [][]byte {
 211  	var afterLines [][]byte
 212  
 213  	// Walk the document forward from the highlight to find the following
 214  	// lines of context.
 215  	rest := document[offset+len(highlight):]
 216  forward:
 217  	for o := 0; o < len(rest) && len(afterLines) <= linesAround; {
 218  		switch {
 219  		case rest[o] == '\n':
 220  			// handle individual lines
 221  			afterLines = append(afterLines, rest[:o])
 222  			rest = rest[o+1:]
 223  			o = 0
 224  
 225  		case o == len(rest)-1:
 226  			// add last line only if it's non-empty
 227  			afterLines = append(afterLines, rest)
 228  
 229  			break forward
 230  		default:
 231  			o++
 232  		}
 233  	}
 234  
 235  	return afterLines
 236  }
 237  
 238  func positionAtEnd(b []byte) (row int, column int) {
 239  	row = 1
 240  	column = 1
 241  
 242  	for _, c := range b {
 243  		if c == '\n' {
 244  			row++
 245  			column = 1
 246  		} else {
 247  			column++
 248  		}
 249  	}
 250  
 251  	return
 252  }
 253