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.
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
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:
The result is the value of the field. Field invocations may be
chained:
.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,
such as
.Method
......@@ -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
the chain may be a method; other others must be struct fields:
.Field1.Field2.Method
Methods can also be evaluated on variables, including chaining:
$x.Field1.Method
- The name of a niladic function, such as
fun
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.
}
}
if pipe.decl != nil {
s.push(pipe.decl.ident, value)
s.push(pipe.decl.ident[0], value)
}
return value
}
......@@ -290,6 +290,8 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.
case *identifierNode:
// Must be a function.
return s.evalFunction(data, n.ident, cmd.args, final)
case *variableNode:
return s.evalVariableNode(n, cmd.args, final)
}
s.notAFunction(cmd.args, final)
switch word := firstWord.(type) {
......@@ -313,21 +315,32 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.
}
case *stringNode:
return reflect.ValueOf(word.text)
case *variableNode:
return s.varValue(word.ident)
}
s.errorf("can't handle command %q", firstWord)
panic("not reached")
}
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.
n := len(field.ident)
n := len(ident)
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.
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 {
......@@ -468,7 +481,7 @@ func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Va
case *fieldNode:
return s.validateType(s.evalFieldNode(data, arg, []node{n}, zero), typ)
case *variableNode:
return s.validateType(s.varValue(arg.ident), typ)
return s.validateType(s.evalVariableNode(arg, nil, zero), typ)
}
switch typ.Kind() {
case reflect.Bool:
......@@ -578,7 +591,7 @@ func (s *state) evalEmptyInterface(data reflect.Value, n node) reflect.Value {
case *stringNode:
return reflect.ValueOf(n.text)
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)
panic("not reached")
......
......@@ -165,6 +165,9 @@ var execTests = []execTest{
{"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 $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.
{"*int", "{{.PI}}", "23", tVal, true},
......@@ -186,6 +189,7 @@ var execTests = []execTest{
{".Method2(3, .X)", "-{{.Method2 3 .X}}-", "-Method2: 3 x-", 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},
{"method on var", "{{if $x := .}}-{{$x.Method2 .U16 $x.X}}{{end}}-", "-Method2: 16 x-", tVal, true},
// Pipelines.
{"pipeline", "-{{.Method0 | .Method2 .U16}}-", "-Method2: 16 M0-", tVal, true},
......
......@@ -329,7 +329,7 @@ Loop:
switch r := l.next(); {
case isAlphaNumeric(r):
// absorb.
case r == '.' && l.input[l.start] == '.':
case r == '.' && (l.input[l.start] == '.' || l.input[l.start] == '$'):
// field chaining; absorb into one token.
default:
l.backup()
......
......@@ -97,7 +97,7 @@ var lexTests = []lexTest{
tRight,
tEOF,
}},
{"variables", "{{$c := printf $ $hello $23 $.Method}}", []item{
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
tLeft,
{itemVariable, "$c"},
{itemColonEquals, ":="},
......@@ -106,6 +106,7 @@ var lexTests = []lexTest{
{itemVariable, "$hello"},
{itemVariable, "$23"},
{itemVariable, "$"},
{itemVariable, "$var.Field"},
{itemField, ".Method"},
tRight,
tEOF,
......
......@@ -214,11 +214,11 @@ func (i *identifierNode) String() string {
// variableNode holds a variable.
type variableNode struct {
nodeType
ident string
ident []string
}
func newVariable(ident string) *variableNode {
return &variableNode{nodeType: nodeVariable, ident: ident}
return &variableNode{nodeType: nodeVariable, ident: strings.Split(ident, ".")}
}
func (v *variableNode) String() string {
......@@ -716,6 +716,9 @@ func (t *Template) pipeline(context string) (pipe *pipeNode) {
if ce := t.peek(); ce.typ == itemColonEquals {
t.next()
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)
} else {
t.backup2(v)
......@@ -854,9 +857,10 @@ Loop:
case itemDot:
cmd.append(newDot())
case itemVariable:
v := newVariable(token.val)
found := false
for _, varName := range t.vars {
if varName == token.val {
if varName == v.ident[0] {
found = true
break
}
......@@ -864,7 +868,7 @@ Loop:
if !found {
t.errorf("undefined variable %q", token.val)
}
cmd.append(newVariable(token.val))
cmd.append(v)
case itemField:
cmd.append(newField(token.val))
case itemBool:
......
......@@ -172,15 +172,17 @@ var parseTests = []parseTest{
{"simple command", "{{printf}}", noError,
`[(action: [(command: [I=printf])])]`},
{"$ invocation", "{{$}}", noError,
"[(action: [(command: [V=$])])]"},
"[(action: [(command: [V=[$]])])]"},
{"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,
"[(action: [(command: [I=printf S=`%d` N=23])])]"},
{"pipeline", "{{.X|.Y}}", noError,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"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,
`[(action: [(command: [F=[X]]) (command: [F=[Y]])])]`},
{"simple if", "{{if .X}}hello{{end}}", noError,
......@@ -217,6 +219,7 @@ var parseTests = []parseTest{
{"undefined function", "hello{{undefined}}", hasError, ""},
{"undefined variable", "{{$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) {
......
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