diff --git a/jinjalint/ast.py b/jinjalint/ast.py index b7314d2..1f0f0b8 100644 --- a/jinjalint/ast.py +++ b/jinjalint/ast.py @@ -170,6 +170,29 @@ def __str__(self): ) +@attr.s(frozen=True) +class JinjaOptionalContainer(Jinja): + first_opening_if = attr.ib() # JinjaTag + opening_tag = attr.ib() # OpeningTag + first_closing_if = attr.ib() # JinjaTag + content = attr.ib() # Interpolated + second_opening_if = attr.ib() # JinjaTag + closing_tag = attr.ib() # ClosingTag + second_closing_if = attr.ib() # JinjaTag + + def __str__(self): + nodes = [ + self.first_opening_if, + self.opening_tag, + self.first_closing_if, + self.content, + self.second_opening_if, + self.closing_tag, + self.second_closing_if, + ] + return ''.join(str(n) for n in nodes) + + @attr.s(frozen=True) class InterpolatedBase(Node): nodes = attr.ib() # [any | Jinja] @@ -225,10 +248,3 @@ def __init__(self, *args, **kwargs): kwargs = kwargs.copy() kwargs['nodes'] = _normalize_nodes(kwargs['nodes']) super().__init__(**kwargs) - - -NODE_TYPE_LIST = [ - Node, Slash, OpeningTag, ClosingTag, Element, String, Integer, - Attribute, Comment, JinjaVariable, JinjaComment, JinjaTag, - JinjaElement, JinjaElementPart, InterpolatedBase, Interpolated, -] diff --git a/jinjalint/check.py b/jinjalint/check.py index a18b13b..8347f7d 100644 --- a/jinjalint/check.py +++ b/jinjalint/check.py @@ -25,6 +25,10 @@ def contains_exclusively(string, char): return string.replace(char, '') == '' +def truncate(s, length=16): + return s[:length] + (s[length:] and '…') + + def check_indentation(file, config): indent_size = config.get('indent_size', 4) @@ -36,8 +40,9 @@ def add_issue(location, msg): def check_indent(expected_level, node, inline=False): node_level = get_indent_level(file.source, node) if node_level is None: - if not inline and not isinstance(node, ast.Jinja): - add_issue(node.begin, 'Should be on the next line') + if not inline: # and not isinstance(node, ast.Jinja): + node_s = repr(truncate(str(node))) + add_issue(node.begin, node_s + ' should be on the next line') return if node_level != expected_level: @@ -118,6 +123,40 @@ def check_jinja_element_part(expected_level, part, inline=False): if part.content is not None: check_content(content_level, part.content, inline=inline) + def check_jinja_optional_container_if(expected_level, o_if, html_tag, c_if, + inline=False): + check_indent(expected_level, o_if, inline=inline) + shift = 0 if inline else indent_size + if isinstance(html_tag, ast.OpeningTag): + check_opening_tag(expected_level + shift, html_tag, inline=inline) + elif isinstance(html_tag, ast.ClosingTag): + check_indent(expected_level + shift, html_tag, inline=inline) + else: + raise AssertionError('invalid tag') + check_indent(expected_level, c_if, inline=inline) + return inline + + def check_jinja_optional_container(expected_level, element, inline=False): + if element.first_opening_if.begin.line == \ + element.second_opening_if.end.line: + inline = True + + inline = check_jinja_optional_container_if( + expected_level, + element.first_opening_if, + element.opening_tag, + element.first_closing_if, + inline=inline) + + check_content(expected_level, element.content, inline=inline) + + check_jinja_optional_container_if( + expected_level, + element.second_opening_if, + element.closing_tag, + element.second_closing_if, + inline=inline) + def check_jinja_element(expected_level, element, inline=False): if element.begin.line == element.end.line: inline = True @@ -155,6 +194,7 @@ def check_node(expected_level, node, inline=False): ast.JinjaComment: check_jinja_comment, ast.JinjaElement: check_jinja_element, ast.JinjaElementPart: check_jinja_element_part, + ast.JinjaOptionalContainer: check_jinja_optional_container, ast.JinjaTag: check_jinja_tag, ast.JinjaVariable: check_jinja_variable, ast.String: check_string, diff --git a/jinjalint/cli.py b/jinjalint/cli.py index d2fb728..d947595 100644 --- a/jinjalint/cli.py +++ b/jinjalint/cli.py @@ -21,7 +21,11 @@ def print_issues(issues, config): sorted_issues = sorted( issues, - key=lambda i: (i.location.file_path, i.location.line), + key=lambda i: ( + i.location.file_path, + i.location.line, + i.location.column + ), ) for issue in sorted_issues: diff --git a/jinjalint/parse.py b/jinjalint/parse.py index ada64fa..67f4758 100644 --- a/jinjalint/parse.py +++ b/jinjalint/parse.py @@ -5,7 +5,7 @@ from .ast import ( Attribute, ClosingTag, Comment, Element, Integer, Interpolated, Jinja, JinjaComment, JinjaElement, JinjaElementPart, JinjaTag, - JinjaVariable, Location, OpeningTag, String, + JinjaVariable, JinjaOptionalContainer, Location, OpeningTag, String, ) from .util import flatten @@ -34,7 +34,7 @@ ('filter', 'endfilter'), ('for', 'else', 'empty', 'endfor'), ('if', 'elif', 'else', 'endif'), - ('ifchanged', 'endifchanged'), + ('ifchanged', 'else', 'endifchanged'), ('ifequal', 'endifequal'), ('ifnotequal', 'endifnotequal'), ('spaceless', 'endspaceless'), @@ -169,6 +169,13 @@ def make_jinja_element_part_parser(name_parser, content): def make_jinja_element_parser(name_parsers, content): + """ + `name_parsers` must be a list of tag name parsers. For example, + `name_parsers` can be defined as follow in order to parse `if` statements: + + name_parsers = [P.string(n) for n in ['if', 'elif', 'else', 'endif']] + """ + if len(name_parsers) == 1: tag = make_jinja_tag_parser(name_parsers[0]) part = locate(P.seq( @@ -454,12 +461,84 @@ def make_raw_text_element_parser(config, tag_name, jinja): quick_text = P.regex(r'[^{<]+', flags=re.DOTALL) -def make_jinja_parser(config, content): - jinja_structured_elements_names = ( - DEFAULT_JINJA_STRUCTURED_ELEMENTS_NAMES + - config.get('jinja_custom_elements_names', []) +def _combine_optional_container(locations, nodes): + return JinjaOptionalContainer( + first_opening_if=nodes[0], + opening_tag=nodes[1], + first_closing_if=nodes[2], + content=nodes[3], + second_opening_if=nodes[4], + closing_tag=nodes[5], + second_closing_if=nodes[6], + **locations, + ) + + +# Awkward hack to handle optional HTML containers, for example: +# +# {% if a %} +#