mrtazz/checkmake

View on GitHub
parser/parser.go

Summary

Maintainability
A
1 hr
Test Coverage
// Package parser implements all the parser functionality for Makefiles. This
// is supposed to be a parser with a very small feature set that just supports
// what is needed to do linting and checking and not actual full Makefile
// parsing. And it's handrolled because apparently GNU Make doesn't have a
// grammar (see http://www.mail-archive.com/help-make@gnu.org/msg02778.html)
package parser

import (
    "errors"
    "fmt"
    "regexp"
    "strings"
    "github.com/mrtazz/checkmake/logger"
)

// Makefile provides a data structure to describe a parsed Makefile
type Makefile struct {
    FileName  string
    Rules     RuleList
    Variables VariableList
}

// Rule represents a Make rule
type Rule struct {
    Target       string
    Dependencies []string
    Body         []string
    FileName     string
    LineNumber   int
}

// RuleList represents a list of rules
type RuleList []Rule

// Variable represents a Make variable
type Variable struct {
    Name            string
    SimplyExpanded  bool
    Assignment      string
    SpecialVariable bool
    FileName        string
    LineNumber      int
}

// VariableList represents a list of variables
type VariableList []Variable

var (
    reFindRule             = regexp.MustCompile("^([a-zA-Z]+):(.*)")
    reFindRuleBody         = regexp.MustCompile("^\t+(.*)")
    reFindSimpleVariable   = regexp.MustCompile("^([a-zA-Z]+) ?:=(.*)")
    reFindExpandedVariable = regexp.MustCompile("^([a-zA-Z]+) ?=(.*)")
    reFindSpecialVariable  = regexp.MustCompile("^\\.([a-zA-Z_]+):(.*)")
)

// Parse is the main function to parse a Makefile from a file path string to a
// Makefile struct. This function should be kept fairly small and ideally most
// of the heavy lifting will live in the specific parsing functions below that
// know how to deal with individual lines.
func Parse(filepath string) (ret Makefile, err error) {

    ret.FileName = filepath
    var scanner *MakefileScanner
    scanner, err = NewMakefileScanner(filepath)
    if err != nil {
        return ret, err
    }

    for {
        switch {
        case strings.HasPrefix(scanner.Text(), "#"):
            // parse comments here, ignoring them for now
            scanner.Scan()
        case strings.HasPrefix(scanner.Text(), "."):
            if matches := reFindSpecialVariable.FindStringSubmatch(scanner.Text()); matches != nil {
                specialVar := Variable{
                    Name:            strings.TrimSpace(matches[1]),
                    Assignment:      strings.TrimSpace(matches[2]),
                    SpecialVariable: true,
                    FileName:        filepath,
                    LineNumber:      scanner.LineNumber}
                ret.Variables = append(ret.Variables, specialVar)
            }
            scanner.Scan()
        default:
            // parse target or variable here, the function advances the scanner
            // itself to be able to detect rule bodies
            ruleOrVariable, parseError := parseRuleOrVariable(scanner)
            if parseError != nil {
                return ret, parseError
            }
            switch ruleOrVariable.(type) {
            case Rule:
                rule, found := ruleOrVariable.(Rule)
                if found != true {
                    return ret, errors.New("Parse error")
                }
                ret.Rules = append(ret.Rules, rule)
            case Variable:
                variable, found := ruleOrVariable.(Variable)
                if found != true {
                    return ret, errors.New("Parse error")
                }
                ret.Variables = append(ret.Variables, variable)
            }
        }

        if scanner.Finished == true {
            return
        }
    }
}

// parseRuleOrVariable gets the parsing scanner in a state where it resides on
// a line that could be a variable or a rule. The function parses the line and
// subsequent lines if there is a rule body to parse and returns an interface
// that is either a Variable or a Rule struct and leaves the scanner in a
// state where it resides on the first line after the content parsed into the
// returned struct. The parsing of line details is done via regexing for now
// since it seems ok as a first pass but will likely have to change later into
// a proper lexer/parser setup.
func parseRuleOrVariable(scanner *MakefileScanner) (ret interface{}, err error) {
    line := scanner.Text()

    if matches := reFindRule.FindStringSubmatch(line); matches != nil {
        // we found a rule so we need to advance the scanner to figure out if
        // there is a body
        beginLineNumber := scanner.LineNumber - 1
        scanner.Scan()
        bodyMatches := reFindRuleBody.FindStringSubmatch(scanner.Text())
        ruleBody := make([]string, 0, 20)
        for bodyMatches != nil {

            ruleBody = append(ruleBody, strings.TrimSpace(bodyMatches[1]))

            // done parsing the rule body line, advance the scanner and potentially
            // go into the next loop iteration
            scanner.Scan()
            bodyMatches = reFindRuleBody.FindStringSubmatch(scanner.Text())
        }
        // trim whitespace from all dependencies
        deps := strings.Split(matches[2], " ")
        filteredDeps := make([]string, 0, cap(deps))

        for idx := range deps {
            item := strings.TrimSpace(deps[idx])
            if item != "" {
                filteredDeps = append(filteredDeps, item)
            }
        }
        ret = Rule{
            Target:       strings.TrimSpace(matches[1]),
            Dependencies: filteredDeps,
            Body:         ruleBody,
            FileName:     scanner.FileHandle.Name(),
            LineNumber:   beginLineNumber}
    } else if matches := reFindSimpleVariable.FindStringSubmatch(line); matches != nil {
        ret = Variable{
            Name:           strings.TrimSpace(matches[1]),
            Assignment:     strings.TrimSpace(matches[2]),
            SimplyExpanded: true,
            FileName:       scanner.FileHandle.Name(),
            LineNumber:     scanner.LineNumber}
        scanner.Scan()
    } else if matches := reFindExpandedVariable.FindStringSubmatch(line); matches != nil {
        ret = Variable{
            Name:           strings.TrimSpace(matches[1]),
            Assignment:     strings.TrimSpace(matches[2]),
            SimplyExpanded: false,
            FileName:       scanner.FileHandle.Name(),
            LineNumber:     scanner.LineNumber}
        scanner.Scan()
    } else {
        logger.Debug(fmt.Sprintf("Unable to match line '%s' to a Rule or Variable", line))
        scanner.Scan()
    }

    return
}