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
1=== added file 'pocketlint/contrib/cssccc.py'
2--- pocketlint/contrib/cssccc.py 1970-01-01 00:00:00 +0000
3+++ pocketlint/contrib/cssccc.py 2011-04-18 01:45:49 +0000
4@@ -0,0 +1,492 @@
5+'''
6+This code is in the public domain.
7+
8+Check CSS code for some common coding conventions.
9+The code must be in a valid CSS format.
10+It is recommend to first parse it using cssutils.
11+It is also recommend to check it with pocket-lint for things like trailing
12+spaces or tab characters.
13+
14+If a comment is on the whole line, it will consume the whole line like it
15+was not there.
16+If a comment is inside a line it will only consume its own content.
17+
18+Bases on Stoyan Stefanov's http://www.phpied.com/css-coding-conventions/
19+
20+'@media' rule is not supported.
21+ @media print {
22+ html {
23+ background: #fff;
24+ color: #000;
25+ }
26+ body {
27+ padding: 1in;
28+ border: 0.5pt solid #666;
29+ }
30+ }
31+
32+The following at-rules are supported:
33+ * keyword / text at-rules
34+ * @charset "ISO-8859-15";
35+ * @import url(/css/screen.css) screen, projection;
36+ * @namespace foo "http://example.com/ns/foo";
37+ * keybord / block rules
38+ * @page { block; }
39+ * @font-face { block; }
40+
41+
42+TODO:
43+ * add warning for using px for fonts.
44+ * add Unicode support.
45+ * add AtRule checks
46+ * add support for TAB as a separator / identation.
47+ * add support for @media
48+'''
49+from __future__ import with_statement
50+
51+__version__ = '0.1.0'
52+
53+import sys
54+
55+SELECTOR_SEPARATOR = ','
56+DECLARATION_SEPARATOR = ';'
57+PROPERTY_SEPARATOR = ':'
58+COMMENT_START = r'/*'
59+COMMENT_END = r'*/'
60+AT_TEXT_RULES = ['import', 'charset', 'namespace']
61+AT_BLOCK_RULES = ['page', 'font-face']
62+# If you want
63+# selector,
64+# selector2
65+# {
66+# property:
67+# }
68+#IGNORED_MESSAGES = ['I013', 'I014']
69+
70+# If you want
71+# selector,
72+# selector {
73+# property:
74+# }
75+#IGNORED_MESSAGES = ['I005', 'I014']
76+
77+# If you want
78+# selector,
79+# selector2 {
80+# property:
81+# }
82+IGNORED_MESSAGES = ['I005', 'I006']
83+
84+
85+class CSSRule(object):
86+ '''A CSS rule.'''
87+
88+ def check(self):
89+ '''Check the rule.'''
90+ raise AssertionError('Method not implemtned.')
91+
92+
93+class CSSAtRule(object):
94+ '''A CSS @rule.'''
95+
96+ type = object()
97+
98+ def __init__(self, identifier, keyword, log, text=None, block=None):
99+ self.identifier = identifier
100+ self.keyword = keyword
101+ self.text = text
102+ self.block = block
103+ self.log = log
104+
105+ def check(self):
106+ '''Check the rule.'''
107+
108+
109+class CSSRuleSet(object):
110+ '''A CSS rule_set.'''
111+
112+ type = object()
113+
114+ def __init__(self, selector, declarations, log):
115+ self.selector = selector
116+ self.declarations = declarations
117+ self.log = log
118+
119+ def __str__(self):
120+ return '%s{%s}' % (str(self.selector), str(self.declarations))
121+
122+ def __repr__(self):
123+ return '%d:%s{%s}' % (
124+ self.selector.start_line,
125+ str(self.selector),
126+ str(self.declarations),
127+ )
128+
129+ def check(self):
130+ '''Check the rule set.'''
131+ self.checkSelector()
132+ self.checkDeclarations()
133+
134+ def checkSelector(self):
135+ '''Check rule-set selector.'''
136+ start_line = self.selector.getStartLine()
137+ selectors = self.selector.text.split(SELECTOR_SEPARATOR)
138+ offset = 0
139+ last_selector = selectors[-1]
140+ first_selector = selectors[0]
141+ rest_selectors = selectors[1:]
142+
143+ if first_selector.startswith('\n\n\n'):
144+ self.log(start_line, 'I002', 'To many newlines before selectors.')
145+ elif first_selector.startswith('\n\n'):
146+ pass
147+ elif start_line > 2:
148+ self.log(start_line, 'I003', 'To few newlines before selectors.')
149+ else:
150+ pass
151+
152+ for selector in rest_selectors:
153+ if not selector.startswith('\n'):
154+ self.log(
155+ start_line + offset,
156+ 'I004',
157+ 'Selector must be on a new line.')
158+ offset += selector.count('\n')
159+
160+ if not last_selector.endswith('\n'):
161+ self.log(
162+ start_line + offset,
163+ 'I005',
164+ 'No newline after last selector.')
165+
166+ if not (last_selector[-2] != ' ' and last_selector[-1] == (' ')):
167+ self.log(
168+ start_line + offset,
169+ 'I013',
170+ 'Last selector must be followed by " {".')
171+
172+ def checkDeclarations(self):
173+ '''Check rule-set declarations.'''
174+ start_line = self.declarations.getStartLine()
175+ declarations = self.declarations.text.split(DECLARATION_SEPARATOR)
176+ offset = 0
177+
178+ # Check all declarations except last as this is the new line.
179+ first_declaration = True
180+ for declaration in declarations[:-1]:
181+ if not declaration.startswith('\n'):
182+ self.log(
183+ start_line + offset,
184+ 'I007',
185+ 'Each declarations should start on a new line.',
186+ )
187+ elif (not declaration.startswith('\n ') or
188+ declaration[5] == ' '):
189+ self.log(
190+ start_line + offset,
191+ 'I008',
192+ 'Each declaration must be indented with 4 spaces.',
193+ )
194+
195+ parts = declaration.split(PROPERTY_SEPARATOR)
196+ if len(parts) != 2:
197+ self.log(
198+ start_line + offset,
199+ 'I009',
200+ 'Wrong separator on property: value pair.',
201+ )
202+ else:
203+ prop, value = parts
204+ if prop.endswith(' '):
205+ self.log(
206+ start_line + offset,
207+ 'I010',
208+ 'Whitespace before ":".',
209+ )
210+ if not (value.startswith(' ') or value.startswith('\n')):
211+ self.log(
212+ start_line + offset,
213+ 'I011',
214+ 'Missing whitespace after ":".',
215+ )
216+ elif value.startswith(' '):
217+ self.log(
218+ start_line + offset,
219+ 'I012',
220+ 'Multiple whitespaces after ":".',
221+ )
222+ if first_declaration:
223+ first_declaration = False
224+ else:
225+ offset += declaration.count('\n')
226+
227+ last_declaration = declarations[-1]
228+ offset += last_declaration.count('\n')
229+ if last_declaration != '\n':
230+ self.log(
231+ start_line + offset,
232+ 'I006',
233+ 'Rule declarations should end with a single new line.',
234+ )
235+ if last_declaration != '\n ':
236+ self.log(
237+ start_line + offset,
238+ 'I014',
239+ 'Rule declarations should end indented on a single new line.',
240+ )
241+
242+
243+class CSSStatementMember(object):
244+ '''A member of CSS statement.'''
245+
246+ def __init__(self, start_line, start_character, text):
247+ self.start_line = start_line
248+ self.start_character = start_character
249+ self.text = text
250+
251+ def getStartLine(self):
252+ '''Return the line number for first character in the statement and
253+ the number of new lines untilg the first character.'''
254+ index = 0
255+ text = self.text
256+ character = text[index]
257+ while character == '\n':
258+ index += 1
259+ character = text[index]
260+
261+ return self.start_line + index + 1
262+
263+ def __str__(self):
264+ return self.text
265+
266+ def __repr__(self):
267+ return '%d:%d:{%s}' % (
268+ self.start_line, self.start_character, self.text)
269+
270+
271+class CSSCodingConventionChecker(object):
272+ '''CSS coding convention checker.'''
273+
274+ icons = {
275+ 'E': 'error',
276+ 'I': 'info',
277+ }
278+
279+ def __init__(self, text, logger=None):
280+ self._text = text.splitlines(True)
281+ self.line_number = 0
282+ self.character_number = 0
283+ if logger:
284+ self._logger = logger
285+ else:
286+ self._logger = self._defaultLog
287+
288+ def log(self, line_number, code, message):
289+ '''Log the message with `code`.'''
290+ if code in IGNORED_MESSAGES:
291+ return
292+ icon = self.icons[code[0]]
293+ self._logger(line_number, code + ': ' + message, icon=icon)
294+
295+ def check(self):
296+ '''Check all rules.'''
297+ for rule in self.getRules():
298+ rule.check()
299+
300+ def getRules(self):
301+ '''Generates the next CSS rule ignoring comments.'''
302+ while True:
303+ yield self.getNextRule()
304+
305+ def getNextRule(self):
306+ '''Return the next parsed rule.
307+
308+ Raise `StopIteration` if we are at the last rule.
309+ '''
310+ if self._nextStatementIsAtRule():
311+ text = None
312+ block = None
313+ keyword = self._parse('@')
314+ # TODO: user regex [ \t {]
315+ keyword_text = self._parse(' ')
316+ keyword_name = keyword_text.text
317+ keyword.text += '@' + keyword_name + ' '
318+
319+ if keyword_name.lower() in AT_TEXT_RULES:
320+ text = self._parse(';')
321+ elif keyword_name.lower() in AT_BLOCK_RULES:
322+ start = self._parse('{')
323+ keyword.text += start.text
324+ block = self._parse('}')
325+ else:
326+ self._parse(';')
327+ raise StopIteration
328+
329+ return CSSAtRule(
330+ identifier=keyword_name,
331+ keyword=keyword,
332+ text=text,
333+ block=block,
334+ log=self.log)
335+ else:
336+ selector = self._parse('{')
337+ declarations = self._parse('}')
338+ return CSSRuleSet(
339+ selector=selector,
340+ declarations=declarations,
341+ log=self.log)
342+
343+ def _defaultLog(self, line_number, message, icon='info'):
344+ '''Log the message to STDOUT.'''
345+ print ' %4s:%s' % (line_number, message)
346+
347+ def _nextStatementIsAtRule(self):
348+ '''Return True if next statement in the buffer is an at-rule.
349+
350+ Just look for open brackets and see if there is an @ before that
351+ braket.
352+ '''
353+ search_buffer = []
354+ line_counter = self.line_number
355+ current_line = self._text[line_counter][self.character_number:]
356+ while current_line.find('@') == -1:
357+ search_buffer.append(current_line)
358+ line_counter += 1
359+ try:
360+ current_line = self._text[line_counter]
361+ except IndexError:
362+ return False
363+
364+ text_buffer = ''.join(search_buffer)
365+ if text_buffer.find('{') == -1:
366+ return True
367+ else:
368+ return False
369+
370+ def _parse(self, stop_character):
371+ '''Return the parsed text until stop_character.'''
372+ try:
373+ self._text[self.line_number][self.character_number]
374+ except IndexError:
375+ raise StopIteration
376+ result = []
377+ start_line = self.line_number
378+ start_character = self.character_number
379+ comment_started = False
380+ while True:
381+ try:
382+ data = self._text[self.line_number][self.character_number:]
383+ except IndexError:
384+ break
385+
386+ # Look for comment start/end.
387+ comment_check = _check_comment(data)
388+ (comment_update,
389+ before_comment,
390+ after_comment,
391+ newline_consumed) = comment_check
392+ if comment_update is not None:
393+ comment_started = comment_update
394+
395+ if comment_started:
396+ # We are inside a comment.
397+ # Add the data before the comment and go to next line.
398+ if before_comment is not None:
399+ result.append(before_comment)
400+ self.character_number = 0
401+ self.line_number += 1
402+ continue
403+
404+ # If we have a comment, strip it from the data.
405+ # Remember the initial cursor position to know where to
406+ # continue.
407+ initial_position = data.find(stop_character)
408+ if before_comment is not None or after_comment is not None:
409+ if before_comment is None:
410+ before_comment = ''
411+ if after_comment is None:
412+ after_comment = ''
413+ data = before_comment + after_comment
414+
415+ if initial_position == -1 or newline_consumed:
416+ # We are not at the end.
417+ # Go to next line and append the data.
418+ result.append(data)
419+ self.character_number = 0
420+ self.line_number += 1
421+ continue
422+ else:
423+ # Delimiter found.
424+ # Find it again in the text that now has no comments.
425+ # Append data until the delimiter.
426+ # Move cursor to next character and stop searching for it.
427+ new_position = data.find(stop_character)
428+ result.append(data[:new_position])
429+ self.character_number += initial_position + 1
430+ break
431+
432+ return CSSStatementMember(
433+ start_line=start_line,
434+ start_character=start_character,
435+ text=''.join(result))
436+
437+
438+def _check_comment(data):
439+ '''Check the data for comment markers.'''
440+
441+ comment_started = None
442+ before_comment = None
443+ after_comment = None
444+ newline_consumed = False
445+ comment_start = data.find(COMMENT_START)
446+ if comment_start != -1:
447+ comment_started = True
448+ before_comment = data[:comment_start]
449+
450+ comment_end = data.find(COMMENT_END)
451+ if comment_end != -1:
452+ comment_started = False
453+ # Comment end after the lenght of the actual comment end
454+ # marker.
455+ comment_end += len(COMMENT_END)
456+ if before_comment is None and data[comment_end] == '\n':
457+ # Consume the new line if it next to the comment end and
458+ # the comment in on the whole line.
459+ comment_end += 1
460+ newline_consumed = True
461+ after_comment = data[comment_end:]
462+ return (comment_started, before_comment, after_comment, newline_consumed)
463+
464+
465+def show_usage():
466+ '''Print the command usage.'''
467+ print 'Usage: cssccc OPTIONS'
468+ print ' -h, --help\t\tShow this help.'
469+ print ' -v, --version\t\tShow version.'
470+ print ' -f FILE, --file=FILE\tCheck FILE'
471+
472+
473+def read_file(filename):
474+ '''Return the content of filename.'''
475+ text = ''
476+ with open(filename, 'r') as f:
477+ text = f.read()
478+ return text
479+
480+
481+if __name__ == '__main__':
482+ if len(sys.argv) < 2:
483+ show_usage()
484+ elif sys.argv[1] in ['-v', '--version']:
485+ print 'CSS Code Convention Checker %s' % (__version__)
486+ sys.exit(0)
487+ elif sys.argv[1] == '-f':
488+ text = read_file(sys.argv[2])
489+ checker = CSSCodingConventionChecker(text)
490+ sys.exit(checker.check())
491+ elif sys.argv[1] == '--file=':
492+ text = read_file(sys.argv[1][len('--file='):])
493+ checker = CSSCodingConventionChecker(text)
494+ sys.exit(checker.check())
495+ else:
496+ show_usage()
497
498=== modified file 'pocketlint/formatcheck.py'
499--- pocketlint/formatcheck.py 2011-04-17 16:32:41 +0000
500+++ pocketlint/formatcheck.py 2011-04-18 01:45:49 +0000
501@@ -31,6 +31,7 @@
502 from formatdoctest import DoctestReviewer
503
504 import contrib.pep8 as pep8
505+from contrib.cssccc import CSSCodingConventionChecker
506 from contrib.pyflakes.checker import Checker
507 try:
508 import cssutils
509@@ -396,7 +397,18 @@
510
511 def check(self):
512 """Check the syntax of the CSS code."""
513- if self.text == '' or not HAS_CSSUTILS:
514+ if self.text == '':
515+ return
516+
517+ self.check_cssutils()
518+ self.check_text()
519+ # CSS coding conventoins checks should go last since they rely
520+ # on previous checks.
521+ self.check_css_coding_conventions()
522+
523+ def check_cssutils(self):
524+ """Check the CSS code by parsing it using CSSUtils module."""
525+ if not HAS_CSSUTILS:
526 return
527 handler = CSSReporterHandler(self)
528 log = logging.getLogger('pocket-lint')
529@@ -405,7 +417,6 @@
530 log=log, loglevel=logging.INFO, raiseExceptions=False)
531 parser.parseString(self.text)
532 log.removeHandler(handler)
533- self.check_text()
534
535 def check_text(self):
536 """Call each line_method for each line in text."""
537@@ -416,6 +427,10 @@
538 self.check_conflicts(line_no, line)
539 self.check_tab(line_no, line)
540
541+ def check_css_coding_conventions(self):
542+ """Check the input using CSS Coding Convention checker."""
543+ CSSCodingConventionChecker(self.text, logger=self.message).check()
544+
545
546 class PythonChecker(BaseChecker, AnyTextMixin):
547 """Check python source code."""
548
549=== modified file 'pocketlint/tests/test_css.py'
550--- pocketlint/tests/test_css.py 2011-04-15 03:14:05 +0000
551+++ pocketlint/tests/test_css.py 2011-04-18 01:45:49 +0000
552@@ -41,7 +41,7 @@
553 if not HAS_CSSUTILS:
554 return
555 checker = CSSChecker('bogus', ill_formed_property, self.reporter)
556- checker.check()
557+ checker.check_cssutils()
558 messages = [
559 (3, "CSSValue: No match: 'CHAR', u':'"),
560 (0, 'CSSStyleDeclaration: Syntax Error in Property: '
561@@ -52,7 +52,7 @@
562 if not HAS_CSSUTILS:
563 return
564 checker = CSSChecker('ballyhoo', invalid_value, self.reporter)
565- checker.check()
566+ checker.check_cssutils()
567 message = (
568 'Invalid value for "CSS Color Module Level 3/CSS Level 2.1" '
569 'property: speckled: color')
570
571=== added file 'pocketlint/tests/test_cssccc.py'
572--- pocketlint/tests/test_cssccc.py 1970-01-01 00:00:00 +0000
573+++ pocketlint/tests/test_cssccc.py 2011-04-18 01:45:49 +0000
574@@ -0,0 +1,529 @@
575+'''Test module for cssccc'''
576+
577+from unittest import TestCase, main as unittest_main
578+
579+
580+from pocketlint.contrib.cssccc import (
581+ CSSCodingConventionChecker, CSSAtRule, CSSRuleSet, CSSStatementMember)
582+
583+
584+class TestCSSCodingConventionChecker(TestCase):
585+ '''Test for parsing the CSS text.'''
586+
587+ def test_getNextRule_start(self):
588+ text = 'selector{}'
589+ lint = CSSCodingConventionChecker(text)
590+ rule = lint.getNextRule()
591+ self.assertTrue(rule.type is CSSRuleSet.type)
592+ self.assertEqual('selector', rule.selector.text)
593+ self.assertEqual(0, rule.selector.start_line)
594+ self.assertEqual(0, rule.selector.start_character)
595+
596+ text = '\nselector{}'
597+ lint = CSSCodingConventionChecker(text)
598+ rule = lint.getNextRule()
599+ self.assertTrue(rule.type is CSSRuleSet.type)
600+ self.assertEqual('\nselector', rule.selector.text)
601+ self.assertEqual(0, rule.selector.start_line)
602+ self.assertEqual(0, rule.selector.start_character)
603+
604+ text = '\n\nselector{}'
605+ lint = CSSCodingConventionChecker(text)
606+ rule = lint.getNextRule()
607+ self.assertTrue(rule.type is CSSRuleSet.type)
608+ self.assertEqual('\n\nselector', rule.selector.text)
609+ self.assertEqual(0, rule.selector.start_line)
610+ self.assertEqual(0, rule.selector.start_character)
611+
612+ text = 'selector\n{}'
613+ lint = CSSCodingConventionChecker(text)
614+ rule = lint.getNextRule()
615+ self.assertTrue(rule.type is CSSRuleSet.type)
616+ self.assertEqual('selector\n', rule.selector.text)
617+ self.assertEqual(0, rule.selector.start_line)
618+ self.assertEqual(0, rule.selector.start_character)
619+
620+ text = 'selector, {}'
621+ lint = CSSCodingConventionChecker(text)
622+ rule = lint.getNextRule()
623+ self.assertTrue(rule.type is CSSRuleSet.type)
624+ self.assertEqual('selector, ', rule.selector.text)
625+ self.assertEqual(0, rule.selector.start_line)
626+ self.assertEqual(0, rule.selector.start_character)
627+
628+ def test_getNextRule_content(self):
629+ text = 'selector { content; }'
630+ lint = CSSCodingConventionChecker(text)
631+ rule = lint.getNextRule()
632+ self.assertTrue(rule.type is CSSRuleSet.type)
633+ self.assertEqual(' content; ', rule.declarations.text)
634+ self.assertEqual(0, rule.declarations.start_line)
635+ self.assertEqual(10, rule.declarations.start_character)
636+
637+ text = 'selector \n{\n content; }'
638+ lint = CSSCodingConventionChecker(text)
639+ rule = lint.getNextRule()
640+ self.assertTrue(rule.type is CSSRuleSet.type)
641+ self.assertEqual('\n content; ', rule.declarations.text)
642+ self.assertEqual(1, rule.declarations.start_line)
643+ self.assertEqual(1, rule.declarations.start_character)
644+
645+ def test_getNextRule_continue(self):
646+ text = 'selector1\n { content1; }\n\nselector2\n{content2}\n'
647+ lint = CSSCodingConventionChecker(text)
648+ rule = lint.getNextRule()
649+ self.assertTrue(rule.type is CSSRuleSet.type)
650+ self.assertEqual('selector1\n ', rule.selector.text)
651+ self.assertEqual(0, rule.selector.start_line)
652+ self.assertEqual(0, rule.selector.start_character)
653+ self.assertEqual(' content1; ', rule.declarations.text)
654+ self.assertEqual(1, rule.declarations.start_line)
655+ self.assertEqual(2, rule.declarations.start_character)
656+
657+ rule = lint.getNextRule()
658+ self.assertTrue(rule.type is CSSRuleSet.type)
659+ self.assertEqual('\n\nselector2\n', rule.selector.text)
660+ self.assertEqual(1, rule.selector.start_line)
661+ self.assertEqual(14, rule.selector.start_character)
662+ self.assertEqual('content2', rule.declarations.text)
663+ self.assertEqual(4, rule.declarations.start_line)
664+ self.assertEqual(1, rule.declarations.start_character)
665+
666+ def test_getNextRule_stop(self):
667+ text ='rule1{st1\n}\n@font-face {\n src: url("u\n u"); \n }\nr2{st2}'
668+ lint = CSSCodingConventionChecker(text)
669+ rule = lint.getNextRule()
670+ self.assertTrue(rule.type is CSSRuleSet.type)
671+ rule = lint.getNextRule()
672+ self.assertTrue(rule.type is CSSAtRule.type)
673+ rule = lint.getNextRule()
674+ self.assertTrue(rule.type is CSSRuleSet.type)
675+ self.failUnlessRaises(StopIteration, lint.getNextRule)
676+
677+ def test_getNextRule_comment(self):
678+ text = '\n\n/* c\nm*/\nsel\n{\ns/*com*/\ncont1;/*com*/\ncont2;}'
679+ lint = CSSCodingConventionChecker(text)
680+ rule = lint.getNextRule()
681+ self.assertTrue(rule.type is CSSRuleSet.type)
682+ self.assertEqual('\n\nsel\n', rule.selector.text)
683+ self.assertEqual(0, rule.selector.start_line)
684+ self.assertEqual(0, rule.selector.start_character)
685+ self.assertEqual('\ns\ncont1;\ncont2;', rule.declarations.text)
686+ self.assertEqual(5, rule.declarations.start_line)
687+ self.assertEqual(1, rule.declarations.start_character)
688+
689+ def test_get_at_import_rule(self):
690+ '''Test for @import url(/css/screen.css) screen, projection;'''
691+ text ='rule1{st1\n}\n@import url(somet) print, soment ;rule2{st2}'
692+ lint = CSSCodingConventionChecker(text)
693+ rule = lint.getNextRule()
694+ self.assertTrue(rule.type is CSSRuleSet.type)
695+ rule = lint.getNextRule()
696+ self.assertTrue(rule.type is CSSAtRule.type)
697+ self.assertTrue(rule.block is None)
698+ self.assertEqual('import', rule.identifier)
699+ self.assertEqual('\n@import ', rule.keyword.text)
700+ self.assertEqual(1, rule.keyword.start_line)
701+ self.assertEqual(1, rule.keyword.start_character)
702+ self.assertEqual(' url(somet) print, soment ', rule.text.text)
703+ self.assertEqual(2, rule.text.start_line)
704+ self.assertEqual(8, rule.text.start_character)
705+
706+ def test_get_at_charset_rule(self):
707+ '''Test for @charset "ISO-8859-15";'''
708+ text ='rule1{st1\n}\n@charset "utf" ;rule2{st2}'
709+ lint = CSSCodingConventionChecker(text)
710+ rule = lint.getNextRule()
711+ self.assertTrue(rule.type is CSSRuleSet.type)
712+ rule = lint.getNextRule()
713+ self.assertTrue(rule.type is CSSAtRule.type)
714+ self.assertTrue(rule.block is None)
715+ self.assertEqual('charset', rule.identifier)
716+ self.assertEqual('\n@charset ', rule.keyword.text)
717+ self.assertEqual(1, rule.keyword.start_line)
718+ self.assertEqual(1, rule.keyword.start_character)
719+ self.assertEqual(' "utf" ', rule.text.text)
720+ self.assertEqual(2, rule.text.start_line)
721+ self.assertEqual(9, rule.text.start_character)
722+
723+ def test_get_at_namespace_rule(self):
724+ '''Test for @namespace foo "http://foo" ;'''
725+ text ='rule1{st1\n}@namespace foo "http://foo" ;rule2{st2}'
726+ lint = CSSCodingConventionChecker(text)
727+ rule = lint.getNextRule()
728+ self.assertTrue(rule.type is CSSRuleSet.type)
729+ rule = lint.getNextRule()
730+ self.assertTrue(rule.type is CSSAtRule.type)
731+ self.assertTrue(rule.block is None)
732+ self.assertEqual('namespace', rule.identifier)
733+ self.assertEqual('@namespace ', rule.keyword.text)
734+ self.assertEqual(1, rule.keyword.start_line)
735+ self.assertEqual(1, rule.keyword.start_character)
736+ self.assertEqual(' foo "http://foo" ', rule.text.text)
737+ self.assertEqual(1, rule.text.start_line)
738+ self.assertEqual(12, rule.text.start_character)
739+
740+ def test_get_at_page_rule(self):
741+ '''Test for @page
742+
743+ @page :left {
744+ margin-left: 5cm; /* left pages only */
745+ }
746+ '''
747+ text ='rule1{st1\n}\n@page :left {\n mar; /*com*/\n }\nrule2{st2}'
748+ lint = CSSCodingConventionChecker(text)
749+ rule = lint.getNextRule()
750+ self.assertTrue(rule.type is CSSRuleSet.type)
751+ rule = lint.getNextRule()
752+ self.assertTrue(rule.type is CSSAtRule.type)
753+ self.assertTrue(rule.text is None)
754+ self.assertEqual('page', rule.identifier)
755+ self.assertEqual('\n@page :left ', rule.keyword.text)
756+ self.assertEqual(1, rule.keyword.start_line)
757+ self.assertEqual(1, rule.keyword.start_character)
758+ self.assertEqual('\n mar; \n ', rule.block.text)
759+ self.assertEqual(2, rule.block.start_line)
760+ self.assertEqual(13, rule.block.start_character)
761+
762+ def test_get_at_font_face_rule(self):
763+ '''Test for @font-face
764+
765+ @font-face {
766+ font-family: "Example Font";
767+ src: url("http://www.example.com
768+ /fonts/example");
769+ }
770+ '''
771+ text ='rule1{st1\n}\n@font-face {\n src: url("u\n u"); \n }\nr2{st2}'
772+ lint = CSSCodingConventionChecker(text)
773+ rule = lint.getNextRule()
774+ self.assertTrue(rule.type is CSSRuleSet.type)
775+ rule = lint.getNextRule()
776+ self.assertTrue(rule.type is CSSAtRule.type)
777+ self.assertTrue(rule.text is None)
778+ self.assertEqual('font-face', rule.identifier)
779+ self.assertEqual('\n@font-face ', rule.keyword.text)
780+ self.assertEqual(1, rule.keyword.start_line)
781+ self.assertEqual(1, rule.keyword.start_character)
782+ self.assertEqual('\n src: url("u\n u"); \n ', rule.block.text)
783+ self.assertEqual(2, rule.block.start_line)
784+ self.assertEqual(12, rule.block.start_character)
785+ rule = lint.getNextRule()
786+ self.assertTrue(rule.type is CSSRuleSet.type)
787+ self.failUnlessRaises(StopIteration, lint.getNextRule)
788+
789+
790+class TestCSSStatementMember(TestCase):
791+ '''Tests for CSSStatementMember.'''
792+
793+ def test_getStartLine(self):
794+ statement = CSSStatementMember(0, 4, 'some')
795+ self.assertEqual(1, statement.getStartLine())
796+ statement = CSSStatementMember(3, 4, 'some')
797+ self.assertEqual(4, statement.getStartLine())
798+ statement = CSSStatementMember(3, 4, '\n\nsome')
799+ self.assertEqual(6, statement.getStartLine())
800+
801+
802+class TestLog(object):
803+ '''Container for a test log.'''
804+
805+ def __init__(self, line_number, code, message):
806+ self.line_number = line_number
807+ self.code = code
808+ self.message = message
809+
810+
811+class RuleTesterBase(TestCase):
812+ '''Base class for rule checkers.'''
813+
814+ ignored_messaged = []
815+
816+ def setUp(self):
817+ self.logs = []
818+
819+ def log(self, line_number, code, message):
820+ if code in self.ignored_messaged:
821+ return
822+ self.logs.append((line_number, code, message))
823+
824+ @property
825+ def last_log(self):
826+ (line_number, code, message) = self.logs.pop()
827+ return TestLog(line_number, code, message)
828+
829+
830+class RuleTesterConventionA(RuleTesterBase):
831+ '''Class for convention A.
832+
833+ selector1,
834+ selecter2
835+ {
836+ property1: value1;
837+ property2: value2;
838+ }
839+ '''
840+
841+ ignored_messaged = ['I013', 'I014']
842+
843+
844+class TestCSSRuleSetSelectorChecksA(RuleTesterConventionA):
845+ '''Test coding conventions for selector from rule sets.'''
846+
847+ def test_valid_selector(self):
848+
849+ selector = CSSStatementMember(0, 0, 'something\n')
850+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
851+ rule.checkSelector()
852+ self.assertEqual([], self.logs)
853+
854+ selector = CSSStatementMember(0, 0, '\nsomething\n')
855+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
856+ rule.checkSelector()
857+ self.assertEqual([], self.logs)
858+
859+ selector = CSSStatementMember(1, 0, '\n\nsomething\n')
860+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
861+ rule.checkSelector()
862+ self.assertEqual([], self.logs)
863+
864+ selector = CSSStatementMember(2, 0, '\n\nsomething,\nsomethi\n')
865+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
866+ rule.checkSelector()
867+ self.assertEqual([], self.logs)
868+
869+ selector = CSSStatementMember(3, 0, '\n\nsom:some some,\n#somethi\n')
870+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
871+ rule.checkSelector()
872+ self.assertEqual([], self.logs)
873+
874+ def test_I002(self):
875+ selector = CSSStatementMember(2, 0, '\n\n\nsomething\n')
876+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
877+ rule.checkSelector()
878+ last_log = self.last_log
879+ self.assertEqual('I002', last_log.code)
880+ self.assertEqual(6, last_log.line_number)
881+
882+ selector = CSSStatementMember(4, 0, '\n\n\n\nsomething\n')
883+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
884+ rule.checkSelector()
885+ last_log = self.last_log
886+ self.assertEqual('I002', last_log.code)
887+ self.assertEqual(9, last_log.line_number)
888+
889+ def test_I003(self):
890+ selector = CSSStatementMember(2, 0, '\nsomething\n')
891+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
892+ rule.checkSelector()
893+ last_log = self.last_log
894+ self.assertEqual('I003', last_log.code)
895+ self.assertEqual(4, last_log.line_number)
896+
897+ selector = CSSStatementMember(2, 0, 'something\n')
898+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
899+ rule.checkSelector()
900+ last_log = self.last_log
901+ self.assertEqual('I003', last_log.code)
902+ self.assertEqual(3, last_log.line_number)
903+
904+ def test_I004(self):
905+ selector = CSSStatementMember(3, 0, '\n\nsomething, something\n')
906+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
907+ rule.checkSelector()
908+ last_log = self.last_log
909+ self.assertEqual('I004', last_log.code)
910+ self.assertEqual(6, last_log.line_number)
911+
912+ def test_I005(self):
913+ selector = CSSStatementMember(4, 0, '\nsomething,\nsomething')
914+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
915+ rule.checkSelector()
916+ last_log = self.last_log
917+ self.assertEqual('I005', last_log.code)
918+ self.assertEqual(7, last_log.line_number)
919+
920+
921+class TestCSSRuleSetDeclarationsChecksA(RuleTesterConventionA):
922+ '''Test coding conventions for declarations from rule sets.'''
923+
924+ def test_valid_declarations(self):
925+ stmt = CSSStatementMember(
926+ 0, 0, '\n some: 3px;\n other:\n url();\n')
927+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
928+ rule.checkDeclarations()
929+ self.assertEqual([], self.logs)
930+
931+ def test_I006(self):
932+ stmt = CSSStatementMember(
933+ 4, 0, '\n some: 3px;\n other: url();')
934+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
935+ rule.checkDeclarations()
936+ last_log = self.last_log
937+ self.assertEqual('I006', last_log.code)
938+ self.assertEqual(7, last_log.line_number)
939+
940+ stmt = CSSStatementMember(
941+ 4, 0, '\n some: 3px;\n other: url();\n ')
942+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
943+ rule.checkDeclarations()
944+ last_log = self.last_log
945+ self.assertEqual('I006', last_log.code)
946+ self.assertEqual(8, last_log.line_number)
947+
948+ stmt = CSSStatementMember(
949+ 4, 0, '\n some: 3px;\n other: url();\n\n ')
950+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
951+ rule.checkDeclarations()
952+ last_log = self.last_log
953+ self.assertEqual('I006', last_log.code)
954+ self.assertEqual(9, last_log.line_number)
955+
956+ def test_I007(self):
957+ stmt = CSSStatementMember(
958+ 4, 0, '\n some: 3px; other: url();\n')
959+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
960+ rule.checkDeclarations()
961+ last_log = self.last_log
962+ self.assertEqual('I007', last_log.code)
963+ self.assertEqual(6, last_log.line_number)
964+
965+ def test_I008(self):
966+ stmt = CSSStatementMember(
967+ 0, 0, '\n some: 3px;\n other: url();\n')
968+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
969+ rule.checkDeclarations()
970+ self.assertEqual('I008', self.last_log.code)
971+
972+ stmt = CSSStatementMember(
973+ 0, 0, '\n some: 3px;\n other: url();\n')
974+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
975+ rule.checkDeclarations()
976+ self.assertEqual('I008', self.last_log.code)
977+
978+ def test_I009(self):
979+ stmt = CSSStatementMember(
980+ 0, 0, '\n some 3px;\n other: url();\n')
981+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
982+ rule.checkDeclarations()
983+ self.assertEqual('I009', self.last_log.code)
984+
985+ stmt = CSSStatementMember(
986+ 0, 0, '\n some: 3:px;\n other: url();\n')
987+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
988+ rule.checkDeclarations()
989+ self.assertEqual('I009', self.last_log.code)
990+
991+ def test_I010(self):
992+ stmt = CSSStatementMember(
993+ 0, 0, '\n some : 3px;\n')
994+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
995+ rule.checkDeclarations()
996+ self.assertEqual('I010', self.last_log.code)
997+
998+ def test_I011(self):
999+ stmt = CSSStatementMember(
1000+ 0, 0, '\n some:3px;\n')
1001+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
1002+ rule.checkDeclarations()
1003+ self.assertEqual('I011', self.last_log.code)
1004+
1005+ def test_I012(self):
1006+ stmt = CSSStatementMember(
1007+ 0, 0, '\n some: 3px;\n')
1008+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
1009+ rule.checkDeclarations()
1010+ self.assertEqual('I012', self.last_log.code)
1011+
1012+
1013+class RuleTesterConventionB(RuleTesterBase):
1014+ '''Class for convention B.
1015+
1016+ selector1,
1017+ selecter2 {
1018+ property1: value1;
1019+ property2: value2;
1020+ }
1021+ '''
1022+
1023+ ignored_messaged = ['I005', 'I014']
1024+
1025+
1026+class TestCSSRuleSetSelectorChecksB(RuleTesterConventionB):
1027+ '''Test coding conventions for selector from rule sets.'''
1028+
1029+ def test_valid_selector(self):
1030+
1031+ selector = CSSStatementMember(0, 0, 'something ')
1032+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1033+ rule.checkSelector()
1034+ self.assertEqual([], self.logs)
1035+
1036+ selector = CSSStatementMember(0, 0, '\nsomething ')
1037+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1038+ rule.checkSelector()
1039+ self.assertEqual([], self.logs)
1040+
1041+ selector = CSSStatementMember(1, 0, '\n\nsomething ')
1042+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1043+ rule.checkSelector()
1044+ self.assertEqual([], self.logs)
1045+
1046+ selector = CSSStatementMember(2, 0, '\n\nsomething,\nsomethi ')
1047+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1048+ rule.checkSelector()
1049+ self.assertEqual([], self.logs)
1050+
1051+ selector = CSSStatementMember(3, 0, '\n\nsom:some some,\n#somethi ')
1052+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1053+ rule.checkSelector()
1054+ self.assertEqual([], self.logs)
1055+
1056+ def test_I013(self):
1057+ selector = CSSStatementMember(2, 0, '\n\nsomething\n')
1058+ rule = CSSRuleSet(selector=selector, declarations=None, log=self.log)
1059+ rule.checkSelector()
1060+ last_log = self.last_log
1061+ self.assertEqual('I013', last_log.code)
1062+ self.assertEqual(5, last_log.line_number)
1063+
1064+
1065+class RuleTesterConventionC(RuleTesterBase):
1066+ '''Class for convention C.
1067+
1068+ selector1,
1069+ selecter2 {
1070+ property1: value1;
1071+ property2: value2;
1072+ }
1073+ '''
1074+
1075+ ignored_messaged = ['I005', 'I006']
1076+
1077+
1078+class TestCSSRuleSetDeclarationsChecksC(RuleTesterConventionC):
1079+ '''Test coding conventions for declarations from rule sets.'''
1080+
1081+ def test_valid_declarations(self):
1082+ stmt = CSSStatementMember(
1083+ 0, 0, '\n some: 3px;\n other:\n url();\n ')
1084+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
1085+ rule.checkDeclarations()
1086+ self.assertEqual([], self.logs)
1087+
1088+ def test_I014(self):
1089+ stmt = CSSStatementMember(
1090+ 0, 0, '\n some: 3px;\n')
1091+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
1092+ rule.checkDeclarations()
1093+ self.assertEqual('I014', self.last_log.code)
1094+
1095+ stmt = CSSStatementMember(
1096+ 0, 0, '\n some: 3px;\n ')
1097+ rule = CSSRuleSet(selector=None, declarations=stmt, log=self.log)
1098+ rule.checkDeclarations()
1099+ self.assertEqual('I014', self.last_log.code)
1100+
1101+
1102+if __name__ == '__main__':
1103+ unittest_main()

Subscribers

People subscribed via source and target branches

to all changes: