Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(327)

Side by Side Diff: chrome/browser/resources/PRESUBMIT.py

Issue 9323016: [WebUI] Add some presubmit checks (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: fixed false negative, changed dir handling Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
4
5 """Presubmit script for Chromium WebUI resources.
6
7 See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
8 for more details about the presubmit API built into gcl/git cl, and see
9 http://www.chromium.org/developers/web-development-style-guide for the rules
10 we're checking against here.
11 """
12
13 # TODO(dbeam): Lazy load these?
14 import unittest
15 from testing_support.super_mox import SuperMoxTestBase, mox
M-A Ruel 2012/02/09 14:09:46 As I said before, I dislike that; presubmit_suppo
16
17
18 def CheckChangeOnUpload(input_api, output_api):
19 return _CommonChecks(input_api, output_api)
20
21
22 def CheckChangeOnCommit(input_api, output_api):
23 return _CommonChecks(input_api, output_api)
24
25
26 def _RunUnitTests():
27 unittest.TextTestRunner().run(
28 unittest.TestLoader().loadTestsFromTestCase(WebDevStyleGuideTest))
29
30
31 def _CommonChecks(input_api, output_api):
32 """Checks common to both upload and commit."""
33 results = []
34 resources = input_api.PresubmitLocalPath()
35
36 presubmit = input_api.os_path.join(resources, 'PRESUBMIT.py')
37 if presubmit in [f.AbsoluteLocalPath() for f in input_api.AffectedFiles()]:
38 _RunUnitTests()
39
40 # TODO(dbeam): Remove this filter eventually when ready.
41 dirs = (input_api.os_path.join(resources, 'ntp4'),
42 input_api.os_path.join(resources, 'options2'))
43 def file_filter(affected_file):
44 f = affected_file.AbsoluteLocalPath()
45 return (f.startswith(dirs) and f.endswith('.css'))
46
47 results.extend(_CheckWebDevStyleGuide(input_api, output_api,
48 source_file_filter=file_filter))
49 return results
50
51
52 # TODO(dbeam): Real CSS parser?
53 def _CheckWebDevStyleGuide(input_api, output_api, source_file_filter=None):
54 # We use this a lot, so make a nick name variable.
55 re = input_api.re
56
57 def _collapseable_hex(s):
58 return (len(s) == 6 and s[0] == s[1] and s[2] == s[3] and s[4] == s[5])
59
60 def _is_gray(s):
61 return s[0] == s[1] == s[2] if len(s) == 3 else s[0:2] == s[2:4] == s[4:6]
62
63 def _remove_ats(s):
64 return re.sub(re.compile(r'@\w+.*?{(.*{.*?})+.*?}', re.DOTALL), '\\1', s)
65
66 def _remove_comments(s):
67 return re.sub(re.compile(r'/\*.*?\*/', re.DOTALL), '', s)
68
69 def _rgb_from_hex(s):
70 if len(s) == 3:
71 r, g, b = s[0] + s[0], s[1] + s[1], s[2] + s[2]
72 else:
73 r, g, b = s[0:2], s[2:4], s[4:6]
74 return int(r, base=16), int(g, base=16), int(b, base=16)
75
76 def alphabetize_props(file):
77 errors = []
78 for block in re.finditer(r'{(.*?)}', file, re.DOTALL):
79 rules = filter(lambda r: r.find(': ') >= 0,
80 map(lambda t: t.strip(), block.group(1).split(';'))[0:-1])
81 props = map(lambda r: r[0:r.find(':')], rules)
82 if props != sorted(props):
83 errors.append(' %s;\n' % (';\n '.join(rules)))
84 return errors
85
86 def braces_have_space_before_and_nothing_after(line):
87 return re.search(r'(?:^|\S){|{\s*\S+\s*$', line)
88
89 def classes_use_dashes(line):
90 # Intentionally dumbed down version of CSS 2.1 grammar for class without
91 # non-ASCII, escape chars, or whitespace.
92 m = re.search(r'\.(-?[_a-zA-Z0-9-]+).*[,{]\s*$', line)
93 return (m and (m.group(1).lower() != m.group(1) or
94 m.group(1).find('_') >= 0))
95
96 def close_brace_on_new_line(line):
97 return (line.find('}') >= 0 and re.search(r'[^ }]', line))
98
99 def colons_have_space_after(line):
100 return re.search(r':(?!\/\/)\S(?!.*[{,]\s*$)', line)
101
102 def favor_single_quotes(lines):
103 return line.find('"') >= 0
104
105 # Shared between hex_could_be_shorter and rgb_if_not_gray.
106 hex_reg = (r'#([a-fA-F0-9]{3}|[a-fA-F0-9]{6})(?=[^_a-zA-Z0-9-]|$)'
107 r'(?!.*(?:{.*|,\s*)$)')
108 def hex_could_be_shorter(line):
109 m = re.search(hex_reg, line)
110 return (m and _collapseable_hex(m.group(1)))
111
112 small_seconds = r'(?:^|[^_a-zA-Z0-9-])(0?\.[0-9]+)s(?!-?[_a-zA-Z0-9-])'
113 def milliseconds_for_small_times(line):
114 return re.search(small_seconds, line)
115
116 def one_rule_per_line(line):
117 return re.search('(.*:(?!\/\/)){2,}(?!.*[,{]\s*$)', line)
118
119 any_reg = re.compile(r':(?:-webkit-)?any\(.*?\)', re.DOTALL)
120 multi_sels = re.compile(r'(?:}[\n\s]*)?([^,]+,(?=[^{}]+?{).*[,{])\s*$',
121 re.MULTILINE)
122 def one_selector_per_line(file):
123 errors = []
124 for b in re.finditer(multi_sels, re.sub(any_reg, '', file)):
125 errors.append(' ' + b.group(1).strip())
126 return errors
127
128 def rgb_if_not_gray(line):
129 m = re.search(hex_reg, line)
130 return (m and not _is_gray(m.group(1)))
131
132 def suggest_ms_from_s(line):
133 ms = int(float(re.search(small_seconds, line).group(1)) * 1000)
134 return ' (replace with %dms)' % ms
135
136 def suggest_rgb_from_hex(line):
137 suggestions = ['rgb(%d, %d, %d)' % _rgb_from_hex(h.group(1))
138 for h in re.finditer(hex_reg, line)]
139 return ' (replace with %s)' % ', '.join(suggestions)
140
141 def suggest_short_hex(line):
142 h = re.search(hex_reg, line).group(1)
143 return ' (replace with #%s)' % (h[0] + h[2] + h[4])
144
145 hsl = r'hsl\([^\)]*(?:[, ]|(?<=\())(?:0?\.?)?0%'
146 zeros = (r'[^0-9]0(?:\.0?)?'
147 r'(?:px|em|%|in|cm|mm|pc|pt|ex|deg|g?rad|m?s|k?hz)'
148 r'(?!\s*\{)')
149 def zero_length_values(line):
150 return (re.search(zeros, line) and not re.search(hsl, line))
151
152 added_or_modified_files_checks = [
153 { 'desc': 'Alphabetize properties and list vendor specific (i.e. -webkit) '
154 'above standard.',
155 'test': alphabetize_props,
156 'multiline': True,
157 },
158 { 'desc': 'Start braces ({) end a selector, have a space before them and '
159 'no rules after.',
160 'test': braces_have_space_before_and_nothing_after,
161 },
162 { 'desc': 'Classes use .dash-form.',
163 'test': classes_use_dashes,
164 },
165 { 'desc': 'Always put a rule closing brace (}) on a new line.',
166 'test': close_brace_on_new_line,
167 },
168 { 'desc': 'Colons (:) should have a space after them.',
169 'test': colons_have_space_after,
170 },
171 { 'desc': 'Use single quotes (\') instead of double quotes (") in strings.',
172 'test': favor_single_quotes,
173 },
174 { 'desc': 'Use abbreviated hex (#rgb) when in form #rrggbb.',
175 'test': hex_could_be_shorter,
176 'after': suggest_short_hex,
177 },
178 { 'desc': 'Use milliseconds for time measurements under 1 second.',
179 'test': milliseconds_for_small_times,
180 'after': suggest_ms_from_s,
181 },
182 { 'desc': 'One rule per line (what not to do: color: red; margin: 0;).',
183 'test': one_rule_per_line,
184 },
185 { 'desc': 'One selector per line (what not to do: a, b {}).',
186 'test': one_selector_per_line,
187 'multiline': True,
188 },
189 { 'desc': 'Use rgb() over #hex when not a shade of gray (like #333).',
190 'test': rgb_if_not_gray,
191 'after': suggest_rgb_from_hex,
192 },
193 { 'desc': 'Make all zero length terms (i.e. 0px) 0 unless inside of hsl() '
194 'or part of @keyframe.',
195 'test': zero_length_values,
196 },
197 ]
198
199 results = []
200 affected_files = input_api.AffectedFiles(include_deletes=False,
201 file_filter=source_file_filter)
202 files = []
203 for f in affected_files:
204 # Remove all /*comments*/ and @at-keywords as we're not using a real parser.
205 file_contents = _remove_ats(_remove_comments('\n'.join(f.NewContents())))
206 files.append((f.LocalPath(), file_contents))
207
208 # Only look at CSS files for now.
209 for f in filter(lambda f: f[0].endswith('.css'), files):
210 file_errors = []
211 for check in added_or_modified_files_checks:
212 # If the check is multiline, it receieves the whole file and gives us back
213 # a list of things wrong. If the check isn't multiline, we pass it each
214 # line and the check returns something truthy if there's an issue.
215 if ('multiline' in check and check['multiline']):
216 check_errors = check['test'](f[1])
217 if len(check_errors) > 0:
218 # There are currently no multiline checks with ['after'].
219 file_errors.append('- %s\n%s' %
220 (check['desc'], '\n'.join(check_errors).rstrip()))
221 else:
222 check_errors = []
223 lines = f[1].splitlines()
224 for lnum in range(0, len(lines)):
225 line = lines[lnum]
226 if check['test'](line):
227 error = ' ' + line.strip()
228 if 'after' in check:
229 error += check['after'](line)
230 check_errors.append(error)
231 if len(check_errors) > 0:
232 file_errors.append('- %s\n%s' %
233 (check['desc'], '\n'.join(check_errors)))
234 if len(file_errors) > 0:
235 results.append(output_api.PresubmitNotifyResult(
236 '%s:\n%s' % (f[0], '\n\n'.join(file_errors))))
237
238 return results
239
240
241 class WebDevStyleGuideTest(SuperMoxTestBase):
242 def runTest(self):
243 self.__testFunc()
244
245 def setUp(self):
246 SuperMoxTestBase.setUp(self)
247
248 self.fake_file_name = 'fake.css'
249
250 self.fake_file = self.mox.CreateMockAnything()
251 self.mox.StubOutWithMock(self.fake_file, 'LocalPath')
252 self.fake_file.LocalPath().AndReturn(self.fake_file_name)
253 # Actual calls to NewContents() are defined in each test.
254 self.mox.StubOutWithMock(self.fake_file, 'NewContents')
255
256 self.input_api = self.mox.CreateMockAnything()
257 self.mox.StubOutWithMock(self.input_api, 'AffectedSourceFiles')
258 self.input_api.AffectedFiles(
259 include_deletes=False, file_filter=None).AndReturn([self.fake_file])
260
261 import re
262 self.input_api.re = re
263
264 # Actual creations of PresubmitNotifyResult are defined in each test.
265 self.output_api = self.mox.CreateMockAnything()
266 self.mox.StubOutWithMock(self.output_api, 'PresubmitNotifyResult',
267 use_mock_anything=True)
268
269 def VerifyContentsProducesOutput(self, contents, output):
270 self.fake_file.NewContents().AndReturn(contents.splitlines())
271 self.output_api.PresubmitNotifyResult(
272 self.fake_file_name + ':\n' + output.strip()).AndReturn(None)
273 self.mox.ReplayAll()
274 _CheckWebDevStyleGuide(self.input_api, self.output_api)
275
276 def testAlphaWithAtBlock(self):
277 self.VerifyContentsProducesOutput("""
278 /* A hopefully safely ignored comment and @media statement. /**/
279 @media print {
280 div {
281 display: block;
282 color: red;
283 }
284 }""", """
285 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
286 display: block;
287 color: red;""")
288
289 def testAlphaWithNonStandard(self):
290 self.VerifyContentsProducesOutput("""
291 div {
292 /* A hopefully safely ignored comment and @media statement. /**/
293 color: red;
294 -webkit-margin-start: 5px;
295 }""", """
296 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
297 color: red;
298 -webkit-margin-start: 5px;""")
299
300 def testAlphaWithLongerDashedProps(self):
301 self.VerifyContentsProducesOutput("""
302 div {
303 border-left: 5px; /* A hopefully removed comment. */
304 border: 5px solid red;
305 }""", """
306 - Alphabetize properties and list vendor specific (i.e. -webkit) above standard.
307 border-left: 5px;
308 border: 5px solid red;""")
309
310 def testBracesHaveSpaceBeforeAndNothingAfter(self):
311 self.VerifyContentsProducesOutput("""
312 /* Hello! */div/* Comment here*/{
313 display: block;
314 }
315
316 blah /* hey! */
317 {
318 rule: value;
319 }
320
321 .this.is { /* allowed */
322 rule: value;
323 }""", """
324 - Start braces ({) end a selector, have a space before them and no rules after.
325 div{
326 {""")
327
328 def testClassesUseDashes(self):
329 self.VerifyContentsProducesOutput("""
330 .className,
331 .ClassName,
332 .class-name /* We should not catch this. */,
333 .class_name {
334 display: block;
335 }""", """
336 - Classes use .dash-form.
337 .className,
338 .ClassName,
339 .class_name {""")
340
341 def testCloseBraceOnNewLine(self):
342 self.VerifyContentsProducesOutput("""
343 @media { /* TODO(dbeam) Fix this case.
344 .rule {
345 display: block;
346 }}
347
348 #rule {
349 rule: value; }""", """
350 - Always put a rule closing brace (}) on a new line.
351 rule: value; }""")
352
353 def testColonsHaveSpaceAfter(self):
354 self.VerifyContentsProducesOutput("""
355 div:not(.class):not([attr]) /* We should not catch this. */ {
356 display:block;
357 }""", """
358 - Colons (:) should have a space after them.
359 display:block;""")
360
361 def testFavorSingleQuotes(self):
362 self.VerifyContentsProducesOutput("""
363 html[dir="rtl"] body,
364 html[dir=ltr] body /* TODO(dbeam): Require '' around rtl in future? */ {
365 background: url("chrome://resources/BLAH");
366 font-family: "Open Sans";
367 }""", """
368 - Use single quotes (') instead of double quotes (") in strings.
369 html[dir="rtl"] body,
370 background: url("chrome://resources/BLAH");
371 font-family: "Open Sans";""")
372
373 def testHexCouldBeShorter(self):
374 self.VerifyContentsProducesOutput("""
375 #abc,
376 #abc-,
377 #abc-ghij,
378 #abcdef-,
379 #abcdef-ghij,
380 #aaaaaa,
381 #bbaacc {
382 color: #999999;
383 color: #666;
384 }""", """
385 - Use abbreviated hex (#rgb) when in form #rrggbb.
386 color: #999999; (replace with #999)""")
387
388 def testUseMillisecondsForSmallTimes(self):
389 self.VerifyContentsProducesOutput("""
390 .transition-0s /* This is gross but may happen. */ {
391 transform: one 0.2s;
392 transform: two .1s;
393 transform: tree 1s;
394 transform: four 300ms;
395 }""", """
396 - Use milliseconds for time measurements under 1 second.
397 transform: one 0.2s; (replace with 200ms)
398 transform: two .1s; (replace with 100ms)""")
399
400 def testOneRulePerLine(self):
401 self.VerifyContentsProducesOutput("""
402 div {
403 rule: value; /* rule: value; */
404 rule: value; rule: value;
405 }""", """
406 - One rule per line (what not to do: color: red; margin: 0;).
407 rule: value; rule: value;""")
408
409 def testOneSelectorPerLine(self):
410 self.VerifyContentsProducesOutput("""
411 a,
412 div,a,
413 div,/* Hello! */ span,
414 #id.class([dir=rtl):not(.class):any(a, b, d) {
415 rule: value;
416 }""", """
417 - One selector per line (what not to do: a, b {}).
418 div,a,
419 div, span,""")
420
421
422 def testRgbIfNotGray(self):
423 self.VerifyContentsProducesOutput("""
424 #abc,
425 #aaa,
426 #aabbcc {
427 background: -webkit-linear-gradient(left, from(#abc), to(#def));
428 color: #bad;
429 color: #bada55;
430 }""", """
431 - Use rgb() over #hex when not a shade of gray (like #333).
432 background: -webkit-linear-gradient(left, from(#abc), to(#def)); """
433 """(replace with rgb(170, 187, 204), rgb(221, 238, 255))
434 color: #bad; (replace with rgb(187, 170, 221))
435 color: #bada55; (replace with rgb(186, 218, 85))""")
436
437 def testZeroLengthTerms(self):
438 self.VerifyContentsProducesOutput("""
439 @-webkit-keyframe anim {
440 0% { /* Ignore key frames */
441 width: 0px;
442 }
443 10% {
444 width: 10px;
445 }
446 100% {
447 width: 100px;
448 }
449 }
450 .animating {
451 -webkit-animation: anim 0s;
452 -webkit-animation-duration: anim 0ms;
453 -webkit-transform: scale(0%),
454 translateX(0deg),
455 translateY(0rad),
456 translateZ(0grad);
457 background-position-x: 0em;
458 background-position-y: 0ex;
459 border-width: 0em;
460 color: hsl(0, 0%, 85%); /* Shouldn't trigger error. */
461 }
462
463 @page {
464 border-width: 0mm;
465 height: 0cm;
466 width: 0in;
467 }""","""
468 - Make all zero length terms (i.e. 0px) 0 unless inside of hsl() or part of"""
469 """ @keyframe.
470 width: 0px;
471 -webkit-animation: anim 0s;
472 -webkit-animation-duration: anim 0ms;
473 -webkit-transform: scale(0%),
474 translateX(0deg),
475 translateY(0rad),
476 translateZ(0grad);
477 background-position-x: 0em;
478 background-position-y: 0ex;
479 border-width: 0em;
480 border-width: 0mm;
481 height: 0cm;
482 width: 0in;
483 """)
484
485
486 if __name__ == '__main__':
487 _RunUnitTests()
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698