Commit 24a088d2 authored by Rob Pike's avatar Rob Pike

text/template: efficient reporting of line numbers

Instead of scanning the text to count newlines, which is n², keep track as we go
and store the line number in the token.

benchmark                 old ns/op      new ns/op     delta
BenchmarkParseLarge-4     1589721293     38783310      -97.56%

Fixes #17851

Change-Id: I231225c61e667535e2ce55cd2facea6d279cc59d
Reviewed-on: https://go-review.googlesource.com/33234
Run-TryBot: Rob Pike <r@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
parent bb00a8d9
...@@ -1152,7 +1152,7 @@ func TestUnterminatedStringError(t *testing.T) { ...@@ -1152,7 +1152,7 @@ func TestUnterminatedStringError(t *testing.T) {
t.Fatal("expected error") t.Fatal("expected error")
} }
str := err.Error() str := err.Error()
if !strings.Contains(str, "X:3: unexpected unterminated raw quoted strin") { if !strings.Contains(str, "X:3: unexpected unterminated raw quoted string") {
t.Fatalf("unexpected error: %s", str) t.Fatalf("unexpected error: %s", str)
} }
} }
......
...@@ -13,9 +13,10 @@ import ( ...@@ -13,9 +13,10 @@ import (
// item represents a token or text string returned from the scanner. // item represents a token or text string returned from the scanner.
type item struct { type item struct {
typ itemType // The type of this item. typ itemType // The type of this item.
pos Pos // The starting position, in bytes, of this item in the input string. pos Pos // The starting position, in bytes, of this item in the input string.
val string // The value of this item. val string // The value of this item.
line int // The line number at the start of this item.
} }
func (i item) String() string { func (i item) String() string {
...@@ -116,6 +117,7 @@ type lexer struct { ...@@ -116,6 +117,7 @@ type lexer struct {
lastPos Pos // position of most recent item returned by nextItem lastPos Pos // position of most recent item returned by nextItem
items chan item // channel of scanned items items chan item // channel of scanned items
parenDepth int // nesting depth of ( ) exprs parenDepth int // nesting depth of ( ) exprs
line int // 1+number of newlines seen
} }
// next returns the next rune in the input. // next returns the next rune in the input.
...@@ -127,6 +129,9 @@ func (l *lexer) next() rune { ...@@ -127,6 +129,9 @@ func (l *lexer) next() rune {
r, w := utf8.DecodeRuneInString(l.input[l.pos:]) r, w := utf8.DecodeRuneInString(l.input[l.pos:])
l.width = Pos(w) l.width = Pos(w)
l.pos += l.width l.pos += l.width
if r == '\n' {
l.line++
}
return r return r
} }
...@@ -140,11 +145,20 @@ func (l *lexer) peek() rune { ...@@ -140,11 +145,20 @@ func (l *lexer) peek() rune {
// backup steps back one rune. Can only be called once per call of next. // backup steps back one rune. Can only be called once per call of next.
func (l *lexer) backup() { func (l *lexer) backup() {
l.pos -= l.width l.pos -= l.width
// Correct newline count.
if l.width == 1 && l.input[l.pos] == '\n' {
l.line--
}
} }
// emit passes an item back to the client. // emit passes an item back to the client.
func (l *lexer) emit(t itemType) { func (l *lexer) emit(t itemType) {
l.items <- item{t, l.start, l.input[l.start:l.pos]} l.items <- item{t, l.start, l.input[l.start:l.pos], l.line}
// Some items contain text internally. If so, count their newlines.
switch t {
case itemText, itemRawString, itemLeftDelim, itemRightDelim:
l.line += strings.Count(l.input[l.start:l.pos], "\n")
}
l.start = l.pos l.start = l.pos
} }
...@@ -169,17 +183,10 @@ func (l *lexer) acceptRun(valid string) { ...@@ -169,17 +183,10 @@ func (l *lexer) acceptRun(valid string) {
l.backup() l.backup()
} }
// lineNumber reports which line we're on, based on the position of
// the previous item returned by nextItem. Doing it this way
// means we don't have to worry about peek double counting.
func (l *lexer) lineNumber() int {
return 1 + strings.Count(l.input[:l.lastPos], "\n")
}
// errorf returns an error token and terminates the scan by passing // errorf returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem. // back a nil pointer that will be the next state, terminating l.nextItem.
func (l *lexer) errorf(format string, args ...interface{}) stateFn { func (l *lexer) errorf(format string, args ...interface{}) stateFn {
l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} l.items <- item{itemError, l.start, fmt.Sprintf(format, args...), l.line}
return nil return nil
} }
...@@ -212,6 +219,7 @@ func lex(name, input, left, right string) *lexer { ...@@ -212,6 +219,7 @@ func lex(name, input, left, right string) *lexer {
leftDelim: left, leftDelim: left,
rightDelim: right, rightDelim: right,
items: make(chan item), items: make(chan item),
line: 1,
} }
go l.run() go l.run()
return l return l
...@@ -602,10 +610,14 @@ Loop: ...@@ -602,10 +610,14 @@ Loop:
// lexRawQuote scans a raw quoted string. // lexRawQuote scans a raw quoted string.
func lexRawQuote(l *lexer) stateFn { func lexRawQuote(l *lexer) stateFn {
startLine := l.line
Loop: Loop:
for { for {
switch l.next() { switch l.next() {
case eof: case eof:
// Restore line number to location of opening quote.
// We will error out so it's ok just to overwrite the field.
l.line = startLine
return l.errorf("unterminated raw quoted string") return l.errorf("unterminated raw quoted string")
case '`': case '`':
break Loop break Loop
......
...@@ -58,39 +58,46 @@ type lexTest struct { ...@@ -58,39 +58,46 @@ type lexTest struct {
items []item items []item
} }
func mkItem(typ itemType, text string) item {
return item{
typ: typ,
val: text,
}
}
var ( var (
tDot = item{itemDot, 0, "."} tDot = mkItem(itemDot, ".")
tBlock = item{itemBlock, 0, "block"} tBlock = mkItem(itemBlock, "block")
tEOF = item{itemEOF, 0, ""} tEOF = mkItem(itemEOF, "")
tFor = item{itemIdentifier, 0, "for"} tFor = mkItem(itemIdentifier, "for")
tLeft = item{itemLeftDelim, 0, "{{"} tLeft = mkItem(itemLeftDelim, "{{")
tLpar = item{itemLeftParen, 0, "("} tLpar = mkItem(itemLeftParen, "(")
tPipe = item{itemPipe, 0, "|"} tPipe = mkItem(itemPipe, "|")
tQuote = item{itemString, 0, `"abc \n\t\" "`} tQuote = mkItem(itemString, `"abc \n\t\" "`)
tRange = item{itemRange, 0, "range"} tRange = mkItem(itemRange, "range")
tRight = item{itemRightDelim, 0, "}}"} tRight = mkItem(itemRightDelim, "}}")
tRpar = item{itemRightParen, 0, ")"} tRpar = mkItem(itemRightParen, ")")
tSpace = item{itemSpace, 0, " "} tSpace = mkItem(itemSpace, " ")
raw = "`" + `abc\n\t\" ` + "`" raw = "`" + `abc\n\t\" ` + "`"
rawNL = "`now is{{\n}}the time`" // Contains newline inside raw quote. rawNL = "`now is{{\n}}the time`" // Contains newline inside raw quote.
tRawQuote = item{itemRawString, 0, raw} tRawQuote = mkItem(itemRawString, raw)
tRawQuoteNL = item{itemRawString, 0, rawNL} tRawQuoteNL = mkItem(itemRawString, rawNL)
) )
var lexTests = []lexTest{ var lexTests = []lexTest{
{"empty", "", []item{tEOF}}, {"empty", "", []item{tEOF}},
{"spaces", " \t\n", []item{{itemText, 0, " \t\n"}, tEOF}}, {"spaces", " \t\n", []item{mkItem(itemText, " \t\n"), tEOF}},
{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}}, {"text", `now is the time`, []item{mkItem(itemText, "now is the time"), tEOF}},
{"text with comment", "hello-{{/* this is a comment */}}-world", []item{ {"text with comment", "hello-{{/* this is a comment */}}-world", []item{
{itemText, 0, "hello-"}, mkItem(itemText, "hello-"),
{itemText, 0, "-world"}, mkItem(itemText, "-world"),
tEOF, tEOF,
}}, }},
{"punctuation", "{{,@% }}", []item{ {"punctuation", "{{,@% }}", []item{
tLeft, tLeft,
{itemChar, 0, ","}, mkItem(itemChar, ","),
{itemChar, 0, "@"}, mkItem(itemChar, "@"),
{itemChar, 0, "%"}, mkItem(itemChar, "%"),
tSpace, tSpace,
tRight, tRight,
tEOF, tEOF,
...@@ -99,7 +106,7 @@ var lexTests = []lexTest{ ...@@ -99,7 +106,7 @@ var lexTests = []lexTest{
tLeft, tLeft,
tLpar, tLpar,
tLpar, tLpar,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
tRpar, tRpar,
tRpar, tRpar,
tRight, tRight,
...@@ -108,54 +115,54 @@ var lexTests = []lexTest{ ...@@ -108,54 +115,54 @@ var lexTests = []lexTest{
{"empty action", `{{}}`, []item{tLeft, tRight, tEOF}}, {"empty action", `{{}}`, []item{tLeft, tRight, tEOF}},
{"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}}, {"for", `{{for}}`, []item{tLeft, tFor, tRight, tEOF}},
{"block", `{{block "foo" .}}`, []item{ {"block", `{{block "foo" .}}`, []item{
tLeft, tBlock, tSpace, {itemString, 0, `"foo"`}, tSpace, tDot, tRight, tEOF, tLeft, tBlock, tSpace, mkItem(itemString, `"foo"`), tSpace, tDot, tRight, tEOF,
}}, }},
{"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}}, {"quote", `{{"abc \n\t\" "}}`, []item{tLeft, tQuote, tRight, tEOF}},
{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}}, {"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},
{"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}}, {"raw quote with newline", "{{" + rawNL + "}}", []item{tLeft, tRawQuoteNL, tRight, tEOF}},
{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{ {"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{
tLeft, tLeft,
{itemNumber, 0, "1"}, mkItem(itemNumber, "1"),
tSpace, tSpace,
{itemNumber, 0, "02"}, mkItem(itemNumber, "02"),
tSpace, tSpace,
{itemNumber, 0, "0x14"}, mkItem(itemNumber, "0x14"),
tSpace, tSpace,
{itemNumber, 0, "-7.2i"}, mkItem(itemNumber, "-7.2i"),
tSpace, tSpace,
{itemNumber, 0, "1e3"}, mkItem(itemNumber, "1e3"),
tSpace, tSpace,
{itemNumber, 0, "+1.2e-4"}, mkItem(itemNumber, "+1.2e-4"),
tSpace, tSpace,
{itemNumber, 0, "4.2i"}, mkItem(itemNumber, "4.2i"),
tSpace, tSpace,
{itemComplex, 0, "1+2i"}, mkItem(itemComplex, "1+2i"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{ {"characters", `{{'a' '\n' '\'' '\\' '\u00FF' '\xFF' '本'}}`, []item{
tLeft, tLeft,
{itemCharConstant, 0, `'a'`}, mkItem(itemCharConstant, `'a'`),
tSpace, tSpace,
{itemCharConstant, 0, `'\n'`}, mkItem(itemCharConstant, `'\n'`),
tSpace, tSpace,
{itemCharConstant, 0, `'\''`}, mkItem(itemCharConstant, `'\''`),
tSpace, tSpace,
{itemCharConstant, 0, `'\\'`}, mkItem(itemCharConstant, `'\\'`),
tSpace, tSpace,
{itemCharConstant, 0, `'\u00FF'`}, mkItem(itemCharConstant, `'\u00FF'`),
tSpace, tSpace,
{itemCharConstant, 0, `'\xFF'`}, mkItem(itemCharConstant, `'\xFF'`),
tSpace, tSpace,
{itemCharConstant, 0, `'本'`}, mkItem(itemCharConstant, `'本'`),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"bools", "{{true false}}", []item{ {"bools", "{{true false}}", []item{
tLeft, tLeft,
{itemBool, 0, "true"}, mkItem(itemBool, "true"),
tSpace, tSpace,
{itemBool, 0, "false"}, mkItem(itemBool, "false"),
tRight, tRight,
tEOF, tEOF,
}}, }},
...@@ -167,178 +174,178 @@ var lexTests = []lexTest{ ...@@ -167,178 +174,178 @@ var lexTests = []lexTest{
}}, }},
{"nil", "{{nil}}", []item{ {"nil", "{{nil}}", []item{
tLeft, tLeft,
{itemNil, 0, "nil"}, mkItem(itemNil, "nil"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"dots", "{{.x . .2 .x.y.z}}", []item{ {"dots", "{{.x . .2 .x.y.z}}", []item{
tLeft, tLeft,
{itemField, 0, ".x"}, mkItem(itemField, ".x"),
tSpace, tSpace,
tDot, tDot,
tSpace, tSpace,
{itemNumber, 0, ".2"}, mkItem(itemNumber, ".2"),
tSpace, tSpace,
{itemField, 0, ".x"}, mkItem(itemField, ".x"),
{itemField, 0, ".y"}, mkItem(itemField, ".y"),
{itemField, 0, ".z"}, mkItem(itemField, ".z"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"keywords", "{{range if else end with}}", []item{ {"keywords", "{{range if else end with}}", []item{
tLeft, tLeft,
{itemRange, 0, "range"}, mkItem(itemRange, "range"),
tSpace, tSpace,
{itemIf, 0, "if"}, mkItem(itemIf, "if"),
tSpace, tSpace,
{itemElse, 0, "else"}, mkItem(itemElse, "else"),
tSpace, tSpace,
{itemEnd, 0, "end"}, mkItem(itemEnd, "end"),
tSpace, tSpace,
{itemWith, 0, "with"}, mkItem(itemWith, "with"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{ {"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{
tLeft, tLeft,
{itemVariable, 0, "$c"}, mkItem(itemVariable, "$c"),
tSpace, tSpace,
{itemColonEquals, 0, ":="}, mkItem(itemColonEquals, ":="),
tSpace, tSpace,
{itemIdentifier, 0, "printf"}, mkItem(itemIdentifier, "printf"),
tSpace, tSpace,
{itemVariable, 0, "$"}, mkItem(itemVariable, "$"),
tSpace, tSpace,
{itemVariable, 0, "$hello"}, mkItem(itemVariable, "$hello"),
tSpace, tSpace,
{itemVariable, 0, "$23"}, mkItem(itemVariable, "$23"),
tSpace, tSpace,
{itemVariable, 0, "$"}, mkItem(itemVariable, "$"),
tSpace, tSpace,
{itemVariable, 0, "$var"}, mkItem(itemVariable, "$var"),
{itemField, 0, ".Field"}, mkItem(itemField, ".Field"),
tSpace, tSpace,
{itemField, 0, ".Method"}, mkItem(itemField, ".Method"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"variable invocation", "{{$x 23}}", []item{ {"variable invocation", "{{$x 23}}", []item{
tLeft, tLeft,
{itemVariable, 0, "$x"}, mkItem(itemVariable, "$x"),
tSpace, tSpace,
{itemNumber, 0, "23"}, mkItem(itemNumber, "23"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{ {"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{
{itemText, 0, "intro "}, mkItem(itemText, "intro "),
tLeft, tLeft,
{itemIdentifier, 0, "echo"}, mkItem(itemIdentifier, "echo"),
tSpace, tSpace,
{itemIdentifier, 0, "hi"}, mkItem(itemIdentifier, "hi"),
tSpace, tSpace,
{itemNumber, 0, "1.2"}, mkItem(itemNumber, "1.2"),
tSpace, tSpace,
tPipe, tPipe,
{itemIdentifier, 0, "noargs"}, mkItem(itemIdentifier, "noargs"),
tPipe, tPipe,
{itemIdentifier, 0, "args"}, mkItem(itemIdentifier, "args"),
tSpace, tSpace,
{itemNumber, 0, "1"}, mkItem(itemNumber, "1"),
tSpace, tSpace,
{itemString, 0, `"hi"`}, mkItem(itemString, `"hi"`),
tRight, tRight,
{itemText, 0, " outro"}, mkItem(itemText, " outro"),
tEOF, tEOF,
}}, }},
{"declaration", "{{$v := 3}}", []item{ {"declaration", "{{$v := 3}}", []item{
tLeft, tLeft,
{itemVariable, 0, "$v"}, mkItem(itemVariable, "$v"),
tSpace, tSpace,
{itemColonEquals, 0, ":="}, mkItem(itemColonEquals, ":="),
tSpace, tSpace,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"2 declarations", "{{$v , $w := 3}}", []item{ {"2 declarations", "{{$v , $w := 3}}", []item{
tLeft, tLeft,
{itemVariable, 0, "$v"}, mkItem(itemVariable, "$v"),
tSpace, tSpace,
{itemChar, 0, ","}, mkItem(itemChar, ","),
tSpace, tSpace,
{itemVariable, 0, "$w"}, mkItem(itemVariable, "$w"),
tSpace, tSpace,
{itemColonEquals, 0, ":="}, mkItem(itemColonEquals, ":="),
tSpace, tSpace,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"field of parenthesized expression", "{{(.X).Y}}", []item{ {"field of parenthesized expression", "{{(.X).Y}}", []item{
tLeft, tLeft,
tLpar, tLpar,
{itemField, 0, ".X"}, mkItem(itemField, ".X"),
tRpar, tRpar,
{itemField, 0, ".Y"}, mkItem(itemField, ".Y"),
tRight, tRight,
tEOF, tEOF,
}}, }},
{"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{ {"trimming spaces before and after", "hello- {{- 3 -}} -world", []item{
{itemText, 0, "hello-"}, mkItem(itemText, "hello-"),
tLeft, tLeft,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
tRight, tRight,
{itemText, 0, "-world"}, mkItem(itemText, "-world"),
tEOF, tEOF,
}}, }},
{"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{ {"trimming spaces before and after comment", "hello- {{- /* hello */ -}} -world", []item{
{itemText, 0, "hello-"}, mkItem(itemText, "hello-"),
{itemText, 0, "-world"}, mkItem(itemText, "-world"),
tEOF, tEOF,
}}, }},
// errors // errors
{"badchar", "#{{\x01}}", []item{ {"badchar", "#{{\x01}}", []item{
{itemText, 0, "#"}, mkItem(itemText, "#"),
tLeft, tLeft,
{itemError, 0, "unrecognized character in action: U+0001"}, mkItem(itemError, "unrecognized character in action: U+0001"),
}}, }},
{"unclosed action", "{{\n}}", []item{ {"unclosed action", "{{\n}}", []item{
tLeft, tLeft,
{itemError, 0, "unclosed action"}, mkItem(itemError, "unclosed action"),
}}, }},
{"EOF in action", "{{range", []item{ {"EOF in action", "{{range", []item{
tLeft, tLeft,
tRange, tRange,
{itemError, 0, "unclosed action"}, mkItem(itemError, "unclosed action"),
}}, }},
{"unclosed quote", "{{\"\n\"}}", []item{ {"unclosed quote", "{{\"\n\"}}", []item{
tLeft, tLeft,
{itemError, 0, "unterminated quoted string"}, mkItem(itemError, "unterminated quoted string"),
}}, }},
{"unclosed raw quote", "{{`xx}}", []item{ {"unclosed raw quote", "{{`xx}}", []item{
tLeft, tLeft,
{itemError, 0, "unterminated raw quoted string"}, mkItem(itemError, "unterminated raw quoted string"),
}}, }},
{"unclosed char constant", "{{'\n}}", []item{ {"unclosed char constant", "{{'\n}}", []item{
tLeft, tLeft,
{itemError, 0, "unterminated character constant"}, mkItem(itemError, "unterminated character constant"),
}}, }},
{"bad number", "{{3k}}", []item{ {"bad number", "{{3k}}", []item{
tLeft, tLeft,
{itemError, 0, `bad number syntax: "3k"`}, mkItem(itemError, `bad number syntax: "3k"`),
}}, }},
{"unclosed paren", "{{(3}}", []item{ {"unclosed paren", "{{(3}}", []item{
tLeft, tLeft,
tLpar, tLpar,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
{itemError, 0, `unclosed left paren`}, mkItem(itemError, `unclosed left paren`),
}}, }},
{"extra right paren", "{{3)}}", []item{ {"extra right paren", "{{3)}}", []item{
tLeft, tLeft,
{itemNumber, 0, "3"}, mkItem(itemNumber, "3"),
tRpar, tRpar,
{itemError, 0, `unexpected right paren U+0029 ')'`}, mkItem(itemError, `unexpected right paren U+0029 ')'`),
}}, }},
// Fixed bugs // Fixed bugs
...@@ -355,17 +362,17 @@ var lexTests = []lexTest{ ...@@ -355,17 +362,17 @@ var lexTests = []lexTest{
tEOF, tEOF,
}}, }},
{"text with bad comment", "hello-{{/*/}}-world", []item{ {"text with bad comment", "hello-{{/*/}}-world", []item{
{itemText, 0, "hello-"}, mkItem(itemText, "hello-"),
{itemError, 0, `unclosed comment`}, mkItem(itemError, `unclosed comment`),
}}, }},
{"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{ {"text with comment close separated from delim", "hello-{{/* */ }}-world", []item{
{itemText, 0, "hello-"}, mkItem(itemText, "hello-"),
{itemError, 0, `comment ends before closing delimiter`}, mkItem(itemError, `comment ends before closing delimiter`),
}}, }},
// This one is an error that we can't catch because it breaks templates with // 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. // minimized JavaScript. Should have fixed it before Go 1.1.
{"unmatched right delimiter", "hello-{.}}-world", []item{ {"unmatched right delimiter", "hello-{.}}-world", []item{
{itemText, 0, "hello-{.}}-world"}, mkItem(itemText, "hello-{.}}-world"),
tEOF, tEOF,
}}, }},
} }
...@@ -414,13 +421,13 @@ func TestLex(t *testing.T) { ...@@ -414,13 +421,13 @@ func TestLex(t *testing.T) {
var lexDelimTests = []lexTest{ var lexDelimTests = []lexTest{
{"punctuation", "$$,@%{{}}@@", []item{ {"punctuation", "$$,@%{{}}@@", []item{
tLeftDelim, tLeftDelim,
{itemChar, 0, ","}, mkItem(itemChar, ","),
{itemChar, 0, "@"}, mkItem(itemChar, "@"),
{itemChar, 0, "%"}, mkItem(itemChar, "%"),
{itemChar, 0, "{"}, mkItem(itemChar, "{"),
{itemChar, 0, "{"}, mkItem(itemChar, "{"),
{itemChar, 0, "}"}, mkItem(itemChar, "}"),
{itemChar, 0, "}"}, mkItem(itemChar, "}"),
tRightDelim, tRightDelim,
tEOF, tEOF,
}}, }},
...@@ -431,8 +438,8 @@ var lexDelimTests = []lexTest{ ...@@ -431,8 +438,8 @@ var lexDelimTests = []lexTest{
} }
var ( var (
tLeftDelim = item{itemLeftDelim, 0, "$$"} tLeftDelim = mkItem(itemLeftDelim, "$$")
tRightDelim = item{itemRightDelim, 0, "@@"} tRightDelim = mkItem(itemRightDelim, "@@")
) )
func TestDelims(t *testing.T) { func TestDelims(t *testing.T) {
...@@ -447,21 +454,21 @@ func TestDelims(t *testing.T) { ...@@ -447,21 +454,21 @@ func TestDelims(t *testing.T) {
var lexPosTests = []lexTest{ var lexPosTests = []lexTest{
{"empty", "", []item{tEOF}}, {"empty", "", []item{tEOF}},
{"punctuation", "{{,@%#}}", []item{ {"punctuation", "{{,@%#}}", []item{
{itemLeftDelim, 0, "{{"}, {itemLeftDelim, 0, "{{", 1},
{itemChar, 2, ","}, {itemChar, 2, ",", 1},
{itemChar, 3, "@"}, {itemChar, 3, "@", 1},
{itemChar, 4, "%"}, {itemChar, 4, "%", 1},
{itemChar, 5, "#"}, {itemChar, 5, "#", 1},
{itemRightDelim, 6, "}}"}, {itemRightDelim, 6, "}}", 1},
{itemEOF, 8, ""}, {itemEOF, 8, "", 1},
}}, }},
{"sample", "0123{{hello}}xyz", []item{ {"sample", "0123{{hello}}xyz", []item{
{itemText, 0, "0123"}, {itemText, 0, "0123", 1},
{itemLeftDelim, 4, "{{"}, {itemLeftDelim, 4, "{{", 1},
{itemIdentifier, 6, "hello"}, {itemIdentifier, 6, "hello", 1},
{itemRightDelim, 11, "}}"}, {itemRightDelim, 11, "}}", 1},
{itemText, 13, "xyz"}, {itemText, 13, "xyz", 1},
{itemEOF, 16, ""}, {itemEOF, 16, "", 1},
}}, }},
} }
......
...@@ -157,7 +157,7 @@ func (t *Tree) ErrorContext(n Node) (location, context string) { ...@@ -157,7 +157,7 @@ func (t *Tree) ErrorContext(n Node) (location, context string) {
// errorf formats the error and terminates processing. // errorf formats the error and terminates processing.
func (t *Tree) errorf(format string, args ...interface{}) { func (t *Tree) errorf(format string, args ...interface{}) {
t.Root = nil t.Root = nil
format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.lex.lineNumber(), format) format = fmt.Sprintf("template: %s:%d: %s", t.ParseName, t.token[0].line, format)
panic(fmt.Errorf(format, args...)) panic(fmt.Errorf(format, args...))
} }
...@@ -376,15 +376,17 @@ func (t *Tree) action() (n Node) { ...@@ -376,15 +376,17 @@ func (t *Tree) action() (n Node) {
return t.withControl() return t.withControl()
} }
t.backup() t.backup()
token := t.peek()
// Do not pop variables; they persist until "end". // Do not pop variables; they persist until "end".
return t.newAction(t.peek().pos, t.lex.lineNumber(), t.pipeline("command")) return t.newAction(token.pos, token.line, t.pipeline("command"))
} }
// Pipeline: // Pipeline:
// declarations? command ('|' command)* // declarations? command ('|' command)*
func (t *Tree) pipeline(context string) (pipe *PipeNode) { func (t *Tree) pipeline(context string) (pipe *PipeNode) {
var decl []*VariableNode var decl []*VariableNode
pos := t.peekNonSpace().pos token := t.peekNonSpace()
pos := token.pos
// Are there declarations? // Are there declarations?
for { for {
if v := t.peekNonSpace(); v.typ == itemVariable { if v := t.peekNonSpace(); v.typ == itemVariable {
...@@ -413,7 +415,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) { ...@@ -413,7 +415,7 @@ func (t *Tree) pipeline(context string) (pipe *PipeNode) {
} }
break break
} }
pipe = t.newPipeline(pos, t.lex.lineNumber(), decl) pipe = t.newPipeline(pos, token.line, decl)
for { for {
switch token := t.nextNonSpace(); token.typ { switch token := t.nextNonSpace(); token.typ {
case itemRightDelim, itemRightParen: case itemRightDelim, itemRightParen:
...@@ -450,7 +452,6 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) { ...@@ -450,7 +452,6 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
defer t.popVars(len(t.vars)) defer t.popVars(len(t.vars))
line = t.lex.lineNumber()
pipe = t.pipeline(context) pipe = t.pipeline(context)
var next Node var next Node
list, next = t.itemList() list, next = t.itemList()
...@@ -479,7 +480,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int ...@@ -479,7 +480,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
t.errorf("expected end; found %s", next) t.errorf("expected end; found %s", next)
} }
} }
return pipe.Position(), line, pipe, list, elseList return pipe.Position(), pipe.Line, pipe, list, elseList
} }
// If: // If:
...@@ -521,9 +522,10 @@ func (t *Tree) elseControl() Node { ...@@ -521,9 +522,10 @@ func (t *Tree) elseControl() Node {
peek := t.peekNonSpace() peek := t.peekNonSpace()
if peek.typ == itemIf { if peek.typ == itemIf {
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ". // We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
return t.newElse(peek.pos, t.lex.lineNumber()) return t.newElse(peek.pos, peek.line)
} }
return t.newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber()) token := t.expect(itemRightDelim, "else")
return t.newElse(token.pos, token.line)
} }
// Block: // Block:
...@@ -550,7 +552,7 @@ func (t *Tree) blockControl() Node { ...@@ -550,7 +552,7 @@ func (t *Tree) blockControl() Node {
block.add() block.add()
block.stopParse() block.stopParse()
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe) return t.newTemplate(token.pos, token.line, name, pipe)
} }
// Template: // Template:
...@@ -567,7 +569,7 @@ func (t *Tree) templateControl() Node { ...@@ -567,7 +569,7 @@ func (t *Tree) templateControl() Node {
// Do not pop variables; they persist until "end". // Do not pop variables; they persist until "end".
pipe = t.pipeline(context) pipe = t.pipeline(context)
} }
return t.newTemplate(token.pos, t.lex.lineNumber(), name, pipe) return t.newTemplate(token.pos, token.line, name, pipe)
} }
func (t *Tree) parseTemplateName(token item, context string) (name string) { func (t *Tree) parseTemplateName(token item, context string) (name string) {
......
...@@ -484,3 +484,37 @@ func TestBlock(t *testing.T) { ...@@ -484,3 +484,37 @@ func TestBlock(t *testing.T) {
t.Errorf("inner template = %q, want %q", g, w) t.Errorf("inner template = %q, want %q", g, w)
} }
} }
func TestLineNum(t *testing.T) {
const count = 100
text := strings.Repeat("{{printf 1234}}\n", count)
tree, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
if err != nil {
t.Fatal(err)
}
// Check the line numbers. Each line is an action containing a template, followed by text.
// That's two nodes per line.
nodes := tree.Root.Nodes
for i := 0; i < len(nodes); i += 2 {
line := 1 + i/2
// Action first.
action := nodes[i].(*ActionNode)
if action.Line != line {
t.Fatalf("line %d: action is line %d", line, action.Line)
}
pipe := action.Pipe
if pipe.Line != line {
t.Fatalf("line %d: pipe is line %d", line, pipe.Line)
}
}
}
func BenchmarkParseLarge(b *testing.B) {
text := strings.Repeat("{{1234}}\n", 10000)
for i := 0; i < b.N; i++ {
_, err := New("bench").Parse(text, "", "", make(map[string]*Tree), builtins)
if err != nil {
b.Fatal(err)
}
}
}
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