Permalink
Browse files

greatly improved error message reporting. This fixes #339

--HG--
branch : trunk
extra : rebase_source : d8f677273490fa73d5603b68478fa3b54f60ccb9
  • Loading branch information...
1 parent d9ea26e commit 5dcb724c92f50574f224da21a622aa81300e6777 @mitsuhiko mitsuhiko committed Feb 6, 2010
Showing with 177 additions and 44 deletions.
  1. +2 −1 CHANGES
  2. +42 −6 jinja2/lexer.py
  3. +105 −36 jinja2/parser.py
  4. +28 −1 tests/test_parser.py
View
@@ -3,7 +3,7 @@ Jinja2 Changelog
Version 2.3
-----------
-(codename to be selected, release date to be selected.)
+(codename to be selected, release date will be February 9th 2010)
- fixes issue with code generator that causes unbound variables
to be generated if set was used in if-blocks and other small
@@ -14,6 +14,7 @@ Version 2.3
- fixed a problem with having call blocks in outer scopes that
have an argument that is also used as local variable in an
inner frame [#360].
+- greatly improved error message reporting (#339)
Version 2.2.1
-------------
View
@@ -139,6 +139,43 @@
TOKEN_COMMENT, TOKEN_LINECOMMENT])
+def _describe_token_type(token_type):
+ if token_type in reverse_operators:
+ return reverse_operators[token_type]
+ return {
+ TOKEN_COMMENT_BEGIN: 'begin of comment',
+ TOKEN_COMMENT_END: 'end of comment',
+ TOKEN_COMMENT: 'comment',
+ TOKEN_LINECOMMENT: 'comment',
+ TOKEN_BLOCK_BEGIN: 'begin of statement block',
+ TOKEN_BLOCK_END: 'end of statement block',
+ TOKEN_VARIABLE_BEGIN: 'begin of print statement',
+ TOKEN_VARIABLE_END: 'end of print statement',
+ TOKEN_LINESTATEMENT_BEGIN: 'begin of line statement',
+ TOKEN_LINESTATEMENT_END: 'end of line statement',
+ TOKEN_DATA: 'template data / text',
+ TOKEN_EOF: 'end of template'
+ }.get(token_type, token_type)
+
+
+def describe_token(token):
+ """Returns a description of the token."""
+ if token.type == 'name':
+ return token.value
+ return _describe_token_type(token.type)
+
+
+def describe_token_expr(expr):
+ """Like `describe_token` but for token expressions."""
+ if ':' in expr:
+ type, value = expr.split(':', 1)
+ if type == 'name':
+ return value
+ else:
+ type = expr
+ return _describe_token_type(type)
+
+
def count_newlines(value):
"""Count the number of newline characters in the string. This is
useful for extensions that filter a stream.
@@ -319,15 +356,14 @@ def expect(self, expr):
argument as :meth:`jinja2.lexer.Token.test`.
"""
if not self.current.test(expr):
- if ':' in expr:
- expr = expr.split(':')[1]
+ expr = describe_token_expr(expr)
if self.current.type is TOKEN_EOF:
raise TemplateSyntaxError('unexpected end of template, '
'expected %r.' % expr,
self.current.lineno,
self.name, self.filename)
raise TemplateSyntaxError("expected token %r, got %r" %
- (expr, str(self.current)),
+ (expr, describe_token(self.current)),
self.current.lineno,
self.name, self.filename)
try:
@@ -582,13 +618,13 @@ def tokeniter(self, source, name, filename=None, state=None):
balancing_stack.append(']')
elif data in ('}', ')', ']'):
if not balancing_stack:
- raise TemplateSyntaxError('unexpected "%s"' %
+ raise TemplateSyntaxError('unexpected \'%s\'' %
data, lineno, name,
filename)
expected_op = balancing_stack.pop()
if expected_op != data:
- raise TemplateSyntaxError('unexpected "%s", '
- 'expected "%s"' %
+ raise TemplateSyntaxError('unexpected \'%s\', '
+ 'expected \'%s\'' %
(data, expected_op),
lineno, name,
filename)
View
@@ -11,8 +11,10 @@
from jinja2 import nodes
from jinja2.exceptions import TemplateSyntaxError, TemplateAssertionError
from jinja2.utils import next
+from jinja2.lexer import describe_token, describe_token_expr
+#: statements that callinto
_statement_keywords = frozenset(['for', 'if', 'block', 'extends', 'print',
'macro', 'include', 'from', 'import',
'set'])
@@ -36,6 +38,8 @@ def __init__(self, environment, source, name=None, filename=None,
for tag in extension.tags:
self.extensions[tag] = extension.parse
self._last_identifier = 0
+ self._tag_stack = []
+ self._end_token_stack = []
def fail(self, msg, lineno=None, exc=TemplateSyntaxError):
"""Convenience method that raises `exc` with the message, passed
@@ -46,6 +50,46 @@ def fail(self, msg, lineno=None, exc=TemplateSyntaxError):
lineno = self.stream.current.lineno
raise exc(msg, lineno, self.name, self.filename)
+ def _fail_ut_eof(self, name, end_token_stack, lineno):
+ expected = []
+ for exprs in end_token_stack:
+ expected.extend(map(describe_token_expr, exprs))
+ currently_looking = ' or '.join("'%s'" % describe_token_expr(expr)
+ for expr in end_token_stack[-1])
+
+ if name is None:
+ message = ['Unexpected end of template.']
+ else:
+ message = ['Encountered unknown tag \'%s\'.' % name]
+
+ if name is not None and name in expected:
+ message.append('You probably made a nesting mistake. Jinja '
+ 'is expecting this tag, but currently looking '
+ 'for %s.' % currently_looking)
+ else:
+ message.append('Jinja was looking for the following tags: '
+ '%s.' % currently_looking)
+
+ if self._tag_stack:
+ message.append('The innermost block that needs to be '
+ 'closed is \'%s\'.' % self._tag_stack[-1])
+
+ self.fail(' '.join(message), lineno)
+
+ def fail_unknown_tag(self, name, lineno=None):
+ """Called if the parser encounters an unknown tag. Tries to fail
+ with a human readable error message that could help to identify
+ the problem.
+ """
+ return self._fail_ut_eof(name, self._end_token_stack, lineno)
+
+ def fail_eof(self, end_tokens=None, lineno=None):
+ """Like fail_unknown_tag but for end of template situations."""
+ stack = list(self._end_token_stack)
+ if end_tokens is not None:
+ stack.append(end_tokens)
+ return self._fail_ut_eof(None, stack, lineno)
+
def is_tuple_end(self, extra_end_rules=None):
"""Are we at the end of a tuple?"""
if self.stream.current.type in ('variable_end', 'block_end', 'rparen'):
@@ -66,16 +110,28 @@ def parse_statement(self):
token = self.stream.current
if token.type != 'name':
self.fail('tag name expected', token.lineno)
- if token.value in _statement_keywords:
- return getattr(self, 'parse_' + self.stream.current.value)()
- if token.value == 'call':
- return self.parse_call_block()
- if token.value == 'filter':
- return self.parse_filter_block()
- ext = self.extensions.get(token.value)
- if ext is not None:
- return ext(self)
- self.fail('unknown tag %r' % token.value, token.lineno)
+ self._tag_stack.append(token.value)
+ pop_tag = True
+ try:
+ if token.value in _statement_keywords:
+ return getattr(self, 'parse_' + self.stream.current.value)()
+ if token.value == 'call':
+ return self.parse_call_block()
+ if token.value == 'filter':
+ return self.parse_filter_block()
+ ext = self.extensions.get(token.value)
+ if ext is not None:
+ return ext(self)
+
+ # did not work out, remove the token we pushed by accident
+ # from the stack so that the unknown tag fail function can
+ # produce a proper error message.
+ self._tag_stack.pop()
+ pop_tag = False
+ self.fail_unknown_tag(token.value, token.lineno)
+ finally:
+ if pop_tag:
+ self._tag_stack.pop()
def parse_statements(self, end_tokens, drop_needle=False):
"""Parse multiple statements into a list until one of the end tokens
@@ -95,6 +151,11 @@ def parse_statements(self, end_tokens, drop_needle=False):
self.stream.expect('block_end')
result = self.subparse(end_tokens)
+ # we reached the end of the template too early, the subparser
+ # does not check for this, so we do that now
+ if self.stream.current.type == 'eof':
+ self.fail_eof(end_tokens)
+
if drop_needle:
next(self.stream)
return result
@@ -494,7 +555,7 @@ def parse_primary(self, with_postfix=True):
elif token.type == 'lbrace':
node = self.parse_dict()
else:
- self.fail("unexpected token '%s'" % (token,), token.lineno)
+ self.fail("unexpected '%s'" % describe_token(token), token.lineno)
if with_postfix:
node = self.parse_postfix(node)
return node
@@ -742,39 +803,47 @@ def subparse(self, end_tokens=None):
data_buffer = []
add_data = data_buffer.append
+ if end_tokens is not None:
+ self._end_token_stack.append(end_tokens)
+
def flush_data():
if data_buffer:
lineno = data_buffer[0].lineno
body.append(nodes.Output(data_buffer[:], lineno=lineno))
del data_buffer[:]
- while self.stream:
- token = self.stream.current
- if token.type == 'data':
- if token.value:
- add_data(nodes.TemplateData(token.value,
- lineno=token.lineno))
- next(self.stream)
- elif token.type == 'variable_begin':
- next(self.stream)
- add_data(self.parse_tuple(with_condexpr=True))
- self.stream.expect('variable_end')
- elif token.type == 'block_begin':
- flush_data()
- next(self.stream)
- if end_tokens is not None and \
- self.stream.current.test_any(*end_tokens):
- return body
- rv = self.parse_statement()
- if isinstance(rv, list):
- body.extend(rv)
+ try:
+ while self.stream:
+ token = self.stream.current
+ if token.type == 'data':
+ if token.value:
+ add_data(nodes.TemplateData(token.value,
+ lineno=token.lineno))
+ next(self.stream)
+ elif token.type == 'variable_begin':
+ next(self.stream)
+ add_data(self.parse_tuple(with_condexpr=True))
+ self.stream.expect('variable_end')
+ elif token.type == 'block_begin':
+ flush_data()
+ next(self.stream)
+ if end_tokens is not None and \
+ self.stream.current.test_any(*end_tokens):
+ return body
+ rv = self.parse_statement()
+ if isinstance(rv, list):
+ body.extend(rv)
+ else:
+ body.append(rv)
+ self.stream.expect('block_end')
else:
- body.append(rv)
- self.stream.expect('block_end')
- else:
- raise AssertionError('internal parsing error')
+ raise AssertionError('internal parsing error')
+
+ flush_data()
+ finally:
+ if end_tokens is not None:
+ self._end_token_stack.pop()
- flush_data()
return body
def parse(self):
View
@@ -6,7 +6,7 @@
:copyright: (c) 2009 by the Jinja Team.
:license: BSD, see LICENSE for more details.
"""
-from jinja2 import Environment
+from jinja2 import Environment, Template, TemplateSyntaxError
env = Environment()
@@ -114,3 +114,30 @@ def test_line_syntax_priority():
env = Environment('{%', '%}', '${', '}', '/*', '*/', '#', '##')
tmpl = env.from_string(LINE_SYNTAX_PRIORITY2)
assert tmpl.render(seq=[1, 2]).strip() == '* 1\n\n* 2'
+
+
+def test_error_messages():
+ def assert_error(code, expected):
+ try:
+ Template(code)
+ except TemplateSyntaxError, e:
+ assert str(e) == expected, 'unexpected error message'
+ else:
+ assert False, 'that was suposed to be an error'
+
+ assert_error('{% for item in seq %}...{% endif %}',
+ "Encountered unknown tag 'endif'. Jinja was looking "
+ "for the following tags: 'endfor' or 'else'. The "
+ "innermost block that needs to be closed is 'for'.")
+ assert_error('{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}',
+ "Encountered unknown tag 'endfor'. Jinja was looking for "
+ "the following tags: 'elif' or 'else' or 'endif'. The "
+ "innermost block that needs to be closed is 'if'.")
+ assert_error('{% if foo %}',
+ "Unexpected end of template. Jinja was looking for the "
+ "following tags: 'elif' or 'else' or 'endif'. The "
+ "innermost block that needs to be closed is 'if'.")
+ assert_error('{% for item in seq %}',
+ "Unexpected end of template. Jinja was looking for the "
+ "following tags: 'endfor' or 'else'. The innermost block "
+ "that needs to be closed is 'for'.")

0 comments on commit 5dcb724

Please sign in to comment.