github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/tpl/internal/go_templates/texttemplate/parse/lex_test.go (about)

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.13
     6  // +build go1.13
     7  
     8  package parse
     9  
    10  import (
    11  	"fmt"
    12  	"testing"
    13  )
    14  
    15  // Make the types prettyprint.
    16  var itemName = map[itemType]string{
    17  	itemError:        "error",
    18  	itemBool:         "bool",
    19  	itemChar:         "char",
    20  	itemCharConstant: "charconst",
    21  	itemComment:      "comment",
    22  	itemComplex:      "complex",
    23  	itemDeclare:      ":=",
    24  	itemEOF:          "EOF",
    25  	itemField:        "field",
    26  	itemIdentifier:   "identifier",
    27  	itemLeftDelim:    "left delim",
    28  	itemLeftParen:    "(",
    29  	itemNumber:       "number",
    30  	itemPipe:         "pipe",
    31  	itemRawString:    "raw string",
    32  	itemRightDelim:   "right delim",
    33  	itemRightParen:   ")",
    34  	itemSpace:        "space",
    35  	itemString:       "string",
    36  	itemVariable:     "variable",
    37  
    38  	// keywords
    39  	itemDot:      ".",
    40  	itemBlock:    "block",
    41  	itemBreak:    "break",
    42  	itemContinue: "continue",
    43  	itemDefine:   "define",
    44  	itemElse:     "else",
    45  	itemIf:       "if",
    46  	itemEnd:      "end",
    47  	itemNil:      "nil",
    48  	itemRange:    "range",
    49  	itemTemplate: "template",
    50  	itemWith:     "with",
    51  }
    52  
    53  func (i itemType) String() string {
    54  	s := itemName[i]
    55  	if s == "" {
    56  		return fmt.Sprintf("item%d", int(i))
    57  	}
    58  	return s
    59  }
    60  
    61  type lexTest struct {
    62  	name  string
    63  	input string
    64  	items []item
    65  }
    66  
    67  func mkItem(typ itemType, text string) item {
    68  	return item{
    69  		typ: typ,
    70  		val: text,
    71  	}
    72  }
    73  
    74  var (
    75  	tDot        = mkItem(itemDot, ".")
    76  	tBlock      = mkItem(itemBlock, "block")
    77  	tEOF        = mkItem(itemEOF, "")
    78  	tFor        = mkItem(itemIdentifier, "for")
    79  	tLeft       = mkItem(itemLeftDelim, "{{")
    80  	tLpar       = mkItem(itemLeftParen, "(")
    81  	tPipe       = mkItem(itemPipe, "|")
    82  	tQuote      = mkItem(itemString, `"abc \n\t\" "`)
    83  	tRange      = mkItem(itemRange, "range")
    84  	tRight      = mkItem(itemRightDelim, "}}")
    85  	tRpar       = mkItem(itemRightParen, ")")
    86  	tSpace      = mkItem(itemSpace, " ")
    87  	raw         = "`" + `abc\n\t\" ` + "`"
    88  	rawNL       = "`now is{{\n}}the time`" // Contains newline inside raw quote.
    89  	tRawQuote   = mkItem(itemRawString, raw)
    90  	tRawQuoteNL = mkItem(itemRawString, rawNL)
    91  )
    92  
    93  var lexTests = []lexTest{
    94  	{"empty", "", []item{tEOF}},
    95  	{"spaces", " \t\n", []item{mkItem(itemText, " \t\n"), tEOF}},
    96  	{"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
    97  	{"text with comment", "hello-{{/* this is a comment */}}-world", []item{
    98  		mkItem(itemText, "hello-"),
    99  		mkItem(itemComment, "/* this is a comment */"),
   100  		mkItem(itemText, "-world"),
   101  		tEOF,
   102  	}},
   103  	{"punctuation", "{{,@% }}", []item{
   104  		tLeft,
   105  		mkItem(itemChar, ","),
   106  		mkItem(itemChar, "@"),
   107  		mkItem(itemChar, "%"),
   108  		tSpace,
   109  		tRight,
   110  		tEOF,
   111  	}},
   112  	{"parens", "{{((3))}}", []item{
   113  		tLeft,
   114  		tLpar,
   115  		tLpar,
   116  		mkItem(itemNumber, "3"),
   117  		tRpar,
   118  		tRpar,
   119  		tRight,
   120  		tEOF,
   121  	}},
   122  	{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
   123  	{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
   124  	{"block", `{{block "foo" .}}`, []item{
   125  		tLeft, tBlock, tSpace, mkItem(itemString, `"foo"`), tSpace, tDot, tRight, tEOF,
   126  	}},
   127  	{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
   128  	{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
   129  	{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
   130  	{"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{
   131  		tLeft,
   132  		mkItem(itemNumber, "1"),
   133  		tSpace,
   134  		mkItem(itemNumber, "02"),
   135  		tSpace,
   136  		mkItem(itemNumber, "0x14"),
   137  		tSpace,
   138  		mkItem(itemNumber, "0X14"),
   139  		tSpace,
   140  		mkItem(itemNumber, "-7.2i"),
   141  		tSpace,
   142  		mkItem(itemNumber, "1e3"),
   143  		tSpace,
   144  		mkItem(itemNumber, "1E3"),
   145  		tSpace,
   146  		mkItem(itemNumber, "+1.2e-4"),
   147  		tSpace,
   148  		mkItem(itemNumber, "4.2i"),
   149  		tSpace,
   150  		mkItem(itemComplex, "1+2i"),
   151  		tSpace,
   152  		mkItem(itemNumber, "1_2"),
   153  		tSpace,
   154  		mkItem(itemNumber, "0x1.e_fp4"),
   155  		tSpace,
   156  		mkItem(itemNumber, "0X1.E_FP4"),
   157  		tRight,
   158  		tEOF,
   159  	}},
   160  	{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
   161  		tLeft,
   162  		mkItem(itemCharConstant, `'a'`),
   163  		tSpace,
   164  		mkItem(itemCharConstant, `'\n'`),
   165  		tSpace,
   166  		mkItem(itemCharConstant, `'\''`),
   167  		tSpace,
   168  		mkItem(itemCharConstant, `'\\'`),
   169  		tSpace,
   170  		mkItem(itemCharConstant, `'\u00FF'`),
   171  		tSpace,
   172  		mkItem(itemCharConstant, `'\xFF'`),
   173  		tSpace,
   174  		mkItem(itemCharConstant, `'本'`),
   175  		tRight,
   176  		tEOF,
   177  	}},
   178  	{"bools", "{{true false}}", []item{
   179  		tLeft,
   180  		mkItem(itemBool, "true"),
   181  		tSpace,
   182  		mkItem(itemBool, "false"),
   183  		tRight,
   184  		tEOF,
   185  	}},
   186  	{"dot", "{{.}}", []item{
   187  		tLeft,
   188  		tDot,
   189  		tRight,
   190  		tEOF,
   191  	}},
   192  	{"nil", "{{nil}}", []item{
   193  		tLeft,
   194  		mkItem(itemNil, "nil"),
   195  		tRight,
   196  		tEOF,
   197  	}},
   198  	{"dots", "{{.x . .2 .x.y.z}}", []item{
   199  		tLeft,
   200  		mkItem(itemField, ".x"),
   201  		tSpace,
   202  		tDot,
   203  		tSpace,
   204  		mkItem(itemNumber, ".2"),
   205  		tSpace,
   206  		mkItem(itemField, ".x"),
   207  		mkItem(itemField, ".y"),
   208  		mkItem(itemField, ".z"),
   209  		tRight,
   210  		tEOF,
   211  	}},
   212  	{"keywords", "{{range if else end with}}", []item{
   213  		tLeft,
   214  		mkItem(itemRange, "range"),
   215  		tSpace,
   216  		mkItem(itemIf, "if"),
   217  		tSpace,
   218  		mkItem(itemElse, "else"),
   219  		tSpace,
   220  		mkItem(itemEnd, "end"),
   221  		tSpace,
   222  		mkItem(itemWith, "with"),
   223  		tRight,
   224  		tEOF,
   225  	}},
   226  	{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
   227  		tLeft,
   228  		mkItem(itemVariable, "$c"),
   229  		tSpace,
   230  		mkItem(itemDeclare, ":="),
   231  		tSpace,
   232  		mkItem(itemIdentifier, "printf"),
   233  		tSpace,
   234  		mkItem(itemVariable, "$"),
   235  		tSpace,
   236  		mkItem(itemVariable, "$hello"),
   237  		tSpace,
   238  		mkItem(itemVariable, "$23"),
   239  		tSpace,
   240  		mkItem(itemVariable, "$"),
   241  		tSpace,
   242  		mkItem(itemVariable, "$var"),
   243  		mkItem(itemField, ".Field"),
   244  		tSpace,
   245  		mkItem(itemField, ".Method"),
   246  		tRight,
   247  		tEOF,
   248  	}},
   249  	{"variable invocation", "{{$x 23}}", []item{
   250  		tLeft,
   251  		mkItem(itemVariable, "$x"),
   252  		tSpace,
   253  		mkItem(itemNumber, "23"),
   254  		tRight,
   255  		tEOF,
   256  	}},
   257  	{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
   258  		mkItem(itemText, "intro "),
   259  		tLeft,
   260  		mkItem(itemIdentifier, "echo"),
   261  		tSpace,
   262  		mkItem(itemIdentifier, "hi"),
   263  		tSpace,
   264  		mkItem(itemNumber, "1.2"),
   265  		tSpace,
   266  		tPipe,
   267  		mkItem(itemIdentifier, "noargs"),
   268  		tPipe,
   269  		mkItem(itemIdentifier, "args"),
   270  		tSpace,
   271  		mkItem(itemNumber, "1"),
   272  		tSpace,
   273  		mkItem(itemString, `"hi"`),
   274  		tRight,
   275  		mkItem(itemText, " outro"),
   276  		tEOF,
   277  	}},
   278  	{"declaration", "{{$v := 3}}", []item{
   279  		tLeft,
   280  		mkItem(itemVariable, "$v"),
   281  		tSpace,
   282  		mkItem(itemDeclare, ":="),
   283  		tSpace,
   284  		mkItem(itemNumber, "3"),
   285  		tRight,
   286  		tEOF,
   287  	}},
   288  	{"2 declarations", "{{$v , $w := 3}}", []item{
   289  		tLeft,
   290  		mkItem(itemVariable, "$v"),
   291  		tSpace,
   292  		mkItem(itemChar, ","),
   293  		tSpace,
   294  		mkItem(itemVariable, "$w"),
   295  		tSpace,
   296  		mkItem(itemDeclare, ":="),
   297  		tSpace,
   298  		mkItem(itemNumber, "3"),
   299  		tRight,
   300  		tEOF,
   301  	}},
   302  	{"field of parenthesized expression", "{{(.X).Y}}", []item{
   303  		tLeft,
   304  		tLpar,
   305  		mkItem(itemField, ".X"),
   306  		tRpar,
   307  		mkItem(itemField, ".Y"),
   308  		tRight,
   309  		tEOF,
   310  	}},
   311  	{"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
   312  		mkItem(itemText, "hello-"),
   313  		tLeft,
   314  		mkItem(itemNumber, "3"),
   315  		tRight,
   316  		mkItem(itemText, "-world"),
   317  		tEOF,
   318  	}},
   319  	{"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
   320  		mkItem(itemText, "hello-"),
   321  		mkItem(itemComment, "/* hello */"),
   322  		mkItem(itemText, "-world"),
   323  		tEOF,
   324  	}},
   325  	// errors
   326  	{"badchar", "#{{\x01}}", []item{
   327  		mkItem(itemText, "#"),
   328  		tLeft,
   329  		mkItem(itemError, "unrecognized character in action: U+0001"),
   330  	}},
   331  	{"unclosed action", "{{", []item{
   332  		tLeft,
   333  		mkItem(itemError, "unclosed action"),
   334  	}},
   335  	{"EOF in action", "{{range", []item{
   336  		tLeft,
   337  		tRange,
   338  		mkItem(itemError, "unclosed action"),
   339  	}},
   340  	{"unclosed quote", "{{\"\n\"}}", []item{
   341  		tLeft,
   342  		mkItem(itemError, "unterminated quoted string"),
   343  	}},
   344  	{"unclosed raw quote", "{{`xx}}", []item{
   345  		tLeft,
   346  		mkItem(itemError, "unterminated raw quoted string"),
   347  	}},
   348  	{"unclosed char constant", "{{'\n}}", []item{
   349  		tLeft,
   350  		mkItem(itemError, "unterminated character constant"),
   351  	}},
   352  	{"bad number", "{{3k}}", []item{
   353  		tLeft,
   354  		mkItem(itemError, `bad number syntax: "3k"`),
   355  	}},
   356  	{"unclosed paren", "{{(3}}", []item{
   357  		tLeft,
   358  		tLpar,
   359  		mkItem(itemNumber, "3"),
   360  		mkItem(itemError, `unclosed left paren`),
   361  	}},
   362  	{"extra right paren", "{{3)}}", []item{
   363  		tLeft,
   364  		mkItem(itemNumber, "3"),
   365  		mkItem(itemError, "unexpected right paren"),
   366  	}},
   367  
   368  	// Fixed bugs
   369  	// Many elements in an action blew the lookahead until
   370  	// we made lexInsideAction not loop.
   371  	{"long pipeline deadlock", "{{|||||}}", []item{
   372  		tLeft,
   373  		tPipe,
   374  		tPipe,
   375  		tPipe,
   376  		tPipe,
   377  		tPipe,
   378  		tRight,
   379  		tEOF,
   380  	}},
   381  	{"text with bad comment", "hello-{{/*/}}-world", []item{
   382  		mkItem(itemText, "hello-"),
   383  		mkItem(itemError, `unclosed comment`),
   384  	}},
   385  	{"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
   386  		mkItem(itemText, "hello-"),
   387  		mkItem(itemError, `comment ends before closing delimiter`),
   388  	}},
   389  	// This one is an error that we can't catch because it breaks templates with
   390  	// minimized JavaScript. Should have fixed it before Go 1.1.
   391  	{"unmatched right delimiter", "hello-{.}}-world", []item{
   392  		mkItem(itemText, "hello-{.}}-world"),
   393  		tEOF,
   394  	}},
   395  }
   396  
   397  // collect gathers the emitted items into a slice.
   398  func collect(t *lexTest, left, right string) (items []item) {
   399  	l := lex(t.name, t.input, left, right)
   400  	l.options = lexOptions{
   401  		emitComment: true,
   402  		breakOK:     true,
   403  		continueOK:  true,
   404  	}
   405  	for {
   406  		item := l.nextItem()
   407  		items = append(items, item)
   408  		if item.typ == itemEOF || item.typ == itemError {
   409  			break
   410  		}
   411  	}
   412  	return
   413  }
   414  
   415  func equal(i1, i2 []item, checkPos bool) bool {
   416  	if len(i1) != len(i2) {
   417  		return false
   418  	}
   419  	for k := range i1 {
   420  		if i1[k].typ != i2[k].typ {
   421  			return false
   422  		}
   423  		if i1[k].val != i2[k].val {
   424  			return false
   425  		}
   426  		if checkPos && i1[k].pos != i2[k].pos {
   427  			return false
   428  		}
   429  		if checkPos && i1[k].line != i2[k].line {
   430  			return false
   431  		}
   432  	}
   433  	return true
   434  }
   435  
   436  func TestLex(t *testing.T) {
   437  	for _, test := range lexTests {
   438  		items := collect(&test, "", "")
   439  		if !equal(items, test.items, false) {
   440  			t.Errorf("%s: got\n\t%+v\nexpected\n\t%v", test.name, items, test.items)
   441  			return // TODO
   442  		}
   443  		t.Log(test.name, "OK")
   444  	}
   445  }
   446  
   447  // Some easy cases from above, but with delimiters $$ and @@
   448  var lexDelimTests = []lexTest{
   449  	{"punctuation", "$$,@%{{}}@@", []item{
   450  		tLeftDelim,
   451  		mkItem(itemChar, ","),
   452  		mkItem(itemChar, "@"),
   453  		mkItem(itemChar, "%"),
   454  		mkItem(itemChar, "{"),
   455  		mkItem(itemChar, "{"),
   456  		mkItem(itemChar, "}"),
   457  		mkItem(itemChar, "}"),
   458  		tRightDelim,
   459  		tEOF,
   460  	}},
   461  	{"empty action", `$$@@`, []item{tLeftDelim, tRightDelim, tEOF}},
   462  	{"for", `$$for@@`, []item{tLeftDelim, tFor, tRightDelim, tEOF}},
   463  	{"quote", `$$"abc \n\t\" "@@`, []item{tLeftDelim, tQuote, tRightDelim, tEOF}},
   464  	{"raw quote", "$$" + raw + "@@", []item{tLeftDelim, tRawQuote, tRightDelim, tEOF}},
   465  }
   466  
   467  var (
   468  	tLeftDelim  = mkItem(itemLeftDelim, "$$")
   469  	tRightDelim = mkItem(itemRightDelim, "@@")
   470  )
   471  
   472  func TestDelims(t *testing.T) {
   473  	for _, test := range lexDelimTests {
   474  		items := collect(&test, "$$", "@@")
   475  		if !equal(items, test.items, false) {
   476  			t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
   477  		}
   478  	}
   479  }
   480  
   481  func TestDelimsAlphaNumeric(t *testing.T) {
   482  	test := lexTest{"right delimiter with alphanumeric start", "{{hub .host hub}}", []item{
   483  		mkItem(itemLeftDelim, "{{hub"),
   484  		mkItem(itemSpace, " "),
   485  		mkItem(itemField, ".host"),
   486  		mkItem(itemSpace, " "),
   487  		mkItem(itemRightDelim, "hub}}"),
   488  		tEOF,
   489  	}}
   490  	items := collect(&test, "{{hub", "hub}}")
   491  
   492  	if !equal(items, test.items, false) {
   493  		t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
   494  	}
   495  }
   496  
   497  func TestDelimsAndMarkers(t *testing.T) {
   498  	test := lexTest{"delims that look like markers", "{{- .x -}} {{- - .x - -}}", []item{
   499  		mkItem(itemLeftDelim, "{{- "),
   500  		mkItem(itemField, ".x"),
   501  		mkItem(itemRightDelim, " -}}"),
   502  		mkItem(itemLeftDelim, "{{- "),
   503  		mkItem(itemField, ".x"),
   504  		mkItem(itemRightDelim, " -}}"),
   505  		tEOF,
   506  	}}
   507  	items := collect(&test, "{{- ", " -}}")
   508  
   509  	if !equal(items, test.items, false) {
   510  		t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
   511  	}
   512  }
   513  
   514  var lexPosTests = []lexTest{
   515  	{"empty", "", []item{{itemEOF, 0, "", 1}}},
   516  	{"punctuation", "{{,@%#}}", []item{
   517  		{itemLeftDelim, 0, "{{", 1},
   518  		{itemChar, 2, ",", 1},
   519  		{itemChar, 3, "@", 1},
   520  		{itemChar, 4, "%", 1},
   521  		{itemChar, 5, "#", 1},
   522  		{itemRightDelim, 6, "}}", 1},
   523  		{itemEOF, 8, "", 1},
   524  	}},
   525  	{"sample", "0123{{hello}}xyz", []item{
   526  		{itemText, 0, "0123", 1},
   527  		{itemLeftDelim, 4, "{{", 1},
   528  		{itemIdentifier, 6, "hello", 1},
   529  		{itemRightDelim, 11, "}}", 1},
   530  		{itemText, 13, "xyz", 1},
   531  		{itemEOF, 16, "", 1},
   532  	}},
   533  	{"trimafter", "{{x -}}\n{{y}}", []item{
   534  		{itemLeftDelim, 0, "{{", 1},
   535  		{itemIdentifier, 2, "x", 1},
   536  		{itemRightDelim, 5, "}}", 1},
   537  		{itemLeftDelim, 8, "{{", 2},
   538  		{itemIdentifier, 10, "y", 2},
   539  		{itemRightDelim, 11, "}}", 2},
   540  		{itemEOF, 13, "", 2},
   541  	}},
   542  	{"trimbefore", "{{x}}\n{{- y}}", []item{
   543  		{itemLeftDelim, 0, "{{", 1},
   544  		{itemIdentifier, 2, "x", 1},
   545  		{itemRightDelim, 3, "}}", 1},
   546  		{itemLeftDelim, 6, "{{", 2},
   547  		{itemIdentifier, 10, "y", 2},
   548  		{itemRightDelim, 11, "}}", 2},
   549  		{itemEOF, 13, "", 2},
   550  	}},
   551  }
   552  
   553  // The other tests don't check position, to make the test cases easier to construct.
   554  // This one does.
   555  func TestPos(t *testing.T) {
   556  	for _, test := range lexPosTests {
   557  		items := collect(&test, "", "")
   558  		if !equal(items, test.items, true) {
   559  			t.Errorf("%s: got\n\t%v\nexpected\n\t%v", test.name, items, test.items)
   560  			if len(items) == len(test.items) {
   561  				// Detailed print; avoid item.String() to expose the position value.
   562  				for i := range items {
   563  					if !equal(items[i:i+1], test.items[i:i+1], true) {
   564  						i1 := items[i]
   565  						i2 := test.items[i]
   566  						t.Errorf("\t#%d: got {%v %d %q %d} expected {%v %d %q %d}",
   567  							i, i1.typ, i1.pos, i1.val, i1.line, i2.typ, i2.pos, i2.val, i2.line)
   568  					}
   569  				}
   570  			}
   571  		}
   572  	}
   573  }
   574  
   575  // parseLexer is a local version of parse that lets us pass in the lexer instead of building it.
   576  // We expect an error, so the tree set and funcs list are explicitly nil.
   577  func (t *Tree) parseLexer(lex *lexer) (tree *Tree, err error) {
   578  	defer t.recover(&err)
   579  	t.ParseName = t.Name
   580  	t.startParse(nil, lex, map[string]*Tree{})
   581  	t.parse()
   582  	t.add()
   583  	t.stopParse()
   584  	return t, nil
   585  }