github.com/errata-ai/vale/v3@v3.4.2/internal/core/error.go (about)

     1  package core
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/pterm/pterm"
    13  )
    14  
    15  type lineError struct {
    16  	content string
    17  	line    int
    18  	span    []int
    19  }
    20  
    21  type errorCondition func(position int, line, target string) bool
    22  
    23  func annotate(file []byte, target string, finder errorCondition) (lineError, error) {
    24  	var sb strings.Builder
    25  
    26  	scanner := bufio.NewScanner(bytes.NewBuffer(file))
    27  	context := lineError{span: []int{1, 1}}
    28  
    29  	idx := 1
    30  	for scanner.Scan() {
    31  		markup := scanner.Text()
    32  		plain := StripANSI(markup)
    33  		if idx-context.line > 2 && context.line != 0 { //nolint:gocritic
    34  			break
    35  		} else if finder(idx, plain, target) && context.line == 0 {
    36  			context.line = idx
    37  
    38  			s := strings.Index(plain, target) + 1
    39  			context.span = []int{s, s + len(target)}
    40  
    41  			sb.WriteString(
    42  				fmt.Sprintf("\033[1;32m%4d\033[0m* %s\n", idx, markup))
    43  		} else {
    44  			sb.WriteString(
    45  				fmt.Sprintf("\033[1;32m%4d\033[0m  %s\n", idx, markup))
    46  		}
    47  		idx++
    48  	}
    49  
    50  	if err := scanner.Err(); err != nil {
    51  		return lineError{}, err
    52  	}
    53  
    54  	lines := []string{}
    55  	for i, l := range strings.Split(sb.String(), "\n") {
    56  		if context.line-i < 3 {
    57  			lines = append(lines, l)
    58  		}
    59  	}
    60  
    61  	context.content = strings.Join(lines, "\n")
    62  	return context, nil
    63  }
    64  
    65  // NewError creates a colored error from the given information.
    66  //
    67  // # The standard format is
    68  //
    69  // ```
    70  // <code> [<context>] <title>
    71  //
    72  // <msg>
    73  // ```
    74  func NewError(code, title, msg string) error {
    75  	return fmt.Errorf(
    76  		"%s %s\n\n%s\n\n%s",
    77  		pterm.BgRed.Sprintf(code),
    78  		title,
    79  		msg,
    80  		pterm.Fuzzy.Sprint(pterm.Italic.Sprintf("Execution stopped with code 1.")),
    81  	)
    82  }
    83  
    84  // NewE100 creates a new, formatted "unexpected" error.
    85  //
    86  // Since E100 errors can occur anywhere, we include a "context" that makes it
    87  // clear where exactly the error was generated.
    88  func NewE100(context string, err error) error {
    89  	title := fmt.Sprintf("[%s] %s", context, "Runtime error")
    90  	return NewError("E100", title, err.Error())
    91  }
    92  
    93  // NewE201 creates a formatted user-generated error.
    94  //
    95  // 201 errors involve a specific configuration asset and should contain
    96  // parsable location information on their last line of the form:
    97  //
    98  // <path>:<line>:<start>:<end>
    99  func NewE201(msg, value, path string, finder errorCondition) error {
   100  	f, err := os.ReadFile(path)
   101  	if err != nil {
   102  		return NewE100("NewE201", errors.New(msg))
   103  	}
   104  
   105  	ctx, err := annotate(f, value, finder)
   106  	if err != nil {
   107  		return NewE100("NewE201/annotate", err)
   108  	}
   109  
   110  	title := fmt.Sprintf(
   111  		"Invalid value [%s:%d:%d]:",
   112  		filepath.ToSlash(path),
   113  		ctx.line,
   114  		ctx.span[0])
   115  
   116  	return NewError(
   117  		"E201",
   118  		title,
   119  		fmt.Sprintf("%s\n%s", ctx.content, msg))
   120  }
   121  
   122  // NewE201FromTarget creates a new E201 error from a target string.
   123  func NewE201FromTarget(msg, value, file string) error {
   124  	return NewE201(
   125  		msg,
   126  		value,
   127  		file,
   128  		func(position int, line, target string) bool {
   129  			return strings.Contains(line, target)
   130  		})
   131  }
   132  
   133  // NewE201FromPosition creates a new E201 error from an in-file location.
   134  func NewE201FromPosition(msg, file string, goal int) error {
   135  	return NewE201(
   136  		msg,
   137  		"",
   138  		file,
   139  		func(position int, line, target string) bool {
   140  			return position == goal
   141  		})
   142  }