Merge lp:~adiroiban/pocket-lint/pocket-lint-css-lint into lp:pocket-lint

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: 378
Merged at revision: 374
Proposed branch: lp:~adiroiban/pocket-lint/pocket-lint-css-lint
Merge into: lp:pocket-lint
Diff against target: 1103 lines (+1040/-4)
4 files modified
pocketlint/contrib/cssccc.py (+492/-0)
pocketlint/formatcheck.py (+17/-2)
pocketlint/tests/test_css.py (+2/-2)
pocketlint/tests/test_cssccc.py (+529/-0)
To merge this branch: bzr merge lp:~adiroiban/pocket-lint/pocket-lint-css-lint
Reviewer Review Type Date Requested Status
Curtis Hovey code Approve
Review via email: mp+58059@code.launchpad.net

Description of the change

pocket lint should support checking CSS style.

To post a comment you must log in.
Revision history for this message
Adi Roiban (adiroiban) wrote :

The test input texts are a bit cryptic.
Maybe I could put them as module constants and define them using multiline string... but then they will be far from the actual test.

Since they are not that long, I think that string concatenation on multiple lines could also solve this problem

So instead of

    text ='rule1{st1\n}\n@font-face {\n src: url("u\n u"); \n }\nr2{st2}'

It would be

    text = (
        'rule1{st1\n'
        '}\n'
        '@font-face {\n'
        ' src: url("u\n u"); \n'
        ' }\n'
        'r2{st2}')

Revision history for this message
Curtis Hovey (sinzui) wrote :

Your library and integration looks good, but test_css is broken. test_css is getting the cssccc errors too.

I think we should treat this like the PythonChecker class where check() calls seperate methods for pyflakes and pep8. Maybe CSSChecker.check() chouls call check_cssutils() and check_cssccc(). Then the two tests modules could be tested separately. What do you think?

I am a little concerned that cssccc raises an error for an indented final brace. I am happy to change the good_css to make that test pass for now. I would like cssccc to permit subordinate indentation for the final brace in the future

I would like to factor CSSUtils out in the future because I think a faster grammar checker could be written that emphasises the current and future standards. Your library could be extended to do this.

review: Needs Information (code)
378. By Adi Roiban

Allow indented closing braces. Integrate tests.

Revision history for this message
Adi Roiban (adiroiban) wrote :
Download full text (7.6 KiB)

I know that the current code break the test suite.
I was not sure if such feature is wanted and this is why I did not invest more time into making the test_cssccc tests work together with test_ccc.

----

I am OK with changing it the code to not raise an error on the indented final brace.
This is also the way I prefer it... but from the blog post and readers comment I saw that this formatting is not that popular.

I was thinking to add these two ways of formatting last brace as options and users could decide their format... in a similar way that the opening brace is implemented.
So for pocket-lint you could chose your preferred format.

----

I am new to CSS world. In fact I also wrote this code as a pretext for learning more about CSS.
I am also new to CSSUtils and I don't know what are its weak points.
With a bit of feedback we could also tackler this issues.

-----

I have made some changes bases of what I understood from your comments.

Feel free to add any comments :)

Cheers

Here is the latest diff
----

=== modified file 'pocketlint/contrib/cssccc.py'
--- pocketlint/contrib/cssccc.py 2011-04-17 23:45:04 +0000
+++ pocketlint/contrib/cssccc.py 2011-04-18 01:41:02 +0000
@@ -42,6 +42,7 @@
  * add support for TAB as a separator / identation.
  * add support for @media
 '''
+from __future__ import with_statement

 __version__ = '0.1.0'

@@ -55,17 +56,26 @@
 AT_TEXT_RULES = ['import', 'charset', 'namespace']
 AT_BLOCK_RULES = ['page', 'font-face']
 # If you want
-# selector
+# selector,
+# selector2
 # {
 # property:
 # }
-#IGNORED_MESSAGES = ['I013']
+#IGNORED_MESSAGES = ['I013', 'I014']

 # If you want
+# selector,
 # selector {
 # property:
 # }
-IGNORED_MESSAGES = ['I005']
+#IGNORED_MESSAGES = ['I005', 'I014']
+
+# If you want
+# selector,
+# selector2 {
+# property:
+# }
+IGNORED_MESSAGES = ['I005', 'I006']

 class CSSRule(object):
@@ -218,6 +228,12 @@
                 'I006',
                 'Rule declarations should end with a single new line.',
                 )
+ if last_declaration != '\n ':
+ self.log(
+ start_line + offset,
+ 'I014',
+ 'Rule declarations should end indented on a single new line.',
+ )

 class CSSStatementMember(object):

=== modified file 'pocketlint/formatcheck.py'
--- pocketlint/formatcheck.py 2011-04-17 14:13:31 +0000
+++ pocketlint/formatcheck.py 2011-04-18 01:42:28 +0000
@@ -382,27 +382,37 @@
         if self.text == '':
             return

- if HAS_CSSUTILS:
- handler = CSSReporterHandler(self)
- log = logging.getLogger('pocket-lint')
- log.addHandler(handler)
- parser = cssutils.CSSParser(
- log=log, loglevel=logging.INFO, raiseExceptions=False)
- parser.parseString(self.text)
- log.removeHandler(handler)
-
+ self.check_cssutils()
         self.check_text()
+ # CSS coding conventoins checks should go last since they rely
+ # on previous checks.
+ self.check_css_coding_conventions()
+
+ def check_cssutils(self):
+ """Check the CSS code by parsing it using CSSUtils module."""...

Read more...

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you very much. I am merging this and building the package in the unstable PPA. If this proves to be reliable, I will copy it to stable and launchpad's ppa.

review: Approve (code)
Revision history for this message
Adi Roiban (adiroiban) wrote :

Hi,

Thanks for the merge.

I have added the unstable PPA and will report any problem... but I am not an hardcore CSS users/developers so I am not sure how much testing can I cover.

I will also look into the CSSUtils replacement part.
Right now I am monkey patching CSSUtils to recognize some of the CSS3 tags or browser specific tags and I would like to stop doing that :)

Cheers

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'pocketlint/contrib/cssccc.py'
--- pocketlint/contrib/cssccc.py 1970-01-01 00:00:00 +0000
+++ pocketlint/contrib/cssccc.py 2011-04-18 01:45:49 +0000
@@ -0,0 +1,492 @@
1'''
2This code is in the public domain.
3
4Check CSS code for some common coding conventions.
5The code must be in a valid CSS format.
6It is recommend to first parse it using cssutils.
7It is also recommend to check it with pocket-lint for things like trailing
8spaces or tab characters.
9
10If a comment is on the whole line, it will consume the whole line like it
11was not there.
12If a comment is inside a line it will only consume its own content.
13
14Bases on Stoyan Stefanov's http://www.phpied.com/css-coding-conventions/
15
16'@media' rule is not supported.
17 @media print {
18 html {
19 background: #fff;
20 color: #000;
21 }
22 body {
23 padding: 1in;
24 border: 0.5pt solid #666;
25 }
26 }
27
28The following at-rules are supported:
29 * keyword / text at-rules
30 * @charset "ISO-8859-15";
31 * @import url(/css/screen.css) screen, projection;
32 * @namespace foo "http://example.com/ns/foo";
33 * keybord / block rules
34 * @page { block; }
35 * @font-face { block; }
36
37
38TODO:
39 * add warning for using px for fonts.
40 * add Unicode support.
41 * add AtRule checks
42 * add support for TAB as a separator / identation.
43 * add support for @media
44'''
45from __future__ import with_statement
46
47__version__ = '0.1.0'
48
49import sys
50
51SELECTOR_SEPARATOR = ','
52DECLARATION_SEPARATOR = ';'
53PROPERTY_SEPARATOR = ':'
54COMMENT_START = r'/*'
55COMMENT_END = r'*/'
56AT_TEXT_RULES = ['import', 'charset', 'namespace']
57AT_BLOCK_RULES = ['page', 'font-face']
58# If you want
59# selector,
60# selector2
61# {
62# property:
63# }
64#IGNORED_MESSAGES = ['I013', 'I014']
65
66# If you want
67# selector,
68# selector {
69# property:
70# }
71#IGNORED_MESSAGES = ['I005', 'I014']
72
73# If you want
74# selector,
75# selector2 {
76# property:
77# }
78IGNORED_MESSAGES = ['I005', 'I006']
79
80
81class CSSRule(object):
82 '''A CSS rule.'''
83
84 def check(self):
85 '''Check the rule.'''
86 raise AssertionError('Method not implemtned.')
87
88
89class CSSAtRule(object):
90 '''A CSS @rule.'''
91
92 type = object()
93
94 def __init__(self, identifier, keyword, log, text=None, block=None):
95 self.identifier = identifier
96 self.keyword = keyword
97 self.text = text
98 self.block = block
99 self.log = log
100
101 def check(self):
102 '''Check the rule.'''
103
104
105class CSSRuleSet(object):
106 '''A CSS rule_set.'''
107
108 type = object()
109
110 def __init__(self, selector, declarations, log):
111 self.selector = selector
112 self.declarations = declarations
113 self.log = log
114
115 def __str__(self):
116 return '%s{%s}' % (str(self.selector), str(self.declarations))
117
118 def __repr__(self):
119 return '%d:%s{%s}' % (
120 self.selector.start_line,
121 str(self.selector),
122 str(self.declarations),
123 )
124
125 def check(self):
126 '''Check the rule set.'''
127 self.checkSelector()
128 self.checkDeclarations()
129
130 def checkSelector(self):
131 '''Check rule-set selector.'''
132 start_line = self.selector.getStartLine()
133 selectors = self.selector.text.split(SELECTOR_SEPARATOR)
134 offset = 0
135 last_selector = selectors[-1]
136 first_selector = selectors[0]
137 rest_selectors = selectors[1:]
138
139 if first_selector.startswith('\n\n\n'):
140 self.log(start_line, 'I002', 'To many newlines before selectors.')
141 elif first_selector.startswith('\n\n'):
142 pass
143 elif start_line > 2:
144 self.log(start_line, 'I003', 'To few newlines before selectors.')
145 else:
146 pass
147
148 for selector in rest_selectors:
149 if not selector.startswith('\n'):
150 self.log(
151 start_line + offset,
152 'I004',
153 'Selector must be on a new line.')
154 offset += selector.count('\n')
155
156 if not last_selector.endswith('\n'):
157 self.log(
158 start_line + offset,
159 'I005',
160 'No newline after last selector.')
161
162 if not (last_selector[-2] != ' ' and last_selector[-1] == (' ')):
163 self.log(
164 start_line + offset,
165 'I013',
166 'Last selector must be followed by " {".')
167
168 def checkDeclarations(self):
169 '''Check rule-set declarations.'''
170 start_line = self.declarations.getStartLine()
171 declarations = self.declarations.text.split(DECLARATION_SEPARATOR)
172 offset = 0
173
174 # Check all declarations except last as this is the new line.
175 first_declaration = True
176 for declaration in declarations[:-1]:
177 if not declaration.startswith('\n'):
178 self.log(
179 start_line + offset,
180 'I007',
181 'Each declarations should start on a new line.',
182 )
183 elif (not declaration.startswith('\n ') or
184 declaration[5] == ' '):
185 self.log(
186 start_line + offset,
187 'I008',
188 'Each declaration must be indented with 4 spaces.',
189 )
190
191 parts = declaration.split(PROPERTY_SEPARATOR)
192 if len(parts) != 2:
193 self.log(
194 start_line + offset,
195 'I009',
196 'Wrong separator on property: value pair.',
197 )
198 else:
199 prop, value = parts
200 if prop.endswith(' '):
201 self.log(
202 start_line + offset,
203 'I010',
204 'Whitespace before ":".',
205 )
206 if not (value.startswith(' ') or value.startswith('\n')):
207 self.log(
208 start_line + offset,
209 'I011',
210 'Missing whitespace after ":".',
211 )
212 elif value.startswith(' '):
213 self.log(
214 start_line + offset,
215 'I012',
216 'Multiple whitespaces after ":".',
217 )
218 if first_declaration:
219 first_declaration = False
220 else:
221 offset += declaration.count('\n')
222
223 last_declaration = declarations[-1]
224 offset += last_declaration.count('\n')
225 if last_declaration != '\n':
226 self.log(
227 start_line + offset,
228 'I006',
229 'Rule declarations should end with a single new line.',
230 )
231 if last_declaration != '\n ':
232 self.log(
233 start_line + offset,
234 'I014',
235 'Rule declarations should end indented on a single new line.',
236 )
237
238
239class CSSStatementMember(object):
240 '''A member of CSS statement.'''
241
242 def __init__(self, start_line, start_character, text):
243 self.start_line = start_line
244 self.start_character = start_character
245 self.text = text
246
247 def getStartLine(self):
248 '''Return the line number for first character in the statement and
249 the number of new lines untilg the first character.'''
250 index = 0
251 text = self.text
252 character = text[index]
253 while character == '\n':
254 index += 1
255 character = text[index]
256
257 return self.start_line + index + 1
258
259 def __str__(self):
260 return self.text
261
262 def __repr__(self):
263 return '%d:%d:{%s}' % (
264 self.start_line, self.start_character, self.text)
265
266
267class CSSCodingConventionChecker(object):
268 '''CSS coding convention checker.'''
269
270 icons = {
271 'E': 'error',
272 'I': 'info',
273 }
274
275 def __init__(self, text, logger=None):
276 self._text = text.splitlines(True)
277 self.line_number = 0
278 self.character_number = 0
279 if logger:
280 self._logger = logger
281 else:
282 self._logger = self._defaultLog
283
284 def log(self, line_number, code, message):
285 '''Log the message with `code`.'''
286 if code in IGNORED_MESSAGES:
287 return
288 icon = self.icons[code[0]]
289 self._logger(line_number, code + ': ' + message, icon=icon)
290
291 def check(self):
292 '''Check all rules.'''
293 for rule in self.getRules():
294 rule.check()
295
296 def getRules(self):
297 '''Generates the next CSS rule ignoring comments.'''
298 while True:
299 yield self.getNextRule()
300
301 def getNextRule(self):
302 '''Return the next parsed rule.
303
304 Raise `StopIteration` if we are at the last rule.
305 '''
306 if self._nextStatementIsAtRule():
307 text = None
308 block = None
309 keyword = self._parse('@')
310 # TODO: user regex [ \t {]
311 keyword_text = self._parse(' ')
312 keyword_name = keyword_text.text
313 keyword.text += '@' + keyword_name + ' '
314
315 if keyword_name.lower() in AT_TEXT_RULES:
316 text = self._parse(';')
317 elif keyword_name.lower() in AT_BLOCK_RULES:
318 start = self._parse('{')
319 keyword.text += start.text
320 block = self._parse('}')
321 else:
322 self._parse(';')
323 raise StopIteration
324
325 return CSSAtRule(
326 identifier=keyword_name,
327 keyword=keyword,
328 text=text,
329 block=block,
330 log=self.log)
331 else:
332 selector = self._parse('{')
333 declarations = self._parse('}')
334 return CSSRuleSet(
335 selector=selector,
336 declarations=declarations,
337 log=self.log)
338
339 def _defaultLog(self, line_number, message, icon='info'):
340 '''Log the message to STDOUT.'''
341 print ' %4s:%s' % (line_number, message)
342
343 def _nextStatementIsAtRule(self):
344 '''Return True if next statement in the buffer is an at-rule.
345
346 Just look for open brackets and see if there is an @ before that
347 braket.
348 '''
349 search_buffer = []
350 line_counter = self.line_number
351 current_line = self._text[line_counter][self.character_number:]
352 while current_line.find('@') == -1:
353 search_buffer.append(current_line)
354 line_counter += 1
355 try:
356 current_line = self._text[line_counter]
357 except IndexError:
358 return False
359
360 text_buffer = ''.join(search_buffer)
361 if text_buffer.find('{') == -1:
362 return True
363 else:
364 return False
365
366 def _parse(self, stop_character):
367 '''Return the parsed text until stop_character.'''
368 try:
369 self._text[self.line_number][self.character_number]
370 except IndexError:
371 raise StopIteration
372 result = []
373 start_line = self.line_number
374 start_character = self.character_number
375 comment_started = False
376 while True:
377 try:
378 data = self._text[self.line_number][self.character_number:]
379 except IndexError:
380 break
381
382 # Look for comment start/end.
383 comment_check = _check_comment(data)
384 (comment_update,
385 before_comment,
386 after_comment,
387 newline_consumed) = comment_check
388 if comment_update is not None:
389 comment_started = comment_update
390
391 if comment_started:
392 # We are inside a comment.
393 # Add the data before the comment and go to next line.
394 if before_comment is not None:
395 result.append(before_comment)
396 self.character_number = 0
397 self.line_number += 1
398 continue
399
400 # If we have a comment, strip it from the data.
401 # Remember the initial cursor position to know where to
402 # continue.
403 initial_position = data.find(stop_character)
404 if before_comment is not None or after_comment is not None:
405 if before_comment is None:
406 before_comment = ''
407 if after_comment is None:
408 after_comment = ''
409 data = before_comment + after_comment
410
411 if initial_position == -1 or newline_consumed:
412 # We are not at the end.
413 # Go to next line and append the data.
414 result.append(data)
415 self.character_number = 0
416 self.line_number += 1
417 continue
418 else:
419 # Delimiter found.
420 # Find it again in the text that now has no comments.
421 # Append data until the delimiter.
422 # Move cursor to next character and stop searching for it.
423 new_position = data.find(stop_character)
424 result.append(data[:new_position])
425 self.character_number += initial_position + 1
426 break
427
428 return CSSStatementMember(
429 start_line=start_line,
430 start_character=start_character,
431 text=''.join(result))
432
433
434def _check_comment(data):
435 '''Check the data for comment markers.'''
436
437 comment_started = None
438 before_comment = None
439 after_comment = None
440 newline_consumed = False
441 comment_start = data.find(COMMENT_START)
442 if comment_start != -1:
443 comment_started = True
444 before_comment = data[:comment_start]
445
446 comment_end = data.find(COMMENT_END)
447 if comment_end != -1:
448 comment_started = False
449 # Comment end after the lenght of the actual comment end
450 # marker.
451 comment_end += len(COMMENT_END)
452 if before_comment is None and data[comment_end] == '\n':
453 # Consume the new line if it next to the comment end and
454 # the comment in on the whole line.
455 comment_end += 1
456 newline_consumed = True
457 after_comment = data[comment_end:]
458 return (comment_started, before_comment, after_comment, newline_consumed)
459
460
461def show_usage():
462 '''Print the command usage.'''
463 print 'Usage: cssccc OPTIONS'
464 print ' -h, --help\t\tShow this help.'
465 print ' -v, --version\t\tShow version.'
466 print ' -f FILE, --file=FILE\tCheck FILE'
467
468
469def read_file(filename):
470 '''Return the content of filename.'''
471 text = ''
472 with open(filename, 'r') as f:
473 text = f.read()
474 return text
475
476
477if __name__ == '__main__':
478 if len(sys.argv) < 2:
479 show_usage()
480 elif sys.argv[1] in ['-v', '--version']:
481 print 'CSS Code Convention Checker %s' % (__version__)
482 sys.exit(0)
483 elif sys.argv[1] == '-f':
484 text = read_file(sys.argv[2])
485 checker = CSSCodingConventionChecker(text)
486 sys.exit(checker.check())
487 elif sys.argv[1] == '--file=':
488 text = read_file(sys.argv[1][len('--file='):])
489 checker = CSSCodingConventionChecker(text)
490 sys.exit(checker.check())
491 else:
492 show_usage()
0493
=== modified file 'pocketlint/formatcheck.py'
--- pocketlint/formatcheck.py 2011-04-17 16:32:41 +0000
+++ pocketlint/formatcheck.py 2011-04-18 01:45:49 +0000
@@ -31,6 +31,7 @@
31from formatdoctest import DoctestReviewer31from formatdoctest import DoctestReviewer
3232
33import contrib.pep8 as pep833import contrib.pep8 as pep8
34from contrib.cssccc import CSSCodingConventionChecker
34from contrib.pyflakes.checker import Checker35from contrib.pyflakes.checker import Checker
35try:36try:
36 import cssutils37 import cssutils
@@ -396,7 +397,18 @@
396397
397 def check(self):398 def check(self):
398 """Check the syntax of the CSS code."""399 """Check the syntax of the CSS code."""
399 if self.text == '' or not HAS_CSSUTILS:400 if self.text == '':
401 return
402
403 self.check_cssutils()
404 self.check_text()
405 # CSS coding conventoins checks should go last since they rely
406 # on previous checks.
407 self.check_css_coding_conventions()
408
409 def check_cssutils(self):
410 """Check the CSS code by parsing it using CSSUtils module."""
411 if not HAS_CSSUTILS:
400 return412 return
401 handler = CSSReporterHandler(self)413 handler = CSSReporterHandler(self)
402 log = logging.getLogger('pocket-lint')414 log = logging.getLogger('pocket-lint')
@@ -405,7 +417,6 @@
405 log=log, loglevel=logging.INFO, raiseExceptions=False)417 log=log, loglevel=logging.INFO, raiseExceptions=False)
406 parser.parseString(self.text)418 parser.parseString(self.text)
407 log.removeHandler(handler)419 log.removeHandler(handler)
408 self.check_text()
409420
410 def check_text(self):421 def check_text(self):
411 """Call each line_method for each line in text."""422 """Call each line_method for each line in text."""
@@ -416,6 +427,10 @@
416 self.check_conflicts(line_no, line)427 self.check_conflicts(line_no, line)
417 self.check_tab(line_no, line)428 self.check_tab(line_no, line)
418429
430 def check_css_coding_conventions(self):
431 """Check the input using CSS Coding Convention checker."""
432 CSSCodingConventionChecker(self.text, logger=self.message).check()
433
419434
420class PythonChecker(BaseChecker, AnyTextMixin):435class PythonChecker(BaseChecker, AnyTextMixin):
421 """Check python source code."""436 """Check python source code."""
422437
=== modified file 'pocketlint/tests/test_css.py'
--- pocketlint/tests/test_css.py 2011-04-15 03:14:05 +0000
+++ pocketlint/tests/test_css.py 2011-04-18 01:45:49 +0000
@@ -41,7 +41,7 @@
41 if not HAS_CSSUTILS:41 if not HAS_CSSUTILS:
42 return42 return
43 checker = CSSChecker('bogus', ill_formed_property, self.reporter)43 checker = CSSChecker('bogus', ill_formed_property, self.reporter)
44 checker.check()44 checker.check_cssutils()
45 messages = [45 messages = [
46 (3, "CSSValue: No match: 'CHAR', u':'"),46 (3, "CSSValue: No match: 'CHAR', u':'"),
47 (0, 'CSSStyleDeclaration: Syntax Error in Property: '47 (0, 'CSSStyleDeclaration: Syntax Error in Property: '
@@ -52,7 +52,7 @@
52 if not HAS_CSSUTILS:52 if not HAS_CSSUTILS:
53 return53 return
54 checker = CSSChecker('ballyhoo', invalid_value, self.reporter)54 checker = CSSChecker('ballyhoo', invalid_value, self.reporter)
55 checker.check()55 checker.check_cssutils()
56 message = (56 message = (
57 'Invalid value for "CSS Color Module Level 3/CSS Level 2.1" '57 'Invalid value for "CSS Color Module Level 3/CSS Level 2.1" '
58 'property: speckled: color')58 'property: speckled: color')
5959
=== added file 'pocketlint/tests/test_cssccc.py'
--- pocketlint/tests/test_cssccc.py 1970-01-01 00:00:00 +0000
+++ pocketlint/tests/test_cssccc.py 2011-04-18 01:45:49 +0000
@@ -0,0 +1,529 @@
1'''Test module for cssccc'''
2
3from unittest import TestCase, main as unittest_main
4
5
6from pocketlint.contrib.cssccc import (
7 CSSCodingConventionChecker, CSSAtRule, CSSRuleSet, CSSStatementMember)
8
9
10class TestCSSCodingConventionChecker(TestCase):
11 '''Test for parsing the CSS text.'''
12
13 def test_getNextRule_start(self):
14 text = 'selector{}'
15 lint = CSSCodingConventionChecker(text)
16 rule = lint.getNextRule()
17 self.assertTrue(rule.type is CSSRuleSet.type)
18 self.assertEqual('selector', rule.selector.text)
19 self.assertEqual(0, rule.selector.start_line)
20 self.assertEqual(0, rule.selector.start_character)
21
22 text = '\nselector{}'
23 lint = CSSCodingConventionChecker(text)
24 rule = lint.getNextRule()
25 self.assertTrue(rule.type is CSSRuleSet.type)
26 self.assertEqual('\nselector', rule.selector.text)
27 self.assertEqual(0, rule.selector.start_line)
28 self.assertEqual(0, rule.selector.start_character)
29
30 text = '\n\nselector{}'
31 lint = CSSCodingConventionChecker(text)
32 rule = lint.getNextRule()
33 self.assertTrue(rule.type is CSSRuleSet.type)
34 self.assertEqual('\n\nselector', rule.selector.text)
35 self.assertEqual(0, rule.selector.start_line)
36 self.assertEqual(0, rule.selector.start_character)
37
38 text = 'selector\n{}'
39 lint = CSSCodingConventionChecker(text)
40 rule = lint.getNextRule()
41 self.assertTrue(rule.type is CSSRuleSet.type)
42 self.assertEqual('selector\n', rule.selector.text)
43 self.assertEqual(0, rule.selector.start_line)
44 self.assertEqual(0, rule.selector.start_character)
45
46 text = 'selector, {}'
47 lint = CSSCodingConventionChecker(text)
48 rule = lint.getNextRule()
49 self.assertTrue(rule.type is CSSRuleSet.type)
50 self.assertEqual('selector, ', rule.selector.text)
51 self.assertEqual(0, rule.selector.start_line)
52 self.assertEqual(0, rule.selector.start_character)
53
54 def test_getNextRule_content(self):
55 text = 'selector { content; }'
56 lint = CSSCodingConventionChecker(text)
57 rule = lint.getNextRule()
58 self.assertTrue(rule.type is CSSRuleSet.type)
59 self.assertEqual(' content; ', rule.declarations.text)
60 self.assertEqual(0, rule.declarations.start_line)
61 self.assertEqual(10, rule.declarations.start_character)
62
63 text = 'selector \n{\n content; }'
64 lint = CSSCodingConventionChecker(text)
65 rule = lint.getNextRule()
66 self.assertTrue(rule.type is CSSRuleSet.type)
67 self.assertEqual('\n content; ', rule.declarations.text)
68 self.assertEqual(1, rule.declarations.start_line)
69 self.assertEqual(1, rule.declarations.start_character)
70
71 def test_getNextRule_continue(self):
72 text = 'selector1\n { content1; }\n\nselector2\n{content2}\n'
73 lint = CSSCodingConventionChecker(text)
74 rule = lint.getNextRule()
75 self.assertTrue(rule.type is CSSRuleSet.type)
76 self.assertEqual('selector1\n ', rule.selector.text)
77 self.assertEqual(0, rule.selector.start_line)
78 self.assertEqual(0, rule.selector.start_character)
79 self.assertEqual(' content1; ', rule.declarations.text)
80 self.assertEqual(1, rule.declarations.start_line)
81 self.assertEqual(2, rule.declarations.start_character)
82
83 rule = lint.getNextRule()
84 self.assertTrue(rule.type is CSSRuleSet.type)
85 self.assertEqual('\n\nselector2\n', rule.selector.text)
86 self.assertEqual(1, rule.selector.start_line)
87 self.assertEqual(14, rule.selector.start_character)
88 self.assertEqual('content2', rule.declarations.text)
89 self.assertEqual(4, rule.declarations.start_line)
90 self.assertEqual(1, rule.declarations.start_character)
91
92 def test_getNextRule_stop(self):
93 text ='rule1{st1\n}\n@font-face {\n src: url("u\n u"); \n }\nr2{st2}'
94 lint = CSSCodingConventionChecker(text)
95 rule = lint.getNextRule()
96 self.assertTrue(rule.type is CSSRuleSet.type)
97 rule = lint.getNextRule()
98 self.assertTrue(rule.type is CSSAtRule.type)
99 rule = lint.getNextRule()
100 self.assertTrue(rule.type is CSSRuleSet.type)
101 self.failUnlessRaises(StopIteration, lint.getNextRule)
102
103 def test_getNextRule_comment(self):
104 text = '\n\n/* c\nm*/\nsel\n{\ns/*com*/\ncont1;/*com*/\ncont2;}'
105 lint = CSSCodingConventionChecker(text)
106 rule = lint.getNextRule()
107 self.assertTrue(rule.type is CSSRuleSet.type)
108 self.assertEqual('\n\nsel\n', rule.selector.text)
109 self.assertEqual(0, rule.selector.start_line)
110 self.assertEqual(0, rule.selector.start_character)
111 self.assertEqual('\ns\ncont1;\ncont2;', rule.declarations.text)
112 self.assertEqual(5, rule.declarations.start_line)
113 self.assertEqual(1, rule.declarations.start_character)
114
115 def test_get_at_import_rule(self):
116 '''Test for @import url(/css/screen.css) screen, projection;'''
117 text ='rule1{st1\n}\n@import url(somet) print, soment ;rule2{st2}'
118 lint = CSSCodingConventionChecker(text)
119 rule = lint.getNextRule()
120 self.assertTrue(rule.type is CSSRuleSet.type)
121 rule = lint.getNextRule()
122 self.assertTrue(rule.type is CSSAtRule.type)
123 self.assertTrue(rule.block is None)
124 self.assertEqual('import', rule.identifier)
125 self.assertEqual('\n@import ', rule.keyword.text)
126 self.assertEqual(1, rule.keyword.start_line)
127 self.assertEqual(1, rule.keyword.start_character)
128 self.assertEqual(' url(somet) print, soment ', rule.text.text)
129 self.assertEqual(2, rule.text.start_line)
130 self.assertEqual(8, rule.text.start_character)
131
132 def test_get_at_charset_rule(self):
133 '''Test for @charset "ISO-8859-15";'''
134 text ='rule1{st1\n}\n@charset "utf" ;rule2{st2}'
135 lint = CSSCodingConventionChecker(text)
136 rule = lint.getNextRule()
137 self.assertTrue(rule.type is CSSRuleSet.type)
138 rule = lint.getNextRule()
139 self.assertTrue(rule.type is CSSAtRule.type)
140 self.assertTrue(rule.block is None)
141 self.assertEqual('charset', rule.identifier)
142 self.assertEqual('\n@charset ', rule.keyword.text)
143 self.assertEqual(1, rule.keyword.start_line)
144 self.assertEqual(1, rule.keyword.start_character)
145 self.assertEqual(' "utf" ', rule.text.text)
146 self.assertEqual(2, rule.text.start_line)
147 self.assertEqual(9, rule.text.start_character)
148
149 def test_get_at_namespace_rule(self):
150 '''Test for @namespace foo "http://foo" ;'''
151 text ='rule1{st1\n}@namespace foo "http://foo" ;rule2{st2}'
152 lint = CSSCodingConventionChecker(text)
153 rule = lint.getNextRule()
154 self.assertTrue(rule.type is CSSRuleSet.type)
155 rule = lint.getNextRule()
156 self.assertTrue(rule.type is CSSAtRule.type)
157 self.assertTrue(rule.block is None)
158 self.assertEqual('namespace', rule.identifier)
159 self.assertEqual('@namespace ', rule.keyword.text)
160 self.assertEqual(1, rule.keyword.start_line)
161 self.assertEqual(1, rule.keyword.start_character)
162 self.assertEqual(' foo "http://foo" ', rule.text.text)
163 self.assertEqual(1, rule.text.start_line)
164 self.assertEqual(12, rule.text.start_character)
165
166 def test_get_at_page_rule(self):
167 '''Test for @page
168
169 @page :left {
170 margin-left: 5cm; /* left pages only */
171 }
172 '''
173 text ='rule1{st1\n}\n@page :left {\n mar; /*com*/\n }\nrule2{st2}'
174 lint = CSSCodingConventionChecker(text)
175 rule = lint.getNextRule()
176 self.assertTrue(rule.type is CSSRuleSet.type)
177 rule = lint.getNextRule()
178 self.assertTrue(rule.type is CSSAtRule.type)
179 self.assertTrue(rule.text is None)
180 self.assertEqual('page', rule.identifier)
181 self.assertEqual('\n@page :left ', rule.keyword.text)
182 self.assertEqual(1, rule.keyword.start_line)
183 self.assertEqual(1, rule.keyword.start_character)
184 self.assertEqual('\n mar; \n ', rule.block.text)
185 self.assertEqual(2, rule.block.start_line)
186 self.assertEqual(13, rule.block.start_character)
187
188 def test_get_at_font_face_rule(self):
189 '''Test for @font-face
190
191 @font-face {
192 font-family: "Example Font";
193 src: url("http://www.example.com
194 /fonts/example");
195 }
196 '''
197 text ='rule1{st1\n}\n@font-face {\n src: url("u\n u"); \n }\nr2{st2}'
198 lint = CSSCodingConventionChecker(text)
199 rule = lint.getNextRule()
200 self.assertTrue(rule.type is CSSRuleSet.type)
201 rule = lint.getNextRule()
202 self.assertTrue(rule.type is CSSAtRule.type)
203 self.assertTrue(rule.text is None)
204 self.assertEqual('font-face', rule.identifier)
205 self.assertEqual('\n@font-face ', rule.keyword.text)
206 self.assertEqual(1, rule.keyword.start_line)
207 self.assertEqual(1, rule.keyword.start_character)
208 self.assertEqual('\n src: url("u\n u"); \n ', rule.block.text)
209 self.assertEqual(2, rule.block.start_line)
210 self.assertEqual(12, rule.block.start_character)
211 rule = lint.getNextRule()
212 self.assertTrue(rule.type is CSSRuleSet.type)
213 self.failUnlessRaises(StopIteration, lint.getNextRule)
214
215
216class TestCSSStatementMember(TestCase):
217 '''Tests for CSSStatementMember.'''
218
219 def test_getStartLine(self):
220 statement = CSSStatementMember(0, 4, 'some')
221 self.assertEqual(1, statement.getStartLine())
222 statement = CSSStatementMember(3, 4, 'some')
223 self.assertEqual(4, statement.getStartLine())
224 statement = CSSStatementMember(3, 4, '\n\nsome')
225 self.assertEqual(6, statement.getStartLine())
226
227
228class TestLog(object):
229 '''Container for a test log.'''
230
231 def __init__(self, line_number, code, message):
232 self.line_number = line_number
233 self.code = code
234 self.message = message
235
236
237class RuleTesterBase(TestCase):
238 '''Base class for rule checkers.'''
239
240 ignored_messaged = []
241
242 def setUp(self):
243 self.logs = []
244
245 def log(self, line_number, code, message):
246 if code in self.ignored_messaged:
247 return
248 self.logs.append((line_number, code, message))
249
250 @property
251 def last_log(self):
252 (line_number, code, message) = self.logs.pop()
253 return TestLog(line_number, code, message)
254
255
256class RuleTesterConventionA(RuleTesterBase):
257 '''Class for convention A.
258
259 selector1,
260 selecter2
261 {
262 property1: value1;
263 property2: value2;
264 }
265 '''
266
267 ignored_messaged = ['I013', 'I014']
268
269
270class TestCSSRuleSetSelectorChecksA(RuleTesterConventionA):
271 '''Test coding conventions for selector from rule sets.'''
272
273 def test_valid_selector(self):
274
275 selector = CSSStatementMember(0, 0, 'something\n')
276 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
277 rule.checkSelector()
278 self.assertEqual([], self.logs)
279
280 selector = CSSStatementMember(0, 0, '\nsomething\n')
281 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
282 rule.checkSelector()
283 self.assertEqual([], self.logs)
284
285 selector = CSSStatementMember(1, 0, '\n\nsomething\n')
286 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
287 rule.checkSelector()
288 self.assertEqual([], self.logs)
289
290 selector = CSSStatementMember(2, 0, '\n\nsomething,\nsomethi\n')
291 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
292 rule.checkSelector()
293 self.assertEqual([], self.logs)
294
295 selector = CSSStatementMember(3, 0, '\n\nsom:some some,\n#somethi\n')
296 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
297 rule.checkSelector()
298 self.assertEqual([], self.logs)
299
300 def test_I002(self):
301 selector = CSSStatementMember(2, 0, '\n\n\nsomething\n')
302 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
303 rule.checkSelector()
304 last_log = self.last_log
305 self.assertEqual('I002', last_log.code)
306 self.assertEqual(6, last_log.line_number)
307
308 selector = CSSStatementMember(4, 0, '\n\n\n\nsomething\n')
309 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
310 rule.checkSelector()
311 last_log = self.last_log
312 self.assertEqual('I002', last_log.code)
313 self.assertEqual(9, last_log.line_number)
314
315 def test_I003(self):
316 selector = CSSStatementMember(2, 0, '\nsomething\n')
317 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
318 rule.checkSelector()
319 last_log = self.last_log
320 self.assertEqual('I003', last_log.code)
321 self.assertEqual(4, last_log.line_number)
322
323 selector = CSSStatementMember(2, 0, 'something\n')
324 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
325 rule.checkSelector()
326 last_log = self.last_log
327 self.assertEqual('I003', last_log.code)
328 self.assertEqual(3, last_log.line_number)
329
330 def test_I004(self):
331 selector = CSSStatementMember(3, 0, '\n\nsomething, something\n')
332 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
333 rule.checkSelector()
334 last_log = self.last_log
335 self.assertEqual('I004', last_log.code)
336 self.assertEqual(6, last_log.line_number)
337
338 def test_I005(self):
339 selector = CSSStatementMember(4, 0, '\nsomething,\nsomething')
340 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
341 rule.checkSelector()
342 last_log = self.last_log
343 self.assertEqual('I005', last_log.code)
344 self.assertEqual(7, last_log.line_number)
345
346
347class TestCSSRuleSetDeclarationsChecksA(RuleTesterConventionA):
348 '''Test coding conventions for declarations from rule sets.'''
349
350 def test_valid_declarations(self):
351 stmt = CSSStatementMember(
352 0, 0, '\n some: 3px;\n other:\n url();\n')
353 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
354 rule.checkDeclarations()
355 self.assertEqual([], self.logs)
356
357 def test_I006(self):
358 stmt = CSSStatementMember(
359 4, 0, '\n some: 3px;\n other: url();')
360 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
361 rule.checkDeclarations()
362 last_log = self.last_log
363 self.assertEqual('I006', last_log.code)
364 self.assertEqual(7, last_log.line_number)
365
366 stmt = CSSStatementMember(
367 4, 0, '\n some: 3px;\n other: url();\n ')
368 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
369 rule.checkDeclarations()
370 last_log = self.last_log
371 self.assertEqual('I006', last_log.code)
372 self.assertEqual(8, last_log.line_number)
373
374 stmt = CSSStatementMember(
375 4, 0, '\n some: 3px;\n other: url();\n\n ')
376 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
377 rule.checkDeclarations()
378 last_log = self.last_log
379 self.assertEqual('I006', last_log.code)
380 self.assertEqual(9, last_log.line_number)
381
382 def test_I007(self):
383 stmt = CSSStatementMember(
384 4, 0, '\n some: 3px; other: url();\n')
385 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
386 rule.checkDeclarations()
387 last_log = self.last_log
388 self.assertEqual('I007', last_log.code)
389 self.assertEqual(6, last_log.line_number)
390
391 def test_I008(self):
392 stmt = CSSStatementMember(
393 0, 0, '\n some: 3px;\n other: url();\n')
394 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
395 rule.checkDeclarations()
396 self.assertEqual('I008', self.last_log.code)
397
398 stmt = CSSStatementMember(
399 0, 0, '\n some: 3px;\n other: url();\n')
400 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
401 rule.checkDeclarations()
402 self.assertEqual('I008', self.last_log.code)
403
404 def test_I009(self):
405 stmt = CSSStatementMember(
406 0, 0, '\n some 3px;\n other: url();\n')
407 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
408 rule.checkDeclarations()
409 self.assertEqual('I009', self.last_log.code)
410
411 stmt = CSSStatementMember(
412 0, 0, '\n some: 3:px;\n other: url();\n')
413 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
414 rule.checkDeclarations()
415 self.assertEqual('I009', self.last_log.code)
416
417 def test_I010(self):
418 stmt = CSSStatementMember(
419 0, 0, '\n some : 3px;\n')
420 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
421 rule.checkDeclarations()
422 self.assertEqual('I010', self.last_log.code)
423
424 def test_I011(self):
425 stmt = CSSStatementMember(
426 0, 0, '\n some:3px;\n')
427 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
428 rule.checkDeclarations()
429 self.assertEqual('I011', self.last_log.code)
430
431 def test_I012(self):
432 stmt = CSSStatementMember(
433 0, 0, '\n some: 3px;\n')
434 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
435 rule.checkDeclarations()
436 self.assertEqual('I012', self.last_log.code)
437
438
439class RuleTesterConventionB(RuleTesterBase):
440 '''Class for convention B.
441
442 selector1,
443 selecter2 {
444 property1: value1;
445 property2: value2;
446 }
447 '''
448
449 ignored_messaged = ['I005', 'I014']
450
451
452class TestCSSRuleSetSelectorChecksB(RuleTesterConventionB):
453 '''Test coding conventions for selector from rule sets.'''
454
455 def test_valid_selector(self):
456
457 selector = CSSStatementMember(0, 0, 'something ')
458 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
459 rule.checkSelector()
460 self.assertEqual([], self.logs)
461
462 selector = CSSStatementMember(0, 0, '\nsomething ')
463 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
464 rule.checkSelector()
465 self.assertEqual([], self.logs)
466
467 selector = CSSStatementMember(1, 0, '\n\nsomething ')
468 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
469 rule.checkSelector()
470 self.assertEqual([], self.logs)
471
472 selector = CSSStatementMember(2, 0, '\n\nsomething,\nsomethi ')
473 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
474 rule.checkSelector()
475 self.assertEqual([], self.logs)
476
477 selector = CSSStatementMember(3, 0, '\n\nsom:some some,\n#somethi ')
478 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
479 rule.checkSelector()
480 self.assertEqual([], self.logs)
481
482 def test_I013(self):
483 selector = CSSStatementMember(2, 0, '\n\nsomething\n')
484 rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
485 rule.checkSelector()
486 last_log = self.last_log
487 self.assertEqual('I013', last_log.code)
488 self.assertEqual(5, last_log.line_number)
489
490
491class RuleTesterConventionC(RuleTesterBase):
492 '''Class for convention C.
493
494 selector1,
495 selecter2 {
496 property1: value1;
497 property2: value2;
498 }
499 '''
500
501 ignored_messaged = ['I005', 'I006']
502
503
504class TestCSSRuleSetDeclarationsChecksC(RuleTesterConventionC):
505 '''Test coding conventions for declarations from rule sets.'''
506
507 def test_valid_declarations(self):
508 stmt = CSSStatementMember(
509 0, 0, '\n some: 3px;\n other:\n url();\n ')
510 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
511 rule.checkDeclarations()
512 self.assertEqual([], self.logs)
513
514 def test_I014(self):
515 stmt = CSSStatementMember(
516 0, 0, '\n some: 3px;\n')
517 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
518 rule.checkDeclarations()
519 self.assertEqual('I014', self.last_log.code)
520
521 stmt = CSSStatementMember(
522 0, 0, '\n some: 3px;\n ')
523 rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
524 rule.checkDeclarations()
525 self.assertEqual('I014', self.last_log.code)
526
527
528if __name__ == '__main__':
529 unittest_main()

Subscribers

People subscribed via source and target branches

to all changes: