platform_build_blueprint/ninja_strings.go

388 lines
9.4 KiB
Go
Raw Normal View History

// Copyright 2014 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 blueprint
import (
"bytes"
"fmt"
"strings"
)
const eof = -1
var (
defaultEscaper = strings.NewReplacer(
"\n", "$\n")
inputEscaper = strings.NewReplacer(
"\n", "$\n",
" ", "$ ")
outputEscaper = strings.NewReplacer(
"\n", "$\n",
" ", "$ ",
":", "$:")
)
type ninjaString struct {
strings []string
variables []Variable
}
type scope interface {
LookupVariable(name string) (Variable, error)
IsRuleVisible(rule Rule) bool
IsPoolVisible(pool Pool) bool
}
func simpleNinjaString(str string) *ninjaString {
return &ninjaString{
strings: []string{str},
}
}
type parseState struct {
scope scope
str string
pendingStr string
stringStart int
varStart int
result *ninjaString
}
func (ps *parseState) pushVariable(v Variable) {
if len(ps.result.variables) == len(ps.result.strings) {
// Last push was a variable, we need a blank string separator
ps.result.strings = append(ps.result.strings, "")
}
if ps.pendingStr != "" {
panic("oops, pushed variable with pending string")
}
ps.result.variables = append(ps.result.variables, v)
}
func (ps *parseState) pushString(s string) {
if len(ps.result.strings) != len(ps.result.variables) {
panic("oops, pushed string after string")
}
ps.result.strings = append(ps.result.strings, ps.pendingStr+s)
ps.pendingStr = ""
}
type stateFunc func(*parseState, int, rune) (stateFunc, error)
// parseNinjaString parses an unescaped ninja string (i.e. all $<something>
// occurrences are expected to be variables or $$) and returns a list of the
// variable names that the string references.
func parseNinjaString(scope scope, str string) (*ninjaString, error) {
// naively pre-allocate slices by counting $ signs
n := strings.Count(str, "$")
result := &ninjaString{
strings: make([]string, 0, n+1),
variables: make([]Variable, 0, n),
}
parseState := &parseState{
scope: scope,
str: str,
result: result,
}
state := parseFirstRuneState
var err error
for i := 0; i < len(str); i++ {
r := rune(str[i])
state, err = state(parseState, i, r)
if err != nil {
return nil, err
}
}
_, err = state(parseState, len(parseState.str), eof)
if err != nil {
return nil, err
}
return result, nil
}
func parseFirstRuneState(state *parseState, i int, r rune) (stateFunc, error) {
if r == ' ' {
state.pendingStr += "$"
}
return parseStringState(state, i, r)
}
func parseStringState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r == '$':
state.varStart = i + 1
return parseDollarStartState, nil
case r == eof:
state.pushString(state.str[state.stringStart:i])
return nil, nil
default:
return parseStringState, nil
}
}
func parseDollarStartState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-':
// The beginning of a of the variable name. Output the string and
// keep going.
state.pushString(state.str[state.stringStart : i-1])
return parseDollarState, nil
case r == '$':
// Just a "$$". Go back to parseStringState without changing
// state.stringStart.
return parseStringState, nil
case r == '{':
// This is a bracketted variable name (e.g. "${blah.blah}"). Output
// the string and keep going.
state.pushString(state.str[state.stringStart : i-1])
state.varStart = i + 1
return parseBracketsState, nil
case r == eof:
return nil, fmt.Errorf("unexpected end of string after '$'")
default:
// This was some arbitrary character following a dollar sign,
// which is not allowed.
return nil, fmt.Errorf("invalid character after '$' at byte "+
"offset %d", i)
}
}
func parseDollarState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-':
// A part of the variable name. Keep going.
return parseDollarState, nil
case r == '$':
// A dollar after the variable name (e.g. "$blah$"). Output the
// variable we have and start a new one.
v, err := state.scope.LookupVariable(state.str[state.varStart:i])
if err != nil {
return nil, err
}
state.pushVariable(v)
state.varStart = i + 1
state.stringStart = i
return parseDollarStartState, nil
case r == eof:
// This is the end of the variable name.
v, err := state.scope.LookupVariable(state.str[state.varStart:i])
if err != nil {
return nil, err
}
state.pushVariable(v)
// We always end with a string, even if it's an empty one.
state.pushString("")
return nil, nil
default:
// We've just gone past the end of the variable name, so record what
// we have.
v, err := state.scope.LookupVariable(state.str[state.varStart:i])
if err != nil {
return nil, err
}
state.pushVariable(v)
state.stringStart = i
return parseStringState, nil
}
}
func parseBracketsState(state *parseState, i int, r rune) (stateFunc, error) {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z',
r >= '0' && r <= '9', r == '_', r == '-', r == '.':
// A part of the variable name. Keep going.
return parseBracketsState, nil
case r == '}':
if state.varStart == i {
// The brackets were immediately closed. That's no good.
return nil, fmt.Errorf("empty variable name at byte offset %d",
i)
}
// This is the end of the variable name.
v, err := state.scope.LookupVariable(state.str[state.varStart:i])
if err != nil {
return nil, err
}
state.pushVariable(v)
state.stringStart = i + 1
return parseStringState, nil
case r == eof:
return nil, fmt.Errorf("unexpected end of string in variable name")
default:
// This character isn't allowed in a variable name.
return nil, fmt.Errorf("invalid character in variable name at "+
"byte offset %d", i)
}
}
func parseNinjaStrings(scope scope, strs []string) ([]*ninjaString,
error) {
if len(strs) == 0 {
return nil, nil
}
result := make([]*ninjaString, len(strs))
for i, str := range strs {
ninjaStr, err := parseNinjaString(scope, str)
if err != nil {
return nil, fmt.Errorf("error parsing element %d: %s", i, err)
}
result[i] = ninjaStr
}
return result, nil
}
func (n *ninjaString) Value(pkgNames map[*packageContext]string) string {
return n.ValueWithEscaper(pkgNames, defaultEscaper)
}
func (n *ninjaString) ValueWithEscaper(pkgNames map[*packageContext]string,
escaper *strings.Replacer) string {
Optimize ninjaString.ValueWithEscaper ninjaString.ValueWithEscaper is a relatively hot function, rewrite it with strings.Builder to avoid repeated string concatenation, which requires an allocation each time. Before: BenchmarkNinjaString_Value/constant/1-72 100000000 11.9 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 18.9 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 22.1 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 39.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 95.1 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 223 ns/op BenchmarkNinjaString_Value/variable/100-72 3000000 437 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 948 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 161 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 368 ns/op BenchmarkNinjaString_Value/variables/3-72 3000000 560 ns/op BenchmarkNinjaString_Value/variables/4-72 2000000 795 ns/op BenchmarkNinjaString_Value/variables/5-72 1000000 1004 ns/op BenchmarkNinjaString_Value/variables/10-72 1000000 2275 ns/op BenchmarkNinjaString_Value/variables/100-72 50000 39667 ns/op BenchmarkNinjaString_Value/variables/1000-72 1000 2146592 ns/op After: BenchmarkNinjaString_Value/constant/1-72 200000000 11.3 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 17.2 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 21.7 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 38.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 91.8 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 199 ns/op BenchmarkNinjaString_Value/variable/100-72 5000000 377 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 855 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 141 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 312 ns/op BenchmarkNinjaString_Value/variables/3-72 5000000 362 ns/op BenchmarkNinjaString_Value/variables/4-72 3000000 417 ns/op BenchmarkNinjaString_Value/variables/5-72 2000000 621 ns/op BenchmarkNinjaString_Value/variables/10-72 2000000 837 ns/op BenchmarkNinjaString_Value/variables/100-72 200000 9141 ns/op BenchmarkNinjaString_Value/variables/1000-72 20000 95094 ns/op Test: ninja_strings_test.go Change-Id: I6c61e747d8e67f7f1e6cff0cc0c705745301a35f
2019-06-20 08:25:39 +02:00
if len(n.strings) == 1 {
return escaper.Replace(n.strings[0])
}
str := strings.Builder{}
str.WriteString(escaper.Replace(n.strings[0]))
for i, v := range n.variables {
Optimize ninjaString.ValueWithEscaper ninjaString.ValueWithEscaper is a relatively hot function, rewrite it with strings.Builder to avoid repeated string concatenation, which requires an allocation each time. Before: BenchmarkNinjaString_Value/constant/1-72 100000000 11.9 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 18.9 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 22.1 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 39.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 95.1 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 223 ns/op BenchmarkNinjaString_Value/variable/100-72 3000000 437 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 948 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 161 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 368 ns/op BenchmarkNinjaString_Value/variables/3-72 3000000 560 ns/op BenchmarkNinjaString_Value/variables/4-72 2000000 795 ns/op BenchmarkNinjaString_Value/variables/5-72 1000000 1004 ns/op BenchmarkNinjaString_Value/variables/10-72 1000000 2275 ns/op BenchmarkNinjaString_Value/variables/100-72 50000 39667 ns/op BenchmarkNinjaString_Value/variables/1000-72 1000 2146592 ns/op After: BenchmarkNinjaString_Value/constant/1-72 200000000 11.3 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 17.2 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 21.7 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 38.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 91.8 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 199 ns/op BenchmarkNinjaString_Value/variable/100-72 5000000 377 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 855 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 141 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 312 ns/op BenchmarkNinjaString_Value/variables/3-72 5000000 362 ns/op BenchmarkNinjaString_Value/variables/4-72 3000000 417 ns/op BenchmarkNinjaString_Value/variables/5-72 2000000 621 ns/op BenchmarkNinjaString_Value/variables/10-72 2000000 837 ns/op BenchmarkNinjaString_Value/variables/100-72 200000 9141 ns/op BenchmarkNinjaString_Value/variables/1000-72 20000 95094 ns/op Test: ninja_strings_test.go Change-Id: I6c61e747d8e67f7f1e6cff0cc0c705745301a35f
2019-06-20 08:25:39 +02:00
str.WriteString("${")
str.WriteString(v.fullName(pkgNames))
str.WriteString("}")
str.WriteString(escaper.Replace(n.strings[i+1]))
}
Optimize ninjaString.ValueWithEscaper ninjaString.ValueWithEscaper is a relatively hot function, rewrite it with strings.Builder to avoid repeated string concatenation, which requires an allocation each time. Before: BenchmarkNinjaString_Value/constant/1-72 100000000 11.9 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 18.9 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 22.1 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 39.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 95.1 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 223 ns/op BenchmarkNinjaString_Value/variable/100-72 3000000 437 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 948 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 161 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 368 ns/op BenchmarkNinjaString_Value/variables/3-72 3000000 560 ns/op BenchmarkNinjaString_Value/variables/4-72 2000000 795 ns/op BenchmarkNinjaString_Value/variables/5-72 1000000 1004 ns/op BenchmarkNinjaString_Value/variables/10-72 1000000 2275 ns/op BenchmarkNinjaString_Value/variables/100-72 50000 39667 ns/op BenchmarkNinjaString_Value/variables/1000-72 1000 2146592 ns/op After: BenchmarkNinjaString_Value/constant/1-72 200000000 11.3 ns/op BenchmarkNinjaString_Value/constant/10-72 100000000 17.2 ns/op BenchmarkNinjaString_Value/constant/100-72 50000000 21.7 ns/op BenchmarkNinjaString_Value/constant/1000-72 30000000 38.3 ns/op BenchmarkNinjaString_Value/variable/1-72 20000000 91.8 ns/op BenchmarkNinjaString_Value/variable/10-72 10000000 199 ns/op BenchmarkNinjaString_Value/variable/100-72 5000000 377 ns/op BenchmarkNinjaString_Value/variable/1000-72 2000000 855 ns/op BenchmarkNinjaString_Value/variables/1-72 10000000 141 ns/op BenchmarkNinjaString_Value/variables/2-72 5000000 312 ns/op BenchmarkNinjaString_Value/variables/3-72 5000000 362 ns/op BenchmarkNinjaString_Value/variables/4-72 3000000 417 ns/op BenchmarkNinjaString_Value/variables/5-72 2000000 621 ns/op BenchmarkNinjaString_Value/variables/10-72 2000000 837 ns/op BenchmarkNinjaString_Value/variables/100-72 200000 9141 ns/op BenchmarkNinjaString_Value/variables/1000-72 20000 95094 ns/op Test: ninja_strings_test.go Change-Id: I6c61e747d8e67f7f1e6cff0cc0c705745301a35f
2019-06-20 08:25:39 +02:00
return str.String()
}
func (n *ninjaString) Eval(variables map[Variable]*ninjaString) (string, error) {
str := n.strings[0]
for i, v := range n.variables {
variable, ok := variables[v]
if !ok {
return "", fmt.Errorf("no such global variable: %s", v)
}
value, err := variable.Eval(variables)
if err != nil {
return "", err
}
str += value + n.strings[i+1]
}
return str, nil
}
func validateNinjaName(name string) error {
for i, r := range name {
valid := (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
(r == '_') ||
(r == '-') ||
(r == '.')
if !valid {
return fmt.Errorf("%q contains an invalid Ninja name character "+
"%q at byte offset %d", name, r, i)
}
}
return nil
}
func toNinjaName(name string) string {
ret := bytes.Buffer{}
ret.Grow(len(name))
for _, r := range name {
valid := (r >= 'a' && r <= 'z') ||
(r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') ||
(r == '_') ||
(r == '-') ||
(r == '.')
if valid {
ret.WriteRune(r)
} else {
// TODO(jeffrygaston): do escaping so that toNinjaName won't ever output duplicate
// names for two different input names
ret.WriteRune('_')
}
}
return ret.String()
}
var builtinRuleArgs = []string{"out", "in"}
func validateArgName(argName string) error {
err := validateNinjaName(argName)
if err != nil {
return err
}
// We only allow globals within the rule's package to be used as rule
// arguments. A global in another package can always be mirrored into
// the rule's package by defining a new variable, so this doesn't limit
// what's possible. This limitation prevents situations where a Build
// invocation in another package must use the rule-defining package's
// import name for a 3rd package in order to set the rule's arguments.
if strings.ContainsRune(argName, '.') {
return fmt.Errorf("%q contains a '.' character", argName)
}
for _, builtin := range builtinRuleArgs {
if argName == builtin {
return fmt.Errorf("%q conflicts with Ninja built-in", argName)
}
}
return nil
}
func validateArgNames(argNames []string) error {
for _, argName := range argNames {
err := validateArgName(argName)
if err != nil {
return err
}
}
return nil
}