Commit 7f4b4c0c authored by Rob Pike's avatar Rob Pike

text/template: better error messages during execution,

They now show the correct name, the byte offset on the line, and context for the failed evaluation.
Before:
        template: three:7: error calling index: index out of range: 5
After:
        template: top:7:20: executing "three" at <index "hi" $>: error calling index: index out of range: 5
Here top is the template that was parsed to create the set, and the error appears with the action
starting at byte 20 of line 7 of "top", inside the template called "three", evaluating the expression
<index "hi" $>.

Also fix a bug in index: it didn't work on strings. Ouch.

Also fix bug in error for index: was showing type of index not slice.
The real previous error was:
        template: three:7: error calling index: can't index item of type int
The html/template package's errors can be improved by building on this;
I'll do that in a separate pass.

Extends the API for text/template/parse but only by addition of a field and method. The
old API still works.

Fixes #3188.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6576058
parent 1659aef3
...@@ -242,10 +242,11 @@ func ensurePipelineContains(p *parse.PipeNode, s []string) { ...@@ -242,10 +242,11 @@ func ensurePipelineContains(p *parse.PipeNode, s []string) {
copy(newCmds, p.Cmds) copy(newCmds, p.Cmds)
// Merge existing identifier commands with the sanitizers needed. // Merge existing identifier commands with the sanitizers needed.
for _, id := range idents { for _, id := range idents {
pos := id.Args[0].Position()
i := indexOfStr((id.Args[0].(*parse.IdentifierNode)).Ident, s, escFnsEq) i := indexOfStr((id.Args[0].(*parse.IdentifierNode)).Ident, s, escFnsEq)
if i != -1 { if i != -1 {
for _, name := range s[:i] { for _, name := range s[:i] {
newCmds = appendCmd(newCmds, newIdentCmd(name)) newCmds = appendCmd(newCmds, newIdentCmd(name, pos))
} }
s = s[i+1:] s = s[i+1:]
} }
...@@ -253,7 +254,7 @@ func ensurePipelineContains(p *parse.PipeNode, s []string) { ...@@ -253,7 +254,7 @@ func ensurePipelineContains(p *parse.PipeNode, s []string) {
} }
// Create any remaining sanitizers. // Create any remaining sanitizers.
for _, name := range s { for _, name := range s {
newCmds = appendCmd(newCmds, newIdentCmd(name)) newCmds = appendCmd(newCmds, newIdentCmd(name, p.Position()))
} }
p.Cmds = newCmds p.Cmds = newCmds
} }
...@@ -315,10 +316,10 @@ func escFnsEq(a, b string) bool { ...@@ -315,10 +316,10 @@ func escFnsEq(a, b string) bool {
} }
// newIdentCmd produces a command containing a single identifier node. // newIdentCmd produces a command containing a single identifier node.
func newIdentCmd(identifier string) *parse.CommandNode { func newIdentCmd(identifier string, pos parse.Pos) *parse.CommandNode {
return &parse.CommandNode{ return &parse.CommandNode{
NodeType: parse.NodeCommand, NodeType: parse.NodeCommand,
Args: []parse.Node{parse.NewIdentifier(identifier)}, Args: []parse.Node{parse.NewIdentifier(identifier).SetPos(pos)},
} }
} }
......
...@@ -20,7 +20,7 @@ import ( ...@@ -20,7 +20,7 @@ import (
type state struct { type state struct {
tmpl *Template tmpl *Template
wr io.Writer wr io.Writer
line int // line number for errors node parse.Node // current node, for errors
vars []variable // push-down stack of variable values. vars []variable // push-down stack of variable values.
} }
...@@ -63,17 +63,32 @@ func (s *state) varValue(name string) reflect.Value { ...@@ -63,17 +63,32 @@ func (s *state) varValue(name string) reflect.Value {
var zero reflect.Value var zero reflect.Value
// at marks the state to be on node n, for error reporting.
func (s *state) at(node parse.Node) {
s.node = node
}
// doublePercent returns the string with %'s replaced by %%, if necessary,
// so it can be used safely inside a Printf format string.
func doublePercent(str string) string {
if strings.Contains(str, "%") {
str = strings.Replace(str, "%", "%%", -1)
}
return str
}
// errorf formats the error and terminates processing. // errorf formats the error and terminates processing.
func (s *state) errorf(format string, args ...interface{}) { func (s *state) errorf(format string, args ...interface{}) {
format = fmt.Sprintf("template: %s:%d: %s", s.tmpl.Name(), s.line, format) name := doublePercent(s.tmpl.Name())
if s.node == nil {
format = fmt.Sprintf("template: %s: %s", name, format)
} else {
location, context := s.tmpl.ErrorContext(s.node)
format = fmt.Sprintf("template: %s: executing %q at <%s>: %s", location, name, doublePercent(context), format)
}
panic(fmt.Errorf(format, args...)) panic(fmt.Errorf(format, args...))
} }
// error terminates processing.
func (s *state) error(err error) {
s.errorf("%s", err)
}
// errRecover is the handler that turns panics into returns from the top // errRecover is the handler that turns panics into returns from the top
// level of Parse. // level of Parse.
func errRecover(errp *error) { func errRecover(errp *error) {
...@@ -108,7 +123,6 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { ...@@ -108,7 +123,6 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
state := &state{ state := &state{
tmpl: t, tmpl: t,
wr: wr, wr: wr,
line: 1,
vars: []variable{{"$", value}}, vars: []variable{{"$", value}},
} }
if t.Tree == nil || t.Root == nil { if t.Tree == nil || t.Root == nil {
...@@ -120,38 +134,34 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) { ...@@ -120,38 +134,34 @@ func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
// Walk functions step through the major pieces of the template structure, // Walk functions step through the major pieces of the template structure,
// generating output as they go. // generating output as they go.
func (s *state) walk(dot reflect.Value, n parse.Node) { func (s *state) walk(dot reflect.Value, node parse.Node) {
switch n := n.(type) { s.at(node)
switch node := node.(type) {
case *parse.ActionNode: case *parse.ActionNode:
s.line = n.Line
// Do not pop variables so they persist until next end. // Do not pop variables so they persist until next end.
// Also, if the action declares variables, don't print the result. // Also, if the action declares variables, don't print the result.
val := s.evalPipeline(dot, n.Pipe) val := s.evalPipeline(dot, node.Pipe)
if len(n.Pipe.Decl) == 0 { if len(node.Pipe.Decl) == 0 {
s.printValue(n, val) s.printValue(node, val)
} }
case *parse.IfNode: case *parse.IfNode:
s.line = n.Line s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)
s.walkIfOrWith(parse.NodeIf, dot, n.Pipe, n.List, n.ElseList)
case *parse.ListNode: case *parse.ListNode:
for _, node := range n.Nodes { for _, node := range node.Nodes {
s.walk(dot, node) s.walk(dot, node)
} }
case *parse.RangeNode: case *parse.RangeNode:
s.line = n.Line s.walkRange(dot, node)
s.walkRange(dot, n)
case *parse.TemplateNode: case *parse.TemplateNode:
s.line = n.Line s.walkTemplate(dot, node)
s.walkTemplate(dot, n)
case *parse.TextNode: case *parse.TextNode:
if _, err := s.wr.Write(n.Text); err != nil { if _, err := s.wr.Write(node.Text); err != nil {
s.error(err) s.errorf("%s", err)
} }
case *parse.WithNode: case *parse.WithNode:
s.line = n.Line s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)
s.walkIfOrWith(parse.NodeWith, dot, n.Pipe, n.List, n.ElseList)
default: default:
s.errorf("unknown node: %s", n) s.errorf("unknown node: %s", node)
} }
} }
...@@ -206,6 +216,7 @@ func isTrue(val reflect.Value) (truth, ok bool) { ...@@ -206,6 +216,7 @@ func isTrue(val reflect.Value) (truth, ok bool) {
} }
func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
s.at(r)
defer s.pop(s.mark()) defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, r.Pipe)) val, _ := indirect(s.evalPipeline(dot, r.Pipe))
// mark top of stack before any variables in the body are pushed. // mark top of stack before any variables in the body are pushed.
...@@ -266,6 +277,7 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) { ...@@ -266,6 +277,7 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
} }
func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) { func (s *state) walkTemplate(dot reflect.Value, t *parse.TemplateNode) {
s.at(t)
tmpl := s.tmpl.tmpl[t.Name] tmpl := s.tmpl.tmpl[t.Name]
if tmpl == nil { if tmpl == nil {
s.errorf("template %q not defined", t.Name) s.errorf("template %q not defined", t.Name)
...@@ -291,6 +303,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref ...@@ -291,6 +303,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
if pipe == nil { if pipe == nil {
return return
} }
s.at(pipe)
for _, cmd := range pipe.Cmds { for _, cmd := range pipe.Cmds {
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg. value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
// If the object has type interface{}, dig down one level to the thing inside. // If the object has type interface{}, dig down one level to the thing inside.
...@@ -319,14 +332,14 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref ...@@ -319,14 +332,14 @@ func (s *state) evalCommand(dot reflect.Value, cmd *parse.CommandNode, final ref
return s.evalChainNode(dot, n, cmd.Args, final) return s.evalChainNode(dot, n, cmd.Args, final)
case *parse.IdentifierNode: case *parse.IdentifierNode:
// Must be a function. // Must be a function.
return s.evalFunction(dot, n.Ident, cmd.Args, final) return s.evalFunction(dot, n, cmd, cmd.Args, final)
case *parse.PipeNode: case *parse.PipeNode:
// Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored. // Parenthesized pipeline. The arguments are all inside the pipeline; final is ignored.
// TODO: is this right?
return s.evalPipeline(dot, n) return s.evalPipeline(dot, n)
case *parse.VariableNode: case *parse.VariableNode:
return s.evalVariableNode(dot, n, cmd.Args, final) return s.evalVariableNode(dot, n, cmd.Args, final)
} }
s.at(firstWord)
s.notAFunction(cmd.Args, final) s.notAFunction(cmd.Args, final)
switch word := firstWord.(type) { switch word := firstWord.(type) {
case *parse.BoolNode: case *parse.BoolNode:
...@@ -352,6 +365,7 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value { ...@@ -352,6 +365,7 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
// These are ideal constants but we don't know the type // These are ideal constants but we don't know the type
// and we have no context. (If it was a method argument, // and we have no context. (If it was a method argument,
// we'd know what we need.) The syntax guides us to some extent. // we'd know what we need.) The syntax guides us to some extent.
s.at(constant)
switch { switch {
case constant.IsComplex: case constant.IsComplex:
return reflect.ValueOf(constant.Complex128) // incontrovertible. return reflect.ValueOf(constant.Complex128) // incontrovertible.
...@@ -370,52 +384,57 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value { ...@@ -370,52 +384,57 @@ func (s *state) idealConstant(constant *parse.NumberNode) reflect.Value {
} }
func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalFieldNode(dot reflect.Value, field *parse.FieldNode, args []parse.Node, final reflect.Value) reflect.Value {
return s.evalFieldChain(dot, dot, field.Ident, args, final) s.at(field)
return s.evalFieldChain(dot, dot, field, field.Ident, args, final)
} }
func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalChainNode(dot reflect.Value, chain *parse.ChainNode, args []parse.Node, final reflect.Value) reflect.Value {
s.at(chain)
// (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields. // (pipe).Field1.Field2 has pipe as .Node, fields as .Field. Eval the pipeline, then the fields.
pipe := s.evalArg(dot, nil, chain.Node) pipe := s.evalArg(dot, nil, chain.Node)
if len(chain.Field) == 0 { if len(chain.Field) == 0 {
s.errorf("internal error: no fields in evalChainNode") s.errorf("internal error: no fields in evalChainNode")
} }
return s.evalFieldChain(dot, pipe, chain.Field, args, final) return s.evalFieldChain(dot, pipe, chain, chain.Field, args, final)
} }
func (s *state) evalVariableNode(dot reflect.Value, v *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalVariableNode(dot reflect.Value, variable *parse.VariableNode, args []parse.Node, final reflect.Value) reflect.Value {
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields. // $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
value := s.varValue(v.Ident[0]) s.at(variable)
if len(v.Ident) == 1 { value := s.varValue(variable.Ident[0])
if len(variable.Ident) == 1 {
s.notAFunction(args, final) s.notAFunction(args, final)
return value return value
} }
return s.evalFieldChain(dot, value, v.Ident[1:], args, final) return s.evalFieldChain(dot, value, variable, variable.Ident[1:], args, final)
} }
// evalFieldChain evaluates .X.Y.Z possibly followed by arguments. // evalFieldChain evaluates .X.Y.Z possibly followed by arguments.
// dot is the environment in which to evaluate arguments, while // dot is the environment in which to evaluate arguments, while
// receiver is the value being walked along the chain. // receiver is the value being walked along the chain.
func (s *state) evalFieldChain(dot, receiver reflect.Value, ident []string, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalFieldChain(dot, receiver reflect.Value, node parse.Node, ident []string, args []parse.Node, final reflect.Value) reflect.Value {
n := len(ident) n := len(ident)
for i := 0; i < n-1; i++ { for i := 0; i < n-1; i++ {
receiver = s.evalField(dot, ident[i], nil, zero, receiver) receiver = s.evalField(dot, ident[i], node, nil, zero, receiver)
} }
// Now if it's a method, it gets the arguments. // Now if it's a method, it gets the arguments.
return s.evalField(dot, ident[n-1], args, final, receiver) return s.evalField(dot, ident[n-1], node, args, final, receiver)
} }
func (s *state) evalFunction(dot reflect.Value, name string, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalFunction(dot reflect.Value, node *parse.IdentifierNode, cmd parse.Node, args []parse.Node, final reflect.Value) reflect.Value {
s.at(node)
name := node.Ident
function, ok := findFunction(name, s.tmpl) function, ok := findFunction(name, s.tmpl)
if !ok { if !ok {
s.errorf("%q is not a defined function", name) s.errorf("%q is not a defined function", name)
} }
return s.evalCall(dot, function, name, args, final) return s.evalCall(dot, function, cmd, name, args, final)
} }
// evalField evaluates an expression like (.Field) or (.Field arg1 arg2). // evalField evaluates an expression like (.Field) or (.Field arg1 arg2).
// The 'final' argument represents the return value from the preceding // The 'final' argument represents the return value from the preceding
// value of the pipeline, if any. // value of the pipeline, if any.
func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node, final, receiver reflect.Value) reflect.Value { func (s *state) evalField(dot reflect.Value, fieldName string, node parse.Node, args []parse.Node, final, receiver reflect.Value) reflect.Value {
if !receiver.IsValid() { if !receiver.IsValid() {
return zero return zero
} }
...@@ -428,7 +447,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node ...@@ -428,7 +447,7 @@ func (s *state) evalField(dot reflect.Value, fieldName string, args []parse.Node
ptr = ptr.Addr() ptr = ptr.Addr()
} }
if method := ptr.MethodByName(fieldName); method.IsValid() { if method := ptr.MethodByName(fieldName); method.IsValid() {
return s.evalCall(dot, method, fieldName, args, final) return s.evalCall(dot, method, node, fieldName, args, final)
} }
hasArgs := len(args) > 1 || final.IsValid() hasArgs := len(args) > 1 || final.IsValid()
// It's not a method; must be a field of a struct or an element of a map. The receiver must not be nil. // It's not a method; must be a field of a struct or an element of a map. The receiver must not be nil.
...@@ -473,7 +492,7 @@ var ( ...@@ -473,7 +492,7 @@ var (
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so // evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
// it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0] // it looks just like a function call. The arg list, if non-nil, includes (in the manner of the shell), arg[0]
// as the function itself. // as the function itself.
func (s *state) evalCall(dot, fun reflect.Value, name string, args []parse.Node, final reflect.Value) reflect.Value { func (s *state) evalCall(dot, fun reflect.Value, node parse.Node, name string, args []parse.Node, final reflect.Value) reflect.Value {
if args != nil { if args != nil {
args = args[1:] // Zeroth arg is function name/node; not passed to function. args = args[1:] // Zeroth arg is function name/node; not passed to function.
} }
...@@ -492,7 +511,8 @@ func (s *state) evalCall(dot, fun reflect.Value, name string, args []parse.Node, ...@@ -492,7 +511,8 @@ func (s *state) evalCall(dot, fun reflect.Value, name string, args []parse.Node,
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), len(args)) s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), len(args))
} }
if !goodFunc(typ) { if !goodFunc(typ) {
s.errorf("can't handle multiple results from method/function %q", name) // TODO: This could still be a confusing error; maybe goodFunc should provide info.
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
} }
// Build the arg list. // Build the arg list.
argv := make([]reflect.Value, numIn) argv := make([]reflect.Value, numIn)
...@@ -519,6 +539,7 @@ func (s *state) evalCall(dot, fun reflect.Value, name string, args []parse.Node, ...@@ -519,6 +539,7 @@ func (s *state) evalCall(dot, fun reflect.Value, name string, args []parse.Node,
result := fun.Call(argv) result := fun.Call(argv)
// If we have an error that is not nil, stop execution and return that error to the caller. // If we have an error that is not nil, stop execution and return that error to the caller.
if len(result) == 2 && !result[1].IsNil() { if len(result) == 2 && !result[1].IsNil() {
s.at(node)
s.errorf("error calling %s: %s", name, result[1].Interface().(error)) s.errorf("error calling %s: %s", name, result[1].Interface().(error))
} }
return result[0] return result[0]
...@@ -567,6 +588,7 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu ...@@ -567,6 +588,7 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu
} }
func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
switch arg := n.(type) { switch arg := n.(type) {
case *parse.DotNode: case *parse.DotNode:
return s.validateType(dot, typ) return s.validateType(dot, typ)
...@@ -605,6 +627,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle ...@@ -605,6 +627,7 @@ func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n parse.Node) refle
} }
func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
if n, ok := n.(*parse.BoolNode); ok { if n, ok := n.(*parse.BoolNode); ok {
value := reflect.New(typ).Elem() value := reflect.New(typ).Elem()
value.SetBool(n.True) value.SetBool(n.True)
...@@ -615,6 +638,7 @@ func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value { ...@@ -615,6 +638,7 @@ func (s *state) evalBool(typ reflect.Type, n parse.Node) reflect.Value {
} }
func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
if n, ok := n.(*parse.StringNode); ok { if n, ok := n.(*parse.StringNode); ok {
value := reflect.New(typ).Elem() value := reflect.New(typ).Elem()
value.SetString(n.Text) value.SetString(n.Text)
...@@ -625,6 +649,7 @@ func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value { ...@@ -625,6 +649,7 @@ func (s *state) evalString(typ reflect.Type, n parse.Node) reflect.Value {
} }
func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
if n, ok := n.(*parse.NumberNode); ok && n.IsInt { if n, ok := n.(*parse.NumberNode); ok && n.IsInt {
value := reflect.New(typ).Elem() value := reflect.New(typ).Elem()
value.SetInt(n.Int64) value.SetInt(n.Int64)
...@@ -635,6 +660,7 @@ func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value { ...@@ -635,6 +660,7 @@ func (s *state) evalInteger(typ reflect.Type, n parse.Node) reflect.Value {
} }
func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
if n, ok := n.(*parse.NumberNode); ok && n.IsUint { if n, ok := n.(*parse.NumberNode); ok && n.IsUint {
value := reflect.New(typ).Elem() value := reflect.New(typ).Elem()
value.SetUint(n.Uint64) value.SetUint(n.Uint64)
...@@ -645,6 +671,7 @@ func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Valu ...@@ -645,6 +671,7 @@ func (s *state) evalUnsignedInteger(typ reflect.Type, n parse.Node) reflect.Valu
} }
func (s *state) evalFloat(typ reflect.Type, n parse.Node) reflect.Value { func (s *state) evalFloat(typ reflect.Type, n parse.Node) reflect.Value {
s.at(n)
if n, ok := n.(*parse.NumberNode); ok && n.IsFloat { if n, ok := n.(*parse.NumberNode); ok && n.IsFloat {
value := reflect.New(typ).Elem() value := reflect.New(typ).Elem()
value.SetFloat(n.Float64) value.SetFloat(n.Float64)
...@@ -665,6 +692,7 @@ func (s *state) evalComplex(typ reflect.Type, n parse.Node) reflect.Value { ...@@ -665,6 +692,7 @@ func (s *state) evalComplex(typ reflect.Type, n parse.Node) reflect.Value {
} }
func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value { func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Value {
s.at(n)
switch n := n.(type) { switch n := n.(type) {
case *parse.BoolNode: case *parse.BoolNode:
return reflect.ValueOf(n.True) return reflect.ValueOf(n.True)
...@@ -673,7 +701,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu ...@@ -673,7 +701,7 @@ func (s *state) evalEmptyInterface(dot reflect.Value, n parse.Node) reflect.Valu
case *parse.FieldNode: case *parse.FieldNode:
return s.evalFieldNode(dot, n, nil, zero) return s.evalFieldNode(dot, n, nil, zero)
case *parse.IdentifierNode: case *parse.IdentifierNode:
return s.evalFunction(dot, n.Ident, nil, zero) return s.evalFunction(dot, n, n, nil, zero)
case *parse.NilNode: case *parse.NilNode:
// NilNode is handled in evalArg, the only place that calls here. // NilNode is handled in evalArg, the only place that calls here.
s.errorf("evalEmptyInterface: nil (can't happen)") s.errorf("evalEmptyInterface: nil (can't happen)")
...@@ -708,6 +736,7 @@ func indirect(v reflect.Value) (rv reflect.Value, isNil bool) { ...@@ -708,6 +736,7 @@ func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
// printValue writes the textual representation of the value to the output of // printValue writes the textual representation of the value to the output of
// the template. // the template.
func (s *state) printValue(n parse.Node, v reflect.Value) { func (s *state) printValue(n parse.Node, v reflect.Value) {
s.at(n)
if v.Kind() == reflect.Ptr { if v.Kind() == reflect.Ptr {
v, _ = indirect(v) // fmt.Fprint handles nil. v, _ = indirect(v) // fmt.Fprint handles nil.
} }
......
...@@ -675,6 +675,32 @@ func TestExecuteError(t *testing.T) { ...@@ -675,6 +675,32 @@ func TestExecuteError(t *testing.T) {
} }
} }
const execErrorText = `line 1
line 2
line 3
{{template "one" .}}
{{define "one"}}{{template "two" .}}{{end}}
{{define "two"}}{{template "three" .}}{{end}}
{{define "three"}}{{index "hi" $}}{{end}}`
// Check that an error from a nested template contains all the relevant information.
func TestExecError(t *testing.T) {
tmpl, err := New("top").Parse(execErrorText)
if err != nil {
t.Fatal("parse error:", err)
}
var b bytes.Buffer
err = tmpl.Execute(&b, 5) // 5 is out of range indexing "hi"
if err == nil {
t.Fatal("expected error")
}
const want = `template: top:7:20: executing "three" at <index "hi" $>: error calling index: index out of range: 5`
got := err.Error()
if got != want {
t.Errorf("expected\n%q\ngot\n%q", want, got)
}
}
func TestJSEscaping(t *testing.T) { func TestJSEscaping(t *testing.T) {
testCases := []struct { testCases := []struct {
in, exp string in, exp string
......
...@@ -54,7 +54,7 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) { ...@@ -54,7 +54,7 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
panic("value for " + name + " not a function") panic("value for " + name + " not a function")
} }
if !goodFunc(v.Type()) { if !goodFunc(v.Type()) {
panic(fmt.Errorf("can't handle multiple results from method/function %q", name)) panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
} }
out[name] = v out[name] = v
} }
...@@ -107,7 +107,7 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) { ...@@ -107,7 +107,7 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("index of nil pointer") return nil, fmt.Errorf("index of nil pointer")
} }
switch v.Kind() { switch v.Kind() {
case reflect.Array, reflect.Slice: case reflect.Array, reflect.Slice, reflect.String:
var x int64 var x int64
switch index.Kind() { switch index.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
...@@ -134,7 +134,7 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) { ...@@ -134,7 +134,7 @@ func index(item interface{}, indices ...interface{}) (interface{}, error) {
v = reflect.Zero(v.Type().Elem()) v = reflect.Zero(v.Type().Elem())
} }
default: default:
return nil, fmt.Errorf("can't index item of type %s", index.Type()) return nil, fmt.Errorf("can't index item of type %s", v.Type())
} }
} }
return v.Interface(), nil return v.Interface(), nil
......
...@@ -14,7 +14,7 @@ import ( ...@@ -14,7 +14,7 @@ import (
// item represents a token or text string returned from the scanner. // item represents a token or text string returned from the scanner.
type item struct { type item struct {
typ itemType // The type of this item. typ itemType // The type of this item.
pos int // The starting position, in bytes, of this item in the input string. pos Pos // The starting position, in bytes, of this item in the input string.
val string // The value of this item. val string // The value of this item.
} }
...@@ -38,7 +38,7 @@ type itemType int ...@@ -38,7 +38,7 @@ type itemType int
const ( const (
itemError itemType = iota // error occurred; value is text of error itemError itemType = iota // error occurred; value is text of error
itemBool // boolean constant itemBool // boolean constant
itemChar // printable ASCII character; grab bag for comma etc itemChar // printable ASCII character; grab bag for comma etc.
itemCharConstant // character constant itemCharConstant // character constant
itemComplex // complex constant (1+2i); imaginary is just a number itemComplex // complex constant (1+2i); imaginary is just a number
itemColonEquals // colon-equals (':=') introducing a declaration itemColonEquals // colon-equals (':=') introducing a declaration
...@@ -93,21 +93,22 @@ type lexer struct { ...@@ -93,21 +93,22 @@ type lexer struct {
leftDelim string // start of action leftDelim string // start of action
rightDelim string // end of action rightDelim string // end of action
state stateFn // the next lexing function to enter state stateFn // the next lexing function to enter
pos int // current position in the input pos Pos // current position in the input
start int // start position of this item start Pos // start position of this item
width int // width of last rune read from input width Pos // width of last rune read from input
lastPos int // position of most recent item returned by nextItem lastPos Pos // position of most recent item returned by nextItem
items chan item // channel of scanned items items chan item // channel of scanned items
parenDepth int // nesting depth of ( ) exprs parenDepth int // nesting depth of ( ) exprs
} }
// next returns the next rune in the input. // next returns the next rune in the input.
func (l *lexer) next() (r rune) { func (l *lexer) next() rune {
if l.pos >= len(l.input) { if int(l.pos) >= len(l.input) {
l.width = 0 l.width = 0
return eof return eof
} }
r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = Pos(w)
l.pos += l.width l.pos += l.width
return r return r
} }
...@@ -230,7 +231,7 @@ func lexText(l *lexer) stateFn { ...@@ -230,7 +231,7 @@ func lexText(l *lexer) stateFn {
// lexLeftDelim scans the left delimiter, which is known to be present. // lexLeftDelim scans the left delimiter, which is known to be present.
func lexLeftDelim(l *lexer) stateFn { func lexLeftDelim(l *lexer) stateFn {
l.pos += len(l.leftDelim) l.pos += Pos(len(l.leftDelim))
if strings.HasPrefix(l.input[l.pos:], leftComment) { if strings.HasPrefix(l.input[l.pos:], leftComment) {
return lexComment return lexComment
} }
...@@ -241,19 +242,19 @@ func lexLeftDelim(l *lexer) stateFn { ...@@ -241,19 +242,19 @@ func lexLeftDelim(l *lexer) stateFn {
// lexComment scans a comment. The left comment marker is known to be present. // lexComment scans a comment. The left comment marker is known to be present.
func lexComment(l *lexer) stateFn { func lexComment(l *lexer) stateFn {
l.pos += len(leftComment) l.pos += Pos(len(leftComment))
i := strings.Index(l.input[l.pos:], rightComment+l.rightDelim) i := strings.Index(l.input[l.pos:], rightComment+l.rightDelim)
if i < 0 { if i < 0 {
return l.errorf("unclosed comment") return l.errorf("unclosed comment")
} }
l.pos += i + len(rightComment) + len(l.rightDelim) l.pos += Pos(i + len(rightComment) + len(l.rightDelim))
l.ignore() l.ignore()
return lexText return lexText
} }
// lexRightDelim scans the right delimiter, which is known to be present. // lexRightDelim scans the right delimiter, which is known to be present.
func lexRightDelim(l *lexer) stateFn { func lexRightDelim(l *lexer) stateFn {
l.pos += len(l.rightDelim) l.pos += Pos(len(l.rightDelim))
l.emit(itemRightDelim) l.emit(itemRightDelim)
return lexText return lexText
} }
...@@ -291,7 +292,7 @@ func lexInsideAction(l *lexer) stateFn { ...@@ -291,7 +292,7 @@ func lexInsideAction(l *lexer) stateFn {
return lexChar return lexChar
case r == '.': case r == '.':
// special look-ahead for ".field" so we don't break l.backup(). // special look-ahead for ".field" so we don't break l.backup().
if l.pos < len(l.input) { if l.pos < Pos(len(l.input)) {
r := l.input[l.pos] r := l.input[l.pos]
if r < '0' || '9' < r { if r < '0' || '9' < r {
return lexField return lexField
......
...@@ -21,11 +21,20 @@ type Node interface { ...@@ -21,11 +21,20 @@ type Node interface {
// To avoid type assertions, some XxxNodes also have specialized // To avoid type assertions, some XxxNodes also have specialized
// CopyXxx methods that return *XxxNode. // CopyXxx methods that return *XxxNode.
Copy() Node Copy() Node
Position() Pos // byte position of start of node in full original input string
} }
// NodeType identifies the type of a parse tree node. // NodeType identifies the type of a parse tree node.
type NodeType int type NodeType int
// Pos represents a byte position in the original input text from which
// this template was parsed.
type Pos int
func (p Pos) Position() Pos {
return p
}
// Type returns itself and provides an easy default implementation // Type returns itself and provides an easy default implementation
// for embedding in a Node. Embedded in all non-trivial Nodes. // for embedding in a Node. Embedded in all non-trivial Nodes.
func (t NodeType) Type() NodeType { func (t NodeType) Type() NodeType {
...@@ -60,11 +69,12 @@ const ( ...@@ -60,11 +69,12 @@ const (
// ListNode holds a sequence of nodes. // ListNode holds a sequence of nodes.
type ListNode struct { type ListNode struct {
NodeType NodeType
Pos
Nodes []Node // The element nodes in lexical order. Nodes []Node // The element nodes in lexical order.
} }
func newList() *ListNode { func newList(pos Pos) *ListNode {
return &ListNode{NodeType: NodeList} return &ListNode{NodeType: NodeList, Pos: pos}
} }
func (l *ListNode) append(n Node) { func (l *ListNode) append(n Node) {
...@@ -83,7 +93,7 @@ func (l *ListNode) CopyList() *ListNode { ...@@ -83,7 +93,7 @@ func (l *ListNode) CopyList() *ListNode {
if l == nil { if l == nil {
return l return l
} }
n := newList() n := newList(l.Pos)
for _, elem := range l.Nodes { for _, elem := range l.Nodes {
n.append(elem.Copy()) n.append(elem.Copy())
} }
...@@ -97,11 +107,12 @@ func (l *ListNode) Copy() Node { ...@@ -97,11 +107,12 @@ func (l *ListNode) Copy() Node {
// TextNode holds plain text. // TextNode holds plain text.
type TextNode struct { type TextNode struct {
NodeType NodeType
Pos
Text []byte // The text; may span newlines. Text []byte // The text; may span newlines.
} }
func newText(text string) *TextNode { func newText(pos Pos, text string) *TextNode {
return &TextNode{NodeType: NodeText, Text: []byte(text)} return &TextNode{NodeType: NodeText, Pos: pos, Text: []byte(text)}
} }
func (t *TextNode) String() string { func (t *TextNode) String() string {
...@@ -115,13 +126,14 @@ func (t *TextNode) Copy() Node { ...@@ -115,13 +126,14 @@ func (t *TextNode) Copy() Node {
// PipeNode holds a pipeline with optional declaration // PipeNode holds a pipeline with optional declaration
type PipeNode struct { type PipeNode struct {
NodeType NodeType
Line int // The line number in the input. Pos
Line int // The line number in the input (deprecated; kept for compatibility)
Decl []*VariableNode // Variable declarations in lexical order. Decl []*VariableNode // Variable declarations in lexical order.
Cmds []*CommandNode // The commands in lexical order. Cmds []*CommandNode // The commands in lexical order.
} }
func newPipeline(line int, decl []*VariableNode) *PipeNode { func newPipeline(pos Pos, line int, decl []*VariableNode) *PipeNode {
return &PipeNode{NodeType: NodePipe, Line: line, Decl: decl} return &PipeNode{NodeType: NodePipe, Pos: pos, Line: line, Decl: decl}
} }
func (p *PipeNode) append(command *CommandNode) { func (p *PipeNode) append(command *CommandNode) {
...@@ -156,7 +168,7 @@ func (p *PipeNode) CopyPipe() *PipeNode { ...@@ -156,7 +168,7 @@ func (p *PipeNode) CopyPipe() *PipeNode {
for _, d := range p.Decl { for _, d := range p.Decl {
decl = append(decl, d.Copy().(*VariableNode)) decl = append(decl, d.Copy().(*VariableNode))
} }
n := newPipeline(p.Line, decl) n := newPipeline(p.Pos, p.Line, decl)
for _, c := range p.Cmds { for _, c := range p.Cmds {
n.append(c.Copy().(*CommandNode)) n.append(c.Copy().(*CommandNode))
} }
...@@ -172,12 +184,13 @@ func (p *PipeNode) Copy() Node { ...@@ -172,12 +184,13 @@ func (p *PipeNode) Copy() Node {
// ones such as field evaluations and parenthesized pipelines. // ones such as field evaluations and parenthesized pipelines.
type ActionNode struct { type ActionNode struct {
NodeType NodeType
Line int // The line number in the input. Pos
Line int // The line number in the input (deprecated; kept for compatibility)
Pipe *PipeNode // The pipeline in the action. Pipe *PipeNode // The pipeline in the action.
} }
func newAction(line int, pipe *PipeNode) *ActionNode { func newAction(pos Pos, line int, pipe *PipeNode) *ActionNode {
return &ActionNode{NodeType: NodeAction, Line: line, Pipe: pipe} return &ActionNode{NodeType: NodeAction, Pos: pos, Line: line, Pipe: pipe}
} }
func (a *ActionNode) String() string { func (a *ActionNode) String() string {
...@@ -186,18 +199,19 @@ func (a *ActionNode) String() string { ...@@ -186,18 +199,19 @@ func (a *ActionNode) String() string {
} }
func (a *ActionNode) Copy() Node { func (a *ActionNode) Copy() Node {
return newAction(a.Line, a.Pipe.CopyPipe()) return newAction(a.Pos, a.Line, a.Pipe.CopyPipe())
} }
// CommandNode holds a command (a pipeline inside an evaluating action). // CommandNode holds a command (a pipeline inside an evaluating action).
type CommandNode struct { type CommandNode struct {
NodeType NodeType
Pos
Args []Node // Arguments in lexical order: Identifier, field, or constant. Args []Node // Arguments in lexical order: Identifier, field, or constant.
} }
func newCommand() *CommandNode { func newCommand(pos Pos) *CommandNode {
return &CommandNode{NodeType: NodeCommand} return &CommandNode{NodeType: NodeCommand, Pos: pos}
} }
func (c *CommandNode) append(arg Node) { func (c *CommandNode) append(arg Node) {
...@@ -223,7 +237,7 @@ func (c *CommandNode) Copy() Node { ...@@ -223,7 +237,7 @@ func (c *CommandNode) Copy() Node {
if c == nil { if c == nil {
return c return c
} }
n := newCommand() n := newCommand(c.Pos)
for _, c := range c.Args { for _, c := range c.Args {
n.append(c.Copy()) n.append(c.Copy())
} }
...@@ -233,6 +247,7 @@ func (c *CommandNode) Copy() Node { ...@@ -233,6 +247,7 @@ func (c *CommandNode) Copy() Node {
// IdentifierNode holds an identifier. // IdentifierNode holds an identifier.
type IdentifierNode struct { type IdentifierNode struct {
NodeType NodeType
Pos
Ident string // The identifier's name. Ident string // The identifier's name.
} }
...@@ -241,23 +256,32 @@ func NewIdentifier(ident string) *IdentifierNode { ...@@ -241,23 +256,32 @@ func NewIdentifier(ident string) *IdentifierNode {
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident} return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
} }
// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
// Chained for convenience.
// TODO: fix one day?
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
i.Pos = pos
return i
}
func (i *IdentifierNode) String() string { func (i *IdentifierNode) String() string {
return i.Ident return i.Ident
} }
func (i *IdentifierNode) Copy() Node { func (i *IdentifierNode) Copy() Node {
return NewIdentifier(i.Ident) return NewIdentifier(i.Ident).SetPos(i.Pos)
} }
// VariableNode holds a list of variable names, possibly with chained field // VariableNode holds a list of variable names, possibly with chained field
// accesses. The dollar sign is part of the (first) name. // accesses. The dollar sign is part of the (first) name.
type VariableNode struct { type VariableNode struct {
NodeType NodeType
Pos
Ident []string // Variable name and fields in lexical order. Ident []string // Variable name and fields in lexical order.
} }
func newVariable(ident string) *VariableNode { func newVariable(pos Pos, ident string) *VariableNode {
return &VariableNode{NodeType: NodeVariable, Ident: strings.Split(ident, ".")} return &VariableNode{NodeType: NodeVariable, Pos: pos, Ident: strings.Split(ident, ".")}
} }
func (v *VariableNode) String() string { func (v *VariableNode) String() string {
...@@ -272,14 +296,16 @@ func (v *VariableNode) String() string { ...@@ -272,14 +296,16 @@ func (v *VariableNode) String() string {
} }
func (v *VariableNode) Copy() Node { func (v *VariableNode) Copy() Node {
return &VariableNode{NodeType: NodeVariable, Ident: append([]string{}, v.Ident...)} return &VariableNode{NodeType: NodeVariable, Pos: v.Pos, Ident: append([]string{}, v.Ident...)}
} }
// DotNode holds the special identifier '.'. It is represented by a nil pointer. // DotNode holds the special identifier '.'.
type DotNode bool type DotNode struct {
Pos
}
func newDot() *DotNode { func newDot(pos Pos) *DotNode {
return nil return &DotNode{Pos: pos}
} }
func (d *DotNode) Type() NodeType { func (d *DotNode) Type() NodeType {
...@@ -291,27 +317,28 @@ func (d *DotNode) String() string { ...@@ -291,27 +317,28 @@ func (d *DotNode) String() string {
} }
func (d *DotNode) Copy() Node { func (d *DotNode) Copy() Node {
return newDot() return newDot(d.Pos)
} }
// NilNode holds the special identifier 'nil' representing an untyped nil constant. // NilNode holds the special identifier 'nil' representing an untyped nil constant.
// It is represented by a nil pointer. type NilNode struct {
type NilNode bool Pos
}
func newNil() *NilNode { func newNil(pos Pos) *NilNode {
return nil return &NilNode{Pos: pos}
} }
func (d *NilNode) Type() NodeType { func (n *NilNode) Type() NodeType {
return NodeNil return NodeNil
} }
func (d *NilNode) String() string { func (n *NilNode) String() string {
return "nil" return "nil"
} }
func (d *NilNode) Copy() Node { func (n *NilNode) Copy() Node {
return newNil() return newNil(n.Pos)
} }
// FieldNode holds a field (identifier starting with '.'). // FieldNode holds a field (identifier starting with '.').
...@@ -319,11 +346,12 @@ func (d *NilNode) Copy() Node { ...@@ -319,11 +346,12 @@ func (d *NilNode) Copy() Node {
// The period is dropped from each ident. // The period is dropped from each ident.
type FieldNode struct { type FieldNode struct {
NodeType NodeType
Pos
Ident []string // The identifiers in lexical order. Ident []string // The identifiers in lexical order.
} }
func newField(ident string) *FieldNode { func newField(pos Pos, ident string) *FieldNode {
return &FieldNode{NodeType: NodeField, Ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period return &FieldNode{NodeType: NodeField, Pos: pos, Ident: strings.Split(ident[1:], ".")} // [1:] to drop leading period
} }
func (f *FieldNode) String() string { func (f *FieldNode) String() string {
...@@ -335,7 +363,7 @@ func (f *FieldNode) String() string { ...@@ -335,7 +363,7 @@ func (f *FieldNode) String() string {
} }
func (f *FieldNode) Copy() Node { func (f *FieldNode) Copy() Node {
return &FieldNode{NodeType: NodeField, Ident: append([]string{}, f.Ident...)} return &FieldNode{NodeType: NodeField, Pos: f.Pos, Ident: append([]string{}, f.Ident...)}
} }
// ChainNode holds a term followed by a chain of field accesses (identifier starting with '.'). // ChainNode holds a term followed by a chain of field accesses (identifier starting with '.').
...@@ -343,12 +371,13 @@ func (f *FieldNode) Copy() Node { ...@@ -343,12 +371,13 @@ func (f *FieldNode) Copy() Node {
// The periods are dropped from each ident. // The periods are dropped from each ident.
type ChainNode struct { type ChainNode struct {
NodeType NodeType
Pos
Node Node Node Node
Field []string // The identifiers in lexical order. Field []string // The identifiers in lexical order.
} }
func newChain(node Node) *ChainNode { func newChain(pos Pos, node Node) *ChainNode {
return &ChainNode{NodeType: NodeChain, Node: node} return &ChainNode{NodeType: NodeChain, Pos: pos, Node: node}
} }
// Add adds the named field (which should start with a period) to the end of the chain. // Add adds the named field (which should start with a period) to the end of the chain.
...@@ -375,17 +404,18 @@ func (c *ChainNode) String() string { ...@@ -375,17 +404,18 @@ func (c *ChainNode) String() string {
} }
func (c *ChainNode) Copy() Node { func (c *ChainNode) Copy() Node {
return &ChainNode{NodeType: NodeChain, Node: c.Node, Field: append([]string{}, c.Field...)} return &ChainNode{NodeType: NodeChain, Pos: c.Pos, Node: c.Node, Field: append([]string{}, c.Field...)}
} }
// BoolNode holds a boolean constant. // BoolNode holds a boolean constant.
type BoolNode struct { type BoolNode struct {
NodeType NodeType
Pos
True bool // The value of the boolean constant. True bool // The value of the boolean constant.
} }
func newBool(true bool) *BoolNode { func newBool(pos Pos, true bool) *BoolNode {
return &BoolNode{NodeType: NodeBool, True: true} return &BoolNode{NodeType: NodeBool, Pos: pos, True: true}
} }
func (b *BoolNode) String() string { func (b *BoolNode) String() string {
...@@ -396,7 +426,7 @@ func (b *BoolNode) String() string { ...@@ -396,7 +426,7 @@ func (b *BoolNode) String() string {
} }
func (b *BoolNode) Copy() Node { func (b *BoolNode) Copy() Node {
return newBool(b.True) return newBool(b.Pos, b.True)
} }
// NumberNode holds a number: signed or unsigned integer, float, or complex. // NumberNode holds a number: signed or unsigned integer, float, or complex.
...@@ -404,6 +434,7 @@ func (b *BoolNode) Copy() Node { ...@@ -404,6 +434,7 @@ func (b *BoolNode) Copy() Node {
// This simulates in a small amount of code the behavior of Go's ideal constants. // This simulates in a small amount of code the behavior of Go's ideal constants.
type NumberNode struct { type NumberNode struct {
NodeType NodeType
Pos
IsInt bool // Number has an integral value. IsInt bool // Number has an integral value.
IsUint bool // Number has an unsigned integral value. IsUint bool // Number has an unsigned integral value.
IsFloat bool // Number has a floating-point value. IsFloat bool // Number has a floating-point value.
...@@ -415,8 +446,8 @@ type NumberNode struct { ...@@ -415,8 +446,8 @@ type NumberNode struct {
Text string // The original textual representation from the input. Text string // The original textual representation from the input.
} }
func newNumber(text string, typ itemType) (*NumberNode, error) { func newNumber(pos Pos, text string, typ itemType) (*NumberNode, error) {
n := &NumberNode{NodeType: NodeNumber, Text: text} n := &NumberNode{NodeType: NodeNumber, Pos: pos, Text: text}
switch typ { switch typ {
case itemCharConstant: case itemCharConstant:
rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0]) rune, _, tail, err := strconv.UnquoteChar(text[1:], text[0])
...@@ -526,12 +557,13 @@ func (n *NumberNode) Copy() Node { ...@@ -526,12 +557,13 @@ func (n *NumberNode) Copy() Node {
// StringNode holds a string constant. The value has been "unquoted". // StringNode holds a string constant. The value has been "unquoted".
type StringNode struct { type StringNode struct {
NodeType NodeType
Pos
Quoted string // The original text of the string, with quotes. Quoted string // The original text of the string, with quotes.
Text string // The string, after quote processing. Text string // The string, after quote processing.
} }
func newString(orig, text string) *StringNode { func newString(pos Pos, orig, text string) *StringNode {
return &StringNode{NodeType: NodeString, Quoted: orig, Text: text} return &StringNode{NodeType: NodeString, Pos: pos, Quoted: orig, Text: text}
} }
func (s *StringNode) String() string { func (s *StringNode) String() string {
...@@ -539,15 +571,17 @@ func (s *StringNode) String() string { ...@@ -539,15 +571,17 @@ func (s *StringNode) String() string {
} }
func (s *StringNode) Copy() Node { func (s *StringNode) Copy() Node {
return newString(s.Quoted, s.Text) return newString(s.Pos, s.Quoted, s.Text)
} }
// endNode represents an {{end}} action. It is represented by a nil pointer. // endNode represents an {{end}} action.
// It does not appear in the final parse tree. // It does not appear in the final parse tree.
type endNode bool type endNode struct {
Pos
}
func newEnd() *endNode { func newEnd(pos Pos) *endNode {
return nil return &endNode{Pos: pos}
} }
func (e *endNode) Type() NodeType { func (e *endNode) Type() NodeType {
...@@ -559,17 +593,18 @@ func (e *endNode) String() string { ...@@ -559,17 +593,18 @@ func (e *endNode) String() string {
} }
func (e *endNode) Copy() Node { func (e *endNode) Copy() Node {
return newEnd() return newEnd(e.Pos)
} }
// elseNode represents an {{else}} action. Does not appear in the final tree. // elseNode represents an {{else}} action. Does not appear in the final tree.
type elseNode struct { type elseNode struct {
NodeType NodeType
Line int // The line number in the input. Pos
Line int // The line number in the input (deprecated; kept for compatibility)
} }
func newElse(line int) *elseNode { func newElse(pos Pos, line int) *elseNode {
return &elseNode{NodeType: nodeElse, Line: line} return &elseNode{NodeType: nodeElse, Pos: pos, Line: line}
} }
func (e *elseNode) Type() NodeType { func (e *elseNode) Type() NodeType {
...@@ -581,13 +616,14 @@ func (e *elseNode) String() string { ...@@ -581,13 +616,14 @@ func (e *elseNode) String() string {
} }
func (e *elseNode) Copy() Node { func (e *elseNode) Copy() Node {
return newElse(e.Line) return newElse(e.Pos, e.Line)
} }
// BranchNode is the common representation of if, range, and with. // BranchNode is the common representation of if, range, and with.
type BranchNode struct { type BranchNode struct {
NodeType NodeType
Line int // The line number in the input. Pos
Line int // The line number in the input (deprecated; kept for compatibility)
Pipe *PipeNode // The pipeline to be evaluated. Pipe *PipeNode // The pipeline to be evaluated.
List *ListNode // What to execute if the value is non-empty. List *ListNode // What to execute if the value is non-empty.
ElseList *ListNode // What to execute if the value is empty (nil if absent). ElseList *ListNode // What to execute if the value is empty (nil if absent).
...@@ -616,12 +652,12 @@ type IfNode struct { ...@@ -616,12 +652,12 @@ type IfNode struct {
BranchNode BranchNode
} }
func newIf(line int, pipe *PipeNode, list, elseList *ListNode) *IfNode { func newIf(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *IfNode {
return &IfNode{BranchNode{NodeType: NodeIf, Line: line, Pipe: pipe, List: list, ElseList: elseList}} return &IfNode{BranchNode{NodeType: NodeIf, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
} }
func (i *IfNode) Copy() Node { func (i *IfNode) Copy() Node {
return newIf(i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList()) return newIf(i.Pos, i.Line, i.Pipe.CopyPipe(), i.List.CopyList(), i.ElseList.CopyList())
} }
// RangeNode represents a {{range}} action and its commands. // RangeNode represents a {{range}} action and its commands.
...@@ -629,12 +665,12 @@ type RangeNode struct { ...@@ -629,12 +665,12 @@ type RangeNode struct {
BranchNode BranchNode
} }
func newRange(line int, pipe *PipeNode, list, elseList *ListNode) *RangeNode { func newRange(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *RangeNode {
return &RangeNode{BranchNode{NodeType: NodeRange, Line: line, Pipe: pipe, List: list, ElseList: elseList}} return &RangeNode{BranchNode{NodeType: NodeRange, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
} }
func (r *RangeNode) Copy() Node { func (r *RangeNode) Copy() Node {
return newRange(r.Line, r.Pipe.CopyPipe(), r.List.CopyList(), r.ElseList.CopyList()) return newRange(r.Pos, r.Line, r.Pipe.CopyPipe(), r.List.CopyList(), r.ElseList.CopyList())
} }
// WithNode represents a {{with}} action and its commands. // WithNode represents a {{with}} action and its commands.
...@@ -642,24 +678,25 @@ type WithNode struct { ...@@ -642,24 +678,25 @@ type WithNode struct {
BranchNode BranchNode
} }
func newWith(line int, pipe *PipeNode, list, elseList *ListNode) *WithNode { func newWith(pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) *WithNode {
return &WithNode{BranchNode{NodeType: NodeWith, Line: line, Pipe: pipe, List: list, ElseList: elseList}} return &WithNode{BranchNode{NodeType: NodeWith, Pos: pos, Line: line, Pipe: pipe, List: list, ElseList: elseList}}
} }
func (w *WithNode) Copy() Node { func (w *WithNode) Copy() Node {
return newWith(w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList()) return newWith(w.Pos, w.Line, w.Pipe.CopyPipe(), w.List.CopyList(), w.ElseList.CopyList())
} }
// TemplateNode represents a {{template}} action. // TemplateNode represents a {{template}} action.
type TemplateNode struct { type TemplateNode struct {
NodeType NodeType
Line int // The line number in the input. Pos
Line int // The line number in the input (deprecated; kept for compatibility)
Name string // The name of the template (unquoted). Name string // The name of the template (unquoted).
Pipe *PipeNode // The command to evaluate as dot for the template. Pipe *PipeNode // The command to evaluate as dot for the template.
} }
func newTemplate(line int, name string, pipe *PipeNode) *TemplateNode { func newTemplate(pos Pos, line int, name string, pipe *PipeNode) *TemplateNode {
return &TemplateNode{NodeType: NodeTemplate, Line: line, Name: name, Pipe: pipe} return &TemplateNode{NodeType: NodeTemplate, Line: line, Pos: pos, Name: name, Pipe: pipe}
} }
func (t *TemplateNode) String() string { func (t *TemplateNode) String() string {
...@@ -670,5 +707,5 @@ func (t *TemplateNode) String() string { ...@@ -670,5 +707,5 @@ func (t *TemplateNode) String() string {
} }
func (t *TemplateNode) Copy() Node { func (t *TemplateNode) Copy() Node {
return newTemplate(t.Line, t.Name, t.Pipe.CopyPipe()) return newTemplate(t.Pos, t.Line, t.Name, t.Pipe.CopyPipe())
} }
...@@ -13,6 +13,7 @@ import ( ...@@ -13,6 +13,7 @@ import (
"fmt" "fmt"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"unicode" "unicode"
) )
...@@ -21,6 +22,7 @@ type Tree struct { ...@@ -21,6 +22,7 @@ type Tree struct {
Name string // name of the template represented by the tree. Name string // name of the template represented by the tree.
ParseName string // name of the top-level template during parsing, for error messages. ParseName string // name of the top-level template during parsing, for error messages.
Root *ListNode // top-level root of the tree. Root *ListNode // top-level root of the tree.
text string // text parsed to create the template (or its parent)
// Parsing only; cleared after parse. // Parsing only; cleared after parse.
funcs []map[string]interface{} funcs []map[string]interface{}
lex *lexer lex *lexer
...@@ -35,7 +37,9 @@ type Tree struct { ...@@ -35,7 +37,9 @@ type Tree struct {
// empty map is returned with the error. // empty map is returned with the error.
func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (treeSet map[string]*Tree, err error) { func Parse(name, text, leftDelim, rightDelim string, funcs ...map[string]interface{}) (treeSet map[string]*Tree, err error) {
treeSet = make(map[string]*Tree) treeSet = make(map[string]*Tree)
_, err = New(name).Parse(text, leftDelim, rightDelim, treeSet, funcs...) t := New(name)
t.text = text
_, err = t.Parse(text, leftDelim, rightDelim, treeSet, funcs...)
return return
} }
...@@ -112,6 +116,25 @@ func New(name string, funcs ...map[string]interface{}) *Tree { ...@@ -112,6 +116,25 @@ func New(name string, funcs ...map[string]interface{}) *Tree {
} }
} }
// ErrorContext returns a textual representation of the location of the node in the input text.
func (t *Tree) ErrorContext(n Node) (location, context string) {
pos := int(n.Position())
text := t.text[:pos]
byteNum := strings.LastIndex(text, "\n")
if byteNum == -1 {
byteNum = pos // On first line.
} else {
byteNum++ // After the newline.
byteNum = pos - byteNum
}
lineNum := 1 + strings.Count(text, "\n")
context = n.String()
if len(context) > 20 {
context = fmt.Sprintf("%.20s...", context)
}
return fmt.Sprintf("%s:%d:%d", t.ParseName, lineNum, byteNum), context
}
// errorf formats the error and terminates processing. // errorf formats the error and terminates processing.
func (t *Tree) errorf(format string, args ...interface{}) { func (t *Tree) errorf(format string, args ...interface{}) {
t.Root = nil t.Root = nil
...@@ -202,10 +225,11 @@ func (t *Tree) atEOF() bool { ...@@ -202,10 +225,11 @@ func (t *Tree) atEOF() bool {
// the template for execution. If either action delimiter string is empty, the // the template for execution. If either action delimiter string is empty, the
// default ("{{" or "}}") is used. Embedded template definitions are added to // default ("{{" or "}}") is used. Embedded template definitions are added to
// the treeSet map. // the treeSet map.
func (t *Tree) Parse(s, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) { func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
defer t.recover(&err) defer t.recover(&err)
t.ParseName = t.Name t.ParseName = t.Name
t.startParse(funcs, lex(t.Name, s, leftDelim, rightDelim)) t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim))
t.text = text
t.parse(treeSet) t.parse(treeSet)
t.add(treeSet) t.add(treeSet)
t.stopParse() t.stopParse()
...@@ -253,12 +277,13 @@ func IsEmptyTree(n Node) bool { ...@@ -253,12 +277,13 @@ func IsEmptyTree(n Node) bool {
// as itemList except it also parses {{define}} actions. // as itemList except it also parses {{define}} actions.
// It runs to EOF. // It runs to EOF.
func (t *Tree) parse(treeSet map[string]*Tree) (next Node) { func (t *Tree) parse(treeSet map[string]*Tree) (next Node) {
t.Root = newList() t.Root = newList(t.peek().pos)
for t.peek().typ != itemEOF { for t.peek().typ != itemEOF {
if t.peek().typ == itemLeftDelim { if t.peek().typ == itemLeftDelim {
delim := t.next() delim := t.next()
if t.nextNonSpace().typ == itemDefine { if t.nextNonSpace().typ == itemDefine {
newT := New("definition") // name will be updated once we know it. newT := New("definition") // name will be updated once we know it.
newT.text = t.text
newT.ParseName = t.ParseName newT.ParseName = t.ParseName
newT.startParse(t.funcs, t.lex) newT.startParse(t.funcs, t.lex)
newT.parseDefinition(treeSet) newT.parseDefinition(treeSet)
...@@ -300,7 +325,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) { ...@@ -300,7 +325,7 @@ func (t *Tree) parseDefinition(treeSet map[string]*Tree) {
// textOrAction* // textOrAction*
// Terminates at {{end}} or {{else}}, returned separately. // Terminates at {{end}} or {{else}}, returned separately.
func (t *Tree) itemList() (list *ListNode, next Node) { func (t *Tree) itemList() (list *ListNode, next Node) {
list = newList() list = newList(t.peekNonSpace().pos)
for t.peekNonSpace().typ != itemEOF { for t.peekNonSpace().typ != itemEOF {
n := t.textOrAction() n := t.textOrAction()
switch n.Type() { switch n.Type() {
...@@ -318,7 +343,7 @@ func (t *Tree) itemList() (list *ListNode, next Node) { ...@@ -318,7 +343,7 @@ func (t *Tree) itemList() (list *ListNode, next Node) {
func (t *Tree) textOrAction() Node { func (t *Tree) textOrAction() Node {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemText: case itemText:
return newText(token.val) return newText(token.pos, token.val)
case itemLeftDelim: case itemLeftDelim:
return t.action() return t.action()
default: default:
...@@ -349,13 +374,14 @@ func (t *Tree) action() (n Node) { ...@@ -349,13 +374,14 @@ func (t *Tree) action() (n Node) {
} }
t.backup() t.backup()
// Do not pop variables; they persist until "end". // Do not pop variables; they persist until "end".
return newAction(t.lex.lineNumber(), t.pipeline("command")) return newAction(t.peek().pos, t.lex.lineNumber(), t.pipeline("command"))
} }
// Pipeline: // Pipeline:
// declarations? command ('|' command)* // declarations? command ('|' command)*
func (t *Tree) pipeline(context string) (pipe *PipeNode) { func (t *Tree) pipeline(context string) (pipe *PipeNode) {
var decl []*VariableNode var decl []*VariableNode
pos := t.peekNonSpace().pos
// Are there declarations? // Are there declarations?
for { for {
if v := t.peekNonSpace(); v.typ == itemVariable { if v := t.peekNonSpace(); v.typ == itemVariable {
...@@ -367,7 +393,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -367,7 +393,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
tokenAfterVariable := t.peek() tokenAfterVariable := t.peek()
if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") { if next := t.peekNonSpace(); next.typ == itemColonEquals || (next.typ == itemChar && next.val == ",") {
t.nextNonSpace() t.nextNonSpace()
variable := newVariable(v.val) variable := newVariable(v.pos, v.val)
decl = append(decl, variable) decl = append(decl, variable)
t.vars = append(t.vars, v.val) t.vars = append(t.vars, v.val)
if next.typ == itemChar && next.val == "," { if next.typ == itemChar && next.val == "," {
...@@ -384,7 +410,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -384,7 +410,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
} }
break break
} }
pipe = newPipeline(t.lex.lineNumber(), decl) pipe = newPipeline(pos, t.lex.lineNumber(), decl)
for { for {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemRightDelim, itemRightParen: case itemRightDelim, itemRightParen:
...@@ -406,9 +432,9 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -406,9 +432,9 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
return return
} }
func (t *Tree) parseControl(context string) (lineNum int, pipe *PipeNode, list, elseList *ListNode) { func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
lineNum = t.lex.lineNumber()
defer t.popVars(len(t.vars)) defer t.popVars(len(t.vars))
line = t.lex.lineNumber()
pipe = t.pipeline(context) pipe = t.pipeline(context)
var next Node var next Node
list, next = t.itemList() list, next = t.itemList()
...@@ -421,7 +447,7 @@ func (t *Tree) parseControl(context string) (lineNum int, pipe *PipeNode, list, ...@@ -421,7 +447,7 @@ func (t *Tree) parseControl(context string) (lineNum int, pipe *PipeNode, list,
} }
elseList = elseList elseList = elseList
} }
return lineNum, pipe, list, elseList return pipe.Position(), line, pipe, list, elseList
} }
// If: // If:
...@@ -452,16 +478,14 @@ func (t *Tree) withControl() Node { ...@@ -452,16 +478,14 @@ func (t *Tree) withControl() Node {
// {{end}} // {{end}}
// End keyword is past. // End keyword is past.
func (t *Tree) endControl() Node { func (t *Tree) endControl() Node {
t.expect(itemRightDelim, "end") return newEnd(t.expect(itemRightDelim, "end").pos)
return newEnd()
} }
// Else: // Else:
// {{else}} // {{else}}
// Else keyword is past. // Else keyword is past.
func (t *Tree) elseControl() Node { func (t *Tree) elseControl() Node {
t.expect(itemRightDelim, "else") return newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
return newElse(t.lex.lineNumber())
} }
// Template: // Template:
...@@ -470,7 +494,8 @@ func (t *Tree) elseControl() Node { ...@@ -470,7 +494,8 @@ func (t *Tree) elseControl() Node {
// to a string. // to a string.
func (t *Tree) templateControl() Node { func (t *Tree) templateControl() Node {
var name string var name string
switch token := t.nextNonSpace(); token.typ { token := t.nextNonSpace()
switch token.typ {
case itemString, itemRawString: case itemString, itemRawString:
s, err := strconv.Unquote(token.val) s, err := strconv.Unquote(token.val)
if err != nil { if err != nil {
...@@ -486,7 +511,7 @@ func (t *Tree) templateControl() Node { ...@@ -486,7 +511,7 @@ func (t *Tree) templateControl() Node {
// Do not pop variables; they persist until "end". // Do not pop variables; they persist until "end".
pipe = t.pipeline("template") pipe = t.pipeline("template")
} }
return newTemplate(t.lex.lineNumber(), name, pipe) return newTemplate(token.pos, t.lex.lineNumber(), name, pipe)
} }
// command: // command:
...@@ -494,7 +519,7 @@ func (t *Tree) templateControl() Node { ...@@ -494,7 +519,7 @@ func (t *Tree) templateControl() Node {
// space-separated arguments up to a pipeline character or right delimiter. // space-separated arguments up to a pipeline character or right delimiter.
// we consume the pipe character but leave the right delim to terminate the action. // we consume the pipe character but leave the right delim to terminate the action.
func (t *Tree) command() *CommandNode { func (t *Tree) command() *CommandNode {
cmd := newCommand() cmd := newCommand(t.peekNonSpace().pos)
for { for {
t.peekNonSpace() // skip leading spaces. t.peekNonSpace() // skip leading spaces.
operand := t.operand() operand := t.operand()
...@@ -531,7 +556,7 @@ func (t *Tree) operand() Node { ...@@ -531,7 +556,7 @@ func (t *Tree) operand() Node {
return nil return nil
} }
if t.peek().typ == itemField { if t.peek().typ == itemField {
chain := newChain(node) chain := newChain(t.peek().pos, node)
for t.peek().typ == itemField { for t.peek().typ == itemField {
chain.Add(t.next().val) chain.Add(t.next().val)
} }
...@@ -541,9 +566,9 @@ func (t *Tree) operand() Node { ...@@ -541,9 +566,9 @@ func (t *Tree) operand() Node {
// TODO: Switch to Chains always when we can. // TODO: Switch to Chains always when we can.
switch node.Type() { switch node.Type() {
case NodeField: case NodeField:
node = newField(chain.String()) node = newField(chain.Position(), chain.String())
case NodeVariable: case NodeVariable:
node = newVariable(chain.String()) node = newVariable(chain.Position(), chain.String())
default: default:
node = chain node = chain
} }
...@@ -568,19 +593,19 @@ func (t *Tree) term() Node { ...@@ -568,19 +593,19 @@ func (t *Tree) term() Node {
if !t.hasFunction(token.val) { if !t.hasFunction(token.val) {
t.errorf("function %q not defined", token.val) t.errorf("function %q not defined", token.val)
} }
return NewIdentifier(token.val) return NewIdentifier(token.val).SetPos(token.pos)
case itemDot: case itemDot:
return newDot() return newDot(token.pos)
case itemNil: case itemNil:
return newNil() return newNil(token.pos)
case itemVariable: case itemVariable:
return t.useVar(token.val) return t.useVar(token.pos, token.val)
case itemField: case itemField:
return newField(token.val) return newField(token.pos, token.val)
case itemBool: case itemBool:
return newBool(token.val == "true") return newBool(token.pos, token.val == "true")
case itemCharConstant, itemComplex, itemNumber: case itemCharConstant, itemComplex, itemNumber:
number, err := newNumber(token.val, token.typ) number, err := newNumber(token.pos, token.val, token.typ)
if err != nil { if err != nil {
t.error(err) t.error(err)
} }
...@@ -596,7 +621,7 @@ func (t *Tree) term() Node { ...@@ -596,7 +621,7 @@ func (t *Tree) term() Node {
if err != nil { if err != nil {
t.error(err) t.error(err)
} }
return newString(token.val, s) return newString(token.pos, token.val, s)
} }
t.backup() t.backup()
return nil return nil
...@@ -622,8 +647,8 @@ func (t *Tree) popVars(n int) { ...@@ -622,8 +647,8 @@ func (t *Tree) popVars(n int) {
// useVar returns a node for a variable reference. It errors if the // useVar returns a node for a variable reference. It errors if the
// variable is not defined. // variable is not defined.
func (t *Tree) useVar(name string) Node { func (t *Tree) useVar(pos Pos, name string) Node {
v := newVariable(name) v := newVariable(pos, name)
for _, varName := range t.vars { for _, varName := range t.vars {
if varName == v.Ident[0] { if varName == v.Ident[0] {
return v return v
......
...@@ -85,7 +85,7 @@ func TestNumberParse(t *testing.T) { ...@@ -85,7 +85,7 @@ func TestNumberParse(t *testing.T) {
typ = itemComplex typ = itemComplex
} }
} }
n, err := newNumber(test.text, typ) n, err := newNumber(0, test.text, typ)
ok := test.isInt || test.isUint || test.isFloat || test.isComplex ok := test.isInt || test.isUint || test.isFloat || test.isComplex
if ok && err != nil { if ok && err != nil {
t.Errorf("unexpected error for %q: %s", test.text, err) t.Errorf("unexpected error for %q: %s", test.text, err)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment