summaryrefslogtreecommitdiffstats
path: root/src/text/template/parse/lex_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/text/template/parse/lex_test.go')
-rw-r--r--src/text/template/parse/lex_test.go557
1 files changed, 557 insertions, 0 deletions
diff --git a/src/text/template/parse/lex_test.go b/src/text/template/parse/lex_test.go
new file mode 100644
index 0000000..6510eed
--- /dev/null
+++ b/src/text/template/parse/lex_test.go
@@ -0,0 +1,557 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package parse
+
+import (
+ "fmt"
+ "testing"
+)
+
+// Make the types prettyprint.
+var itemName = map[itemType]string{
+ itemError: "error",
+ itemBool: "bool",
+ itemChar: "char",
+ itemCharConstant: "charconst",
+ itemComment: "comment",
+ itemComplex: "complex",
+ itemDeclare: ":=",
+ itemEOF: "EOF",
+ itemField: "field",
+ itemIdentifier: "identifier",
+ itemLeftDelim: "left delim",
+ itemLeftParen: "(",
+ itemNumber: "number",
+ itemPipe: "pipe",
+ itemRawString: "raw string",
+ itemRightDelim: "right delim",
+ itemRightParen: ")",
+ itemSpace: "space",
+ itemString: "string",
+ itemVariable: "variable",
+
+ // keywords
+ itemDot: ".",
+ itemBlock: "block",
+ itemDefine: "define",
+ itemElse: "else",
+ itemIf: "if",
+ itemEnd: "end",
+ itemNil: "nil",
+ itemRange: "range",
+ itemTemplate: "template",
+ itemWith: "with",
+}
+
+func (i itemType) String() string {
+ s := itemName[i]
+ if s == "" {
+ return fmt.Sprintf("item%d", int(i))
+ }
+ return s
+}
+
+type lexTest struct {
+ name string
+ input string
+ items []item
+}
+
+func mkItem(typ itemType, text string) item {
+ return item{
+ typ: typ,
+ val: text,
+ }
+}
+
+var (
+ tDot = mkItem(itemDot, ".")
+ tBlock = mkItem(itemBlock, "block")
+ tEOF = mkItem(itemEOF, "")
+ tFor = mkItem(itemIdentifier, "for")
+ tLeft = mkItem(itemLeftDelim, "{{")
+ tLpar = mkItem(itemLeftParen, "(")
+ tPipe = mkItem(itemPipe, "|")
+ tQuote = mkItem(itemString, `"abc \n\t\" "`)
+ tRange = mkItem(itemRange, "range")
+ tRight = mkItem(itemRightDelim, "}}")
+ tRpar = mkItem(itemRightParen, ")")
+ tSpace = mkItem(itemSpace, " ")
+ raw = "`" + `abc\n\t\" ` + "`"
+ rawNL = "`now is{{\n}}the time`" // Contains newline inside raw quote.
+ tRawQuote = mkItem(itemRawString, raw)
+ tRawQuoteNL = mkItem(itemRawString, rawNL)
+)
+
+var lexTests = []lexTest{
+ {"empty", "", []item{tEOF}},
+ {"spaces", " \t\n", []item{mkItem(itemText, " \t\n"), tEOF}},
+ {"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
+ {"text with comment", "hello-{{/* this is a comment */}}-world", []item{
+ mkItem(itemText, "hello-"),
+ mkItem(itemComment, "/* this is a comment */"),
+ mkItem(itemText, "-world"),
+ tEOF,
+ }},
+ {"punctuation", "{{,@% }}", []item{
+ tLeft,
+ mkItem(itemChar, ","),
+ mkItem(itemChar, "@"),
+ mkItem(itemChar, "%"),
+ tSpace,
+ tRight,
+ tEOF,
+ }},
+ {"parens", "{{((3))}}", []item{
+ tLeft,
+ tLpar,
+ tLpar,
+ mkItem(itemNumber, "3"),
+ tRpar,
+ tRpar,
+ tRight,
+ tEOF,
+ }},
+ {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
+ {"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
+ {"block", `{{block "foo" .}}`, []item{
+ tLeft, tBlock, tSpace, mkItem(itemString, `"foo"`), tSpace, tDot, tRight, tEOF,
+ }},
+ {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
+ {"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
+ {"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
+ {"numbers", "{{1 02 0x14 0X14 -7.2i 1e3 1E3 +1.2e-4 4.2i 1+2i 1_2 0x1.e_fp4 0X1.E_FP4}}", []item{
+ tLeft,
+ mkItem(itemNumber, "1"),
+ tSpace,
+ mkItem(itemNumber, "02"),
+ tSpace,
+ mkItem(itemNumber, "0x14"),
+ tSpace,
+ mkItem(itemNumber, "0X14"),
+ tSpace,
+ mkItem(itemNumber, "-7.2i"),
+ tSpace,
+ mkItem(itemNumber, "1e3"),
+ tSpace,
+ mkItem(itemNumber, "1E3"),
+ tSpace,
+ mkItem(itemNumber, "+1.2e-4"),
+ tSpace,
+ mkItem(itemNumber, "4.2i"),
+ tSpace,
+ mkItem(itemComplex, "1+2i"),
+ tSpace,
+ mkItem(itemNumber, "1_2"),
+ tSpace,
+ mkItem(itemNumber, "0x1.e_fp4"),
+ tSpace,
+ mkItem(itemNumber, "0X1.E_FP4"),
+ tRight,
+ tEOF,
+ }},
+ {"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
+ tLeft,
+ mkItem(itemCharConstant, `'a'`),
+ tSpace,
+ mkItem(itemCharConstant, `'\n'`),
+ tSpace,
+ mkItem(itemCharConstant, `'\''`),
+ tSpace,
+ mkItem(itemCharConstant, `'\\'`),
+ tSpace,
+ mkItem(itemCharConstant, `'\u00FF'`),
+ tSpace,
+ mkItem(itemCharConstant, `'\xFF'`),
+ tSpace,
+ mkItem(itemCharConstant, `'本'`),
+ tRight,
+ tEOF,
+ }},
+ {"bools", "{{true false}}", []item{
+ tLeft,
+ mkItem(itemBool, "true"),
+ tSpace,
+ mkItem(itemBool, "false"),
+ tRight,
+ tEOF,
+ }},
+ {"dot", "{{.}}", []item{
+ tLeft,
+ tDot,
+ tRight,
+ tEOF,
+ }},
+ {"nil", "{{nil}}", []item{
+ tLeft,
+ mkItem(itemNil, "nil"),
+ tRight,
+ tEOF,
+ }},
+ {"dots", "{{.x . .2 .x.y.z}}", []item{
+ tLeft,
+ mkItem(itemField, ".x"),
+ tSpace,
+ tDot,
+ tSpace,
+ mkItem(itemNumber, ".2"),
+ tSpace,
+ mkItem(itemField, ".x"),
+ mkItem(itemField, ".y"),
+ mkItem(itemField, ".z"),
+ tRight,
+ tEOF,
+ }},
+ {"keywords", "{{range if else end with}}", []item{
+ tLeft,
+ mkItem(itemRange, "range"),
+ tSpace,
+ mkItem(itemIf, "if"),
+ tSpace,
+ mkItem(itemElse, "else"),
+ tSpace,
+ mkItem(itemEnd, "end"),
+ tSpace,
+ mkItem(itemWith, "with"),
+ tRight,
+ tEOF,
+ }},
+ {"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
+ tLeft,
+ mkItem(itemVariable, "$c"),
+ tSpace,
+ mkItem(itemDeclare, ":="),
+ tSpace,
+ mkItem(itemIdentifier, "printf"),
+ tSpace,
+ mkItem(itemVariable, "$"),
+ tSpace,
+ mkItem(itemVariable, "$hello"),
+ tSpace,
+ mkItem(itemVariable, "$23"),
+ tSpace,
+ mkItem(itemVariable, "$"),
+ tSpace,
+ mkItem(itemVariable, "$var"),
+ mkItem(itemField, ".Field"),
+ tSpace,
+ mkItem(itemField, ".Method"),
+ tRight,
+ tEOF,
+ }},
+ {"variable invocation", "{{$x 23}}", []item{
+ tLeft,
+ mkItem(itemVariable, "$x"),
+ tSpace,
+ mkItem(itemNumber, "23"),
+ tRight,
+ tEOF,
+ }},
+ {"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
+ mkItem(itemText, "intro "),
+ tLeft,
+ mkItem(itemIdentifier, "echo"),
+ tSpace,
+ mkItem(itemIdentifier, "hi"),
+ tSpace,
+ mkItem(itemNumber, "1.2"),
+ tSpace,
+ tPipe,
+ mkItem(itemIdentifier, "noargs"),
+ tPipe,
+ mkItem(itemIdentifier, "args"),
+ tSpace,
+ mkItem(itemNumber, "1"),
+ tSpace,
+ mkItem(itemString, `"hi"`),
+ tRight,
+ mkItem(itemText, " outro"),
+ tEOF,
+ }},
+ {"declaration", "{{$v := 3}}", []item{
+ tLeft,
+ mkItem(itemVariable, "$v"),
+ tSpace,
+ mkItem(itemDeclare, ":="),
+ tSpace,
+ mkItem(itemNumber, "3"),
+ tRight,
+ tEOF,
+ }},
+ {"2 declarations", "{{$v , $w := 3}}", []item{
+ tLeft,
+ mkItem(itemVariable, "$v"),
+ tSpace,
+ mkItem(itemChar, ","),
+ tSpace,
+ mkItem(itemVariable, "$w"),
+ tSpace,
+ mkItem(itemDeclare, ":="),
+ tSpace,
+ mkItem(itemNumber, "3"),
+ tRight,
+ tEOF,
+ }},
+ {"field of parenthesized expression", "{{(.X).Y}}", []item{
+ tLeft,
+ tLpar,
+ mkItem(itemField, ".X"),
+ tRpar,
+ mkItem(itemField, ".Y"),
+ tRight,
+ tEOF,
+ }},
+ {"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
+ mkItem(itemText, "hello-"),
+ tLeft,
+ mkItem(itemNumber, "3"),
+ tRight,
+ mkItem(itemText, "-world"),
+ tEOF,
+ }},
+ {"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
+ mkItem(itemText, "hello-"),
+ mkItem(itemComment, "/* hello */"),
+ mkItem(itemText, "-world"),
+ tEOF,
+ }},
+ // errors
+ {"badchar", "#{{\x01}}", []item{
+ mkItem(itemText, "#"),
+ tLeft,
+ mkItem(itemError, "unrecognized character in action: U+0001"),
+ }},
+ {"unclosed action", "{{", []item{
+ tLeft,
+ mkItem(itemError, "unclosed action"),
+ }},
+ {"EOF in action", "{{range", []item{
+ tLeft,
+ tRange,
+ mkItem(itemError, "unclosed action"),
+ }},
+ {"unclosed quote", "{{\"\n\"}}", []item{
+ tLeft,
+ mkItem(itemError, "unterminated quoted string"),
+ }},
+ {"unclosed raw quote", "{{`xx}}", []item{
+ tLeft,
+ mkItem(itemError, "unterminated raw quoted string"),
+ }},
+ {"unclosed char constant", "{{'\n}}", []item{
+ tLeft,
+ mkItem(itemError, "unterminated character constant"),
+ }},
+ {"bad number", "{{3k}}", []item{
+ tLeft,
+ mkItem(itemError, `bad number syntax: "3k"`),
+ }},
+ {"unclosed paren", "{{(3}}", []item{
+ tLeft,
+ tLpar,
+ mkItem(itemNumber, "3"),
+ mkItem(itemError, `unclosed left paren`),
+ }},
+ {"extra right paren", "{{3)}}", []item{
+ tLeft,
+ mkItem(itemNumber, "3"),
+ tRpar,
+ mkItem(itemError, `unexpected right paren U+0029 ')'`),
+ }},
+
+ // Fixed bugs
+ // Many elements in an action blew the lookahead until
+ // we made lexInsideAction not loop.
+ {"long pipeline deadlock", "{{|||||}}", []item{
+ tLeft,
+ tPipe,
+ tPipe,
+ tPipe,
+ tPipe,
+ tPipe,
+ tRight,
+ tEOF,
+ }},
+ {"text with bad comment", "hello-{{/*/}}-world", []item{
+ mkItem(itemText, "hello-"),
+ mkItem(itemError, `unclosed comment`),
+ }},
+ {"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
+ mkItem(itemText, "hello-"),
+ mkItem(itemError, `comment ends before closing delimiter`),
+ }},
+ // This one is an error that we can't catch because it breaks templates with
+ // minimized JavaScript. Should have fixed it before Go 1.1.
+ {"unmatched right delimiter", "hello-{.}}-world", []item{
+ mkItem(itemText, "hello-{.}}-world"),
+ tEOF,
+ }},
+}
+
+// collect gathers the emitted items into a slice.
+func collect(t *lexTest, left, right string) (items []item) {
+ l := lex(t.name, t.input, left, right, true)
+ for {
+ item := l.nextItem()
+ items = append(items, item)
+ if item.typ == itemEOF || item.typ == itemError {
+ break
+ }
+ }
+ return
+}
+
+func equal(i1, i2 []item, checkPos bool) bool {
+ if len(i1) != len(i2) {
+ return false
+ }
+ for k := range i1 {
+ if i1[k].typ != i2[k].typ {
+ return false
+ }
+ if i1[k].val != i2[k].val {
+ return false
+ }
+ if checkPos && i1[k].pos != i2[k].pos {
+ return false
+ }
+ if checkPos && i1[k].line != i2[k].line {
+ return false
+ }
+ }
+ return true
+}
+
+func TestLex(t *testing.T) {
+ for _, test := range lexTests {
+ items := collect(&test, "", "")
+ if !equal(items, test.items, false) {
+ t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
+ }
+ }
+}
+
+// Some easy cases from above, but with delimiters $$ and @@
+var lexDelimTests = []lexTest{
+ {"punctuation", "$$,@%{{}}@@", []item{
+ tLeftDelim,
+ mkItem(itemChar, ","),
+ mkItem(itemChar, "@"),
+ mkItem(itemChar, "%"),
+ mkItem(itemChar, "{"),
+ mkItem(itemChar, "{"),
+ mkItem(itemChar, "}"),
+ mkItem(itemChar, "}"),
+ tRightDelim,
+ tEOF,
+ }},
+ {"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
+ {"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
+ {"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
+ {"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
+}
+
+var (
+ tLeftDelim = mkItem(itemLeftDelim, "$$")
+ tRightDelim = mkItem(itemRightDelim, "@@")
+)
+
+func TestDelims(t *testing.T) {
+ for _, test := range lexDelimTests {
+ items := collect(&test, "$$", "@@")
+ if !equal(items, test.items, false) {
+ t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
+ }
+ }
+}
+
+var lexPosTests = []lexTest{
+ {"empty", "", []item{{itemEOF, 0, "", 1}}},
+ {"punctuation", "{{,@%#}}", []item{
+ {itemLeftDelim, 0, "{{", 1},
+ {itemChar, 2, ",", 1},
+ {itemChar, 3, "@", 1},
+ {itemChar, 4, "%", 1},
+ {itemChar, 5, "#", 1},
+ {itemRightDelim, 6, "}}", 1},
+ {itemEOF, 8, "", 1},
+ }},
+ {"sample", "0123{{hello}}xyz", []item{
+ {itemText, 0, "0123", 1},
+ {itemLeftDelim, 4, "{{", 1},
+ {itemIdentifier, 6, "hello", 1},
+ {itemRightDelim, 11, "}}", 1},
+ {itemText, 13, "xyz", 1},
+ {itemEOF, 16, "", 1},
+ }},
+ {"trimafter", "{{x -}}\n{{y}}", []item{
+ {itemLeftDelim, 0, "{{", 1},
+ {itemIdentifier, 2, "x", 1},
+ {itemRightDelim, 5, "}}", 1},
+ {itemLeftDelim, 8, "{{", 2},
+ {itemIdentifier, 10, "y", 2},
+ {itemRightDelim, 11, "}}", 2},
+ {itemEOF, 13, "", 2},
+ }},
+ {"trimbefore", "{{x}}\n{{- y}}", []item{
+ {itemLeftDelim, 0, "{{", 1},
+ {itemIdentifier, 2, "x", 1},
+ {itemRightDelim, 3, "}}", 1},
+ {itemLeftDelim, 6, "{{", 2},
+ {itemIdentifier, 10, "y", 2},
+ {itemRightDelim, 11, "}}", 2},
+ {itemEOF, 13, "", 2},
+ }},
+}
+
+// The other tests don't check position, to make the test cases easier to construct.
+// This one does.
+func TestPos(t *testing.T) {
+ for _, test := range lexPosTests {
+ items := collect(&test, "", "")
+ if !equal(items, test.items, true) {
+ t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
+ if len(items) == len(test.items) {
+ // Detailed print; avoid item.String() to expose the position value.
+ for i := range items {
+ if !equal(items[i:i+1], test.items[i:i+1], true) {
+ i1 := items[i]
+ i2 := test.items[i]
+ t.Errorf("\t#%d: got {%v %d %q %d} expected {%v %d %q %d}",
+ i, i1.typ, i1.pos, i1.val, i1.line, i2.typ, i2.pos, i2.val, i2.line)
+ }
+ }
+ }
+ }
+ }
+}
+
+// Test that an error shuts down the lexing goroutine.
+func TestShutdown(t *testing.T) {
+ // We need to duplicate template.Parse here to hold on to the lexer.
+ const text = "erroneous{{define}}{{else}}1234"
+ lexer := lex("foo", text, "{{", "}}", false)
+ _, err := New("root").parseLexer(lexer)
+ if err == nil {
+ t.Fatalf("expected error")
+ }
+ // The error should have drained the input. Therefore, the lexer should be shut down.
+ token, ok := <-lexer.items
+ if ok {
+ t.Fatalf("input was not drained; got %v", token)
+ }
+}
+
+// parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
+// We expect an error, so the tree set and funcs list are explicitly nil.
+func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
+ defer t.recover(&err)
+ t.ParseName = t.Name
+ t.startParse(nil, lex, map[string]*Tree{})
+ t.parse()
+ t.add()
+ t.stopParse()
+ return t, nil
+}