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

exp/template: fields and methods on variables.

Not strictly necessary (you could achieve the same, clumsily,
via with blocks) but great to have: $x.Field, $y.Method.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/4678047
parent 96bbcc42
...@@ -79,7 +79,7 @@ maps, and strings, any value v with len(v)==0 counts as a zero value. ...@@ -79,7 +79,7 @@ maps, and strings, any value v with len(v)==0 counts as a zero value.
Arguments Arguments
An argument is a simple value, denoted by one of the following: An argument is a simple value, denoted by one of the following.
- A boolean, string, character, integer, floating-point, imaginary - A boolean, string, character, integer, floating-point, imaginary
or complex constant in Go syntax. These behave like Go's untyped or complex constant in Go syntax. These behave like Go's untyped
...@@ -100,6 +100,8 @@ An argument is a simple value, denoted by one of the following: ...@@ -100,6 +100,8 @@ An argument is a simple value, denoted by one of the following:
The result is the value of the field. Field invocations may be The result is the value of the field. Field invocations may be
chained: chained:
.Field1.Field2 .Field1.Field2
Fields can also be evaluated on variables, including chaining:
$x.Field1.Field2
- The name of a niladic method of the data, preceded by a period, - The name of a niladic method of the data, preceded by a period,
such as such as
.Method .Method
...@@ -111,6 +113,8 @@ An argument is a simple value, denoted by one of the following: ...@@ -111,6 +113,8 @@ An argument is a simple value, denoted by one of the following:
Method invocations may be chained, but only the last element of Method invocations may be chained, but only the last element of
the chain may be a method; other others must be struct fields: the chain may be a method; other others must be struct fields:
.Field1.Field2.Method .Field1.Field2.Method
Methods can also be evaluated on variables, including chaining:
$x.Field1.Method
- The name of a niladic function, such as - The name of a niladic function, such as
fun fun
The result is the value of invoking the function, fun(). The return The result is the value of invoking the function, fun(). The return
......
...@@ -271,7 +271,7 @@ func (s *state) evalPipeline(data reflect.Value, pipe *pipeNode) (value reflect. ...@@ -271,7 +271,7 @@ func (s *state) evalPipeline(data reflect.Value, pipe *pipeNode) (value reflect.
} }
} }
if pipe.decl != nil { if pipe.decl != nil {
s.push(pipe.decl.ident, value) s.push(pipe.decl.ident[0], value)
} }
return value return value
} }
...@@ -290,6 +290,8 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect. ...@@ -290,6 +290,8 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.
case *identifierNode: case *identifierNode:
// Must be a function. // Must be a function.
return s.evalFunction(data, n.ident, cmd.args, final) return s.evalFunction(data, n.ident, cmd.args, final)
case *variableNode:
return s.evalVariableNode(n, cmd.args, final)
} }
s.notAFunction(cmd.args, final) s.notAFunction(cmd.args, final)
switch word := firstWord.(type) { switch word := firstWord.(type) {
...@@ -313,21 +315,32 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect. ...@@ -313,21 +315,32 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.
} }
case *stringNode: case *stringNode:
return reflect.ValueOf(word.text) return reflect.ValueOf(word.text)
case *variableNode:
return s.varValue(word.ident)
} }
s.errorf("can't handle command %q", firstWord) s.errorf("can't handle command %q", firstWord)
panic("not reached") panic("not reached")
} }
func (s *state) evalFieldNode(data reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value { func (s *state) evalFieldNode(data reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value {
return s.evalFieldChain(data, field.ident, args, final)
}
func (s *state) evalVariableNode(v *variableNode, args []node, final reflect.Value) reflect.Value {
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
data := s.varValue(v.ident[0])
if len(v.ident) == 1 {
return data
}
return s.evalFieldChain(data, v.ident[1:], args, final)
}
func (s *state) evalFieldChain(data reflect.Value, ident []string, args []node, final reflect.Value) reflect.Value {
// Up to the last entry, it must be a field. // Up to the last entry, it must be a field.
n := len(field.ident) n := len(ident)
for i := 0; i < n-1; i++ { for i := 0; i < n-1; i++ {
data = s.evalField(data, field.ident[i], nil, zero, false) data = s.evalField(data, ident[i], nil, zero, false)
} }
// Now it can be a field or method and if a method, gets arguments. // Now it can be a field or method and if a method, gets arguments.
return s.evalField(data, field.ident[n-1], args, final, true) return s.evalField(data, ident[n-1], args, final, true)
} }
func (s *state) evalFunction(data reflect.Value, name string, args []node, final reflect.Value) reflect.Value { func (s *state) evalFunction(data reflect.Value, name string, args []node, final reflect.Value) reflect.Value {
...@@ -468,7 +481,7 @@ func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Va ...@@ -468,7 +481,7 @@ func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Va
case *fieldNode: case *fieldNode:
return s.validateType(s.evalFieldNode(data, arg, []node{n}, zero), typ) return s.validateType(s.evalFieldNode(data, arg, []node{n}, zero), typ)
case *variableNode: case *variableNode:
return s.validateType(s.varValue(arg.ident), typ) return s.validateType(s.evalVariableNode(arg, nil, zero), typ)
} }
switch typ.Kind() { switch typ.Kind() {
case reflect.Bool: case reflect.Bool:
...@@ -578,7 +591,7 @@ func (s *state) evalEmptyInterface(data reflect.Value, n node) reflect.Value { ...@@ -578,7 +591,7 @@ func (s *state) evalEmptyInterface(data reflect.Value, n node) reflect.Value {
case *stringNode: case *stringNode:
return reflect.ValueOf(n.text) return reflect.ValueOf(n.text)
case *variableNode: case *variableNode:
return s.varValue(n.ident) return s.evalVariableNode(n, nil, zero)
} }
s.errorf("can't handle assignment of %s to empty interface argument", n) s.errorf("can't handle assignment of %s to empty interface argument", n)
panic("not reached") panic("not reached")
......
...@@ -165,6 +165,9 @@ var execTests = []execTest{ ...@@ -165,6 +165,9 @@ var execTests = []execTest{
{"range $x PSI", "{{range $x := .PSI}}<{{$x}}>{{end}}", "<21><22><23>", tVal, true}, {"range $x PSI", "{{range $x := .PSI}}<{{$x}}>{{end}}", "<21><22><23>", tVal, true},
{"if $x with $y int", "{{if $x := true}}{{with $y := .I}}{{$x}},{{$y}}{{end}}{{end}}", "true,17", tVal, true}, {"if $x with $y int", "{{if $x := true}}{{with $y := .I}}{{$x}},{{$y}}{{end}}{{end}}", "true,17", tVal, true},
{"if $x with $x int", "{{if $x := true}}{{with $x := .I}}{{$x}},{{end}}{{$x}}{{end}}", "17,true", tVal, true}, {"if $x with $x int", "{{if $x := true}}{{with $x := .I}}{{$x}},{{end}}{{$x}}{{end}}", "17,true", tVal, true},
{"$.I", "{{$.I}}", "17", tVal, true},
{"$.U.V", "{{$.U.V}}", "v", tVal, true},
{"with $x struct.U.V", "{{with $x := $}}{{$.U.V}}{{end}}", "v", tVal, true},
// Pointers. // Pointers.
{"*int", "{{.PI}}", "23", tVal, true}, {"*int", "{{.PI}}", "23", tVal, true},
...@@ -186,6 +189,7 @@ var execTests = []execTest{ ...@@ -186,6 +189,7 @@ var execTests = []execTest{
{".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", tVal, true}, {".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", tVal, true},
{".Method2(.U16, `str`)", "-{{.Method2 .U16 `str`}}-", "-Method2: 16 str-", tVal, true}, {".Method2(.U16, `str`)", "-{{.Method2 .U16 `str`}}-", "-Method2: 16 str-", tVal, true},
{".Method2(.U16, $x)", "{{if $x := .X}}-{{.Method2 .U16 $x}}{{end}}-", "-Method2: 16 x-", tVal, true}, {".Method2(.U16, $x)", "{{if $x := .X}}-{{.Method2 .U16 $x}}{{end}}-", "-Method2: 16 x-", tVal, true},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
// Pipelines. // Pipelines.
{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true}, {"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true},
......
...@@ -329,7 +329,7 @@ Loop: ...@@ -329,7 +329,7 @@ Loop:
switch r := l.next(); { switch r := l.next(); {
case isAlphaNumeric(r): case isAlphaNumeric(r):
// absorb. // absorb.
case r == '.' && l.input[l.start] == '.': case r == '.' && (l.input[l.start] == '.' || l.input[l.start] == '$'):
// field chaining; absorb into one token. // field chaining; absorb into one token.
default: default:
l.backup() l.backup()
......
...@@ -97,7 +97,7 @@ var lexTests = []lexTest{ ...@@ -97,7 +97,7 @@ var lexTests = []lexTest{
tRight, tRight,
tEOF, tEOF,
}}, }},
{"variables", "{{$c := printf $ $hello $23 $.Method}}", []item{ {"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
tLeft, tLeft,
{itemVariable, "$c"}, {itemVariable, "$c"},
{itemColonEquals, ":="}, {itemColonEquals, ":="},
...@@ -106,6 +106,7 @@ var lexTests = []lexTest{ ...@@ -106,6 +106,7 @@ var lexTests = []lexTest{
{itemVariable, "$hello"}, {itemVariable, "$hello"},
{itemVariable, "$23"}, {itemVariable, "$23"},
{itemVariable, "$"}, {itemVariable, "$"},
{itemVariable, "$var.Field"},
{itemField, ".Method"}, {itemField, ".Method"},
tRight, tRight,
tEOF, tEOF,
......
...@@ -214,11 +214,11 @@ func (i *identifierNode) String() string { ...@@ -214,11 +214,11 @@ func (i *identifierNode) String() string {
// variableNode holds a variable. // variableNode holds a variable.
type variableNode struct { type variableNode struct {
nodeType nodeType
ident string ident []string
} }
func newVariable(ident string) *variableNode { func newVariable(ident string) *variableNode {
return &variableNode{nodeType: nodeVariable, ident: ident} return &variableNode{nodeType: nodeVariable, ident: strings.Split(ident, ".")}
} }
func (v *variableNode) String() string { func (v *variableNode) String() string {
...@@ -716,6 +716,9 @@ func (t *Template) pipeline(context string) (pipe *pipeNode) { ...@@ -716,6 +716,9 @@ func (t *Template) pipeline(context string) (pipe *pipeNode) {
if ce := t.peek(); ce.typ == itemColonEquals { if ce := t.peek(); ce.typ == itemColonEquals {
t.next() t.next()
decl = newVariable(v.val) decl = newVariable(v.val)
if len(decl.ident) != 1 {
t.errorf("illegal variable in declaration: %s", v.val)
}
t.vars = append(t.vars, v.val) t.vars = append(t.vars, v.val)
} else { } else {
t.backup2(v) t.backup2(v)
...@@ -854,9 +857,10 @@ Loop: ...@@ -854,9 +857,10 @@ Loop:
case itemDot: case itemDot:
cmd.append(newDot()) cmd.append(newDot())
case itemVariable: case itemVariable:
v := newVariable(token.val)
found := false found := false
for _, varName := range t.vars { for _, varName := range t.vars {
if varName == token.val { if varName == v.ident[0] {
found = true found = true
break break
} }
...@@ -864,7 +868,7 @@ Loop: ...@@ -864,7 +868,7 @@ Loop:
if !found { if !found {
t.errorf("undefined variable %q", token.val) t.errorf("undefined variable %q", token.val)
} }
cmd.append(newVariable(token.val)) cmd.append(v)
case itemField: case itemField:
cmd.append(newField(token.val)) cmd.append(newField(token.val))
case itemBool: case itemBool:
......
...@@ -172,15 +172,17 @@ var parseTests = []parseTest{ ...@@ -172,15 +172,17 @@ var parseTests = []parseTest{
{"simple command", "{{printf}}", noError, {"simple command", "{{printf}}", noError,
`[(action: [(command: [I=printf])])]`}, `[(action: [(command: [I=printf])])]`},
{"$ invocation", "{{$}}", noError, {"$ invocation", "{{$}}", noError,
"[(action: [(command: [V=$])])]"}, "[(action: [(command: [V=[$]])])]"},
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError, {"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
"[({{with $x := [(command: [N=3])]}} [(action: [(command: [V=$x N=23])])])]"}, "[({{with [$x] := [(command: [N=3])]}} [(action: [(command: [V=[$x] N=23])])])]"},
{"variable with fields", "{{$.I}}", noError,
"[(action: [(command: [V=[$ I]])])]"},
{"multi-word command", "{{printf `%d` 23}}", noError, {"multi-word command", "{{printf `%d` 23}}", noError,
"[(action: [(command: [I=printf S=`%d` N=23])])]"}, "[(action: [(command: [I=printf S=`%d` N=23])])]"},
{"pipeline", "{{.X|.Y}}", noError, {"pipeline", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`}, `[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"pipeline with decl", "{{$x := .X|.Y}}", noError, {"pipeline with decl", "{{$x := .X|.Y}}", noError,
`[(action: $x := [(command: [F=[X]]) (command: [F=[Y]])])]`}, `[(action: [$x] := [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"declaration", "{{.X|.Y}}", noError, {"declaration", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`}, `[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"simple if", "{{if .X}}hello{{end}}", noError, {"simple if", "{{if .X}}hello{{end}}", noError,
...@@ -217,6 +219,7 @@ var parseTests = []parseTest{ ...@@ -217,6 +219,7 @@ var parseTests = []parseTest{
{"undefined function", "hello{{undefined}}", hasError, ""}, {"undefined function", "hello{{undefined}}", hasError, ""},
{"undefined variable", "{{$x}}", hasError, ""}, {"undefined variable", "{{$x}}", hasError, ""},
{"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""}, {"variable undefined after end", "{{with $x := 4}}{{end}}{{$x}}", hasError, ""},
{"declare with field", "{{with $x.Y := 4}}{{end}}", hasError, ""},
} }
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
......
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