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

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