Commit ff2e6dc1 by Robert Raposa

Merge pull request #12082 from edx/robrap/linter-mako

TNL-4324: Enhance linter for HTML() and Text()
parents 498ab887 96fe8afb
......@@ -3,6 +3,7 @@
A linting tool to check if templates are safe
"""
from __future__ import print_function
import argparse
from enum import Enum
import os
import re
......@@ -205,8 +206,8 @@ class Rules(Enum):
'mako-multiple-page-tags',
'A Mako template can only have one <%page> tag.'
)
mako_unparsable_expression = (
'mako-unparsable-expression',
mako_unparseable_expression = (
'mako-unparseable-expression',
'The expression could not be properly parsed.'
)
mako_unwanted_html_filter = (
......@@ -217,15 +218,30 @@ class Rules(Enum):
'mako-invalid-html-filter',
'The expression is using an invalid filter in an HTML context.'
)
mako_invalid_js_filter = (
'mako-invalid-js-filter',
'The expression is using an invalid filter in a JavaScript context.'
)
mako_deprecated_display_name = (
'mako-deprecated-display-name',
'Replace deprecated display_name_with_default_escaped with display_name_with_default.'
)
mako_invalid_js_filter = (
'mako-invalid-js-filter',
'The expression is using an invalid filter in a JavaScript context.'
mako_html_requires_text = (
'mako-html-requires-text',
'You must begin with Text() if you use HTML() during interpolation.'
)
mako_close_before_format = (
'mako-close-before-format',
'You must close any call to Text() or HTML() before calling format().'
)
mako_text_redundant = (
'mako-text-redundant',
'Using Text() function without HTML() is unnecessary.'
)
mako_html_alone = (
'mako-html-alone',
"Only use HTML() alone with properly escaped HTML(), and make sure it is really alone."
)
underscore_not_escaped = (
'underscore-not-escaped',
'Expressions should be escaped using <%- expression %>.'
......@@ -421,7 +437,7 @@ class ExpressionRuleViolation(RuleViolation):
line_number,
column,
rule_id,
self.lines[line_number - self.start_line - 1].encode(encoding='utf-8')
self.lines[line_number - self.start_line].encode(encoding='utf-8')
), file=out)
......@@ -630,13 +646,14 @@ class MakoTemplateLinter(object):
for expression in expressions:
if expression['expression'] is None:
results.violations.append(ExpressionRuleViolation(
Rules.mako_unparsable_expression, expression
Rules.mako_unparseable_expression, expression
))
continue
context = self._get_context(contexts, expression['start_index'])
self._check_filters(mako_template, expression, context, has_page_default, results)
self._check_deprecated_display_name(expression, results)
self._check_html_and_text(expression, results)
def _check_deprecated_display_name(self, expression, results):
"""
......@@ -654,6 +671,52 @@ class MakoTemplateLinter(object):
Rules.mako_deprecated_display_name, expression
))
def _check_html_and_text(self, expression, results):
"""
Checks rules related to proper use of HTML() and Text().
Rule 1: If HTML() is called, the expression must begin with Text(), or
Rule 2: If HTML() is called alone, it must be the only call.
Rule 3: Both HTML() and Text() must be closed before any call to
format().
Rule 4: Using Text() without HTML() is unnecessary.
Arguments:
expression: A dict containing the start_index, end_index, and
expression (text) of the expression.
results: A list of results into which violations will be added.
"""
# strip '${' and '}' and whitespace from ends
expression_inner = expression['expression'][2:-1].strip()
if 'HTML(' in expression_inner:
if expression_inner.startswith('HTML('):
close_paren_index = self._find_closing_char_index(None, "(", ")", expression_inner, len('HTML('), 0)
# check that the close paren is at the end of the expression.
if close_paren_index != len(expression_inner) - 1:
results.violations.append(ExpressionRuleViolation(
Rules.mako_html_alone, expression
))
elif expression_inner.startswith('Text(') is False:
results.violations.append(ExpressionRuleViolation(
Rules.mako_html_requires_text, expression
))
else:
if 'Text(' in expression_inner:
results.violations.append(ExpressionRuleViolation(
Rules.mako_text_redundant, expression
))
for match in re.finditer("(HTML\(|Text\()", expression_inner):
close_paren_index = self._find_closing_char_index(None, "(", ")", expression_inner, match.end(), 0)
if 0 <= close_paren_index:
# the argument sent to HTML() or Text()
argument = expression_inner[match.end():close_paren_index]
if ".format(" in argument:
results.violations.append(ExpressionRuleViolation(
Rules.mako_close_before_format, expression
))
def _check_filters(self, mako_template, expression, context, has_page_default, results):
"""
Checks that the filters used in the given Mako expression are valid
......@@ -796,9 +859,9 @@ class MakoTemplateLinter(object):
while True:
start_index = mako_template.find(start_delim, start_index)
if (start_index < 0):
if start_index < 0:
break
end_index = self._find_balanced_end_curly(mako_template, start_index + len(start_delim), 0)
end_index = self._find_closing_char_index(start_delim, '{', '}', mako_template, start_index + len(start_delim), 0)
if end_index < 0:
expression = None
......@@ -817,39 +880,117 @@ class MakoTemplateLinter(object):
return expressions
def _find_balanced_end_curly(self, mako_template, start_index, num_open_curlies):
def _find_closing_char_index(self, start_delim, open_char, close_char, template, start_index, num_open_chars):
"""
Finds the end index of the Mako expression's ending curly brace. Skips
any additional open/closed braces that are balanced inside. Does not
take into consideration strings.
Finds the index of the closing char that matches the opening char.
For example, this could be used to find the end of a Mako expression,
where the open and close characters would be '{' and '}'.
Arguments:
mako_template: The template text.
start_index: The start index of the Mako expression.
num_open_curlies: The current number of open expressions.
start_delim: If provided (e.g. '${' for Mako expressions), the
closing character must be found before the next start_delim.
open_char: The opening character to be matched (e.g '{')
close_char: The closing character to be matched (e.g '}')
template: The template to be searched.
start_index: The start index of the last open char.
num_open_chars: The current number of open chars.
Returns:
The end index of the expression, or -1 if unparseable.
The index of the closing character, or -1 if unparseable.
"""
end_curly_index = mako_template.find('}', start_index)
if end_curly_index < 0:
# if we can't find an end_curly, let's just quit
return end_curly_index
close_char_index = template.find(close_char, start_index)
if close_char_index < 0:
# if we can't find an end_char, let's just quit
return -1
open_curly_index = mako_template.find('{', start_index, end_curly_index)
open_char_index = template.find(open_char, start_index, close_char_index)
start_quote_index = self._find_string_start(template, start_index, close_char_index)
if (open_curly_index >= 0) and (open_curly_index < end_curly_index):
if mako_template[open_curly_index - 1] == '$':
# assume if we find "${" it is the start of the next expression
# and we have a parse error
if 0 <= start_quote_index:
string_end_index = self._parse_string(template, start_quote_index)['end_index']
if string_end_index < 0:
return -1
else:
return self._find_balanced_end_curly(mako_template, open_curly_index + 1, num_open_curlies + 1)
if num_open_curlies == 0:
return end_curly_index
return self._find_closing_char_index(start_delim, open_char, close_char, template, string_end_index, num_open_chars)
if (open_char_index >= 0) and (open_char_index < close_char_index):
if start_delim is not None:
# if we find another starting delim, consider this unparseable
start_delim_index = template.find(start_delim, start_index, close_char_index)
if start_delim_index < open_char_index:
return -1
return self._find_closing_char_index(start_delim, open_char, close_char, template, open_char_index + 1, num_open_chars + 1)
if num_open_chars == 0:
return close_char_index
else:
return self._find_balanced_end_curly(mako_template, end_curly_index + 1, num_open_curlies - 1)
return self._find_closing_char_index(start_delim, open_char, close_char, template, close_char_index + 1, num_open_chars - 1)
def _find_string_start(self, template, start_index, end_index):
"""
Finds the index of the end of start of a string. In other words, the
first single or double quote.
Arguments:
template: The template to be searched.
start_index: The start index to search.
end_index: The end index to search before.
num_open_chars: The current number of open expressions.
Returns:
The start index of the first single or double quote, or -1 if
no quote was found.
"""
double_quote_index = template.find('"', start_index, end_index)
single_quote_index = template.find("'", start_index, end_index)
if 0 <= single_quote_index or 0 <= double_quote_index:
if 0 <= single_quote_index and 0 <= double_quote_index:
return min(single_quote_index, double_quote_index)
else:
return max(single_quote_index, double_quote_index)
return -1
def _parse_string(self, template, start_index):
"""
Finds the indices of a string inside a template.
Arguments:
template: The template to be searched.
start_index: The start index of the open quote.
Returns:
A dict containing the following:
start_index: The index of the first quote.
end_index: The index following the closing quote, or -1 if
unparseable
quote_length: The length of the quote. Could be 3 for a Python
triple quote.
"""
quote = template[start_index]
if quote not in ["'", '"']:
raise ValueError("start_index must refer to a single or double quote.")
triple_quote = quote * 3
if template.startswith(triple_quote, start_index):
quote = triple_quote
result = {
'start_index': start_index,
'end_index': -1,
'quote_length': len(quote),
}
start_index += len(quote)
while True:
quote_end_index = template.find(quote, start_index)
backslash_index = template.find("\\", start_index)
if quote_end_index < 0:
return result
if 0 <= backslash_index < quote_end_index:
start_index = backslash_index + 2
else:
result['end_index'] = quote_end_index + len(quote)
return result
class UnderscoreTemplateLinter(object):
......@@ -1001,6 +1142,25 @@ class UnderscoreTemplateLinter(object):
return expressions
def _process_file(full_path, template_linters, options, out):
"""
For each linter, lints the provided file. This means finding and printing
violations.
Arguments:
full_path: The full path of the file to lint.
template_linters: A list of linting objects.
options: A list of the options.
out: output file
"""
directory = os.path.dirname(full_path)
file = os.path.basename(full_path)
for template_linter in template_linters:
results = template_linter.process_file(directory, file)
results.print_results(options, out)
def _process_current_walk(current_walk, template_linters, options, out):
"""
For each linter, lints all the files in the current os walk. This means
......@@ -1016,10 +1176,8 @@ def _process_current_walk(current_walk, template_linters, options, out):
walk_directory = os.path.normpath(current_walk[0])
walk_files = current_walk[2]
for walk_file in walk_files:
walk_file = os.path.normpath(walk_file)
for template_linter in template_linters:
results = template_linter.process_file(walk_directory, walk_file)
results.print_results(options, out)
full_path = os.path.join(walk_directory, walk_file)
_process_file(full_path, template_linters, options, out)
def _process_os_walk(starting_dir, template_linters, options, out):
......@@ -1037,33 +1195,60 @@ def _process_os_walk(starting_dir, template_linters, options, out):
_process_current_walk(current_walk, template_linters, options, out)
def _parse_arg(arg, option):
"""
Parses an argument searching for --[option]=[OPTION_VALUE]
Arguments:
arg: The system argument
option: The specific option to be searched for (e.g. "file")
Returns:
The option value for a match, or None if arg is not for this option
"""
if arg.startswith('--{}='.format(option)):
option_value = arg.split('=')[1]
if option_value.startswith("'") or option_value.startswith('"'):
option_value = option_value[1:-1]
return option_value
else:
return None
def main():
"""
Used to execute the linter. Use --help option for help.
Prints all of the violations.
Prints all violations.
"""
epilog = 'rules:\n'
for rule in Rules.__members__.values():
epilog += " {0[0]}: {0[1]}\n".format(rule.value)
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description='Checks that templates are safe.',
epilog=epilog
)
parser.add_argument('--quiet', dest='quiet', action='store_true', help='only display the filenames that contain violations')
parser.add_argument('--file', dest='file', nargs=1, default=None, help='a single file to lint')
parser.add_argument('--dir', dest='directory', nargs=1, default=['.'], help='the directory to lint (including sub-directories)')
#TODO: Use click
if '--help' in sys.argv:
print("Check that templates are safe.")
print("Options:")
print(" --quiet Just display the filenames that have violations.")
print("")
print("Rules:")
for rule in Rules.__members__.values():
print(" {0[0]}: {0[1]}".format(rule.value))
return
is_quiet = '--quiet' in sys.argv
# TODO --file=...
args = parser.parse_args()
options = {
'is_quiet': is_quiet,
'is_quiet': args.quiet,
}
template_linters = [MakoTemplateLinter(), UnderscoreTemplateLinter()]
_process_os_walk('.', template_linters, options, out=sys.stdout)
if args.file is not None:
if os.path.isfile(args.file[0]) is False:
raise ValueError("File [{}] is not a valid file.".format(args.file[0]))
_process_file(args.file[0], template_linters, options, out=sys.stdout)
else:
if os.path.exists(args.directory[0]) is False or os.path.isfile(args.directory[0]) is True:
raise ValueError("Directory [{}] is not a valid directory.".format(args.directory[0]))
_process_os_walk(args.directory[0], template_linters, options, out=sys.stdout)
if __name__ == "__main__":
......
......@@ -109,7 +109,14 @@ class TestMakoTemplateLinter(TestCase):
if data['violations'] > 0:
self.assertEqual(results.violations[0].rule, data['rule'])
def test_check_mako_expressions_in_html(self):
@data(
{'expression': '${x}', 'rule': None},
{'expression': '${{unbalanced}', 'rule': Rules.mako_unparseable_expression},
{'expression': '${x | n}', 'rule': Rules.mako_invalid_html_filter},
{'expression': '${x | h}', 'rule': Rules.mako_unwanted_html_filter},
{'expression': '${x | n, dump_js_escaped_json}', 'rule': Rules.mako_invalid_html_filter},
)
def test_check_mako_expressions_in_html(self, data):
"""
Test _check_mako_file_is_safe in html context provides appropriate violations
"""
......@@ -118,25 +125,12 @@ class TestMakoTemplateLinter(TestCase):
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
${x}
${'{{unbalanced-nested'}
${x | n}
${x | h}
${x | n, dump_js_escaped_json}
""")
{expression}
""".format(expression=data['expression']))
linter._check_mako_file_is_safe(mako_template, results)
self.assertEqual(len(results.violations), 4)
self.assertEqual(results.violations[0].rule, Rules.mako_unparsable_expression)
start_index = results.violations[0].expression['start_index']
self.assertEqual(mako_template[start_index:start_index + 24], "${'{{unbalanced-nested'}")
self.assertEqual(results.violations[1].rule, Rules.mako_invalid_html_filter)
self.assertEqual(results.violations[1].expression['expression'], "${x | n}")
self.assertEqual(results.violations[2].rule, Rules.mako_unwanted_html_filter)
self.assertEqual(results.violations[2].expression['expression'], "${x | h}")
self.assertEqual(results.violations[3].rule, Rules.mako_invalid_html_filter)
self.assertEqual(results.violations[3].expression['expression'], "${x | n, dump_js_escaped_json}")
self._validate_data_rule(data, results)
def test_check_mako_expression_display_name(self):
"""
......@@ -156,6 +150,87 @@ class TestMakoTemplateLinter(TestCase):
self.assertEqual(len(results.violations), 1)
self.assertEqual(results.violations[0].rule, Rules.mako_deprecated_display_name)
@data(
{
'expression':
textwrap.dedent("""
${"Mixed {span_start}text{span_end}".format(
span_start=HTML("<span>"),
span_end=HTML("</span>"),
)}
"""),
'rule': Rules.mako_html_requires_text
},
{
'expression':
textwrap.dedent("""
${Text("Mixed {span_start}text{span_end}").format(
span_start=HTML("<span>"),
span_end=HTML("</span>"),
)}
"""),
'rule': None
},
{
'expression':
textwrap.dedent("""
${"Mixed {span_start}{text}{span_end}".format(
span_start=HTML("<span>"),
text=Text("This should still break."),
span_end=HTML("</span>"),
)}
"""),
'rule': Rules.mako_html_requires_text
},
{
'expression':
textwrap.dedent("""
${Text("Mixed {link_start}text{link_end}".format(
link_start=HTML("<a href='{}'>").format(url),
link_end=HTML("</a>"),
))}
"""),
'rule': Rules.mako_close_before_format
},
{
'expression':
textwrap.dedent("""
${Text("Mixed {link_start}text{link_end}").format(
link_start=HTML("<a href='{}'>".format(url)),
link_end=HTML("</a>"),
)}
"""),
'rule': Rules.mako_close_before_format
},
{
'expression': """${ Text("text") }""",
'rule': Rules.mako_text_redundant
},
{
'expression': """${ HTML("<span></span>") }""",
'rule': None
},
{
'expression': """${ HTML("<span></span>") + "some other text" }""",
'rule': Rules.mako_html_alone
},
)
def test_check_mako_with_text_and_html(self, data):
"""
Test _check_mako_file_is_safe tests for proper use of Text() and Html().
"""
linter = MakoTemplateLinter()
results = FileResults('')
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
{expression}
""".format(expression=data['expression']))
linter._check_mako_file_is_safe(mako_template, results)
self._validate_data_rule(data, results)
def test_check_mako_expression_default_disabled(self):
"""
Test _check_mako_file_is_safe with disable pragma for safe-by-default
......@@ -228,7 +303,14 @@ class TestMakoTemplateLinter(TestCase):
self.assertEqual(len(results.violations), 1)
self.assertEqual(results.violations[0].rule, Rules.mako_missing_default)
def test_check_mako_expressions_in_javascript(self):
@data(
{'expression': '${x}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${{unbalanced}', 'rule': Rules.mako_unparseable_expression},
{'expression': '${x | n}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | h}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | n, dump_js_escaped_json}', 'rule': None},
)
def test_check_mako_expressions_in_javascript(self, data):
"""
Test _check_mako_file_is_safe in JavaScript script context provides
appropriate violations
......@@ -239,29 +321,19 @@ class TestMakoTemplateLinter(TestCase):
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
<script>
${x}
${'{{unbalanced-nested'}
${x | n}
${x | h}
${x | n, dump_js_escaped_json}
"${x-with-quotes | n, js_escaped_string}"
{expression}
</script>
""")
""".format(expression=data['expression']))
linter._check_mako_file_is_safe(mako_template, results)
self.assertEqual(len(results.violations), 4)
self.assertEqual(results.violations[0].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[0].expression['expression'], "${x}")
self.assertEqual(results.violations[1].rule, Rules.mako_unparsable_expression)
start_index = results.violations[1].expression['start_index']
self.assertEqual(mako_template[start_index:start_index + 24], "${'{{unbalanced-nested'}")
self.assertEqual(results.violations[2].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[2].expression['expression'], "${x | n}")
self.assertEqual(results.violations[3].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[3].expression['expression'], "${x | h}")
self._validate_data_rule(data, results)
def test_check_mako_expressions_in_require_js(self):
@data(
{'expression': '${x}', 'rule': Rules.mako_invalid_js_filter},
{'expression': '${x | n, js_escaped_string}', 'rule': None},
)
def test_check_mako_expressions_in_require_js(self, data):
"""
Test _check_mako_file_is_safe in JavaScript require context provides
appropriate violations
......@@ -271,17 +343,14 @@ class TestMakoTemplateLinter(TestCase):
mako_template = textwrap.dedent("""
<%page expression_filter="h"/>
<%static:require_module module_name="${x}" class_name="TestFactory">
${x}
${x | n, js_escaped_string}
<%static:require_module module_name="${{x}}" class_name="TestFactory">
{expression}
</%static:require_module>
""")
""".format(expression=data['expression']))
linter._check_mako_file_is_safe(mako_template, results)
self.assertEqual(len(results.violations), 1)
self.assertEqual(results.violations[0].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[0].expression['expression'], "${x}")
self._validate_data_rule(data, results)
@data(
{'media_type': 'text/javascript', 'expected_violations': 0},
......@@ -339,7 +408,20 @@ class TestMakoTemplateLinter(TestCase):
self.assertEqual(results.violations[3].rule, Rules.mako_invalid_js_filter)
self.assertEqual(results.violations[4].rule, Rules.mako_unwanted_html_filter)
def test_expression_detailed_results(self):
@data(
{'template': "\n${x | n}", 'parseable': True},
{
'template': textwrap.dedent(
"""
<div>${(
'tabbed-multi-line-expression'
) | n}</div>
"""),
'parseable': True
},
{'template': "${{unparseable}", 'parseable': False},
)
def test_expression_detailed_results(self, data):
"""
Test _check_mako_file_is_safe provides detailed results, including line
numbers, columns, and line
......@@ -347,86 +429,115 @@ class TestMakoTemplateLinter(TestCase):
linter = MakoTemplateLinter()
results = FileResults('')
mako_template = textwrap.dedent("""
${x | n}
<div>${(
'tabbed-multi-line-expression'
) | n}</div>
${'{{unbalanced-nested' | n}
""")
linter._check_mako_file_is_safe(mako_template, results)
linter._check_mako_file_is_safe(data['template'], results)
self.assertEqual(len(results.violations), 4)
self.assertEqual(len(results.violations), 2)
self.assertEqual(results.violations[0].rule, Rules.mako_missing_default)
self.assertEqual(results.violations[1].start_line, 2)
self.assertEqual(results.violations[1].start_column, 1)
self.assertEqual(results.violations[1].end_line, 2)
self.assertEqual(results.violations[1].end_column, 8)
self.assertEqual(len(results.violations[1].lines), 1)
self.assertEqual(results.violations[1].lines[0], "${x | n}")
self.assertEqual(results.violations[2].start_line, 3)
self.assertEqual(results.violations[2].start_column, 10)
self.assertEqual(results.violations[2].end_line, 5)
self.assertEqual(results.violations[2].end_column, 10)
self.assertEqual(len(results.violations[2].lines), 3)
self.assertEqual(results.violations[2].lines[0], " <div>${(")
self.assertEqual(
results.violations[2].lines[1],
" 'tabbed-multi-line-expression'"
)
self.assertEqual(results.violations[2].lines[2], " ) | n}</div>")
self.assertEqual(results.violations[3].start_line, 6)
self.assertEqual(results.violations[3].start_column, 1)
self.assertEqual(results.violations[3].end_line, 6)
self.assertEqual(results.violations[3].end_column, "?")
self.assertEqual(len(results.violations[3].lines), 1)
self.assertEqual(
results.violations[3].lines[0],
"${'{{unbalanced-nested' | n}"
)
def test_find_mako_expressions(self):
"""
Test _find_mako_expressions finds appropriate expressions
violation = results.violations[1]
lines = list(data['template'].splitlines())
self.assertTrue("${" in lines[violation.start_line - 1])
self.assertTrue(lines[violation.start_line - 1].startswith("${", violation.start_column - 1))
if data['parseable']:
self.assertTrue("}" in lines[violation.end_line - 1])
self.assertTrue(lines[violation.end_line - 1].startswith("}", violation.end_column - 1))
else:
self.assertEqual(violation.start_line, violation.end_line)
self.assertEqual(violation.end_column, "?")
self.assertEqual(len(violation.lines), violation.end_line - violation.start_line + 1)
for line_index in range(0, len(violation.lines)):
self.assertEqual(violation.lines[line_index], lines[line_index + violation.start_line - 1])
@data(
{'template': "${x}"},
{'template': "\n ${x}"},
{'template': "${x} "},
{'template': "${{test-balanced-delims}} "},
{'template': "${'{unbalanced in string'}"},
{'template': "${'unbalanced in string}'}"},
{'template': "${(\n 'tabbed-multi-line-expression'\n )}"},
)
def test_find_mako_expressions(self, data):
"""
Test _find_mako_expressions for parseable expressions
"""
linter = MakoTemplateLinter()
mako_template = textwrap.dedent("""
${x}
${tabbed-x}
${(
'tabbed-multi-line-expression'
)}
${'{{unbalanced-nested'}
${'{{nested}}'}
<div>no expression</div>
""")
expressions = linter._find_mako_expressions(data['template'])
expressions = linter._find_mako_expressions(mako_template)
self.assertEqual(len(expressions), 1)
start_index = expressions[0]['start_index']
end_index = expressions[0]['end_index']
self.assertEqual(data['template'][start_index:end_index + 1], data['template'].strip())
self.assertEqual(expressions[0]['expression'], data['template'].strip())
self.assertEqual(len(expressions), 5)
self._validate_expression(mako_template, expressions[0], '${x}')
self._validate_expression(mako_template, expressions[1], '${tabbed-x}')
self._validate_expression(mako_template, expressions[2], "${(\n 'tabbed-multi-line-expression'\n )}")
@data(
{'template': " ${{unparseable} ${}", 'start_index': 1},
{'template': " ${'unparseable} ${}", 'start_index': 1},
)
def test_find_mako_expressions(self, data):
"""
Test _find_mako_expressions for unparseable expressions
"""
linter = MakoTemplateLinter()
# won't parse unbalanced nested {}'s
unbalanced_expression = "${'{{unbalanced-nested'}"
self.assertEqual(expressions[3]['end_index'], -1)
start_index = expressions[3]['start_index']
self.assertEqual(mako_template[start_index:start_index + len(unbalanced_expression)], unbalanced_expression)
self.assertEqual(expressions[3]['expression'], None)
expressions = linter._find_mako_expressions(data['template'])
self.assertTrue(2 <= len(expressions))
self.assertEqual(expressions[0]['start_index'], data['start_index'])
self.assertIsNone(expressions[0]['expression'])
self._validate_expression(mako_template, expressions[4], "${'{{nested}}'}")
@data(
{'template': """${""}""", 'start_index': 0, 'end_index': 5, 'expected_index': 2},
{'template': """${''}""", 'start_index': 0, 'end_index': 5, 'expected_index': 2},
{'template': """${"''"}""", 'start_index': 0, 'end_index': 7, 'expected_index': 2},
{'template': """${'""'}""", 'start_index': 0, 'end_index': 7, 'expected_index': 2},
{'template': """${'""'}""", 'start_index': 3, 'end_index': 7, 'expected_index': 3},
{'template': """${'""'}""", 'start_index': 0, 'end_index': 1, 'expected_index': -1},
)
def test_find_string_start(self, data):
"""
Test _find_string_start helper
"""
linter = MakoTemplateLinter()
string_start_index = linter._find_string_start(data['template'], data['start_index'], data['end_index'])
self.assertEqual(string_start_index, data['expected_index'])
@data(
{
'template': '${""}',
'result': {'start_index': 2, 'end_index': 4, 'quote_length': 1}
},
{
'template': "${'Hello'}",
'result': {'start_index': 2, 'end_index': 9, 'quote_length': 1}
},
{
'template': '${""" triple """}',
'result': {'start_index': 2, 'end_index': 16, 'quote_length': 3}
},
{
'template': r""" ${" \" \\"} """,
'result': {'start_index': 3, 'end_index': 11, 'quote_length': 1}
},
)
def test_parse_string(self, data):
"""
Test _parse_string helper
"""
linter = MakoTemplateLinter()
result = linter._parse_string(data['template'], data['result']['start_index'])
def _validate_expression(self, template_string, expression, expected_expression):
start_index = expression['start_index']
end_index = expression['end_index']
self.assertEqual(template_string[start_index:end_index + 1], expected_expression)
self.assertEqual(expression['expression'], expected_expression)
self.assertDictEqual(result, data['result'])
def _validate_data_rule(self, data, results):
if data['rule'] is None:
self.assertEqual(len(results.violations), 0)
else:
self.assertEqual(len(results.violations), 1)
self.assertEqual(results.violations[0].rule, data['rule'])
@ddt
......
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