Define XSS-linter rules on Linter classes.
This commit is contained in:
@@ -11,7 +11,6 @@ from ddt import data, ddt
|
||||
|
||||
from xsslint.linters import JavaScriptLinter, MakoTemplateLinter, PythonLinter, UnderscoreTemplateLinter
|
||||
from xsslint.reporting import FileResults
|
||||
from xsslint.rules import Rules
|
||||
from xsslint.utils import ParseString
|
||||
|
||||
|
||||
@@ -64,6 +63,8 @@ class TestUnderscoreTemplateLinter(TestLinter):
|
||||
Test UnderscoreTemplateLinter
|
||||
"""
|
||||
|
||||
ruleset = UnderscoreTemplateLinter.ruleset
|
||||
|
||||
def test_check_underscore_file_is_safe(self):
|
||||
"""
|
||||
Test check_underscore_file_is_safe with safe template
|
||||
@@ -101,8 +102,8 @@ class TestUnderscoreTemplateLinter(TestLinter):
|
||||
linter.check_underscore_file_is_safe(template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 2)
|
||||
self.assertEqual(results.violations[0].rule, Rules.underscore_not_escaped)
|
||||
self.assertEqual(results.violations[1].rule, Rules.underscore_not_escaped)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.underscore_not_escaped)
|
||||
self.assertEqual(results.violations[1].rule, self.ruleset.underscore_not_escaped)
|
||||
|
||||
@data(
|
||||
{
|
||||
@@ -210,6 +211,9 @@ class TestJavaScriptLinter(TestLinter):
|
||||
"""
|
||||
Test JavaScriptLinter
|
||||
"""
|
||||
|
||||
ruleset = JavaScriptLinter.ruleset + UnderscoreTemplateLinter.ruleset
|
||||
|
||||
@data(
|
||||
{'template': 'var m = "Plain text " + message + "plain text"', 'rule': None},
|
||||
{'template': 'var m = "檌檒濦 " + message + "plain text"', 'rule': None},
|
||||
@@ -219,14 +223,14 @@ class TestJavaScriptLinter(TestLinter):
|
||||
""" value: gettext("Copy Email To Editor"), id: 'copy_email_' + email_id))"""),
|
||||
'rule': None
|
||||
},
|
||||
{'template': 'var m = "<p>" + message + "</p>"', 'rule': Rules.javascript_concat_html},
|
||||
{'template': 'var m = "<p>" + message + "</p>"', 'rule': ruleset.javascript_concat_html},
|
||||
{
|
||||
'template': r'var m = "<p>\"escaped quote\"" + message + "\"escaped quote\"</p>"',
|
||||
'rule': Rules.javascript_concat_html
|
||||
'rule': ruleset.javascript_concat_html
|
||||
},
|
||||
{'template': ' // var m = "<p>" + commentedOutMessage + "</p>"', 'rule': None},
|
||||
{'template': 'var m = " <p> " + message + " </p> "', 'rule': Rules.javascript_concat_html},
|
||||
{'template': 'var m = " <p> " + message + " broken string', 'rule': Rules.javascript_concat_html},
|
||||
{'template': 'var m = " <p> " + message + " </p> "', 'rule': ruleset.javascript_concat_html},
|
||||
{'template': 'var m = " <p> " + message + " broken string', 'rule': ruleset.javascript_concat_html},
|
||||
)
|
||||
def test_concat_with_html(self, data):
|
||||
"""
|
||||
@@ -247,16 +251,16 @@ class TestJavaScriptLinter(TestLinter):
|
||||
# plain text is ok because any & will be escaped, and it stops false
|
||||
# negatives on some other objects with an append() method
|
||||
{'template': 'test.append("plain text")', 'rule': None},
|
||||
{'template': 'test.append("<div/>")', 'rule': Rules.javascript_jquery_append},
|
||||
{'template': 'test.append("<div/>")', 'rule': ruleset.javascript_jquery_append},
|
||||
{'template': 'graph.svg.append("g")', 'rule': None},
|
||||
{'template': 'test.append( $( "<div>" ) )', 'rule': None},
|
||||
{'template': 'test.append($("<div>"))', 'rule': None},
|
||||
{'template': 'test.append($("<div/>"))', 'rule': None},
|
||||
{'template': 'test.append(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
|
||||
{'template': 'HtmlUtils.append($el, someHtml)', 'rule': None},
|
||||
{'template': 'test.append("fail on concat" + test.render().el)', 'rule': Rules.javascript_jquery_append},
|
||||
{'template': 'test.append("fail on concat" + testEl)', 'rule': Rules.javascript_jquery_append},
|
||||
{'template': 'test.append(message)', 'rule': Rules.javascript_jquery_append},
|
||||
{'template': 'test.append("fail on concat" + test.render().el)', 'rule': ruleset.javascript_jquery_append},
|
||||
{'template': 'test.append("fail on concat" + testEl)', 'rule': ruleset.javascript_jquery_append},
|
||||
{'template': 'test.append(message)', 'rule': ruleset.javascript_jquery_append},
|
||||
)
|
||||
def test_jquery_append(self, data):
|
||||
"""
|
||||
@@ -281,10 +285,10 @@ class TestJavaScriptLinter(TestLinter):
|
||||
{'template': 'test.prepend($("<div/>"))', 'rule': None},
|
||||
{'template': 'test.prepend(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
|
||||
{'template': 'HtmlUtils.prepend($el, someHtml)', 'rule': None},
|
||||
{'template': 'test.prepend("broken string)', 'rule': Rules.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend("fail on concat" + test.render().el)', 'rule': Rules.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend("fail on concat" + testEl)', 'rule': Rules.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend(message)', 'rule': Rules.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend("broken string)', 'rule': ruleset.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend("fail on concat" + test.render().el)', 'rule': ruleset.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend("fail on concat" + testEl)', 'rule': ruleset.javascript_jquery_prepend},
|
||||
{'template': 'test.prepend(message)', 'rule': ruleset.javascript_jquery_prepend},
|
||||
)
|
||||
def test_jquery_prepend(self, data):
|
||||
"""
|
||||
@@ -307,14 +311,14 @@ class TestJavaScriptLinter(TestLinter):
|
||||
{'template': 'test.replaceAll(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
|
||||
{'template': 'test.replaceWith(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
|
||||
{'template': 'test.replaceWith(edx.HtmlUtils.HTML(htmlString).toString())', 'rule': None},
|
||||
{'template': 'test.unwrap(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.wrap(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.wrapAll(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.wrapInner(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.after(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.before(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.replaceAll(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.replaceWith(anything)', 'rule': Rules.javascript_jquery_insertion},
|
||||
{'template': 'test.unwrap(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.wrap(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.wrapAll(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.wrapInner(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.after(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.before(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.replaceAll(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
{'template': 'test.replaceWith(anything)', 'rule': ruleset.javascript_jquery_insertion},
|
||||
)
|
||||
def test_jquery_insertion(self, data):
|
||||
"""
|
||||
@@ -341,11 +345,11 @@ class TestJavaScriptLinter(TestLinter):
|
||||
{'template': 'testEl.prependTo(target);', 'rule': None},
|
||||
{'template': 'testEl.insertAfter(target);', 'rule': None},
|
||||
{'template': 'testEl.insertBefore(target);', 'rule': None},
|
||||
{'template': 'anycall().appendTo(target)', 'rule': Rules.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.appendTo(target)', 'rule': Rules.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.prependTo(target)', 'rule': Rules.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.insertAfter(target)', 'rule': Rules.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.insertBefore(target)', 'rule': Rules.javascript_jquery_insert_into_target},
|
||||
{'template': 'anycall().appendTo(target)', 'rule': ruleset.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.appendTo(target)', 'rule': ruleset.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.prependTo(target)', 'rule': ruleset.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.insertAfter(target)', 'rule': ruleset.javascript_jquery_insert_into_target},
|
||||
{'template': 'anything.insertBefore(target)', 'rule': ruleset.javascript_jquery_insert_into_target},
|
||||
)
|
||||
def test_jquery_insert_to_target(self, data):
|
||||
"""
|
||||
@@ -368,10 +372,10 @@ class TestJavaScriptLinter(TestLinter):
|
||||
{'template': 'test.html("")', 'rule': None},
|
||||
{'template': 'test.html(HtmlUtils.ensureHtml(htmlSnippet).toString())', 'rule': None},
|
||||
{'template': 'HtmlUtils.setHtml($el, someHtml)', 'rule': None},
|
||||
{'template': 'test.html("any string")', 'rule': Rules.javascript_jquery_html},
|
||||
{'template': 'test.html("broken string)', 'rule': Rules.javascript_jquery_html},
|
||||
{'template': 'test.html("檌檒濦")', 'rule': Rules.javascript_jquery_html},
|
||||
{'template': 'test.html(anything)', 'rule': Rules.javascript_jquery_html},
|
||||
{'template': 'test.html("any string")', 'rule': ruleset.javascript_jquery_html},
|
||||
{'template': 'test.html("broken string)', 'rule': ruleset.javascript_jquery_html},
|
||||
{'template': 'test.html("檌檒濦")', 'rule': ruleset.javascript_jquery_html},
|
||||
{'template': 'test.html(anything)', 'rule': ruleset.javascript_jquery_html},
|
||||
)
|
||||
def test_jquery_html(self, data):
|
||||
"""
|
||||
@@ -386,7 +390,7 @@ class TestJavaScriptLinter(TestLinter):
|
||||
@data(
|
||||
{'template': 'StringUtils.interpolate()', 'rule': None},
|
||||
{'template': 'HtmlUtils.interpolateHtml()', 'rule': None},
|
||||
{'template': 'interpolate(anything)', 'rule': Rules.javascript_interpolate},
|
||||
{'template': 'interpolate(anything)', 'rule': ruleset.javascript_interpolate},
|
||||
)
|
||||
def test_javascript_interpolate(self, data):
|
||||
"""
|
||||
@@ -401,7 +405,7 @@ class TestJavaScriptLinter(TestLinter):
|
||||
|
||||
@data(
|
||||
{'template': '_.escape(message)', 'rule': None},
|
||||
{'template': 'anything.escape(message)', 'rule': Rules.javascript_escape},
|
||||
{'template': 'anything.escape(message)', 'rule': ruleset.javascript_escape},
|
||||
)
|
||||
def test_javascript_interpolate(self, data):
|
||||
"""
|
||||
@@ -420,13 +424,16 @@ class TestPythonLinter(TestLinter):
|
||||
"""
|
||||
Test PythonLinter
|
||||
"""
|
||||
|
||||
ruleset = PythonLinter.ruleset
|
||||
|
||||
@data(
|
||||
{'template': 'm = "Plain text " + message + "plain text"', 'rule': None},
|
||||
{'template': 'm = "檌檒濦 " + message + "plain text"', 'rule': None},
|
||||
{'template': ' # m = "<p>" + commentedOutMessage + "</p>"', 'rule': None},
|
||||
{'template': 'm = "<p>" + message + "</p>"', 'rule': [Rules.python_concat_html, Rules.python_concat_html]},
|
||||
{'template': 'm = " <p> " + message + " </p> "', 'rule': [Rules.python_concat_html, Rules.python_concat_html]},
|
||||
{'template': 'm = " <p> " + message + " broken string', 'rule': Rules.python_parse_error},
|
||||
{'template': 'm = "<p>" + message + "</p>"', 'rule': [ruleset.python_concat_html, ruleset.python_concat_html]},
|
||||
{'template': 'm = " <p> " + message + " </p> "', 'rule': [ruleset.python_concat_html, ruleset.python_concat_html]},
|
||||
{'template': 'm = " <p> " + message + " broken string', 'rule': ruleset.python_parse_error},
|
||||
)
|
||||
def test_concat_with_html(self, data):
|
||||
"""
|
||||
@@ -456,7 +463,7 @@ class TestPythonLinter(TestLinter):
|
||||
linter.check_python_file_is_safe(python_file, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.python_deprecated_display_name)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.python_deprecated_display_name)
|
||||
|
||||
def test_check_custom_escaping(self):
|
||||
"""
|
||||
@@ -472,7 +479,7 @@ class TestPythonLinter(TestLinter):
|
||||
linter.check_python_file_is_safe(python_file, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.python_custom_escape)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.python_custom_escape)
|
||||
|
||||
@data(
|
||||
{
|
||||
@@ -493,7 +500,7 @@ class TestPythonLinter(TestLinter):
|
||||
span_end=HTML("</span>"),
|
||||
)
|
||||
"""),
|
||||
'rule': Rules.python_requires_html_or_text
|
||||
'rule': ruleset.python_requires_html_or_text
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -504,7 +511,7 @@ class TestPythonLinter(TestLinter):
|
||||
span_end=HTML("</span>"),
|
||||
)
|
||||
"""),
|
||||
'rule': Rules.python_requires_html_or_text
|
||||
'rule': ruleset.python_requires_html_or_text
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -514,7 +521,7 @@ class TestPythonLinter(TestLinter):
|
||||
link_end=HTML("</a>"),
|
||||
))
|
||||
"""),
|
||||
'rule': [Rules.python_close_before_format, Rules.python_requires_html_or_text]
|
||||
'rule': [ruleset.python_close_before_format, ruleset.python_requires_html_or_text]
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -524,7 +531,7 @@ class TestPythonLinter(TestLinter):
|
||||
link_end=HTML("</a>"),
|
||||
)
|
||||
"""),
|
||||
'rule': Rules.python_close_before_format
|
||||
'rule': ruleset.python_close_before_format
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -536,10 +543,10 @@ class TestPythonLinter(TestLinter):
|
||||
"""),
|
||||
'rule':
|
||||
[
|
||||
Rules.python_close_before_format,
|
||||
Rules.python_requires_html_or_text,
|
||||
Rules.python_close_before_format,
|
||||
Rules.python_requires_html_or_text
|
||||
ruleset.python_close_before_format,
|
||||
ruleset.python_requires_html_or_text,
|
||||
ruleset.python_close_before_format,
|
||||
ruleset.python_requires_html_or_text
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -550,7 +557,7 @@ class TestPythonLinter(TestLinter):
|
||||
span_end="</span>",
|
||||
)
|
||||
"""),
|
||||
'rule': [Rules.python_wrap_html, Rules.python_wrap_html]
|
||||
'rule': [ruleset.python_wrap_html, ruleset.python_wrap_html]
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -581,11 +588,11 @@ class TestPythonLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'python': r"""msg = '<a href="{}"'.format(url)""",
|
||||
'rule': Rules.python_wrap_html
|
||||
'rule': ruleset.python_wrap_html
|
||||
},
|
||||
{
|
||||
'python': r"""msg = '{}</p>'.format(message)""",
|
||||
'rule': Rules.python_wrap_html
|
||||
'rule': ruleset.python_wrap_html
|
||||
},
|
||||
{
|
||||
'python': r"""r'regex with {} and named group(?P<id>\d+)?$'.format(test)""",
|
||||
@@ -593,7 +600,7 @@ class TestPythonLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'python': r"""msg = '<a href="%s"' % url""",
|
||||
'rule': Rules.python_interpolate_html
|
||||
'rule': ruleset.python_interpolate_html
|
||||
},
|
||||
{
|
||||
'python':
|
||||
@@ -606,11 +613,11 @@ class TestPythonLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'python': r"""msg = '%s</p>' % message""",
|
||||
'rule': Rules.python_interpolate_html
|
||||
'rule': ruleset.python_interpolate_html
|
||||
},
|
||||
{
|
||||
'python': "msg = HTML('<span></span>'",
|
||||
'rule': Rules.python_parse_error
|
||||
'rule': ruleset.python_parse_error
|
||||
},
|
||||
)
|
||||
def test_check_python_with_text_and_html(self, data):
|
||||
@@ -651,11 +658,11 @@ class TestPythonLinter(TestLinter):
|
||||
results.violations.sort(key=lambda violation: violation.sort_key())
|
||||
|
||||
self.assertEqual(len(results.violations), 5)
|
||||
self.assertEqual(results.violations[0].rule, Rules.python_wrap_html)
|
||||
self.assertEqual(results.violations[1].rule, Rules.python_requires_html_or_text)
|
||||
self.assertEqual(results.violations[2].rule, Rules.python_close_before_format)
|
||||
self.assertEqual(results.violations[3].rule, Rules.python_wrap_html)
|
||||
self.assertEqual(results.violations[4].rule, Rules.python_interpolate_html)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.python_wrap_html)
|
||||
self.assertEqual(results.violations[1].rule, self.ruleset.python_requires_html_or_text)
|
||||
self.assertEqual(results.violations[2].rule, self.ruleset.python_close_before_format)
|
||||
self.assertEqual(results.violations[3].rule, self.ruleset.python_wrap_html)
|
||||
self.assertEqual(results.violations[4].rule, self.ruleset.python_interpolate_html)
|
||||
|
||||
@data(
|
||||
{
|
||||
@@ -667,7 +674,7 @@ class TestPythonLinter(TestLinter):
|
||||
</div>
|
||||
''').format(response=response_text)
|
||||
""",
|
||||
'rule': Rules.python_wrap_html,
|
||||
'rule': ruleset.python_wrap_html,
|
||||
'start_line': 2,
|
||||
},
|
||||
{
|
||||
@@ -683,7 +690,7 @@ class TestPythonLinter(TestLinter):
|
||||
</div>
|
||||
''').format(response=response_text)
|
||||
""",
|
||||
'rule': Rules.python_wrap_html,
|
||||
'rule': ruleset.python_wrap_html,
|
||||
'start_line': 6,
|
||||
},
|
||||
{
|
||||
@@ -695,7 +702,7 @@ class TestPythonLinter(TestLinter):
|
||||
'''
|
||||
response_str = '''<h3 class="result">{response}</h3>'''.format(response=response_text)
|
||||
""",
|
||||
'rule': Rules.python_wrap_html,
|
||||
'rule': ruleset.python_wrap_html,
|
||||
'start_line': 6,
|
||||
},
|
||||
{
|
||||
@@ -712,7 +719,7 @@ class TestPythonLinter(TestLinter):
|
||||
</div>
|
||||
''').format(response=response_text)
|
||||
""",
|
||||
'rule': Rules.python_wrap_html,
|
||||
'rule': ruleset.python_wrap_html,
|
||||
'start_line': 6,
|
||||
},
|
||||
)
|
||||
@@ -738,6 +745,10 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
Test MakoTemplateLinter
|
||||
"""
|
||||
|
||||
ruleset = (
|
||||
MakoTemplateLinter.ruleset + PythonLinter.ruleset + JavaScriptLinter.ruleset + UnderscoreTemplateLinter.ruleset
|
||||
)
|
||||
|
||||
@data(
|
||||
{'directory': 'lms/templates', 'expected': True},
|
||||
{'directory': 'lms/templates/support', 'expected': True},
|
||||
@@ -767,13 +778,13 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'template': '\n ## <%page expression_filter="h"/>',
|
||||
'rule': Rules.mako_missing_default
|
||||
'rule': ruleset.mako_missing_default
|
||||
},
|
||||
{
|
||||
'template':
|
||||
'\n <%page expression_filter="h" /> '
|
||||
'\n <%page args="section_data"/>',
|
||||
'rule': Rules.mako_multiple_page_tags
|
||||
'rule': ruleset.mako_multiple_page_tags
|
||||
},
|
||||
{
|
||||
'template':
|
||||
@@ -783,16 +794,16 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'template': '\n <%page args="section_data" /> ',
|
||||
'rule': Rules.mako_missing_default
|
||||
'rule': ruleset.mako_missing_default
|
||||
},
|
||||
{
|
||||
'template':
|
||||
'\n <%page args="section_data"/> <some-other-tag expression_filter="h" /> ',
|
||||
'rule': Rules.mako_missing_default
|
||||
'rule': ruleset.mako_missing_default
|
||||
},
|
||||
{
|
||||
'template': '\n',
|
||||
'rule': Rules.mako_missing_default
|
||||
'rule': ruleset.mako_missing_default
|
||||
},
|
||||
)
|
||||
def test_check_page_default(self, data):
|
||||
@@ -811,12 +822,12 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
|
||||
@data(
|
||||
{'expression': '${x}', 'rule': None},
|
||||
{'expression': '${{unbalanced}', 'rule': Rules.mako_unparseable_expression},
|
||||
{'expression': '${x | n}', 'rule': Rules.mako_invalid_html_filter},
|
||||
{'expression': '${{unbalanced}', 'rule': ruleset.mako_unparseable_expression},
|
||||
{'expression': '${x | n}', 'rule': ruleset.mako_invalid_html_filter},
|
||||
{'expression': '${x | n, decode.utf8}', 'rule': None},
|
||||
{'expression': '${x | h}', 'rule': Rules.mako_unwanted_html_filter},
|
||||
{'expression': '${x | h}', 'rule': ruleset.mako_unwanted_html_filter},
|
||||
{'expression': ' ## ${commented_out | h}', 'rule': None},
|
||||
{'expression': '${x | n, dump_js_escaped_json}', 'rule': Rules.mako_invalid_html_filter},
|
||||
{'expression': '${x | n, dump_js_escaped_json}', 'rule': ruleset.mako_invalid_html_filter},
|
||||
)
|
||||
def test_check_mako_expressions_in_html(self, data):
|
||||
"""
|
||||
@@ -850,7 +861,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.python_deprecated_display_name)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.python_deprecated_display_name)
|
||||
|
||||
@data(
|
||||
{
|
||||
@@ -867,7 +878,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
text = _("Introductions, prerequisites, FAQs that are used on %s (formatted in HTML)") % a_link
|
||||
%>
|
||||
"""),
|
||||
'rule': [Rules.python_wrap_html, Rules.python_concat_html, Rules.python_wrap_html]
|
||||
'rule': [ruleset.python_wrap_html, ruleset.python_concat_html, ruleset.python_wrap_html]
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -877,7 +888,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
span_end=HTML("</span>"),
|
||||
)}
|
||||
"""),
|
||||
'rule': Rules.python_requires_html_or_text
|
||||
'rule': ruleset.python_requires_html_or_text
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -898,7 +909,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
span_end=HTML("</span>"),
|
||||
)}
|
||||
"""),
|
||||
'rule': Rules.python_requires_html_or_text
|
||||
'rule': ruleset.python_requires_html_or_text
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -908,7 +919,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
link_end=HTML("</a>"),
|
||||
))}
|
||||
"""),
|
||||
'rule': [Rules.python_close_before_format, Rules.python_requires_html_or_text]
|
||||
'rule': [ruleset.python_close_before_format, ruleset.python_requires_html_or_text]
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -918,7 +929,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
link_end=HTML("</a>"),
|
||||
)}
|
||||
"""),
|
||||
'rule': Rules.python_close_before_format
|
||||
'rule': ruleset.python_close_before_format
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -928,7 +939,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
span_end="</span>",
|
||||
)}
|
||||
"""),
|
||||
'rule': [Rules.python_wrap_html, Rules.python_wrap_html]
|
||||
'rule': [ruleset.python_wrap_html, ruleset.python_wrap_html]
|
||||
},
|
||||
{
|
||||
'expression':
|
||||
@@ -950,11 +961,11 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'expression': "${'<span></span>'}",
|
||||
'rule': Rules.python_wrap_html
|
||||
'rule': ruleset.python_wrap_html
|
||||
},
|
||||
{
|
||||
'expression': "${'Embedded HTML <strong></strong>'}",
|
||||
'rule': Rules.python_wrap_html
|
||||
'rule': ruleset.python_wrap_html
|
||||
},
|
||||
{
|
||||
'expression': "${ HTML('<span></span>') }",
|
||||
@@ -966,19 +977,19 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
},
|
||||
{
|
||||
'expression': "${ '<span></span>' + 'some other text' }",
|
||||
'rule': [Rules.python_concat_html, Rules.python_wrap_html]
|
||||
'rule': [ruleset.python_concat_html, ruleset.python_wrap_html]
|
||||
},
|
||||
{
|
||||
'expression': "${ HTML('<span>missing closing parentheses.</span>' }",
|
||||
'rule': Rules.python_parse_error
|
||||
'rule': ruleset.python_parse_error
|
||||
},
|
||||
{
|
||||
'expression': "${'Rock & Roll'}",
|
||||
'rule': Rules.mako_html_entities
|
||||
'rule': ruleset.mako_html_entities
|
||||
},
|
||||
{
|
||||
'expression': "${'Rock & Roll'}",
|
||||
'rule': Rules.mako_html_entities
|
||||
'rule': ruleset.mako_html_entities
|
||||
},
|
||||
)
|
||||
def test_check_mako_with_text_and_html(self, data):
|
||||
@@ -1010,7 +1021,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.mako_missing_default)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.mako_missing_default)
|
||||
|
||||
def test_check_mako_expression_default_disabled(self):
|
||||
"""
|
||||
@@ -1099,13 +1110,13 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.mako_missing_default)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.mako_missing_default)
|
||||
|
||||
@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}', 'rule': ruleset.mako_invalid_js_filter},
|
||||
{'expression': '${{unbalanced}', 'rule': ruleset.mako_unparseable_expression},
|
||||
{'expression': '${x | n}', 'rule': ruleset.mako_invalid_js_filter},
|
||||
{'expression': '${x | h}', 'rule': ruleset.mako_invalid_js_filter},
|
||||
{'expression': '${x | n, dump_js_escaped_json}', 'rule': None},
|
||||
{'expression': '${x | n, decode.utf8}', 'rule': None},
|
||||
)
|
||||
@@ -1132,7 +1143,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
self._validate_data_rules(data, results)
|
||||
|
||||
@data(
|
||||
{'expression': '${x}', 'rule': Rules.mako_invalid_js_filter},
|
||||
{'expression': '${x}', 'rule': ruleset.mako_invalid_js_filter},
|
||||
{'expression': '"${x | n, js_escaped_string}"', 'rule': None},
|
||||
)
|
||||
def test_check_mako_expressions_in_require_module(self, data):
|
||||
@@ -1158,7 +1169,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
self._validate_data_rules(data, results)
|
||||
|
||||
@data(
|
||||
{'expression': '${x}', 'rule': Rules.mako_invalid_js_filter},
|
||||
{'expression': '${x}', 'rule': ruleset.mako_invalid_js_filter},
|
||||
{'expression': '"${x | n, js_escaped_string}"', 'rule': None},
|
||||
)
|
||||
def test_check_mako_expressions_in_require_js(self, data):
|
||||
@@ -1190,8 +1201,8 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
{'media_type': 'application/javascript', 'rule': None},
|
||||
{'media_type': 'text/x-mathjax-config', 'rule': None},
|
||||
{'media_type': 'json/xblock-args', 'rule': None},
|
||||
{'media_type': 'text/template', 'rule': Rules.mako_invalid_html_filter},
|
||||
{'media_type': 'unknown/type', 'rule': Rules.mako_unknown_context},
|
||||
{'media_type': 'text/template', 'rule': ruleset.mako_invalid_html_filter},
|
||||
{'media_type': 'unknown/type', 'rule': ruleset.mako_unknown_context},
|
||||
)
|
||||
def test_check_mako_expressions_in_script_type(self, data):
|
||||
"""
|
||||
@@ -1242,13 +1253,13 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 7)
|
||||
self.assertEqual(results.violations[0].rule, Rules.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[1].rule, Rules.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[2].rule, Rules.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[3].rule, Rules.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[4].rule, Rules.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[5].rule, Rules.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[6].rule, Rules.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[1].rule, self.ruleset.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[2].rule, self.ruleset.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[3].rule, self.ruleset.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[4].rule, self.ruleset.mako_unwanted_html_filter)
|
||||
self.assertEqual(results.violations[5].rule, self.ruleset.mako_invalid_js_filter)
|
||||
self.assertEqual(results.violations[6].rule, self.ruleset.mako_unwanted_html_filter)
|
||||
|
||||
def test_check_mako_expressions_javascript_strings(self):
|
||||
"""
|
||||
@@ -1282,9 +1293,9 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 3)
|
||||
self.assertEqual(results.violations[0].rule, Rules.mako_js_missing_quotes)
|
||||
self.assertEqual(results.violations[1].rule, Rules.mako_js_html_string)
|
||||
self.assertEqual(results.violations[2].rule, Rules.mako_js_html_string)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.mako_js_missing_quotes)
|
||||
self.assertEqual(results.violations[1].rule, self.ruleset.mako_js_html_string)
|
||||
self.assertEqual(results.violations[2].rule, self.ruleset.mako_js_html_string)
|
||||
|
||||
def test_check_javascript_in_mako_javascript_context(self):
|
||||
"""
|
||||
@@ -1304,7 +1315,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(mako_template, results)
|
||||
|
||||
self.assertEqual(len(results.violations), 1)
|
||||
self.assertEqual(results.violations[0].rule, Rules.javascript_concat_html)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.javascript_concat_html)
|
||||
self.assertEqual(results.violations[0].start_line, 4)
|
||||
|
||||
@data(
|
||||
@@ -1331,7 +1342,7 @@ class TestMakoTemplateLinter(TestLinter):
|
||||
linter._check_mako_file_is_safe(data['template'], results)
|
||||
|
||||
self.assertEqual(len(results.violations), 2)
|
||||
self.assertEqual(results.violations[0].rule, Rules.mako_missing_default)
|
||||
self.assertEqual(results.violations[0].rule, self.ruleset.mako_missing_default)
|
||||
|
||||
violation = results.violations[1]
|
||||
lines = list(data['template'].splitlines())
|
||||
|
||||
@@ -10,9 +10,8 @@ from unittest import TestCase
|
||||
import mock
|
||||
|
||||
from xsslint.linters import JavaScriptLinter, MakoTemplateLinter, PythonLinter, UnderscoreTemplateLinter
|
||||
from xsslint.main import _lint
|
||||
from xsslint.main import _lint, _build_ruleset
|
||||
from xsslint.reporting import SummaryResults
|
||||
from xsslint.rules import Rules
|
||||
|
||||
|
||||
class TestXSSLinter(TestCase):
|
||||
@@ -33,6 +32,11 @@ class TestXSSLinter(TestCase):
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
self.out = StringIO()
|
||||
self.template_linters = self._build_linters()
|
||||
self.ruleset = _build_ruleset(self.template_linters)
|
||||
self.summary_results = SummaryResults(self.ruleset)
|
||||
|
||||
def patch_is_valid_directory(self, linter_class):
|
||||
"""
|
||||
Creates a mock patch for _is_valid_directory on a Linter to always
|
||||
@@ -57,42 +61,39 @@ class TestXSSLinter(TestCase):
|
||||
"""
|
||||
Tests the top-level linting with default options.
|
||||
"""
|
||||
out = StringIO()
|
||||
summary_results = SummaryResults()
|
||||
|
||||
_lint(
|
||||
'scripts/xsslint/tests/templates',
|
||||
template_linters=self._build_linters(),
|
||||
template_linters=self.template_linters,
|
||||
options={
|
||||
'list_files': False,
|
||||
'verbose': False,
|
||||
'rule_totals': False,
|
||||
'skip_dirs': ()
|
||||
},
|
||||
summary_results=summary_results,
|
||||
out=out,
|
||||
summary_results=self.summary_results,
|
||||
out=self.out,
|
||||
)
|
||||
|
||||
output = out.getvalue()
|
||||
output = self.out.getvalue()
|
||||
# Assert violation details are displayed.
|
||||
self.assertIsNotNone(re.search('test\.html.*{}'.format(Rules.mako_missing_default.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.coffee.*{}'.format(Rules.javascript_concat_html.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.coffee.*{}'.format(Rules.underscore_not_escaped.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.js.*{}'.format(Rules.javascript_concat_html.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.js.*{}'.format(Rules.underscore_not_escaped.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.html.*{}'.format(self.ruleset.mako_missing_default.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.coffee.*{}'.format(self.ruleset.javascript_concat_html.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.coffee.*{}'.format(self.ruleset.underscore_not_escaped.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.js.*{}'.format(self.ruleset.javascript_concat_html.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.js.*{}'.format(self.ruleset.underscore_not_escaped.rule_id), output))
|
||||
lines_with_rule = 0
|
||||
lines_without_rule = 0 # Output with verbose setting only.
|
||||
for underscore_match in re.finditer('test\.underscore:.*\n', output):
|
||||
if re.search(Rules.underscore_not_escaped.rule_id, underscore_match.group()) is not None:
|
||||
if re.search(self.ruleset.underscore_not_escaped.rule_id, underscore_match.group()) is not None:
|
||||
lines_with_rule += 1
|
||||
else:
|
||||
lines_without_rule += 1
|
||||
self.assertGreaterEqual(lines_with_rule, 1)
|
||||
self.assertEquals(lines_without_rule, 0)
|
||||
self.assertIsNone(re.search('test\.py.*{}'.format(Rules.python_parse_error.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.py.*{}'.format(Rules.python_wrap_html.rule_id), output))
|
||||
self.assertIsNone(re.search('test\.py.*{}'.format(self.ruleset.python_parse_error.rule_id), output))
|
||||
self.assertIsNotNone(re.search('test\.py.*{}'.format(self.ruleset.python_wrap_html.rule_id), output))
|
||||
# Assert no rule totals.
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(Rules.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(self.ruleset.python_parse_error.rule_id, 0), output))
|
||||
# Assert final total
|
||||
self.assertIsNotNone(re.search('{} violations total'.format(7), output))
|
||||
|
||||
@@ -100,34 +101,31 @@ class TestXSSLinter(TestCase):
|
||||
"""
|
||||
Tests the top-level linting with verbose option.
|
||||
"""
|
||||
out = StringIO()
|
||||
summary_results = SummaryResults()
|
||||
|
||||
_lint(
|
||||
'scripts/xsslint/tests/templates',
|
||||
template_linters=self._build_linters(),
|
||||
template_linters=self.template_linters,
|
||||
options={
|
||||
'list_files': False,
|
||||
'verbose': True,
|
||||
'rule_totals': False,
|
||||
'skip_dirs': ()
|
||||
},
|
||||
summary_results=summary_results,
|
||||
out=out,
|
||||
summary_results=self.summary_results,
|
||||
out=self.out,
|
||||
)
|
||||
|
||||
output = out.getvalue()
|
||||
output = self.out.getvalue()
|
||||
lines_with_rule = 0
|
||||
lines_without_rule = 0 # Output with verbose setting only.
|
||||
for underscore_match in re.finditer('test\.underscore:.*\n', output):
|
||||
if re.search(Rules.underscore_not_escaped.rule_id, underscore_match.group()) is not None:
|
||||
if re.search(self.ruleset.underscore_not_escaped.rule_id, underscore_match.group()) is not None:
|
||||
lines_with_rule += 1
|
||||
else:
|
||||
lines_without_rule += 1
|
||||
self.assertGreaterEqual(lines_with_rule, 1)
|
||||
self.assertGreaterEqual(lines_without_rule, 1)
|
||||
# Assert no rule totals.
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(Rules.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(self.ruleset.python_parse_error.rule_id, 0), output))
|
||||
# Assert final total
|
||||
self.assertIsNotNone(re.search('{} violations total'.format(7), output))
|
||||
|
||||
@@ -135,55 +133,50 @@ class TestXSSLinter(TestCase):
|
||||
"""
|
||||
Tests the top-level linting with rule totals option.
|
||||
"""
|
||||
out = StringIO()
|
||||
summary_results = SummaryResults()
|
||||
|
||||
_lint(
|
||||
'scripts/xsslint/tests/templates',
|
||||
template_linters=self._build_linters(),
|
||||
template_linters=self.template_linters,
|
||||
options={
|
||||
'list_files': False,
|
||||
'verbose': False,
|
||||
'rule_totals': True,
|
||||
'skip_dirs': ()
|
||||
},
|
||||
summary_results=summary_results,
|
||||
out=out,
|
||||
summary_results=self.summary_results,
|
||||
out=self.out,
|
||||
)
|
||||
|
||||
output = out.getvalue()
|
||||
self.assertIsNotNone(re.search('test\.py.*{}'.format(Rules.python_wrap_html.rule_id), output))
|
||||
output = self.out.getvalue()
|
||||
self.assertIsNotNone(re.search('test\.py.*{}'.format(self.ruleset.python_wrap_html.rule_id), output))
|
||||
|
||||
# Assert totals output.
|
||||
self.assertIsNotNone(re.search('{}:\s*{} violations'.format(Rules.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNotNone(re.search('{}:\s*{} violations'.format(Rules.python_wrap_html.rule_id, 1), output))
|
||||
self.assertIsNotNone(re.search('{}:\s*{} violations'.format(self.ruleset.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNotNone(re.search('{}:\s*{} violations'.format(self.ruleset.python_wrap_html.rule_id, 1), output))
|
||||
self.assertIsNotNone(re.search('{} violations total'.format(7), output))
|
||||
|
||||
def test_lint_with_list_files(self):
|
||||
"""
|
||||
Tests the top-level linting with list files option.
|
||||
"""
|
||||
out = StringIO()
|
||||
summary_results = SummaryResults()
|
||||
_lint(
|
||||
'scripts/xsslint/tests/templates',
|
||||
template_linters=self._build_linters(),
|
||||
template_linters=self.template_linters,
|
||||
options={
|
||||
'list_files': True,
|
||||
'verbose': False,
|
||||
'rule_totals': False,
|
||||
'skip_dirs': ()
|
||||
},
|
||||
summary_results=summary_results,
|
||||
out=out,
|
||||
summary_results=self.summary_results,
|
||||
out=self.out,
|
||||
)
|
||||
|
||||
output = out.getvalue()
|
||||
output = self.out.getvalue()
|
||||
# Assert file with rule is not output.
|
||||
self.assertIsNone(re.search('test\.py.*{}'.format(Rules.python_wrap_html.rule_id), output))
|
||||
self.assertIsNone(re.search('test\.py.*{}'.format(self.ruleset.python_wrap_html.rule_id), output))
|
||||
# Assert file is output.
|
||||
self.assertIsNotNone(re.search('test\.py', output))
|
||||
|
||||
# Assert no totals.
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(Rules.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNone(re.search('{}:\s*{} violations'.format(self.ruleset.python_parse_error.rule_id, 0), output))
|
||||
self.assertIsNone(re.search('{} violations total'.format(7), output))
|
||||
|
||||
@@ -6,10 +6,10 @@ import os
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from xsslint import visitors
|
||||
from xsslint.reporting import ExpressionRuleViolation, FileResults, RuleViolation
|
||||
from xsslint.rules import Rules
|
||||
from xsslint.rules import RuleSet
|
||||
from xsslint.utils import Expression, ParseString, StringLines, is_skip_dir
|
||||
from xsslint.visitors import AllNodeVisitor, HtmlStringVisitor, OuterFormatVisitor
|
||||
|
||||
|
||||
class BaseLinter(object):
|
||||
@@ -194,6 +194,11 @@ class UnderscoreTemplateLinter(BaseLinter):
|
||||
"""
|
||||
The linter for Underscore.js template files.
|
||||
"""
|
||||
|
||||
ruleset = RuleSet(
|
||||
underscore_not_escaped='underscore-not-escaped',
|
||||
)
|
||||
|
||||
def __init__(self, skip_dirs=None):
|
||||
"""
|
||||
Init method.
|
||||
@@ -250,7 +255,7 @@ class UnderscoreTemplateLinter(BaseLinter):
|
||||
for expression in expressions:
|
||||
if not self._is_safe_unescaped_expression(expression):
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.underscore_not_escaped, expression
|
||||
self.ruleset.underscore_not_escaped, expression
|
||||
))
|
||||
|
||||
def _is_safe_unescaped_expression(self, expression):
|
||||
@@ -309,12 +314,24 @@ class JavaScriptLinter(BaseLinter):
|
||||
|
||||
LINE_COMMENT_DELIM = "//"
|
||||
|
||||
ruleset = RuleSet(
|
||||
javascript_jquery_append='javascript-jquery-append',
|
||||
javascript_jquery_prepend='javascript-jquery-prepend',
|
||||
javascript_jquery_insertion='javascript-jquery-insertion',
|
||||
javascript_jquery_insert_into_target='javascript-jquery-insert-into-target',
|
||||
javascript_jquery_html='javascript-jquery-html',
|
||||
javascript_concat_html='javascript-concat-html',
|
||||
javascript_escape='javascript-escape',
|
||||
javascript_interpolate='javascript-interpolate',
|
||||
)
|
||||
|
||||
def __init__(self, underscore_linter, javascript_skip_dirs=None, coffeescript_skip_dirs=None):
|
||||
"""
|
||||
Init method.
|
||||
"""
|
||||
super(JavaScriptLinter, self).__init__()
|
||||
self.underscore_linter = underscore_linter
|
||||
self.ruleset = self.ruleset + self.underscore_linter.ruleset
|
||||
self._skip_javascript_dirs = javascript_skip_dirs or ()
|
||||
self._skip_coffeescript_dirs = coffeescript_skip_dirs or ()
|
||||
|
||||
@@ -361,28 +378,28 @@ class JavaScriptLinter(BaseLinter):
|
||||
no_caller_check = None
|
||||
no_argument_check = None
|
||||
self._check_jquery_function(
|
||||
file_contents, "append", Rules.javascript_jquery_append, no_caller_check,
|
||||
file_contents, "append", self.ruleset.javascript_jquery_append, no_caller_check,
|
||||
self._is_jquery_argument_safe, results
|
||||
)
|
||||
self._check_jquery_function(
|
||||
file_contents, "prepend", Rules.javascript_jquery_prepend, no_caller_check,
|
||||
file_contents, "prepend", self.ruleset.javascript_jquery_prepend, no_caller_check,
|
||||
self._is_jquery_argument_safe, results
|
||||
)
|
||||
self._check_jquery_function(
|
||||
file_contents, "unwrap|wrap|wrapAll|wrapInner|after|before|replaceAll|replaceWith",
|
||||
Rules.javascript_jquery_insertion, no_caller_check, self._is_jquery_argument_safe, results
|
||||
self.ruleset.javascript_jquery_insertion, no_caller_check, self._is_jquery_argument_safe, results
|
||||
)
|
||||
self._check_jquery_function(
|
||||
file_contents, "appendTo|prependTo|insertAfter|insertBefore",
|
||||
Rules.javascript_jquery_insert_into_target, self._is_jquery_insert_caller_safe, no_argument_check, results
|
||||
self.ruleset.javascript_jquery_insert_into_target, self._is_jquery_insert_caller_safe, no_argument_check, results
|
||||
)
|
||||
self._check_jquery_function(
|
||||
file_contents, "html", Rules.javascript_jquery_html, no_caller_check,
|
||||
file_contents, "html", self.ruleset.javascript_jquery_html, no_caller_check,
|
||||
self._is_jquery_html_argument_safe, results
|
||||
)
|
||||
self._check_javascript_interpolate(file_contents, results)
|
||||
self._check_javascript_escape(file_contents, results)
|
||||
self._check_concat_with_html(file_contents, Rules.javascript_concat_html, results)
|
||||
self._check_concat_with_html(file_contents, self.ruleset.javascript_concat_html, results)
|
||||
self.underscore_linter.check_underscore_file_is_safe(file_contents, results)
|
||||
results.prepare_results(file_contents, line_comment_delim=self.LINE_COMMENT_DELIM)
|
||||
|
||||
@@ -430,7 +447,7 @@ class JavaScriptLinter(BaseLinter):
|
||||
regex = re.compile(r"(?<!StringUtils).interpolate\(")
|
||||
for function_match in regex.finditer(file_contents):
|
||||
expression = self._get_expression_for_function(file_contents, function_match)
|
||||
results.violations.append(ExpressionRuleViolation(Rules.javascript_interpolate, expression))
|
||||
results.violations.append(ExpressionRuleViolation(self.ruleset.javascript_interpolate, expression))
|
||||
|
||||
def _check_javascript_escape(self, file_contents, results):
|
||||
"""
|
||||
@@ -447,7 +464,7 @@ class JavaScriptLinter(BaseLinter):
|
||||
regex = regex = re.compile(r"(?<!_).escape\(")
|
||||
for function_match in regex.finditer(file_contents):
|
||||
expression = self._get_expression_for_function(file_contents, function_match)
|
||||
results.violations.append(ExpressionRuleViolation(Rules.javascript_escape, expression))
|
||||
results.violations.append(ExpressionRuleViolation(self.ruleset.javascript_escape, expression))
|
||||
|
||||
def _check_jquery_function(self, file_contents, function_names, rule, is_caller_safe, is_argument_safe, results):
|
||||
"""
|
||||
@@ -459,7 +476,7 @@ class JavaScriptLinter(BaseLinter):
|
||||
function_names: A pipe delimited list of names of the functions
|
||||
(e.g. "wrap|after|before").
|
||||
rule: The name of the rule to use for validation errors (e.g.
|
||||
Rules.javascript_jquery_append).
|
||||
self.ruleset.javascript_jquery_append).
|
||||
is_caller_safe: A function to test if caller of the JQuery function
|
||||
is safe.
|
||||
is_argument_safe: A function to test if the argument passed to the
|
||||
@@ -697,6 +714,14 @@ class PythonLinter(BaseLinter):
|
||||
|
||||
LINE_COMMENT_DELIM = "#"
|
||||
|
||||
ruleset = RuleSet(
|
||||
python_parse_error='python-parse-error',
|
||||
python_custom_escape='python-custom-escape',
|
||||
|
||||
# The Visitor classes are python-specific and should be moved into the PythonLinter once they have
|
||||
# been decoupled from the MakoTemplateLinter.
|
||||
) + visitors.ruleset
|
||||
|
||||
def __init__(self, skip_dirs=None):
|
||||
"""
|
||||
Init method.
|
||||
@@ -756,7 +781,7 @@ class PythonLinter(BaseLinter):
|
||||
# different assumptions.
|
||||
if root_node is not None:
|
||||
# check format() rules that can be run on outer-most format() calls
|
||||
visitor = OuterFormatVisitor(file_contents, results)
|
||||
visitor = visitors.OuterFormatVisitor(file_contents, results)
|
||||
visitor.visit(root_node)
|
||||
results.prepare_results(file_contents, line_comment_delim=self.LINE_COMMENT_DELIM)
|
||||
|
||||
@@ -773,7 +798,7 @@ class PythonLinter(BaseLinter):
|
||||
"""
|
||||
if root_node is not None:
|
||||
# check illegal concatenation and interpolation
|
||||
visitor = AllNodeVisitor(python_code, results)
|
||||
visitor = visitors.AllNodeVisitor(python_code, results)
|
||||
visitor.visit(root_node)
|
||||
# check rules parse with regex
|
||||
self._check_custom_escape(python_code, results)
|
||||
@@ -801,7 +826,7 @@ class PythonLinter(BaseLinter):
|
||||
line_start_index = lines.line_number_to_start_index(e.lineno)
|
||||
expression = Expression(line_start_index + e.offset)
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_parse_error, expression
|
||||
self.ruleset.python_parse_error, expression
|
||||
))
|
||||
return None
|
||||
|
||||
@@ -846,7 +871,7 @@ class PythonLinter(BaseLinter):
|
||||
for match in re.finditer("(<.*<|<.*<)", file_contents):
|
||||
expression = Expression(match.start(), match.end())
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_custom_escape, expression
|
||||
self.ruleset.python_custom_escape, expression
|
||||
))
|
||||
|
||||
|
||||
@@ -856,6 +881,25 @@ class MakoTemplateLinter(BaseLinter):
|
||||
"""
|
||||
LINE_COMMENT_DELIM = "##"
|
||||
|
||||
ruleset = RuleSet(
|
||||
mako_missing_default='mako-missing-default',
|
||||
mako_multiple_page_tags='mako-multiple-page-tags',
|
||||
mako_unparseable_expression='mako-unparseable-expression',
|
||||
mako_unwanted_html_filter='mako-unwanted-html-filter',
|
||||
mako_invalid_html_filter='mako-invalid-html-filter',
|
||||
mako_invalid_js_filter='mako-invalid-js-filter',
|
||||
mako_js_missing_quotes='mako-js-missing-quotes',
|
||||
mako_js_html_string='mako-js-html-string',
|
||||
mako_html_entities='mako-html-entities',
|
||||
mako_unknown_context='mako-unknown-context',
|
||||
|
||||
# NOTE The MakoTemplateLinter directly checks for python_wrap_html and directly
|
||||
# instantiates Visitor instances to check for python issues. This logic should
|
||||
# be moved into the PythonLinter. The MakoTemplateLinter should only check for
|
||||
# Mako-specific issues.
|
||||
python_wrap_html='python-wrap-html',
|
||||
) + visitors.ruleset
|
||||
|
||||
def __init__(self, javascript_linter, python_linter, skip_dirs=None):
|
||||
"""
|
||||
Init method.
|
||||
@@ -863,6 +907,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
super(MakoTemplateLinter, self).__init__()
|
||||
self.javascript_linter = javascript_linter
|
||||
self.python_linter = python_linter
|
||||
self.ruleset = self.ruleset + self.javascript_linter.ruleset + self.python_linter.ruleset
|
||||
self._skip_mako_dirs = skip_dirs or ()
|
||||
|
||||
def process_file(self, directory, file_name):
|
||||
@@ -986,18 +1031,18 @@ class MakoTemplateLinter(BaseLinter):
|
||||
page_tag_count = self._get_page_tag_count(mako_template)
|
||||
# check if there are too many page expressions
|
||||
if 2 <= page_tag_count:
|
||||
results.violations.append(RuleViolation(Rules.mako_multiple_page_tags))
|
||||
results.violations.append(RuleViolation(self.ruleset.mako_multiple_page_tags))
|
||||
return False
|
||||
# make sure there is exactly 1 page expression, excluding commented out
|
||||
# page expressions, before proceeding
|
||||
elif page_tag_count != 1:
|
||||
results.violations.append(RuleViolation(Rules.mako_missing_default))
|
||||
results.violations.append(RuleViolation(self.ruleset.mako_missing_default))
|
||||
return False
|
||||
# check that safe by default (h filter) is turned on
|
||||
page_h_filter_regex = re.compile('<%page[^>]*expression_filter=(?:"h"|\'h\')[^>]*/>')
|
||||
page_match = page_h_filter_regex.search(mako_template)
|
||||
if not page_match:
|
||||
results.violations.append(RuleViolation(Rules.mako_missing_default))
|
||||
results.violations.append(RuleViolation(self.ruleset.mako_missing_default))
|
||||
return page_match
|
||||
|
||||
def _check_mako_expressions(self, mako_template, has_page_default, results):
|
||||
@@ -1019,7 +1064,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
for expression in expressions:
|
||||
if expression.end_index is None:
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_unparseable_expression, expression
|
||||
self.ruleset.mako_unparseable_expression, expression
|
||||
))
|
||||
continue
|
||||
|
||||
@@ -1128,16 +1173,16 @@ class MakoTemplateLinter(BaseLinter):
|
||||
self.python_linter.check_python_code_is_safe(python_code, root_node, python_results)
|
||||
# Check mako expression specific Python rules.
|
||||
if root_node is not None:
|
||||
visitor = HtmlStringVisitor(python_code, python_results, True)
|
||||
visitor = visitors.HtmlStringVisitor(python_code, python_results, True)
|
||||
visitor.visit(root_node)
|
||||
for unsafe_html_string_node in visitor.unsafe_html_string_nodes:
|
||||
python_results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_wrap_html, visitor.node_to_expression(unsafe_html_string_node)
|
||||
self.ruleset.python_wrap_html, visitor.node_to_expression(unsafe_html_string_node)
|
||||
))
|
||||
if has_page_default:
|
||||
for over_escaped_entity_string_node in visitor.over_escaped_entity_string_nodes:
|
||||
python_results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_html_entities, visitor.node_to_expression(over_escaped_entity_string_node)
|
||||
self.ruleset.mako_html_entities, visitor.node_to_expression(over_escaped_entity_string_node)
|
||||
))
|
||||
python_results.prepare_results(python_code, line_comment_delim=self.LINE_COMMENT_DELIM)
|
||||
self._shift_and_add_violations(python_results, start_offset, results)
|
||||
@@ -1183,7 +1228,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
"""
|
||||
if context == 'unknown':
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_unknown_context, expression
|
||||
self.ruleset.mako_unknown_context, expression
|
||||
))
|
||||
return
|
||||
|
||||
@@ -1202,7 +1247,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
if filters_match is None:
|
||||
if context == 'javascript':
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_invalid_js_filter, expression
|
||||
self.ruleset.mako_invalid_js_filter, expression
|
||||
))
|
||||
return
|
||||
filters = filters_match.group(1).replace(" ", "").split(",")
|
||||
@@ -1215,14 +1260,14 @@ class MakoTemplateLinter(BaseLinter):
|
||||
# suppress this violation if the page default hasn't been set,
|
||||
# otherwise the template might get less safe
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_unwanted_html_filter, expression
|
||||
self.ruleset.mako_unwanted_html_filter, expression
|
||||
))
|
||||
elif filters == ['n', 'strip_all_tags_but_br']:
|
||||
# {x | n, strip_all_tags_but_br} is valid in html context
|
||||
pass
|
||||
else:
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_invalid_html_filter, expression
|
||||
self.ruleset.mako_invalid_html_filter, expression
|
||||
))
|
||||
elif context == 'javascript':
|
||||
self._check_js_expression_not_with_html(mako_template, expression, results)
|
||||
@@ -1234,7 +1279,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
self._check_js_string_expression_in_quotes(mako_template, expression, results)
|
||||
else:
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_invalid_js_filter, expression
|
||||
self.ruleset.mako_invalid_js_filter, expression
|
||||
))
|
||||
|
||||
def _check_js_string_expression_in_quotes(self, mako_template, expression, results):
|
||||
@@ -1250,7 +1295,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
parse_string = self._find_string_wrapping_expression(mako_template, expression)
|
||||
if parse_string is None:
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_js_missing_quotes, expression
|
||||
self.ruleset.mako_js_missing_quotes, expression
|
||||
))
|
||||
|
||||
def _check_js_expression_not_with_html(self, mako_template, expression, results):
|
||||
@@ -1266,7 +1311,7 @@ class MakoTemplateLinter(BaseLinter):
|
||||
parse_string = self._find_string_wrapping_expression(mako_template, expression)
|
||||
if parse_string is not None and re.search('[<>]', parse_string.string) is not None:
|
||||
results.violations.append(ExpressionRuleViolation(
|
||||
Rules.mako_js_html_string, expression
|
||||
self.ruleset.mako_js_html_string, expression
|
||||
))
|
||||
|
||||
def _find_string_wrapping_expression(self, mako_template, expression):
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import sys
|
||||
|
||||
from xsslint.reporting import SummaryResults
|
||||
from xsslint.rules import RuleSet
|
||||
from xsslint.utils import is_skip_dir
|
||||
|
||||
|
||||
@@ -18,6 +19,23 @@ def _load_config_module(module_path):
|
||||
return importlib.import_module(module_path)
|
||||
|
||||
|
||||
def _build_ruleset(template_linters):
|
||||
"""
|
||||
Combines the RuleSets from the provided template_linters into a single, aggregate RuleSet.
|
||||
|
||||
Arguments:
|
||||
template_linters: A list of linting objects.
|
||||
|
||||
Returns:
|
||||
The combined RuleSet.
|
||||
"""
|
||||
return reduce(
|
||||
lambda combined, current: combined + current.ruleset,
|
||||
template_linters,
|
||||
RuleSet()
|
||||
)
|
||||
|
||||
|
||||
def _process_file(full_path, template_linters, options, summary_results, out):
|
||||
"""
|
||||
For each linter, lints the provided file. This means finding and printing
|
||||
@@ -148,9 +166,10 @@ def main():
|
||||
'verbose': args.verbose,
|
||||
'skip_dirs': getattr(config, 'SKIP_DIRS', ())
|
||||
}
|
||||
summary_results = SummaryResults()
|
||||
template_linters = getattr(config, 'LINTERS', ())
|
||||
if not template_linters:
|
||||
raise ValueError("LINTERS is empty or undefined in the config module ({}).".format(args.config))
|
||||
|
||||
ruleset = _build_ruleset(template_linters)
|
||||
summary_results = SummaryResults(ruleset)
|
||||
_lint(args.path, template_linters, options, summary_results, out=sys.stdout)
|
||||
|
||||
@@ -6,7 +6,6 @@ from __future__ import print_function
|
||||
import os
|
||||
import re
|
||||
|
||||
from xsslint.rules import Rules
|
||||
from xsslint.utils import StringLines
|
||||
|
||||
|
||||
@@ -238,13 +237,16 @@ class SummaryResults(object):
|
||||
Contains the summary results for all violations.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, ruleset):
|
||||
"""
|
||||
Init method.
|
||||
|
||||
Arguments:
|
||||
ruleset: A RuleSet object containing all of the possible Rules.
|
||||
"""
|
||||
self.total_violations = 0
|
||||
self.totals_by_rule = dict.fromkeys(
|
||||
[rule.rule_id for rule in Rules.__members__.values()], 0
|
||||
[rule.rule_id for rule in ruleset.rules.values()], 0
|
||||
)
|
||||
|
||||
def add_violation(self, violation):
|
||||
|
||||
@@ -1,39 +1,22 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Rules(Enum):
|
||||
"""
|
||||
An Enum of each rule which the linter will check.
|
||||
"""
|
||||
# IMPORTANT: Do not edit without also updating the docs:
|
||||
# - http://edx.readthedocs.org/projects/edx-developer-guide/en/latest/conventions/preventing_xss.html#xss-linter
|
||||
mako_missing_default = 'mako-missing-default'
|
||||
mako_multiple_page_tags = 'mako-multiple-page-tags'
|
||||
mako_unparseable_expression = 'mako-unparseable-expression'
|
||||
mako_unwanted_html_filter = 'mako-unwanted-html-filter'
|
||||
mako_invalid_html_filter = 'mako-invalid-html-filter'
|
||||
mako_invalid_js_filter = 'mako-invalid-js-filter'
|
||||
mako_js_missing_quotes = 'mako-js-missing-quotes'
|
||||
mako_js_html_string = 'mako-js-html-string'
|
||||
mako_html_entities = 'mako-html-entities'
|
||||
mako_unknown_context = 'mako-unknown-context'
|
||||
underscore_not_escaped = 'underscore-not-escaped'
|
||||
javascript_jquery_append = 'javascript-jquery-append'
|
||||
javascript_jquery_prepend = 'javascript-jquery-prepend'
|
||||
javascript_jquery_insertion = 'javascript-jquery-insertion'
|
||||
javascript_jquery_insert_into_target = 'javascript-jquery-insert-into-target'
|
||||
javascript_jquery_html = 'javascript-jquery-html'
|
||||
javascript_concat_html = 'javascript-concat-html'
|
||||
javascript_escape = 'javascript-escape'
|
||||
javascript_interpolate = 'javascript-interpolate'
|
||||
python_concat_html = 'python-concat-html'
|
||||
python_custom_escape = 'python-custom-escape'
|
||||
python_deprecated_display_name = 'python-deprecated-display-name'
|
||||
python_requires_html_or_text = 'python-requires-html-or-text'
|
||||
python_close_before_format = 'python-close-before-format'
|
||||
python_wrap_html = 'python-wrap-html'
|
||||
python_interpolate_html = 'python-interpolate-html'
|
||||
python_parse_error = 'python-parse-error'
|
||||
|
||||
class Rule(object):
|
||||
def __init__(self, rule_id):
|
||||
self.rule_id = rule_id
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.rule_id == other.rule_id
|
||||
|
||||
|
||||
class RuleSet(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.rules = {}
|
||||
for k, v in kwargs.items():
|
||||
self.rules[k] = Rule(v)
|
||||
|
||||
def __getattr__(self, attr_name):
|
||||
return self.rules[attr_name]
|
||||
|
||||
def __add__(self, other):
|
||||
result = self.__class__()
|
||||
result.rules.update(self.rules)
|
||||
result.rules.update(other.rules)
|
||||
return result
|
||||
|
||||
@@ -5,10 +5,20 @@ import ast
|
||||
import re
|
||||
|
||||
from xsslint.reporting import ExpressionRuleViolation
|
||||
from xsslint.rules import Rules
|
||||
from xsslint.rules import RuleSet
|
||||
from xsslint.utils import Expression, ParseString, StringLines
|
||||
|
||||
|
||||
ruleset = RuleSet(
|
||||
python_concat_html='python-concat-html',
|
||||
python_deprecated_display_name='python-deprecated-display-name',
|
||||
python_requires_html_or_text='python-requires-html-or-text',
|
||||
python_close_before_format='python-close-before-format',
|
||||
python_wrap_html='python-wrap-html',
|
||||
python_interpolate_html='python-interpolate-html',
|
||||
)
|
||||
|
||||
|
||||
class BaseVisitor(ast.NodeVisitor):
|
||||
"""
|
||||
Base class for AST NodeVisitor used for Python xss linting.
|
||||
@@ -238,7 +248,7 @@ class OuterFormatVisitor(BaseVisitor):
|
||||
visitor.visit(node)
|
||||
for unsafe_html_string_node in visitor.unsafe_html_string_nodes:
|
||||
self.results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_wrap_html, self.node_to_expression(unsafe_html_string_node)
|
||||
ruleset.python_wrap_html, self.node_to_expression(unsafe_html_string_node)
|
||||
))
|
||||
# Do not continue processing child nodes of this format() node.
|
||||
else:
|
||||
@@ -264,7 +274,7 @@ class AllNodeVisitor(BaseVisitor):
|
||||
"""
|
||||
if node.attr == 'display_name_with_default_escaped':
|
||||
self.results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_deprecated_display_name, self.node_to_expression(node)
|
||||
ruleset.python_deprecated_display_name, self.node_to_expression(node)
|
||||
))
|
||||
self.generic_visit(node)
|
||||
|
||||
@@ -293,14 +303,14 @@ class AllNodeVisitor(BaseVisitor):
|
||||
# Text() or HTML().
|
||||
if is_caller_html_or_text is False:
|
||||
self.results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_requires_html_or_text, self.node_to_expression(node.func)
|
||||
ruleset.python_requires_html_or_text, self.node_to_expression(node.func)
|
||||
))
|
||||
elif isinstance(node.func, ast.Name) and node.func.id in ['HTML', 'Text']:
|
||||
visitor = ContainsFormatVisitor(self.file_contents, self.results)
|
||||
visitor.visit(node)
|
||||
if visitor.contains_format_call:
|
||||
self.results.violations.append(ExpressionRuleViolation(
|
||||
Rules.python_close_before_format, self.node_to_expression(node.func)
|
||||
ruleset.python_close_before_format, self.node_to_expression(node.func)
|
||||
))
|
||||
|
||||
self.generic_visit(node)
|
||||
@@ -313,9 +323,9 @@ class AllNodeVisitor(BaseVisitor):
|
||||
"""
|
||||
rule = None
|
||||
if isinstance(node.op, ast.Mod):
|
||||
rule = Rules.python_interpolate_html
|
||||
rule = ruleset.python_interpolate_html
|
||||
elif isinstance(node.op, ast.Add):
|
||||
rule = Rules.python_concat_html
|
||||
rule = ruleset.python_concat_html
|
||||
if rule is not None:
|
||||
visitor = HtmlStringVisitor(self.file_contents, self.results)
|
||||
visitor.visit(node.left)
|
||||
|
||||
Reference in New Issue
Block a user