mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
The CSS linter was pretty hard to reason about. It was pretty flexible about certain things, but then it would prevent seemingly innocuous code from getting checked in. This commit overhauls the pretty-printer to be more composable, where every object in the AST knows how to render itself. It also cleans up a little bit of the pre_fluff/post_fluff logic in the parser itself, so comments are more likely to be "attached" to the AST node that make sense. The linter is actually a bit more finicky about newlines, but this is mostly a good thing, as most of the variations before this commit were pretty arbitrary.
181 lines
5.1 KiB
Python
181 lines
5.1 KiB
Python
|
|
from typing import cast, Any
|
|
|
|
import sys
|
|
import unittest
|
|
|
|
try:
|
|
from tools.lib.css_parser import (
|
|
CssParserException,
|
|
CssSection,
|
|
parse,
|
|
)
|
|
except ImportError:
|
|
print('ERROR!!! You need to run this via tools/test-tools.')
|
|
sys.exit(1)
|
|
|
|
class ParserTestHappyPath(unittest.TestCase):
|
|
def test_basic_parse(self):
|
|
# type: () -> None
|
|
my_selector = 'li.foo'
|
|
my_block = '''{
|
|
color: red;
|
|
}'''
|
|
my_css = my_selector + ' ' + my_block
|
|
res = parse(my_css)
|
|
self.assertEqual(res.text().strip(), 'li.foo {\n color: red;\n}')
|
|
section = cast(CssSection, res.sections[0])
|
|
block = section.declaration_block
|
|
self.assertEqual(block.text().strip(), '{\n color: red;\n}')
|
|
declaration = block.declarations[0]
|
|
self.assertEqual(declaration.css_property, 'color')
|
|
self.assertEqual(declaration.css_value.text().strip(), 'red')
|
|
|
|
def test_same_line_comment(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
li.hide {
|
|
display: none; /* comment here */
|
|
/* Not to be confused
|
|
with this comment */
|
|
color: green;
|
|
}'''
|
|
res = parse(my_css)
|
|
section = cast(CssSection, res.sections[0])
|
|
block = section.declaration_block
|
|
declaration = block.declarations[0]
|
|
self.assertIn('/* comment here */', declaration.text())
|
|
|
|
def test_no_semicolon(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
p { color: red }
|
|
'''
|
|
|
|
reformatted_css = 'p {\n color: red;\n}'
|
|
|
|
res = parse(my_css)
|
|
|
|
self.assertEqual(res.text().strip(), reformatted_css)
|
|
|
|
section = cast(CssSection, res.sections[0])
|
|
|
|
self.assertFalse(section.declaration_block.declarations[0].semicolon)
|
|
|
|
def test_empty_block(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
div {
|
|
}'''
|
|
error = 'Empty declaration'
|
|
with self.assertRaisesRegex(CssParserException, error):
|
|
parse(my_css)
|
|
|
|
def test_multi_line_selector(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
h1,
|
|
h2,
|
|
h3 {
|
|
top: 0
|
|
}'''
|
|
res = parse(my_css)
|
|
section = res.sections[0]
|
|
selectors = section.selector_list.selectors
|
|
self.assertEqual(len(selectors), 3)
|
|
|
|
def test_media_block(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
@media (max-width: 300px) {
|
|
h5 {
|
|
margin: 0;
|
|
}
|
|
}'''
|
|
res = parse(my_css)
|
|
self.assertEqual(len(res.sections), 1)
|
|
expected = '@media (max-width: 300px) {\n h5 {\n margin: 0;\n }\n}'
|
|
self.assertEqual(res.text().strip(), expected)
|
|
|
|
class ParserTestSadPath(unittest.TestCase):
|
|
'''
|
|
Use this class for tests that verify the parser will
|
|
appropriately choke on malformed CSS.
|
|
|
|
We prevent some things that are technically legal
|
|
in CSS, like having comments in the middle of list
|
|
of selectors. Some of this is just for expediency;
|
|
some of this is to enforce consistent formatting.
|
|
'''
|
|
def _assert_error(self, my_css, error):
|
|
# type: (str, str) -> None
|
|
with self.assertRaisesRegex(CssParserException, error):
|
|
parse(my_css)
|
|
|
|
def test_unexpected_end_brace(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
@media (max-width: 975px) {
|
|
body {
|
|
color: red;
|
|
}
|
|
}} /* whoops */'''
|
|
error = 'unexpected }'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_empty_section(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
|
|
/* nothing to see here, move along */
|
|
'''
|
|
error = 'unexpected empty section'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_missing_colon(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
.hide
|
|
{
|
|
display none /* no colon here */
|
|
}'''
|
|
error = 'We expect a colon here'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_unclosed_comment(self):
|
|
# type: () -> None
|
|
my_css = ''' /* comment with no end'''
|
|
error = 'unclosed comment'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_missing_selectors(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
/* no selectors here */
|
|
{
|
|
bottom: 0
|
|
}'''
|
|
error = 'Missing selector'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_missing_value(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
h1
|
|
{
|
|
bottom:
|
|
}'''
|
|
error = 'Missing value'
|
|
self._assert_error(my_css, error)
|
|
|
|
def test_disallow_comments_in_selectors(self):
|
|
# type: () -> None
|
|
my_css = '''
|
|
h1,
|
|
h2, /* comment here not allowed by Zulip */
|
|
h3 {
|
|
top: 0
|
|
}'''
|
|
error = 'Comments in selector section are not allowed'
|
|
self._assert_error(my_css, error)
|