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