Skip to content

Commit 29ae32a

Browse files
author
Rob Figueiredo
committed
Support for {@param ...}, aka "header params"
Upstream Closure Templates has removed support for params specified in soydoc and now requires them specified using "header params". This change adds support for parsing header params and treating them similarly to soydoc params, having an unknown type. For backwards compatibility, the header params are added to `Template.SoyDoc.Params`, so calling code should not require updates. The result is that this library will not report errors that the Java library would report, but that level of compatibility is sufficient for our purposes, and a lot of development would be required to support the param type system.
1 parent 54e9f54 commit 29ae32a

File tree

11 files changed

+373
-14
lines changed

11 files changed

+373
-14
lines changed

ast/node.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,49 @@ func (n *TemplateNode) Children() []Node {
121121
return []Node{n.Body}
122122
}
123123

124+
// TypeNode holds a type definition for a template parameter.
125+
//
126+
// Presently this is just a string value which is not validated or processed.
127+
// Backwards-incompatible changes in the future may elaborate this data model to
128+
// add functionality.
129+
type TypeNode struct {
130+
Pos
131+
Expr string
132+
}
133+
134+
func (n TypeNode) String() string {
135+
return n.Expr
136+
}
137+
138+
// HeaderParamNode holds a parameter declaration.
139+
//
140+
// HeaderParams MUST appear at the beginning of a TemplateNode's Body.
141+
type HeaderParamNode struct {
142+
Pos
143+
Optional bool
144+
Name string
145+
Type TypeNode // empty if inferred from the default value
146+
Default Node // nil if no default was specified
147+
}
148+
149+
func (n *HeaderParamNode) String() string {
150+
var expr string
151+
if !n.Optional {
152+
expr = "{@param "
153+
} else {
154+
expr = "{@param? "
155+
}
156+
expr += n.Name + ":"
157+
if typ := n.Type.String(); typ != "" {
158+
expr += " " + typ + " "
159+
}
160+
if n.Default != nil {
161+
expr += "= " + n.Default.String()
162+
}
163+
expr += "}\n"
164+
return expr
165+
}
166+
124167
type SoyDocNode struct {
125168
Pos
126169
Params []*SoyDocParamNode

parse/lexer.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ const (
101101
itemSoyDocEnd // */
102102
itemComment // line comments (//) or block comments (/*)
103103

104+
// Headers
105+
itemHeaderParam // {@param NAME: TYPE = DEFAULT}
106+
itemHeaderOptionalParam // {@param? NAME: TYPE = DEFAULT}
107+
itemHeaderParamType // e.g. ? any int string map<int, string> list<string> [age: int, name: string]
108+
104109
// Commands
105110
itemCommand // used only to delimit the commands
106111
itemAlias // {alias ...}
@@ -612,6 +617,8 @@ func lexInsideTag(l *lexer) stateFn {
612617
return lexIdent
613618
case r == ',':
614619
l.emit(itemComma)
620+
case r == '@':
621+
return lexHeaderParam
615622
default:
616623
return l.errorf("unrecognized character in action: %#U", r)
617624
}
@@ -839,6 +846,49 @@ func lexIdent(l *lexer) stateFn {
839846
return lexInsideTag
840847
}
841848

849+
// lexHeaderParam lexes a "header param" such as {@param ...}.
850+
// '@' has just been read.
851+
func lexHeaderParam(l *lexer) stateFn {
852+
if !strings.HasPrefix(l.input[l.pos:], "param") {
853+
return l.errorf("expected {@param ...}")
854+
}
855+
l.pos += ast.Pos(len("param"))
856+
857+
if l.next() == '?' {
858+
l.emit(itemHeaderOptionalParam)
859+
} else {
860+
l.backup()
861+
l.emit(itemHeaderParam)
862+
}
863+
skipSpace(l)
864+
865+
// Consume the (simple) identifier.
866+
for isAlphaNumeric(l.next()) {}
867+
l.backup()
868+
l.emit(itemIdent)
869+
skipSpace(l)
870+
871+
// Consume the ':'
872+
if l.next() != ':' {
873+
return l.errorf("expected {@param name: ...}")
874+
}
875+
l.emit(itemColon)
876+
skipSpace(l)
877+
878+
// Consume until the equals or end of the tag.
879+
var lastNonSpace = l.pos
880+
for ch := l.next(); ch != '=' && ch != '}' ; ch = l.next() {
881+
if !isSpace(ch) {
882+
lastNonSpace = l.pos
883+
}
884+
}
885+
l.pos = lastNonSpace
886+
l.emit(itemHeaderParamType)
887+
skipSpace(l)
888+
889+
return lexInsideTag
890+
}
891+
842892
// lexCss scans the body of the {css} command into an itemText.
843893
// This is required because css classes are unquoted and may have hyphens (and
844894
// thus are not recognized as idents).
@@ -1033,3 +1083,12 @@ func allSpaceWithNewline(str string) bool {
10331083
}
10341084
return seenNewline
10351085
}
1086+
1087+
func skipSpace(l *lexer) {
1088+
ch := l.next()
1089+
for isSpaceEOL(ch) {
1090+
ch = l.next()
1091+
}
1092+
l.backup()
1093+
l.ignore()
1094+
}

parse/lexer_test.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package parse
22

3-
import "testing"
3+
import (
4+
"testing"
5+
)
46

57
type lexTest struct {
68
name string
@@ -540,6 +542,78 @@ var lexTests = []lexTest{
540542
tEOF,
541543
}},
542544

545+
{"@param", `
546+
{@param NAME: ?} // A required param.
547+
{@param NAME: any} // A required param of type any.
548+
{@param NAME:= 'default'} // A default param with an inferred type.
549+
{@param NAME: int = 10} // A default param with an explicit type.
550+
{@param? NAME: [age: int, name: string]} // An optional param.
551+
{@param? NAME: map<int, string>} // A map param.
552+
{@param? NAME: list<string>} // A list param.
553+
`, []item{
554+
tLeft,
555+
{itemHeaderParam, 0, "@param"},
556+
{itemIdent, 0, "NAME"},
557+
{itemColon, 0, ":"},
558+
{itemHeaderParamType, 0, "?"},
559+
tRight,
560+
{itemComment, 0, "// A required param.\n"},
561+
562+
tLeft,
563+
{itemHeaderParam, 0, "@param"},
564+
{itemIdent, 0, "NAME"},
565+
{itemColon, 0, ":"},
566+
{itemHeaderParamType, 0, "any"},
567+
tRight,
568+
{itemComment, 0, "// A required param of type any.\n"},
569+
570+
tLeft,
571+
{itemHeaderParam, 0, "@param"},
572+
{itemIdent, 0, "NAME"},
573+
{itemColon, 0, ":"},
574+
{itemHeaderParamType, 0, ""},
575+
{itemEquals, 0, "="},
576+
{itemString, 0, "'default'"},
577+
tRight,
578+
{itemComment, 0, "// A default param with an inferred type.\n"},
579+
580+
tLeft,
581+
{itemHeaderParam, 0, "@param"},
582+
{itemIdent, 0, "NAME"},
583+
{itemColon, 0, ":"},
584+
{itemHeaderParamType, 0, "int"},
585+
{itemEquals, 0, "="},
586+
{itemInteger, 0, "10"},
587+
tRight,
588+
{itemComment, 0, "// A default param with an explicit type.\n"},
589+
590+
tLeft,
591+
{itemHeaderOptionalParam, 0, "@param?"},
592+
{itemIdent, 0, "NAME"},
593+
{itemColon, 0, ":"},
594+
{itemHeaderParamType, 0, "[age: int, name: string]"},
595+
tRight,
596+
{itemComment, 0, "// An optional param.\n"},
597+
598+
tLeft,
599+
{itemHeaderOptionalParam, 0, "@param?"},
600+
{itemIdent, 0, "NAME"},
601+
{itemColon, 0, ":"},
602+
{itemHeaderParamType, 0, "map<int, string>"},
603+
tRight,
604+
{itemComment, 0, "// A map param.\n"},
605+
606+
tLeft,
607+
{itemHeaderOptionalParam, 0, "@param?"},
608+
{itemIdent, 0, "NAME"},
609+
{itemColon, 0, ":"},
610+
{itemHeaderParamType, 0, "list<string>"},
611+
tRight,
612+
{itemComment, 0, "// A list param.\n"},
613+
614+
tEOF,
615+
}},
616+
543617
{"let", `{let $ident: 1+1/}{let $ident}content{/let}`, []item{
544618
tLeft,
545619
{itemLet, 0, "let"},

parse/parse.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,8 @@ func (t *tree) beginTag() ast.Node {
133133
return t.parseNamespace(token)
134134
case itemTemplate:
135135
return t.parseTemplate(token)
136+
case itemHeaderParam, itemHeaderOptionalParam:
137+
return t.parseHeaderParam(token)
136138
case itemIf:
137139
t.notmsg(token)
138140
return t.parseIf(token)
@@ -759,6 +761,28 @@ func (t *tree) parseTemplate(token item) ast.Node {
759761
return tmpl
760762
}
761763

764+
func (t *tree) parseHeaderParam(token item) ast.Node {
765+
const ctx = "@param tag"
766+
var opt = token.typ == itemHeaderOptionalParam
767+
var name = t.expect(itemIdent, ctx)
768+
t.expect(itemColon, ctx)
769+
var typ = t.expect(itemHeaderParamType, ctx)
770+
var defval ast.Node
771+
if tok := t.next(); tok.typ == itemEquals {
772+
defval = t.parseExpr(0)
773+
} else {
774+
t.backup()
775+
}
776+
t.expect(itemRightDelim, ctx)
777+
return &ast.HeaderParamNode{
778+
Pos: token.pos,
779+
Optional: opt,
780+
Name: name.val,
781+
Type: ast.TypeNode{Pos: typ.pos, Expr: typ.val},
782+
Default: defval,
783+
}
784+
}
785+
762786
// Expressions ----------
763787

764788
// Expr returns the parsed representation of the given Soy expression.

parse/parse_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ var parseTests = []parseTest{
9797
{0, "name", false},
9898
}})},
9999

100+
{"header param", `{template .main}
101+
{@param NAME:?}
102+
{@param NAME:any} // A required param of type any.
103+
{@param NAME:='default'}
104+
{@param NAME:int=10}
105+
{@param? NAME:[age: int, name: string]}
106+
{@param? NAME:map<int, string>}
107+
{@param? NAME:list<string>}
108+
Hello world.
109+
{/template}
110+
`, tFile(&ast.TemplateNode{Name: ".main", Body: &ast.ListNode{Nodes: []ast.Node{
111+
&ast.HeaderParamNode{0, false, "NAME", ast.TypeNode{0, "?"}, nil},
112+
&ast.HeaderParamNode{0, false, "NAME", ast.TypeNode{0, "any"}, nil},
113+
&ast.HeaderParamNode{0, false, "NAME", ast.TypeNode{0, ""}, &ast.StringNode{0, "'default'", "default"}},
114+
&ast.HeaderParamNode{0, false, "NAME", ast.TypeNode{0, "int"}, &ast.IntNode{0, 10}},
115+
&ast.HeaderParamNode{0, true, "NAME", ast.TypeNode{0, "[age: int, name: string]"}, nil},
116+
&ast.HeaderParamNode{0, true, "NAME", ast.TypeNode{0, "map<int, string>"}, nil},
117+
&ast.HeaderParamNode{0, true, "NAME", ast.TypeNode{0, "list<string>"}, nil},
118+
&ast.RawTextNode{0, []byte("Hello world.")},
119+
}}})},
120+
100121
{"rawtext (linejoin)", "\n a \n\tb\r\n c \n\n", tFile(newText(0, "a b c"))},
101122
{"rawtext+html", "\n a <br>\n\tb\r\n\n c\n\n<br> ", tFile(newText(0, "a <br>b c<br> "))},
102123
{"rawtext+comment", "a <br> // comment \n\tb\t// comment2\r\n c\n\n", tFile(
@@ -654,6 +675,12 @@ func eqTree(t *testing.T, expected, actual ast.Node) bool {
654675
case *ast.SwitchCaseNode:
655676
return eqTree(t, expected.(*ast.SwitchCaseNode).Body, actual.(*ast.SwitchCaseNode).Body) &&
656677
eqNodes(t, expected.(*ast.SwitchCaseNode).Values, actual.(*ast.SwitchCaseNode).Values)
678+
679+
case *ast.HeaderParamNode:
680+
return eqbool(t, "@param optional?", expected.(*ast.HeaderParamNode).Optional, actual.(*ast.HeaderParamNode).Optional) &&
681+
eqstr(t, "@param name", expected.(*ast.HeaderParamNode).Name, actual.(*ast.HeaderParamNode).Name) &&
682+
eqstr(t, "@param type", expected.(*ast.HeaderParamNode).Type.Expr, actual.(*ast.HeaderParamNode).Type.Expr) &&
683+
eqTree(t, expected.(*ast.HeaderParamNode).Default, actual.(*ast.HeaderParamNode).Default)
657684
}
658685
panic(fmt.Sprintf("type not implemented: %T", actual))
659686
}

parsepasses/datarefcheck.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
// 5. {call}'d templates actually exist in the registry.
1717
// 6. any variable created by {let} is used somewhere
1818
// 7. {let} variable names are valid. ('ij' is not allowed.)
19+
// 8. Only one parameter declaration mechanism (soydoc vs headers) is used.
1920
func CheckDataRefs(reg template.Registry) (err error) {
2021
var currentTemplate string
2122
defer func() {
@@ -26,8 +27,8 @@ func CheckDataRefs(reg template.Registry) (err error) {
2627

2728
for _, t := range reg.Templates {
2829
currentTemplate = t.Node.Name
29-
tc := newTemplateChecker(reg, t.Doc.Params)
30-
tc.checkTemplate(t.Node.Body)
30+
tc := newTemplateChecker(reg, t)
31+
tc.checkTemplate(t.Node)
3132

3233
// check that all params appear in the usedKeys
3334
for _, param := range tc.params {
@@ -47,9 +48,9 @@ type templateChecker struct {
4748
usedKeys []string
4849
}
4950

50-
func newTemplateChecker(reg template.Registry, params []*ast.SoyDocParamNode) *templateChecker {
51+
func newTemplateChecker(reg template.Registry, tpl template.Template) *templateChecker {
5152
var paramNames []string
52-
for _, param := range params {
53+
for _, param := range tpl.Doc.Params {
5354
paramNames = append(paramNames, param.Name)
5455
}
5556
return &templateChecker{reg, paramNames, nil, nil, nil}
@@ -69,6 +70,8 @@ func (tc *templateChecker) checkTemplate(node ast.Node) {
6970
tc.forVars = append(tc.forVars, node.Var)
7071
case *ast.DataRefNode:
7172
tc.visitKey(node.Key)
73+
case *ast.HeaderParamNode:
74+
panic(fmt.Errorf("unexpected {@param ...} tag found"))
7275
}
7376
if parent, ok := node.(ast.ParentNode); ok {
7477
tc.recurse(parent)

0 commit comments

Comments
 (0)