Commit c837e612 authored by Rob Pike's avatar Rob Pike

text/template/parse: use human error prints

The previous version of all the node.String methods printed the parse
tree and was useful for developing the parse tree code. Now that that's done,
we might as well print the nodes using the standard template syntax.
It's much easier to read and makes error reporting look more natural.

Helps issue 2644.

R=rsc, n13m3y3r
CC=golang-dev
https://golang.org/cl/5553066
parent c354f93b
...@@ -899,7 +899,7 @@ func TestErrors(t *testing.T) { ...@@ -899,7 +899,7 @@ func TestErrors(t *testing.T) {
}, },
{ {
`<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`, `<a href="{{if .F}}/foo?a={{else}}/bar/{{end}}{{.H}}">`,
"z:1: (action: [(command: [F=[H]])]) appears in an ambiguous URL context", "z:1: {{.H}} appears in an ambiguous URL context",
}, },
{ {
`<a onclick="alert('Hello \`, `<a onclick="alert('Hello \`,
...@@ -1490,62 +1490,62 @@ func TestEnsurePipelineContains(t *testing.T) { ...@@ -1490,62 +1490,62 @@ func TestEnsurePipelineContains(t *testing.T) {
}{ }{
{ {
"{{.X}}", "{{.X}}",
"[(command: [F=[X]])]", ".X",
[]string{}, []string{},
}, },
{ {
"{{.X | html}}", "{{.X | html}}",
"[(command: [F=[X]]) (command: [I=html])]", ".X | html",
[]string{}, []string{},
}, },
{ {
"{{.X}}", "{{.X}}",
"[(command: [F=[X]]) (command: [I=html])]", ".X | html",
[]string{"html"}, []string{"html"},
}, },
{ {
"{{.X | html}}", "{{.X | html}}",
"[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", ".X | html | urlquery",
[]string{"urlquery"}, []string{"urlquery"},
}, },
{ {
"{{.X | html | urlquery}}", "{{.X | html | urlquery}}",
"[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", ".X | html | urlquery",
[]string{"urlquery"}, []string{"urlquery"},
}, },
{ {
"{{.X | html | urlquery}}", "{{.X | html | urlquery}}",
"[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", ".X | html | urlquery",
[]string{"html", "urlquery"}, []string{"html", "urlquery"},
}, },
{ {
"{{.X | html | urlquery}}", "{{.X | html | urlquery}}",
"[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", ".X | html | urlquery",
[]string{"html"}, []string{"html"},
}, },
{ {
"{{.X | urlquery}}", "{{.X | urlquery}}",
"[(command: [F=[X]]) (command: [I=html]) (command: [I=urlquery])]", ".X | html | urlquery",
[]string{"html", "urlquery"}, []string{"html", "urlquery"},
}, },
{ {
"{{.X | html | print}}", "{{.X | html | print}}",
"[(command: [F=[X]]) (command: [I=urlquery]) (command: [I=html]) (command: [I=print])]", ".X | urlquery | html | print",
[]string{"urlquery", "html"}, []string{"urlquery", "html"},
}, },
} }
for _, test := range tests { for i, test := range tests {
tmpl := template.Must(template.New("test").Parse(test.input)) tmpl := template.Must(template.New("test").Parse(test.input))
action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode)) action, ok := (tmpl.Tree.Root.Nodes[0].(*parse.ActionNode))
if !ok { if !ok {
t.Errorf("First node is not an action: %s", test.input) t.Errorf("#%d: First node is not an action: %s", i, test.input)
continue continue
} }
pipe := action.Pipe pipe := action.Pipe
ensurePipelineContains(pipe, test.ids) ensurePipelineContains(pipe, test.ids)
got := pipe.String() got := pipe.String()
if got != test.output { if got != test.output {
t.Errorf("%s, %v: want\n\t%s\ngot\n\t%s", test.input, test.ids, test.output, got) t.Errorf("#%d: %s, %v: want\n\t%s\ngot\n\t%s", i, test.input, test.ids, test.output, got)
} }
} }
} }
......
...@@ -33,10 +33,10 @@ var multiParseTests = []multiParseTest{ ...@@ -33,10 +33,10 @@ var multiParseTests = []multiParseTest{
nil}, nil},
{"one", `{{define "foo"}} FOO {{end}}`, noError, {"one", `{{define "foo"}} FOO {{end}}`, noError,
[]string{"foo"}, []string{"foo"},
[]string{`[(text: " FOO ")]`}}, []string{`" FOO "`}},
{"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError, {"two", `{{define "foo"}} FOO {{end}}{{define "bar"}} BAR {{end}}`, noError,
[]string{"foo", "bar"}, []string{"foo", "bar"},
[]string{`[(text: " FOO ")]`, `[(text: " BAR ")]`}}, []string{`" FOO "`, `" BAR "`}},
// errors // errors
{"missing end", `{{define "foo"}} FOO `, hasError, {"missing end", `{{define "foo"}} FOO `, hasError,
nil, nil,
......
...@@ -67,11 +67,9 @@ func (l *ListNode) append(n Node) { ...@@ -67,11 +67,9 @@ func (l *ListNode) append(n Node) {
func (l *ListNode) String() string { func (l *ListNode) String() string {
b := new(bytes.Buffer) b := new(bytes.Buffer)
fmt.Fprint(b, "[")
for _, n := range l.Nodes { for _, n := range l.Nodes {
fmt.Fprint(b, n) fmt.Fprint(b, n)
} }
fmt.Fprint(b, "]")
return b.String() return b.String()
} }
...@@ -86,7 +84,7 @@ func newText(text string) *TextNode { ...@@ -86,7 +84,7 @@ func newText(text string) *TextNode {
} }
func (t *TextNode) String() string { func (t *TextNode) String() string {
return fmt.Sprintf("(text: %q)", t.Text) return fmt.Sprintf("%q", t.Text)
} }
// PipeNode holds a pipeline with optional declaration // PipeNode holds a pipeline with optional declaration
...@@ -106,10 +104,23 @@ func (p *PipeNode) append(command *CommandNode) { ...@@ -106,10 +104,23 @@ func (p *PipeNode) append(command *CommandNode) {
} }
func (p *PipeNode) String() string { func (p *PipeNode) String() string {
if p.Decl != nil { s := ""
return fmt.Sprintf("%v := %v", p.Decl, p.Cmds) if len(p.Decl) > 0 {
for i, v := range p.Decl {
if i > 0 {
s += ", "
}
s += v.String()
}
s += " := "
}
for i, c := range p.Cmds {
if i > 0 {
s += " | "
}
s += c.String()
} }
return fmt.Sprintf("%v", p.Cmds) return s
} }
// ActionNode holds an action (something bounded by delimiters). // ActionNode holds an action (something bounded by delimiters).
...@@ -126,7 +137,8 @@ func newAction(line int, pipe *PipeNode) *ActionNode { ...@@ -126,7 +137,8 @@ func newAction(line int, pipe *PipeNode) *ActionNode {
} }
func (a *ActionNode) String() string { func (a *ActionNode) String() string {
return fmt.Sprintf("(action: %v)", a.Pipe) return fmt.Sprintf("{{%s}}", a.Pipe)
} }
// CommandNode holds a command (a pipeline inside an evaluating action). // CommandNode holds a command (a pipeline inside an evaluating action).
...@@ -144,7 +156,14 @@ func (c *CommandNode) append(arg Node) { ...@@ -144,7 +156,14 @@ func (c *CommandNode) append(arg Node) {
} }
func (c *CommandNode) String() string { func (c *CommandNode) String() string {
return fmt.Sprintf("(command: %v)", c.Args) s := ""
for i, arg := range c.Args {
if i > 0 {
s += " "
}
s += arg.String()
}
return s
} }
// IdentifierNode holds an identifier. // IdentifierNode holds an identifier.
...@@ -159,7 +178,7 @@ func NewIdentifier(ident string) *IdentifierNode { ...@@ -159,7 +178,7 @@ func NewIdentifier(ident string) *IdentifierNode {
} }
func (i *IdentifierNode) String() string { func (i *IdentifierNode) String() string {
return fmt.Sprintf("I=%s", i.Ident) return i.Ident
} }
// VariableNode holds a list of variable names. The dollar sign is // VariableNode holds a list of variable names. The dollar sign is
...@@ -174,7 +193,14 @@ func newVariable(ident string) *VariableNode { ...@@ -174,7 +193,14 @@ func newVariable(ident string) *VariableNode {
} }
func (v *VariableNode) String() string { func (v *VariableNode) String() string {
return fmt.Sprintf("V=%s", v.Ident) s := ""
for i, id := range v.Ident {
if i > 0 {
s += "."
}
s += id
}
return s
} }
// DotNode holds the special identifier '.'. It is represented by a nil pointer. // DotNode holds the special identifier '.'. It is represented by a nil pointer.
...@@ -189,7 +215,7 @@ func (d *DotNode) Type() NodeType { ...@@ -189,7 +215,7 @@ func (d *DotNode) Type() NodeType {
} }
func (d *DotNode) String() string { func (d *DotNode) String() string {
return "{{<.>}}" return "."
} }
// FieldNode holds a field (identifier starting with '.'). // FieldNode holds a field (identifier starting with '.').
...@@ -205,7 +231,11 @@ func newField(ident string) *FieldNode { ...@@ -205,7 +231,11 @@ func newField(ident string) *FieldNode {
} }
func (f *FieldNode) String() string { func (f *FieldNode) String() string {
return fmt.Sprintf("F=%s", f.Ident) s := ""
for _, id := range f.Ident {
s += "." + id
}
return s
} }
// BoolNode holds a boolean constant. // BoolNode holds a boolean constant.
...@@ -219,7 +249,10 @@ func newBool(true bool) *BoolNode { ...@@ -219,7 +249,10 @@ func newBool(true bool) *BoolNode {
} }
func (b *BoolNode) String() string { func (b *BoolNode) String() string {
return fmt.Sprintf("B=%t", b.True) if b.True {
return "true"
}
return "false"
} }
// NumberNode holds a number: signed or unsigned integer, float, or complex. // NumberNode holds a number: signed or unsigned integer, float, or complex.
...@@ -337,7 +370,7 @@ func (n *NumberNode) simplifyComplex() { ...@@ -337,7 +370,7 @@ func (n *NumberNode) simplifyComplex() {
} }
func (n *NumberNode) String() string { func (n *NumberNode) String() string {
return fmt.Sprintf("N=%s", n.Text) return n.Text
} }
// StringNode holds a string constant. The value has been "unquoted". // StringNode holds a string constant. The value has been "unquoted".
...@@ -352,7 +385,7 @@ func newString(orig, text string) *StringNode { ...@@ -352,7 +385,7 @@ func newString(orig, text string) *StringNode {
} }
func (s *StringNode) String() string { func (s *StringNode) String() string {
return fmt.Sprintf("S=%#q", s.Text) return s.Quoted
} }
// endNode represents an {{end}} action. It is represented by a nil pointer. // endNode represents an {{end}} action. It is represented by a nil pointer.
...@@ -411,9 +444,9 @@ func (b *BranchNode) String() string { ...@@ -411,9 +444,9 @@ func (b *BranchNode) String() string {
panic("unknown branch type") panic("unknown branch type")
} }
if b.ElseList != nil { if b.ElseList != nil {
return fmt.Sprintf("({{%s %s}} %s {{else}} %s)", name, b.Pipe, b.List, b.ElseList) return fmt.Sprintf("{{%s %s}}%s{{else}}%s{{end}}", name, b.Pipe, b.List, b.ElseList)
} }
return fmt.Sprintf("({{%s %s}} %s)", name, b.Pipe, b.List) return fmt.Sprintf("{{%s %s}}%s{{end}}", name, b.Pipe, b.List)
} }
// IfNode represents an {{if}} action and its commands. // IfNode represents an {{if}} action and its commands.
......
...@@ -150,7 +150,7 @@ type parseTest struct { ...@@ -150,7 +150,7 @@ type parseTest struct {
name string name string
input string input string
ok bool ok bool
result string result string // what the user would see in an error message.
} }
const ( const (
...@@ -160,59 +160,57 @@ const ( ...@@ -160,59 +160,57 @@ const (
var parseTests = []parseTest{ var parseTests = []parseTest{
{"empty", "", noError, {"empty", "", noError,
`[]`}, ``},
{"comment", "{{/*\n\n\n*/}}", noError, {"comment", "{{/*\n\n\n*/}}", noError,
`[]`}, ``},
{"spaces", " \t\n", noError, {"spaces", " \t\n", noError,
`[(text: " \t\n")]`}, `" \t\n"`},
{"text", "some text", noError, {"text", "some text", noError,
`[(text: "some text")]`}, `"some text"`},
{"emptyAction", "{{}}", hasError, {"emptyAction", "{{}}", hasError,
`[(action: [])]`}, `{{}}`},
{"field", "{{.X}}", noError, {"field", "{{.X}}", noError,
`[(action: [(command: [F=[X]])])]`}, `{{.X}}`},
{"simple command", "{{printf}}", noError, {"simple command", "{{printf}}", noError,
`[(action: [(command: [I=printf])])]`}, `{{printf}}`},
{"$ invocation", "{{$}}", noError, {"$ invocation", "{{$}}", noError,
"[(action: [(command: [V=[$]])])]"}, "{{$}}"},
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError, {"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
"[({{with [V=[$x]] := [(command: [N=3])]}} [(action: [(command: [V=[$x] N=23])])])]"}, "{{with $x := 3}}{{$x 23}}{{end}}"},
{"variable with fields", "{{$.I}}", noError, {"variable with fields", "{{$.I}}", noError,
"[(action: [(command: [V=[$ I]])])]"}, "{{$.I}}"},
{"multi-word command", "{{printf `%d` 23}}", noError, {"multi-word command", "{{printf `%d` 23}}", noError,
"[(action: [(command: [I=printf S=`%d` N=23])])]"}, "{{printf `%d` 23}}"},
{"pipeline", "{{.X|.Y}}", noError, {"pipeline", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`}, `{{.X | .Y}}`},
{"pipeline with decl", "{{$x := .X|.Y}}", noError, {"pipeline with decl", "{{$x := .X|.Y}}", noError,
`[(action: [V=[$x]] := [(command: [F=[X]]) (command: [F=[Y]])])]`}, `{{$x := .X | .Y}}`},
{"declaration", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"simple if", "{{if .X}}hello{{end}}", noError, {"simple if", "{{if .X}}hello{{end}}", noError,
`[({{if [(command: [F=[X]])]}} [(text: "hello")])]`}, `{{if .X}}"hello"{{end}}`},
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError, {"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
`[({{if [(command: [F=[X]])]}} [(text: "true")] {{else}} [(text: "false")])]`}, `{{if .X}}"true"{{else}}"false"{{end}}`},
{"simple range", "{{range .X}}hello{{end}}", noError, {"simple range", "{{range .X}}hello{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "hello")])]`}, `{{range .X}}"hello"{{end}}`},
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError, {"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
`[({{range [(command: [F=[X Y Z]])]}} [(text: "hello")])]`}, `{{range .X.Y.Z}}"hello"{{end}}`},
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError, {"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "hello")({{range [(command: [F=[Y]])]}} [(text: "goodbye")])])]`}, `{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError, {"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
`[({{range [(command: [F=[X]])]}} [(text: "true")] {{else}} [(text: "false")])]`}, `{{range .X}}"true"{{else}}"false"{{end}}`},
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError, {"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
`[({{range [(command: [F=[X]]) (command: [F=[M]])]}} [(text: "true")] {{else}} [(text: "false")])]`}, `{{range .X | .M}}"true"{{else}}"false"{{end}}`},
{"range []int", "{{range .SI}}{{.}}{{end}}", noError, {"range []int", "{{range .SI}}{{.}}{{end}}", noError,
`[({{range [(command: [F=[SI]])]}} [(action: [(command: [{{<.>}}])])])]`}, `{{range .SI}}{{.}}{{end}}`},
{"constants", "{{range .SI 1 -3.2i true false 'a'}}{{end}}", noError, {"constants", "{{range .SI 1 -3.2i true false 'a'}}{{end}}", noError,
`[({{range [(command: [F=[SI] N=1 N=-3.2i B=true B=false N='a'])]}} [])]`}, `{{range .SI 1 -3.2i true false 'a'}}{{end}}`},
{"template", "{{template `x`}}", noError, {"template", "{{template `x`}}", noError,
`[{{template "x"}}]`}, `{{template "x"}}`},
{"template with arg", "{{template `x` .Y}}", noError, {"template with arg", "{{template `x` .Y}}", noError,
`[{{template "x" [(command: [F=[Y]])]}}]`}, `{{template "x" .Y}}`},
{"with", "{{with .X}}hello{{end}}", noError, {"with", "{{with .X}}hello{{end}}", noError,
`[({{with [(command: [F=[X]])]}} [(text: "hello")])]`}, `{{with .X}}"hello"{{end}}`},
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError, {"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
`[({{with [(command: [F=[X]])]}} [(text: "hello")] {{else}} [(text: "goodbye")])]`}, `{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
// Errors. // Errors.
{"unclosed action", "hello{{range", hasError, ""}, {"unclosed action", "hello{{range", hasError, ""},
{"unmatched end", "{{end}}", hasError, ""}, {"unmatched end", "{{end}}", hasError, ""},
......
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