// Copyright 2017 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package parser import ( "errors" "fmt" "io" "sort" "text/scanner" ) var errTooManyErrors = errors.New("too many errors") const maxErrors = 100 type ParseError struct { Err error Pos scanner.Position } func (e *ParseError) Error() string { return fmt.Sprintf("%s: %s", e.Pos, e.Err) } const builtinDollar = "__builtin_dollar" var builtinDollarName = SimpleMakeString(builtinDollar, NoPos) func (p *parser) Parse() ([]Node, []error) { defer func() { if r := recover(); r != nil { if r == errTooManyErrors { return } panic(r) } }() p.parseLines() p.accept(scanner.EOF) p.nodes = append(p.nodes, p.comments...) sort.Sort(byPosition(p.nodes)) return p.nodes, p.errors } type parser struct { scanner scanner.Scanner tok rune errors []error comments []Node nodes []Node lines []int } func NewParser(filename string, r io.Reader) *parser { p := &parser{} p.lines = []int{0} p.scanner.Init(r) p.scanner.Error = func(sc *scanner.Scanner, msg string) { p.errorf(msg) } p.scanner.Whitespace = 0 p.scanner.IsIdentRune = func(ch rune, i int) bool { return ch > 0 && ch != ':' && ch != '#' && ch != '=' && ch != '+' && ch != '$' && ch != '\\' && ch != '(' && ch != ')' && ch != '{' && ch != '}' && ch != ';' && ch != '|' && ch != '?' && ch != '\r' && !isWhitespace(ch) } p.scanner.Mode = scanner.ScanIdents p.scanner.Filename = filename p.next() return p } func (p *parser) Unpack(pos Pos) scanner.Position { offset := int(pos) line := sort.Search(len(p.lines), func(i int) bool { return p.lines[i] > offset }) - 1 return scanner.Position{ Filename: p.scanner.Filename, Line: line + 1, Column: offset - p.lines[line] + 1, Offset: offset, } } func (p *parser) pos() Pos { pos := p.scanner.Position if !pos.IsValid() { pos = p.scanner.Pos() } return Pos(pos.Offset) } func (p *parser) errorf(format string, args ...interface{}) { err := &ParseError{ Err: fmt.Errorf(format, args...), Pos: p.scanner.Position, } p.errors = append(p.errors, err) if len(p.errors) >= maxErrors { panic(errTooManyErrors) } } func (p *parser) accept(toks ...rune) bool { for _, tok := range toks { if p.tok != tok { p.errorf("expected %s, found %s", scanner.TokenString(tok), scanner.TokenString(p.tok)) return false } p.next() } return true } func (p *parser) next() { if p.tok != scanner.EOF { p.tok = p.scanner.Scan() for p.tok == '\r' { p.tok = p.scanner.Scan() } } if p.tok == '\n' { p.lines = append(p.lines, p.scanner.Position.Offset+1) } } func (p *parser) parseLines() { for { p.ignoreWhitespace() if p.parseDirective() { continue } ident := p.parseExpression('=', '?', ':', '#', '\n') p.ignoreSpaces() switch p.tok { case '?': p.accept('?') if p.tok == '=' { p.parseAssignment("?=", nil, ident) } else { p.errorf("expected = after ?") } case '+': p.accept('+') if p.tok == '=' { p.parseAssignment("+=", nil, ident) } else { p.errorf("expected = after +") } case ':': p.accept(':') switch p.tok { case '=': p.parseAssignment(":=", nil, ident) default: p.parseRule(ident) } case '=': p.parseAssignment("=", nil, ident) case '#', '\n', scanner.EOF: ident.TrimRightSpaces() if v, ok := toVariable(ident); ok { p.nodes = append(p.nodes, &v) } else if !ident.Empty() { p.errorf("expected directive, rule, or assignment after ident " + ident.Dump()) } switch p.tok { case scanner.EOF: return case '\n': p.accept('\n') case '#': p.parseComment() } default: p.errorf("expected assignment or rule definition, found %s\n", p.scanner.TokenText()) return } } } func (p *parser) parseDirective() bool { if p.tok != scanner.Ident || !isDirective(p.scanner.TokenText()) { return false } d := p.scanner.TokenText() pos := p.pos() p.accept(scanner.Ident) endPos := NoPos expression := SimpleMakeString("", pos) switch d { case "endif", "endef": // Nothing case "else": p.ignoreSpaces() if p.tok != '\n' { d = p.scanner.TokenText() p.accept(scanner.Ident) if d == "ifdef" || d == "ifndef" || d == "ifeq" || d == "ifneq" { d = "el" + d p.ignoreSpaces() expression = p.parseExpression() } else { p.errorf("expected ifdef/ifndef/ifeq/ifneq, found %s", d) } } case "define": expression, endPos = p.parseDefine() default: p.ignoreSpaces() expression = p.parseExpression() } p.nodes = append(p.nodes, &Directive{ NamePos: pos, Name: d, Args: expression, EndPos: endPos, }) return true } func (p *parser) parseDefine() (*MakeString, Pos) { value := SimpleMakeString("", p.pos()) loop: for { switch p.tok { case scanner.Ident: value.appendString(p.scanner.TokenText()) if p.scanner.TokenText() == "endef" { p.accept(scanner.Ident) break loop } p.accept(scanner.Ident) case '\\': p.parseEscape() switch p.tok { case '\n': value.appendString(" ") case scanner.EOF: p.errorf("expected escaped character, found %s", scanner.TokenString(p.tok)) break loop default: value.appendString(`\` + string(p.tok)) } p.accept(p.tok) //TODO: handle variables inside defines? result depends if //define is used in make or rule context //case '$': // variable := p.parseVariable() // value.appendVariable(variable) case scanner.EOF: p.errorf("unexpected EOF while looking for endef") break loop default: value.appendString(p.scanner.TokenText()) p.accept(p.tok) } } return value, p.pos() } func (p *parser) parseEscape() { p.scanner.Mode = 0 p.accept('\\') p.scanner.Mode = scanner.ScanIdents } func (p *parser) parseExpression(end ...rune) *MakeString { value := SimpleMakeString("", p.pos()) endParen := false for _, r := range end { if r == ')' { endParen = true } } parens := 0 loop: for { if endParen && parens > 0 && p.tok == ')' { parens-- value.appendString(")") p.accept(')') continue } for _, r := range end { if p.tok == r { break loop } } switch p.tok { case '\n': break loop case scanner.Ident: value.appendString(p.scanner.TokenText()) p.accept(scanner.Ident) case '\\': p.parseEscape() switch p.tok { case '\n': value.appendString(" ") case scanner.EOF: p.errorf("expected escaped character, found %s", scanner.TokenString(p.tok)) return value default: value.appendString(`\` + string(p.tok)) } p.accept(p.tok) case '#': p.parseComment() break loop case '$': var variable Variable variable = p.parseVariable() if variable.Name == builtinDollarName { value.appendString("$") } else { value.appendVariable(variable) } case scanner.EOF: break loop case '(': if endParen { parens++ } value.appendString("(") p.accept('(') default: value.appendString(p.scanner.TokenText()) p.accept(p.tok) } } if parens > 0 { p.errorf("expected closing paren %s", value.Dump()) } return value } func (p *parser) parseVariable() Variable { pos := p.pos() p.accept('$') var name *MakeString switch p.tok { case '(': return p.parseBracketedVariable('(', ')', pos) case '{': return p.parseBracketedVariable('{', '}', pos) case '$': name = builtinDollarName p.accept(p.tok) case scanner.EOF: p.errorf("expected variable name, found %s", scanner.TokenString(p.tok)) default: name = p.parseExpression(variableNameEndRunes...) } return p.nameToVariable(name) } func (p *parser) parseBracketedVariable(start, end rune, pos Pos) Variable { p.accept(start) name := p.parseExpression(end) p.accept(end) return p.nameToVariable(name) } func (p *parser) nameToVariable(name *MakeString) Variable { return Variable{ Name: name, } } func (p *parser) parseRule(target *MakeString) { prerequisites, newLine := p.parseRulePrerequisites(target) recipe := "" recipePos := p.pos() loop: for { if newLine { if p.tok == '\t' { p.accept('\t') newLine = false continue loop } else if p.parseDirective() { newLine = false continue } else { break loop } } newLine = false switch p.tok { case '\\': p.parseEscape() recipe += string(p.tok) p.accept(p.tok) case '\n': newLine = true recipe += "\n" p.accept('\n') case scanner.EOF: break loop default: recipe += p.scanner.TokenText() p.accept(p.tok) } } if prerequisites != nil { p.nodes = append(p.nodes, &Rule{ Target: target, Prerequisites: prerequisites, Recipe: recipe, RecipePos: recipePos, }) } } func (p *parser) parseRulePrerequisites(target *MakeString) (*MakeString, bool) { newLine := false p.ignoreSpaces() prerequisites := p.parseExpression('#', '\n', ';', ':', '=') switch p.tok { case '\n': p.accept('\n') newLine = true case '#': p.parseComment() newLine = true case ';': p.accept(';') case ':': p.accept(':') if p.tok == '=' { p.parseAssignment(":=", target, prerequisites) return nil, true } else { more := p.parseExpression('#', '\n', ';') prerequisites.appendMakeString(more) } case '=': p.parseAssignment("=", target, prerequisites) return nil, true case scanner.EOF: // do nothing default: p.errorf("unexpected token %s after rule prerequisites", scanner.TokenString(p.tok)) } return prerequisites, newLine } func (p *parser) parseComment() { pos := p.pos() p.accept('#') comment := "" loop: for { switch p.tok { case '\\': p.parseEscape() comment += "\\" + p.scanner.TokenText() p.accept(p.tok) case '\n': p.accept('\n') break loop case scanner.EOF: break loop default: comment += p.scanner.TokenText() p.accept(p.tok) } } p.comments = append(p.comments, &Comment{ CommentPos: pos, Comment: comment, }) } func (p *parser) parseAssignment(t string, target *MakeString, ident *MakeString) { // The value of an assignment is everything including and after the first // non-whitespace character after the = until the end of the logical line, // which may included escaped newlines p.accept('=') value := p.parseExpression() value.TrimLeftSpaces() if ident.EndsWith('+') && t == "=" { ident.TrimRightOne() t = "+=" } ident.TrimRightSpaces() p.nodes = append(p.nodes, &Assignment{ Name: ident, Value: value, Target: target, Type: t, }) } type androidMkModule struct { assignments map[string]string } type androidMkFile struct { assignments map[string]string modules []androidMkModule includes []string } var directives = [...]string{ "define", "else", "endef", "endif", "export", "ifdef", "ifeq", "ifndef", "ifneq", "include", "-include", "unexport", } var functions = [...]string{ "abspath", "addprefix", "addsuffix", "basename", "dir", "notdir", "subst", "suffix", "filter", "filter-out", "findstring", "firstword", "flavor", "join", "lastword", "patsubst", "realpath", "shell", "sort", "strip", "wildcard", "word", "wordlist", "words", "origin", "foreach", "call", "info", "error", "warning", "if", "or", "and", "value", "eval", "file", } func init() { sort.Strings(directives[:]) sort.Strings(functions[:]) } func isDirective(s string) bool { for _, d := range directives { if s == d { return true } else if s < d { return false } } return false } func isFunctionName(s string) bool { for _, f := range functions { if s == f { return true } else if s < f { return false } } return false } func isWhitespace(ch rune) bool { return ch == ' ' || ch == '\t' || ch == '\n' } func isValidVariableRune(ch rune) bool { return ch != scanner.Ident && ch != ':' && ch != '=' && ch != '#' } var whitespaceRunes = []rune{' ', '\t', '\n'} var variableNameEndRunes = append([]rune{':', '=', '#', ')', '}'}, whitespaceRunes...) func (p *parser) ignoreSpaces() int { skipped := 0 for p.tok == ' ' || p.tok == '\t' { p.accept(p.tok) skipped++ } return skipped } func (p *parser) ignoreWhitespace() { for isWhitespace(p.tok) { p.accept(p.tok) } }