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  }