github.com/nikron/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 }