github.com/Zenithar/prototool@v1.3.0/internal/text/text.go (about)

     1  // Copyright (c) 2018 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package text
    22  
    23  import (
    24  	"bytes"
    25  	"fmt"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"text/scanner"
    30  )
    31  
    32  const (
    33  	// FailureFieldFilename references the Filename field of a Failure.
    34  	FailureFieldFilename FailureField = iota
    35  	// FailureFieldLine references the Line field of a Failure.
    36  	FailureFieldLine
    37  	// FailureFieldColumn references the Column field of a Failure.
    38  	FailureFieldColumn
    39  	// FailureFieldID references the ID field of a Failure.
    40  	FailureFieldID
    41  	// FailureFieldMessage references the Message field of a Failure.
    42  	FailureFieldMessage
    43  )
    44  
    45  var (
    46  	// DefaultFailureFields are the default FailureFields.
    47  	DefaultFailureFields = []FailureField{
    48  		FailureFieldFilename,
    49  		FailureFieldLine,
    50  		FailureFieldColumn,
    51  		FailureFieldMessage,
    52  	}
    53  
    54  	_failureFieldToString = map[FailureField]string{
    55  		FailureFieldFilename: "filename",
    56  		FailureFieldLine:     "line",
    57  		FailureFieldColumn:   "column",
    58  		FailureFieldID:       "id",
    59  		FailureFieldMessage:  "message",
    60  	}
    61  	_stringToFailureField = map[string]FailureField{
    62  		"filename": FailureFieldFilename,
    63  		"line":     FailureFieldLine,
    64  		"column":   FailureFieldColumn,
    65  		"id":       FailureFieldID,
    66  		"message":  FailureFieldMessage,
    67  	}
    68  )
    69  
    70  // FailureField references a field of a Failure.
    71  type FailureField int
    72  
    73  // String implements fmt.Stringer.
    74  func (f FailureField) String() string {
    75  	if s, ok := _failureFieldToString[f]; ok {
    76  		return s
    77  	}
    78  	return strconv.Itoa(int(f))
    79  }
    80  
    81  // ParseFailureField parses the FailureField from the given string.
    82  //
    83  // Input is case-insensitive.
    84  func ParseFailureField(s string) (FailureField, error) {
    85  	failureField, ok := _stringToFailureField[strings.ToLower(s)]
    86  	if !ok {
    87  		return 0, fmt.Errorf("could not parse %s to a FailureField", s)
    88  	}
    89  	return failureField, nil
    90  }
    91  
    92  // ParseColonSeparatedFailureFields parses FailureFields from the given string. FailureFields are expected to be colon-separated in the given string. Input is case-insensitive. If the string is empty, DefaultFailureFields will be returned.
    93  func ParseColonSeparatedFailureFields(s string) ([]FailureField, error) {
    94  	if len(s) == 0 {
    95  		return DefaultFailureFields, nil
    96  	}
    97  	printFields := strings.Split(s, ":")
    98  	failureFields := make([]FailureField, len(printFields))
    99  	for i, failureFieldString := range printFields {
   100  		failureField, err := ParseFailureField(failureFieldString)
   101  		if err != nil {
   102  			return nil, err
   103  		}
   104  		failureFields[i] = failureField
   105  	}
   106  	return failureFields, nil
   107  }
   108  
   109  // Failure is a failure with a position in text.
   110  type Failure struct {
   111  	Filename string `json:"filename,omitempty"`
   112  	Line     int    `json:"line,omitempty"`
   113  	Column   int    `json:"column,omitempty"`
   114  	LintID   string `json:"lint_id,omitempty"`
   115  	Message  string `json:"message,omitempty"`
   116  }
   117  
   118  // FailureWriter is a writer that Failure.Println can accept.
   119  //
   120  // Both bytes.Buffer and bufio.Writer implement this.
   121  type FailureWriter interface {
   122  	WriteRune(rune) (int, error)
   123  	WriteString(string) (int, error)
   124  }
   125  
   126  // Fprintln prints the Failure to the writer with the given ordered fields.
   127  func (f *Failure) Fprintln(writer FailureWriter, fields ...FailureField) error {
   128  	if len(fields) == 0 {
   129  		fields = DefaultFailureFields
   130  	}
   131  	written := false
   132  	for i, field := range fields {
   133  		printColon := true
   134  		switch field {
   135  		case FailureFieldFilename:
   136  			filename := f.Filename
   137  			if filename == "" {
   138  				filename = "<input>"
   139  			}
   140  			if _, err := writer.WriteString(filename); err != nil {
   141  				return err
   142  			}
   143  			written = true
   144  		case FailureFieldLine:
   145  			line := strconv.Itoa(f.Line)
   146  			if line == "0" {
   147  				line = "1"
   148  			}
   149  			if _, err := writer.WriteString(line); err != nil {
   150  				return err
   151  			}
   152  			written = true
   153  		case FailureFieldColumn:
   154  			column := strconv.Itoa(f.Column)
   155  			if column == "0" {
   156  				column = "1"
   157  			}
   158  			if _, err := writer.WriteString(column); err != nil {
   159  				return err
   160  			}
   161  			written = true
   162  		case FailureFieldID:
   163  			if f.LintID != "" {
   164  				if _, err := writer.WriteString(f.LintID); err != nil {
   165  					return err
   166  				}
   167  				written = true
   168  			} else {
   169  				printColon = false
   170  			}
   171  		case FailureFieldMessage:
   172  			if f.Message != "" {
   173  				if _, err := writer.WriteString(f.Message); err != nil {
   174  					return err
   175  				}
   176  				written = true
   177  			} else {
   178  				printColon = false
   179  			}
   180  		default:
   181  			return fmt.Errorf("unknown FailureField: %v", field)
   182  		}
   183  		if printColon && i != len(fields)-1 {
   184  			if _, err := writer.WriteRune(':'); err != nil {
   185  				return err
   186  			}
   187  			written = true
   188  		}
   189  	}
   190  	if written {
   191  		_, err := writer.WriteRune('\n')
   192  		return err
   193  	}
   194  	return nil
   195  }
   196  
   197  // String implements fmt.Stringer.
   198  func (f *Failure) String() string {
   199  	filename := f.Filename
   200  	if filename == "" {
   201  		filename = "<input>"
   202  	}
   203  	line := strconv.Itoa(f.Line)
   204  	if line == "0" {
   205  		line = "1"
   206  	}
   207  	column := strconv.Itoa(f.Column)
   208  	if column == "0" {
   209  		column = "1"
   210  	}
   211  	buffer := bytes.NewBuffer(nil)
   212  	buffer.WriteString(filename)
   213  	buffer.WriteString(":")
   214  	buffer.WriteString(line)
   215  	buffer.WriteString(":")
   216  	buffer.WriteString(column)
   217  	buffer.WriteString(":")
   218  	if f.LintID != "" {
   219  		buffer.WriteString(f.LintID)
   220  		buffer.WriteString(" ")
   221  	}
   222  	buffer.WriteString(f.Message)
   223  	return buffer.String()
   224  }
   225  
   226  // NewFailuref is a helper that returns a new Failure.
   227  func NewFailuref(position scanner.Position, lintID string, format string, args ...interface{}) *Failure {
   228  	return &Failure{
   229  		LintID:   lintID,
   230  		Filename: position.Filename,
   231  		Line:     position.Line,
   232  		Column:   position.Column,
   233  		Message:  fmt.Sprintf(format, args...),
   234  	}
   235  }
   236  
   237  // SortFailures sorts the Failures, by filename, line, column, id, message.
   238  func SortFailures(failures []*Failure) {
   239  	sort.Stable(sortFailures(failures))
   240  }
   241  
   242  type sortFailures []*Failure
   243  
   244  func (f sortFailures) Len() int          { return len(f) }
   245  func (f sortFailures) Swap(i int, j int) { f[i], f[j] = f[j], f[i] }
   246  func (f sortFailures) Less(i int, j int) bool {
   247  	if f[i] == nil && f[j] == nil {
   248  		return false
   249  	}
   250  	if f[i] == nil && f[j] != nil {
   251  		return true
   252  	}
   253  	if f[i] != nil && f[j] == nil {
   254  		return false
   255  	}
   256  	if f[i].Filename < f[j].Filename {
   257  		return true
   258  	}
   259  	if f[i].Filename > f[j].Filename {
   260  		return false
   261  	}
   262  	if f[i].Line < f[j].Line {
   263  		return true
   264  	}
   265  	if f[i].Line > f[j].Line {
   266  		return false
   267  	}
   268  	if f[i].Column < f[j].Column {
   269  		return true
   270  	}
   271  	if f[i].Column > f[j].Column {
   272  		return false
   273  	}
   274  	if f[i].LintID < f[j].LintID {
   275  		return true
   276  	}
   277  	if f[i].LintID > f[j].LintID {
   278  		return false
   279  	}
   280  	if f[i].Message < f[j].Message {
   281  		return true
   282  	}
   283  	return false
   284  }