github.com/jdhenke/godel@v0.0.0-20161213181855-abeb3861bf0d/apps/okgo/checkoutput/parser.go (about) 1 // Copyright 2016 Palantir Technologies, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package checkoutput 16 17 import ( 18 "bufio" 19 "io" 20 "regexp" 21 "strconv" 22 "strings" 23 24 "github.com/palantir/pkg/pkgpath" 25 "github.com/pkg/errors" 26 ) 27 28 type IssueParser interface { 29 // IsStartToken returns true if the provided line contains a token that signals that it represents a new Issue. 30 // If the parser is strictly a line-based one (every line is a new issue), the implementation can always return 31 // true. 32 IsStartToken(line string) bool 33 34 // ParseSingleIssue parses the provided string and returns the Issue that is parsed from it. The first line of 35 // the input must be a line that causes "IsStartToken" to return true. Returns an error if the provided input 36 // cannot be parsed as an issue. 37 ParseSingleIssue(input string) (Issue, error) 38 } 39 40 type LineParser func(line, rootDir string) (Issue, error) 41 42 type SingleLineIssueParser struct { 43 LineParser LineParser 44 RootDir string 45 } 46 47 func (p *SingleLineIssueParser) IsStartToken(line string) bool { 48 _, err := p.LineParser(line, p.RootDir) 49 return err == nil 50 } 51 52 func (p *SingleLineIssueParser) ParseSingleIssue(input string) (Issue, error) { 53 return p.LineParser(input, p.RootDir) 54 } 55 56 func ParseIssues(reader io.Reader, parser IssueParser, rawLineFilter func(line string) bool) ([]Issue, error) { 57 var issues []Issue 58 59 numLinesRead := 0 60 scanner := bufio.NewScanner(reader) 61 62 // read the first line 63 atEnd, firstLineOfCurrIssue, err := nextValidLine(scanner, &numLinesRead, rawLineFilter) 64 if err != nil { 65 return nil, errors.Wrapf(err, "failed at line %d", numLinesRead) 66 } 67 68 if atEnd { 69 return issues, nil 70 } 71 72 for { 73 // verify that first line is valid 74 if !parser.IsStartToken(firstLineOfCurrIssue) { 75 return nil, errors.Errorf("failed on line %d: line %s is not valid as the start token for an issue", numLinesRead, firstLineOfCurrIssue) 76 } 77 78 currIssueText := firstLineOfCurrIssue 79 80 // read until first line of next issue or end 81 firstLineOfNextIssue := "" 82 nextIssueExists := false 83 for { 84 atEnd, nextLine, err := nextValidLine(scanner, &numLinesRead, rawLineFilter) 85 if err != nil { 86 return nil, errors.Wrapf(err, "failed at line %d", numLinesRead) 87 } 88 89 if atEnd { 90 break 91 } 92 93 // if the next line is the start of a new issue, break. "firstLineOfNextIssue" contains the line. 94 if parser.IsStartToken(nextLine) { 95 nextIssueExists = true 96 firstLineOfNextIssue = nextLine 97 break 98 } 99 100 // otherwise, append the current line to the text for the current issue 101 currIssueText = currIssueText + "\n" + nextLine 102 } 103 104 // parse current issue 105 currIssue, err := parser.ParseSingleIssue(currIssueText) 106 if err != nil { 107 return nil, errors.Wrapf(err, "failed to parse issue from text %s", currIssueText) 108 } 109 issues = append(issues, currIssue) 110 111 if !nextIssueExists { 112 break 113 } 114 115 // update 116 firstLineOfCurrIssue = firstLineOfNextIssue 117 } 118 119 return issues, nil 120 } 121 122 // reads the next line that does not pass the given filter 123 func nextValidLine(scanner *bufio.Scanner, numLinesRead *int, rawLineFilter func(line string) bool) (bool, string, error) { 124 for scanner.Scan() { 125 nextLine := scanner.Text() 126 (*numLinesRead)++ 127 128 // passes, return 129 if rawLineFilter == nil || !rawLineFilter(nextLine) { 130 return false, nextLine, nil 131 } 132 } 133 134 if err := scanner.Err(); err != nil { 135 return true, "", errors.Wrapf(err, "failed reading line %d", numLinesRead) 136 } 137 138 // reached end of buffer but no error 139 return true, "", nil 140 } 141 142 func DefaultParser(pathType pkgpath.Type) LineParser { 143 return func(line, rootDir string) (Issue, error) { 144 return parseStandardLine(line, pathType, rootDir, false) 145 } 146 } 147 148 func MultiLineParser(pathType pkgpath.Type) LineParser { 149 return func(line, rootDir string) (Issue, error) { 150 return parseStandardLine(line, pathType, rootDir, true) 151 } 152 } 153 154 func StartAfterFirstWhitespaceParser(pathType pkgpath.Type) LineParser { 155 // some tools have output of the form "(text):(whitespace)" before the standard output, so provide a parser 156 // that skips everything up to after the first chunk of whitespace 157 return func(line, rootDir string) (Issue, error) { 158 spaceIndex := whitespace.FindStringIndex(line) 159 return parseStandardLine(line[spaceIndex[1]:], pathType, rootDir, false) 160 } 161 } 162 163 func RawParser() LineParser { 164 return func(line, rootDir string) (Issue, error) { 165 return Issue{ 166 message: line, 167 baseDir: rootDir, 168 }, nil 169 } 170 } 171 172 var whitespace = regexp.MustCompile(`\s+`) 173 174 func parseStandardLine(line string, pathType pkgpath.Type, rootDir string, strict bool) (Issue, error) { 175 spaceIndex := whitespace.FindStringIndex(line) 176 if spaceIndex == nil { 177 return Issue{}, errors.Errorf("failed to find whitespace in line %s", line) 178 } 179 180 filePath, lineNum, columnNum, err := parseStandardLocation(line[0:spaceIndex[0]], strict) 181 if err != nil { 182 return Issue{}, errors.Wrapf(err, "failed to parse location from line %s", line) 183 } 184 185 pkgPather := newPkgPather(filePath, rootDir, pathType) 186 if pkgPather == nil { 187 return Issue{}, errors.Errorf("failed to create PkgPather for %s", filePath) 188 } 189 190 relPath, err := pkgPather.Rel(rootDir) 191 if err != nil { 192 return Issue{}, errors.WithStack(err) 193 } 194 relPath = strings.TrimPrefix(relPath, "./") 195 196 messagePart := line[spaceIndex[1]:] 197 message := strings.TrimSpace(messagePart) 198 199 return Issue{ 200 path: relPath, 201 line: lineNum, 202 column: columnNum, 203 message: message, 204 baseDir: rootDir, 205 }, nil 206 } 207 208 func newPkgPather(path string, rootDir string, pathType pkgpath.Type) pkgpath.PkgPather { 209 switch pathType { 210 case pkgpath.Absolute: 211 return pkgpath.NewAbsPkgPath(path) 212 case pkgpath.GoPathSrcRelative: 213 return pkgpath.NewGoPathSrcRelPkgPath(path) 214 case pkgpath.Relative: 215 return pkgpath.NewRelPkgPath(path, rootDir) 216 default: 217 return nil 218 } 219 } 220 221 func parseStandardLocation(locationString string, strict bool) (string, int, int, error) { 222 // trim final ":" so split is cleaner 223 if strings.HasSuffix(locationString, ":") { 224 locationString = locationString[:len(locationString)-1] 225 } else if strict { 226 return "", 0, 0, errors.Errorf("location input %s did not have suffix ':'", locationString) 227 } 228 229 locationParts := strings.Split(locationString, ":") 230 if len(locationParts) > 3 { 231 // too many parts -- max is "message:line:col", which is 3 parts 232 return "", 0, 0, errors.Errorf(`splitting %q on character ':' resulted in greater than 3 parts: %v`, locationString, locationParts) 233 } else if strict && len(locationString) < 2 { 234 return "", 0, 0, errors.Errorf("splitting %q on character ':' resulted in fewer than 2 parts: %v", locationString, locationParts) 235 } 236 237 currIndex := 0 238 239 filePath := locationParts[currIndex] 240 currIndex++ 241 242 var lineNum int 243 var err error 244 if currIndex < len(locationParts) { 245 lineNum, err = parsePartAsInt(locationParts, currIndex) 246 if err != nil { 247 return "", 0, 0, errors.Wrapf(err, "failed to parse element %d from parts %v of line %s as an integer", currIndex, locationParts, locationString) 248 } 249 } 250 currIndex++ 251 252 var columnNum int 253 if currIndex < len(locationParts) { 254 columnNum, err = parsePartAsInt(locationParts, currIndex) 255 if err != nil { 256 return "", 0, 0, errors.Wrapf(err, "failed to parse element %d from parts %v of line %s as an integer", currIndex, locationParts, locationString) 257 } 258 } 259 260 return filePath, lineNum, columnNum, nil 261 } 262 263 func parsePartAsInt(parts []string, index int) (int, error) { 264 numStr := parts[index] 265 return strconv.Atoi(strings.TrimSpace(numStr)) 266 }