github.com/opiuman/genqlient@v1.0.0/generate/errors.go (about)

     1  package generate
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"go/scanner"
     8  	"math"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/vektah/gqlparser/v2/ast"
    13  	"github.com/vektah/gqlparser/v2/gqlerror"
    14  )
    15  
    16  type errorPos struct {
    17  	filename  string
    18  	line, col int
    19  }
    20  
    21  func (pos *errorPos) String() string {
    22  	filename, lineOffset := splitFilename(pos.filename)
    23  	line := lineOffset + pos.line
    24  	if line != 0 {
    25  		return fmt.Sprintf("%v:%v", filename, line)
    26  	} else {
    27  		return filename
    28  	}
    29  }
    30  
    31  type genqlientError struct {
    32  	pos     *errorPos
    33  	msg     string
    34  	wrapped error
    35  }
    36  
    37  func splitFilename(filename string) (name string, lineOffset int) {
    38  	split := strings.Split(filename, ":")
    39  	if len(split) != 2 {
    40  		return filename, 0
    41  	}
    42  
    43  	offset, err := strconv.Atoi(split[1])
    44  	if err != nil {
    45  		return split[0], 0
    46  	}
    47  	return split[0], offset - 1
    48  }
    49  
    50  func (err *genqlientError) Error() string {
    51  	if err.pos != nil {
    52  		return err.pos.String() + ": " + err.msg
    53  	} else {
    54  		return err.msg
    55  	}
    56  }
    57  
    58  func (err *genqlientError) Unwrap() error {
    59  	return err.wrapped
    60  }
    61  
    62  func errorf(pos *ast.Position, msg string, args ...interface{}) error {
    63  	// TODO: alternately accept a filename only, or maybe even a go-parser pos
    64  
    65  	// We do all our own wrapping, because if the wrapped error already has a
    66  	// pos, we want to extract it out and put it at the front, not in the
    67  	// middle.
    68  
    69  	var wrapped error
    70  	var wrapIndex int
    71  	for i, arg := range args {
    72  		if wrapped == nil {
    73  			var ok bool
    74  			wrapped, ok = arg.(error)
    75  			if ok {
    76  				wrapIndex = i
    77  			}
    78  		}
    79  	}
    80  
    81  	var wrappedGenqlient *genqlientError
    82  	isGenqlient := errors.As(wrapped, &wrappedGenqlient)
    83  	var wrappedGraphQL *gqlerror.Error
    84  	isGraphQL := errors.As(wrapped, &wrappedGraphQL)
    85  	if !isGraphQL {
    86  		var wrappedGraphQLList gqlerror.List
    87  		isGraphQLList := errors.As(wrapped, &wrappedGraphQLList)
    88  		if isGraphQLList && len(wrappedGraphQLList) > 0 {
    89  			isGraphQL = true
    90  			wrappedGraphQL = wrappedGraphQLList[0]
    91  		}
    92  	}
    93  
    94  	var errPos *errorPos
    95  	if pos != nil {
    96  		errPos = &errorPos{
    97  			filename: pos.Src.Name,
    98  			line:     pos.Line,
    99  			col:      pos.Column,
   100  		}
   101  	} else if isGenqlient {
   102  		errPos = wrappedGenqlient.pos
   103  	} else if isGraphQL {
   104  		filename, _ := wrappedGraphQL.Extensions["file"].(string)
   105  		if filename != "" {
   106  			var loc gqlerror.Location
   107  			if len(wrappedGraphQL.Locations) > 0 {
   108  				loc = wrappedGraphQL.Locations[0]
   109  			}
   110  			errPos = &errorPos{
   111  				filename: filename,
   112  				line:     loc.Line,
   113  				col:      loc.Column,
   114  			}
   115  		}
   116  	}
   117  
   118  	if wrapped != nil {
   119  		errText := wrapped.Error()
   120  		if isGenqlient {
   121  			errText = wrappedGenqlient.msg
   122  		} else if isGraphQL {
   123  			errText = wrappedGraphQL.Message
   124  		}
   125  		args[wrapIndex] = errText
   126  	}
   127  
   128  	msg = fmt.Sprintf(msg, args...)
   129  
   130  	return &genqlientError{
   131  		msg:     msg,
   132  		pos:     errPos,
   133  		wrapped: wrapped,
   134  	}
   135  }
   136  
   137  // goSourceError processes the error(s) returned by go tooling (gofmt, etc.)
   138  // into a nice error message.
   139  //
   140  // In practice, such errors are genqlient internal errors, but it's still
   141  // useful to format them nicely for debugging.
   142  func goSourceError(
   143  	failedOperation string, // e.g. "gofmt", for the error message
   144  	source []byte,
   145  	err error,
   146  ) error {
   147  	var errTexts []string
   148  	var scanErrs scanner.ErrorList
   149  	var scanErr *scanner.Error
   150  	var badLines map[int]bool
   151  
   152  	if errors.As(err, &scanErrs) {
   153  		errTexts = make([]string, len(scanErrs))
   154  		badLines = make(map[int]bool, len(scanErrs))
   155  		for i, scanErr := range scanErrs {
   156  			errTexts[i] = err.Error()
   157  			badLines[scanErr.Pos.Line] = true
   158  		}
   159  	} else if errors.As(err, &scanErr) {
   160  		errTexts = []string{scanErr.Error()}
   161  		badLines = map[int]bool{scanErr.Pos.Line: true}
   162  	} else {
   163  		errTexts = []string{err.Error()}
   164  	}
   165  
   166  	lines := bytes.SplitAfter(source, []byte("\n"))
   167  	lineNoWidth := int(math.Ceil(math.Log10(float64(len(lines) + 1))))
   168  	for i, line := range lines {
   169  		prefix := "  "
   170  		if badLines[i] {
   171  			prefix = "> "
   172  		}
   173  		lineNo := strconv.Itoa(i + 1)
   174  		padding := strings.Repeat(" ", lineNoWidth-len(lineNo))
   175  		lines[i] = []byte(fmt.Sprintf("%s%s%s | %s", prefix, padding, lineNo, line))
   176  	}
   177  
   178  	return errorf(nil,
   179  		"genqlient internal error: failed to %s code:\n\t%s---source code---\n%s",
   180  		failedOperation, strings.Join(errTexts, "\n\t"), bytes.Join(lines, nil))
   181  }