469 lines
10 KiB
Go
469 lines
10 KiB
Go
|
// 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",
|
||
|
itemComplex: "complex",
|
||
|
itemColonEquals: ":=",
|
||
|
itemEOF: "EOF",
|
||
|
itemField: "field",
|
||
|
itemIdentifier: "identifier",
|
||
|
itemLeftDelim: "left delim",
|
||
|
itemLeftParen: "(",
|
||
|
itemNumber: "number",
|
||
|
itemPipe: "pipe",
|
||
|
itemRawString: "raw string",
|
||
|
itemRightDelim: "right delim",
|
||
|
itemElideNewline: "elide newline",
|
||
|
itemRightParen: ")",
|
||
|
itemSpace: "space",
|
||
|
itemString: "string",
|
||
|
itemVariable: "variable",
|
||
|
|
||
|
// keywords
|
||
|
itemDot: ".",
|
||
|
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
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
tEOF = item{itemEOF, 0, ""}
|
||
|
tFor = item{itemIdentifier, 0, "for"}
|
||
|
tLeft = item{itemLeftDelim, 0, "{{"}
|
||
|
tLpar = item{itemLeftParen, 0, "("}
|
||
|
tPipe = item{itemPipe, 0, "|"}
|
||
|
tQuote = item{itemString, 0, `"abc \n\t\" "`}
|
||
|
tRange = item{itemRange, 0, "range"}
|
||
|
tRight = item{itemRightDelim, 0, "}}"}
|
||
|
tElideNewline = item{itemElideNewline, 0, "\\"}
|
||
|
tRpar = item{itemRightParen, 0, ")"}
|
||
|
tSpace = item{itemSpace, 0, " "}
|
||
|
raw = "`" + `abc\n\t\" ` + "`"
|
||
|
tRawQuote = item{itemRawString, 0, raw}
|
||
|
)
|
||
|
|
||
|
var lexTests = []lexTest{
|
||
|
{"empty", "", []item{tEOF}},
|
||
|
{"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}},
|
||
|
{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},
|
||
|
{"elide newline", "{{}}\\", []item{tLeft, tRight, tElideNewline, tEOF}},
|
||
|
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
|
||
|
{itemText, 0, "hello-"},
|
||
|
{itemText, 0, "-world"},
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"punctuation", "{{,@% }}", []item{
|
||
|
tLeft,
|
||
|
{itemChar, 0, ","},
|
||
|
{itemChar, 0, "@"},
|
||
|
{itemChar, 0, "%"},
|
||
|
tSpace,
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"parens", "{{((3))}}", []item{
|
||
|
tLeft,
|
||
|
tLpar,
|
||
|
tLpar,
|
||
|
{itemNumber, 0, "3"},
|
||
|
tRpar,
|
||
|
tRpar,
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
|
||
|
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
|
||
|
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
|
||
|
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
|
||
|
{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
|
||
|
tLeft,
|
||
|
{itemNumber, 0, "1"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "02"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "0x14"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "-7.2i"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "1e3"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "+1.2e-4"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "4.2i"},
|
||
|
tSpace,
|
||
|
{itemComplex, 0, "1+2i"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
|
||
|
tLeft,
|
||
|
{itemCharConstant, 0, `'a'`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'\n'`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'\''`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'\\'`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'\u00FF'`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'\xFF'`},
|
||
|
tSpace,
|
||
|
{itemCharConstant, 0, `'本'`},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"bools", "{{true false}}", []item{
|
||
|
tLeft,
|
||
|
{itemBool, 0, "true"},
|
||
|
tSpace,
|
||
|
{itemBool, 0, "false"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"dot", "{{.}}", []item{
|
||
|
tLeft,
|
||
|
{itemDot, 0, "."},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"nil", "{{nil}}", []item{
|
||
|
tLeft,
|
||
|
{itemNil, 0, "nil"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"dots", "{{.x . .2 .x.y.z}}", []item{
|
||
|
tLeft,
|
||
|
{itemField, 0, ".x"},
|
||
|
tSpace,
|
||
|
{itemDot, 0, "."},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, ".2"},
|
||
|
tSpace,
|
||
|
{itemField, 0, ".x"},
|
||
|
{itemField, 0, ".y"},
|
||
|
{itemField, 0, ".z"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"keywords", "{{range if else end with}}", []item{
|
||
|
tLeft,
|
||
|
{itemRange, 0, "range"},
|
||
|
tSpace,
|
||
|
{itemIf, 0, "if"},
|
||
|
tSpace,
|
||
|
{itemElse, 0, "else"},
|
||
|
tSpace,
|
||
|
{itemEnd, 0, "end"},
|
||
|
tSpace,
|
||
|
{itemWith, 0, "with"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
|
||
|
tLeft,
|
||
|
{itemVariable, 0, "$c"},
|
||
|
tSpace,
|
||
|
{itemColonEquals, 0, ":="},
|
||
|
tSpace,
|
||
|
{itemIdentifier, 0, "printf"},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$"},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$hello"},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$23"},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$"},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$var"},
|
||
|
{itemField, 0, ".Field"},
|
||
|
tSpace,
|
||
|
{itemField, 0, ".Method"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"variable invocation", "{{$x 23}}", []item{
|
||
|
tLeft,
|
||
|
{itemVariable, 0, "$x"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "23"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
|
||
|
{itemText, 0, "intro "},
|
||
|
tLeft,
|
||
|
{itemIdentifier, 0, "echo"},
|
||
|
tSpace,
|
||
|
{itemIdentifier, 0, "hi"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "1.2"},
|
||
|
tSpace,
|
||
|
tPipe,
|
||
|
{itemIdentifier, 0, "noargs"},
|
||
|
tPipe,
|
||
|
{itemIdentifier, 0, "args"},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "1"},
|
||
|
tSpace,
|
||
|
{itemString, 0, `"hi"`},
|
||
|
tRight,
|
||
|
{itemText, 0, " outro"},
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"declaration", "{{$v := 3}}", []item{
|
||
|
tLeft,
|
||
|
{itemVariable, 0, "$v"},
|
||
|
tSpace,
|
||
|
{itemColonEquals, 0, ":="},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "3"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"2 declarations", "{{$v , $w := 3}}", []item{
|
||
|
tLeft,
|
||
|
{itemVariable, 0, "$v"},
|
||
|
tSpace,
|
||
|
{itemChar, 0, ","},
|
||
|
tSpace,
|
||
|
{itemVariable, 0, "$w"},
|
||
|
tSpace,
|
||
|
{itemColonEquals, 0, ":="},
|
||
|
tSpace,
|
||
|
{itemNumber, 0, "3"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
{"field of parenthesized expression", "{{(.X).Y}}", []item{
|
||
|
tLeft,
|
||
|
tLpar,
|
||
|
{itemField, 0, ".X"},
|
||
|
tRpar,
|
||
|
{itemField, 0, ".Y"},
|
||
|
tRight,
|
||
|
tEOF,
|
||
|
}},
|
||
|
// errors
|
||
|
{"badchar", "#{{\x01}}", []item{
|
||
|
{itemText, 0, "#"},
|
||
|
tLeft,
|
||
|
{itemError, 0, "unrecognized character in action: U+0001"},
|
||
|
}},
|
||
|
{"unclosed action", "{{\n}}", []item{
|
||
|
tLeft,
|
||
|
{itemError, 0, "unclosed action"},
|
||
|
}},
|
||
|
{"EOF in action", "{{range", []item{
|
||
|
tLeft,
|
||
|
tRange,
|
||
|
{itemError, 0, "unclosed action"},
|
||
|
}},
|
||
|
{"unclosed quote", "{{\"\n\"}}", []item{
|
||
|
tLeft,
|
||
|
{itemError, 0, "unterminated quoted string"},
|
||
|
}},
|
||
|
{"unclosed raw quote", "{{`xx\n`}}", []item{
|
||
|
tLeft,
|
||
|
{itemError, 0, "unterminated raw quoted string"},
|
||
|
}},
|
||
|
{"unclosed char constant", "{{'\n}}", []item{
|
||
|
tLeft,
|
||
|
{itemError, 0, "unterminated character constant"},
|
||
|
}},
|
||
|
{"bad number", "{{3k}}", []item{
|
||
|
tLeft,
|
||
|
{itemError, 0, `bad number syntax: "3k"`},
|
||
|
}},
|
||
|
{"unclosed paren", "{{(3}}", []item{
|
||
|
tLeft,
|
||
|
tLpar,
|
||
|
{itemNumber, 0, "3"},
|
||
|
{itemError, 0, `unclosed left paren`},
|
||
|
}},
|
||
|
{"extra right paren", "{{3)}}", []item{
|
||
|
tLeft,
|
||
|
{itemNumber, 0, "3"},
|
||
|
tRpar,
|
||
|
{itemError, 0, `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{
|
||
|
{itemText, 0, "hello-"},
|
||
|
{itemError, 0, `unclosed comment`},
|
||
|
}},
|
||
|
{"text with comment close separted from delim", "hello-{{/* */ }}-world", []item{
|
||
|
{itemText, 0, "hello-"},
|
||
|
{itemError, 0, `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{
|
||
|
{itemText, 0, "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)
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
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,
|
||
|
{itemChar, 0, ","},
|
||
|
{itemChar, 0, "@"},
|
||
|
{itemChar, 0, "%"},
|
||
|
{itemChar, 0, "{"},
|
||
|
{itemChar, 0, "{"},
|
||
|
{itemChar, 0, "}"},
|
||
|
{itemChar, 0, "}"},
|
||
|
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 = item{itemLeftDelim, 0, "$$"}
|
||
|
tRightDelim = item{itemRightDelim, 0, "@@"}
|
||
|
)
|
||
|
|
||
|
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{tEOF}},
|
||
|
{"punctuation", "{{,@%#}}", []item{
|
||
|
{itemLeftDelim, 0, "{{"},
|
||
|
{itemChar, 2, ","},
|
||
|
{itemChar, 3, "@"},
|
||
|
{itemChar, 4, "%"},
|
||
|
{itemChar, 5, "#"},
|
||
|
{itemRightDelim, 6, "}}"},
|
||
|
{itemEOF, 8, ""},
|
||
|
}},
|
||
|
{"sample", "0123{{hello}}xyz", []item{
|
||
|
{itemText, 0, "0123"},
|
||
|
{itemLeftDelim, 4, "{{"},
|
||
|
{itemIdentifier, 6, "hello"},
|
||
|
{itemRightDelim, 11, "}}"},
|
||
|
{itemText, 13, "xyz"},
|
||
|
{itemEOF, 16, ""},
|
||
|
}},
|
||
|
}
|
||
|
|
||
|
// 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} expected {%v %d %q}", i, i1.typ, i1.pos, i1.val, i2.typ, i2.pos, i2.val)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|