##// END OF EJS Templates
check-code: add warning on lines over 80 characters
Matt Mackall -
r11672:dad18576 default
parent child Browse files
Show More
@@ -1,263 +1,264 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-code - a style and portability checker for Mercurial
3 # check-code - a style and portability checker for Mercurial
4 #
4 #
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
5 # Copyright 2010 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 import re, glob, os
10 import re, glob, os
11 import optparse
11 import optparse
12
12
13 def repquote(m):
13 def repquote(m):
14 t = re.sub(r"\w", "x", m.group('text'))
14 t = re.sub(r"\w", "x", m.group('text'))
15 t = re.sub(r"[^\sx]", "o", t)
15 t = re.sub(r"[^\sx]", "o", t)
16 return m.group('quote') + t + m.group('quote')
16 return m.group('quote') + t + m.group('quote')
17
17
18 def reppython(m):
18 def reppython(m):
19 comment = m.group('comment')
19 comment = m.group('comment')
20 if comment:
20 if comment:
21 return "#" * len(comment)
21 return "#" * len(comment)
22 return repquote(m)
22 return repquote(m)
23
23
24 def repcomment(m):
24 def repcomment(m):
25 return m.group(1) + "#" * len(m.group(2))
25 return m.group(1) + "#" * len(m.group(2))
26
26
27 def repccomment(m):
27 def repccomment(m):
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
28 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
29 return m.group(1) + t + "*/"
29 return m.group(1) + t + "*/"
30
30
31 def repcallspaces(m):
31 def repcallspaces(m):
32 t = re.sub(r"\n\s+", "\n", m.group(2))
32 t = re.sub(r"\n\s+", "\n", m.group(2))
33 return m.group(1) + t
33 return m.group(1) + t
34
34
35 def repinclude(m):
35 def repinclude(m):
36 return m.group(1) + "<foo>"
36 return m.group(1) + "<foo>"
37
37
38 def rephere(m):
38 def rephere(m):
39 t = re.sub(r"\S", "x", m.group(2))
39 t = re.sub(r"\S", "x", m.group(2))
40 return m.group(1) + t
40 return m.group(1) + t
41
41
42
42
43 testpats = [
43 testpats = [
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
44 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
45 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
46 (r'^function', "don't use 'function', use old style"),
46 (r'^function', "don't use 'function', use old style"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
47 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
48 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
49 (r'^diff.*-\w*N', "don't use 'diff -N'"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
50 (r'(^| )wc[^|]*$', "filter wc output"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
51 (r'head -c', "don't use 'head -c', use 'dd'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
52 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
53 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
54 (r'printf.*\\x', "don't use printf \\x, use Python"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
55 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
56 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
57 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
58 "use egrep for extended grep syntax"),
58 "use egrep for extended grep syntax"),
59 (r'/bin/', "don't use explicit paths for tools"),
59 (r'/bin/', "don't use explicit paths for tools"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
60 (r'\$PWD', "don't use $PWD, use `pwd`"),
61 (r'[^\n]\Z', "no trailing newline"),
61 (r'[^\n]\Z', "no trailing newline"),
62 (r'export.*=', "don't export and assign at once"),
62 (r'export.*=', "don't export and assign at once"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
63 ('^([^"\']|("[^"]*")|(\'[^\']*\'))*\\^', "^ must be quoted"),
64 (r'^source\b', "don't use 'source', use '.'"),
64 (r'^source\b', "don't use 'source', use '.'"),
65 ]
65 ]
66
66
67 testfilters = [
67 testfilters = [
68 (r"( *)(#([^\n]*\S)?)", repcomment),
68 (r"( *)(#([^\n]*\S)?)", repcomment),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
70 ]
70 ]
71
71
72 pypats = [
72 pypats = [
73 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
73 (r'^\s*def\s*\w+\s*\(.*,\s*\(',
74 "tuple parameter unpacking not available in Python 3+"),
74 "tuple parameter unpacking not available in Python 3+"),
75 (r'lambda\s*\(.*,.*\)',
75 (r'lambda\s*\(.*,.*\)',
76 "tuple parameter unpacking not available in Python 3+"),
76 "tuple parameter unpacking not available in Python 3+"),
77 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
77 (r'\breduce\s*\(.*', "reduce is not available in Python 3+"),
78 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
78 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
79 (r'^\s*\t', "don't use tabs"),
79 (r'^\s*\t', "don't use tabs"),
80 (r'\S;\s*\n', "semicolon"),
80 (r'\S;\s*\n', "semicolon"),
81 (r'\w,\w', "missing whitespace after ,"),
81 (r'\w,\w', "missing whitespace after ,"),
82 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
82 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
83 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
83 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
84 (r'.{85}', "line too long"),
84 (r'.{85}', "line too long"),
85 (r'.{81}', "warning: line over 80 characters"),
85 (r'[^\n]\Z', "no trailing newline"),
86 (r'[^\n]\Z', "no trailing newline"),
86 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
87 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
87 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
88 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
88 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
89 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
89 "linebreak after :"),
90 "linebreak after :"),
90 (r'class\s[^(]:', "old-style class, use class foo(object)"),
91 (r'class\s[^(]:', "old-style class, use class foo(object)"),
91 (r'^\s+del\(', "del isn't a function"),
92 (r'^\s+del\(', "del isn't a function"),
92 (r'^\s+except\(', "except isn't a function"),
93 (r'^\s+except\(', "except isn't a function"),
93 (r',]', "unneeded trailing ',' in list"),
94 (r',]', "unneeded trailing ',' in list"),
94 # (r'class\s[A-Z][^\(]*\((?!Exception)',
95 # (r'class\s[A-Z][^\(]*\((?!Exception)',
95 # "don't capitalize non-exception classes"),
96 # "don't capitalize non-exception classes"),
96 # (r'in range\(', "use xrange"),
97 # (r'in range\(', "use xrange"),
97 # (r'^\s*print\s+', "avoid using print in core and extensions"),
98 # (r'^\s*print\s+', "avoid using print in core and extensions"),
98 (r'[\x80-\xff]', "non-ASCII character literal"),
99 (r'[\x80-\xff]', "non-ASCII character literal"),
99 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
100 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
100 (r'^\s*with\s+', "with not available in Python 2.4"),
101 (r'^\s*with\s+', "with not available in Python 2.4"),
101 (r'(?<!def)\s+(any|all|format)\(',
102 (r'(?<!def)\s+(any|all|format)\(',
102 "any/all/format not available in Python 2.4"),
103 "any/all/format not available in Python 2.4"),
103 (r'(?<!def)\s+(callable)\(',
104 (r'(?<!def)\s+(callable)\(',
104 "callable not available in Python 3, use hasattr(f, '__call__')"),
105 "callable not available in Python 3, use hasattr(f, '__call__')"),
105 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
106 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
106 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
107 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
107 # (r'\s\s=', "gratuitous whitespace before ="),
108 # (r'\s\s=', "gratuitous whitespace before ="),
108 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
109 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
109 "missing whitespace around operator"),
110 "missing whitespace around operator"),
110 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
111 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s',
111 "missing whitespace around operator"),
112 "missing whitespace around operator"),
112 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
113 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S',
113 "missing whitespace around operator"),
114 "missing whitespace around operator"),
114 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
115 (r'[^+=*!<>&| -](\s=|=\s)[^= ]',
115 "wrong whitespace around ="),
116 "wrong whitespace around ="),
116 (r'raise Exception', "don't raise generic exceptions"),
117 (r'raise Exception', "don't raise generic exceptions"),
117 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
118 (r'ui\.(status|progress|write|note|warn)\([\'\"]x',
118 "warning: unwrapped ui message"),
119 "warning: unwrapped ui message"),
119 ]
120 ]
120
121
121 pyfilters = [
122 pyfilters = [
122 (r"""(?msx)(?P<comment>\#.*?$)|
123 (r"""(?msx)(?P<comment>\#.*?$)|
123 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
124 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
124 (?P<text>(([^\\]|\\.)*?))
125 (?P<text>(([^\\]|\\.)*?))
125 (?P=quote))""", reppython),
126 (?P=quote))""", reppython),
126 ]
127 ]
127
128
128 cpats = [
129 cpats = [
129 (r'//', "don't use //-style comments"),
130 (r'//', "don't use //-style comments"),
130 (r'^ ', "don't use spaces to indent"),
131 (r'^ ', "don't use spaces to indent"),
131 (r'\S\t', "don't use tabs except for indent"),
132 (r'\S\t', "don't use tabs except for indent"),
132 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
133 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
133 (r'.{85}', "line too long"),
134 (r'.{85}', "line too long"),
134 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
135 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
135 (r'return\(', "return is not a function"),
136 (r'return\(', "return is not a function"),
136 (r' ;', "no space before ;"),
137 (r' ;', "no space before ;"),
137 (r'\w+\* \w+', "use int *foo, not int* foo"),
138 (r'\w+\* \w+', "use int *foo, not int* foo"),
138 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
139 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
139 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
140 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
140 (r'\w,\w', "missing whitespace after ,"),
141 (r'\w,\w', "missing whitespace after ,"),
141 (r'\w[+/*]\w', "missing whitespace in expression"),
142 (r'\w[+/*]\w', "missing whitespace in expression"),
142 (r'^#\s+\w', "use #foo, not # foo"),
143 (r'^#\s+\w', "use #foo, not # foo"),
143 (r'[^\n]\Z', "no trailing newline"),
144 (r'[^\n]\Z', "no trailing newline"),
144 ]
145 ]
145
146
146 cfilters = [
147 cfilters = [
147 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
148 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
148 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
149 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
149 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
150 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
150 (r'(\()([^)]+\))', repcallspaces),
151 (r'(\()([^)]+\))', repcallspaces),
151 ]
152 ]
152
153
153 checks = [
154 checks = [
154 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
155 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
155 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
156 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
156 ('c', r'.*\.c$', cfilters, cpats),
157 ('c', r'.*\.c$', cfilters, cpats),
157 ]
158 ]
158
159
159 class norepeatlogger(object):
160 class norepeatlogger(object):
160 def __init__(self):
161 def __init__(self):
161 self._lastseen = None
162 self._lastseen = None
162
163
163 def log(self, fname, lineno, line, msg, blame):
164 def log(self, fname, lineno, line, msg, blame):
164 """print error related a to given line of a given file.
165 """print error related a to given line of a given file.
165
166
166 The faulty line will also be printed but only once in the case
167 The faulty line will also be printed but only once in the case
167 of multiple errors.
168 of multiple errors.
168
169
169 :fname: filename
170 :fname: filename
170 :lineno: line number
171 :lineno: line number
171 :line: actual content of the line
172 :line: actual content of the line
172 :msg: error message
173 :msg: error message
173 """
174 """
174 msgid = fname, lineno, line
175 msgid = fname, lineno, line
175 if msgid != self._lastseen:
176 if msgid != self._lastseen:
176 if blame:
177 if blame:
177 print "%s:%d (%s):" % (fname, lineno, blame)
178 print "%s:%d (%s):" % (fname, lineno, blame)
178 else:
179 else:
179 print "%s:%d:" % (fname, lineno)
180 print "%s:%d:" % (fname, lineno)
180 print " > %s" % line
181 print " > %s" % line
181 self._lastseen = msgid
182 self._lastseen = msgid
182 print " " + msg
183 print " " + msg
183
184
184 _defaultlogger = norepeatlogger()
185 _defaultlogger = norepeatlogger()
185
186
186 def getblame(f):
187 def getblame(f):
187 lines = []
188 lines = []
188 for l in os.popen('hg annotate -un %s' % f):
189 for l in os.popen('hg annotate -un %s' % f):
189 start, line = l.split(':', 1)
190 start, line = l.split(':', 1)
190 user, rev = start.split()
191 user, rev = start.split()
191 lines.append((line[1:-1], user, rev))
192 lines.append((line[1:-1], user, rev))
192 return lines
193 return lines
193
194
194 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
195 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False,
195 blame=False):
196 blame=False):
196 """checks style and portability of a given file
197 """checks style and portability of a given file
197
198
198 :f: filepath
199 :f: filepath
199 :logfunc: function used to report error
200 :logfunc: function used to report error
200 logfunc(filename, linenumber, linecontent, errormessage)
201 logfunc(filename, linenumber, linecontent, errormessage)
201 :maxerr: number of error to display before arborting.
202 :maxerr: number of error to display before arborting.
202 Set to None (default) to report all errors
203 Set to None (default) to report all errors
203
204
204 return True if no error is found, False otherwise.
205 return True if no error is found, False otherwise.
205 """
206 """
206 blamecache = None
207 blamecache = None
207 result = True
208 result = True
208 for name, match, filters, pats in checks:
209 for name, match, filters, pats in checks:
209 fc = 0
210 fc = 0
210 if not re.match(match, f):
211 if not re.match(match, f):
211 continue
212 continue
212 pre = post = open(f).read()
213 pre = post = open(f).read()
213 if "no-" + "check-code" in pre:
214 if "no-" + "check-code" in pre:
214 break
215 break
215 for p, r in filters:
216 for p, r in filters:
216 post = re.sub(p, r, post)
217 post = re.sub(p, r, post)
217 # print post # uncomment to show filtered version
218 # print post # uncomment to show filtered version
218 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
219 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
219 for n, l in z:
220 for n, l in z:
220 if "check-code" + "-ignore" in l[0]:
221 if "check-code" + "-ignore" in l[0]:
221 continue
222 continue
222 for p, msg in pats:
223 for p, msg in pats:
223 if not warnings and msg.startswith("warning"):
224 if not warnings and msg.startswith("warning"):
224 continue
225 continue
225 if re.search(p, l[1]):
226 if re.search(p, l[1]):
226 bd = ""
227 bd = ""
227 if blame:
228 if blame:
228 bd = 'working directory'
229 bd = 'working directory'
229 if not blamecache:
230 if not blamecache:
230 blamecache = getblame(f)
231 blamecache = getblame(f)
231 if n < len(blamecache):
232 if n < len(blamecache):
232 bl, bu, br = blamecache[n]
233 bl, bu, br = blamecache[n]
233 if bl == l[0]:
234 if bl == l[0]:
234 bd = '%s@%s' % (bu, br)
235 bd = '%s@%s' % (bu, br)
235 logfunc(f, n + 1, l[0], msg, bd)
236 logfunc(f, n + 1, l[0], msg, bd)
236 fc += 1
237 fc += 1
237 result = False
238 result = False
238 if maxerr is not None and fc >= maxerr:
239 if maxerr is not None and fc >= maxerr:
239 print " (too many errors, giving up)"
240 print " (too many errors, giving up)"
240 break
241 break
241 break
242 break
242 return result
243 return result
243
244
244 if __name__ == "__main__":
245 if __name__ == "__main__":
245 parser = optparse.OptionParser("%prog [options] [files]")
246 parser = optparse.OptionParser("%prog [options] [files]")
246 parser.add_option("-w", "--warnings", action="store_true",
247 parser.add_option("-w", "--warnings", action="store_true",
247 help="include warning-level checks")
248 help="include warning-level checks")
248 parser.add_option("-p", "--per-file", type="int",
249 parser.add_option("-p", "--per-file", type="int",
249 help="max warnings per file")
250 help="max warnings per file")
250 parser.add_option("-b", "--blame", action="store_true",
251 parser.add_option("-b", "--blame", action="store_true",
251 help="use annotate to generate blame info")
252 help="use annotate to generate blame info")
252
253
253 parser.set_defaults(per_file=15, warnings=False, blame=False)
254 parser.set_defaults(per_file=15, warnings=False, blame=False)
254 (options, args) = parser.parse_args()
255 (options, args) = parser.parse_args()
255
256
256 if len(args) == 0:
257 if len(args) == 0:
257 check = glob.glob("*")
258 check = glob.glob("*")
258 else:
259 else:
259 check = args
260 check = args
260
261
261 for f in check:
262 for f in check:
262 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
263 checkfile(f, maxerr=options.per_file, warnings=options.warnings,
263 blame=options.blame)
264 blame=options.blame)
General Comments 0
You need to be logged in to leave comments. Login now