##// END OF EJS Templates
check-code: add check for 'source'
Yuya Nishihara -
r11210:0c008888 default
parent child Browse files
Show More
@@ -1,225 +1,226 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
10 import re, glob
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 ]
65 ]
65
66
66 testfilters = [
67 testfilters = [
67 (r"( *)(#([^\n]*\S)?)", repcomment),
68 (r"( *)(#([^\n]*\S)?)", repcomment),
68 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
69 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
69 ]
70 ]
70
71
71 pypats = [
72 pypats = [
72 (r'^\s*\t', "don't use tabs"),
73 (r'^\s*\t', "don't use tabs"),
73 (r'\S;\s*\n', "semicolon"),
74 (r'\S;\s*\n', "semicolon"),
74 (r'\w,\w', "missing whitespace after ,"),
75 (r'\w,\w', "missing whitespace after ,"),
75 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
76 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
76 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
77 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
77 (r'.{85}', "line too long"),
78 (r'.{85}', "line too long"),
78 (r'[^\n]\Z', "no trailing newline"),
79 (r'[^\n]\Z', "no trailing newline"),
79 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
80 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
80 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
81 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
81 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
82 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
82 "linebreak after :"),
83 "linebreak after :"),
83 (r'class\s[^(]:', "old-style class, use class foo(object)"),
84 (r'class\s[^(]:', "old-style class, use class foo(object)"),
84 (r'^\s+del\(', "del isn't a function"),
85 (r'^\s+del\(', "del isn't a function"),
85 (r'^\s+except\(', "except isn't a function"),
86 (r'^\s+except\(', "except isn't a function"),
86 (r',]', "unneeded trailing ',' in list"),
87 (r',]', "unneeded trailing ',' in list"),
87 # (r'class\s[A-Z][^\(]*\((?!Exception)',
88 # (r'class\s[A-Z][^\(]*\((?!Exception)',
88 # "don't capitalize non-exception classes"),
89 # "don't capitalize non-exception classes"),
89 # (r'in range\(', "use xrange"),
90 # (r'in range\(', "use xrange"),
90 # (r'^\s*print\s+', "avoid using print in core and extensions"),
91 # (r'^\s*print\s+', "avoid using print in core and extensions"),
91 (r'[\x80-\xff]', "non-ASCII character literal"),
92 (r'[\x80-\xff]', "non-ASCII character literal"),
92 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
93 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
93 (r'^\s*with\s+', "with not available in Python 2.4"),
94 (r'^\s*with\s+', "with not available in Python 2.4"),
94 (r'^\s*(any|all)\(', "any/all not available in Python 2.4"),
95 (r'^\s*(any|all)\(', "any/all not available in Python 2.4"),
95 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
96 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
96 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
97 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
97 # (r'\s\s=', "gratuitous whitespace before ="),
98 # (r'\s\s=', "gratuitous whitespace before ="),
98 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
99 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
99 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s', "missing whitespace around operator"),
100 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s', "missing whitespace around operator"),
100 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
101 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
101 (r'[^+=*!<>&| -](\s=|=\s)[^= ]', "wrong whitespace around ="),
102 (r'[^+=*!<>&| -](\s=|=\s)[^= ]', "wrong whitespace around ="),
102 (r'raise Exception', "don't raise generic exceptions"),
103 (r'raise Exception', "don't raise generic exceptions"),
103 (r'ui\.(status|progress|write|note)\([\'\"]x',
104 (r'ui\.(status|progress|write|note)\([\'\"]x',
104 "warning: unwrapped ui message"),
105 "warning: unwrapped ui message"),
105 ]
106 ]
106
107
107 pyfilters = [
108 pyfilters = [
108 (r"""(?msx)(?P<comment>\#.*?$)|
109 (r"""(?msx)(?P<comment>\#.*?$)|
109 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
110 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
110 (?P<text>(([^\\]|\\.)*?))
111 (?P<text>(([^\\]|\\.)*?))
111 (?P=quote))""", reppython),
112 (?P=quote))""", reppython),
112 ]
113 ]
113
114
114 cpats = [
115 cpats = [
115 (r'//', "don't use //-style comments"),
116 (r'//', "don't use //-style comments"),
116 (r'^ ', "don't use spaces to indent"),
117 (r'^ ', "don't use spaces to indent"),
117 (r'\S\t', "don't use tabs except for indent"),
118 (r'\S\t', "don't use tabs except for indent"),
118 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
119 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
119 (r'.{85}', "line too long"),
120 (r'.{85}', "line too long"),
120 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
121 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
121 (r'return\(', "return is not a function"),
122 (r'return\(', "return is not a function"),
122 (r' ;', "no space before ;"),
123 (r' ;', "no space before ;"),
123 (r'\w+\* \w+', "use int *foo, not int* foo"),
124 (r'\w+\* \w+', "use int *foo, not int* foo"),
124 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
125 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
125 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
126 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
126 (r'\w,\w', "missing whitespace after ,"),
127 (r'\w,\w', "missing whitespace after ,"),
127 (r'\w[+/*]\w', "missing whitespace in expression"),
128 (r'\w[+/*]\w', "missing whitespace in expression"),
128 (r'^#\s+\w', "use #foo, not # foo"),
129 (r'^#\s+\w', "use #foo, not # foo"),
129 (r'[^\n]\Z', "no trailing newline"),
130 (r'[^\n]\Z', "no trailing newline"),
130 ]
131 ]
131
132
132 cfilters = [
133 cfilters = [
133 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
134 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
134 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
135 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
135 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
136 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
136 (r'(\()([^)]+\))', repcallspaces),
137 (r'(\()([^)]+\))', repcallspaces),
137 ]
138 ]
138
139
139 checks = [
140 checks = [
140 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
141 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
141 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
142 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
142 ('c', r'.*\.c$', cfilters, cpats),
143 ('c', r'.*\.c$', cfilters, cpats),
143 ]
144 ]
144
145
145 class norepeatlogger(object):
146 class norepeatlogger(object):
146 def __init__(self):
147 def __init__(self):
147 self._lastseen = None
148 self._lastseen = None
148
149
149 def log(self, fname, lineno, line, msg):
150 def log(self, fname, lineno, line, msg):
150 """print error related a to given line of a given file.
151 """print error related a to given line of a given file.
151
152
152 The faulty line will also be printed but only once in the case
153 The faulty line will also be printed but only once in the case
153 of multiple errors.
154 of multiple errors.
154
155
155 :fname: filename
156 :fname: filename
156 :lineno: line number
157 :lineno: line number
157 :line: actual content of the line
158 :line: actual content of the line
158 :msg: error message
159 :msg: error message
159 """
160 """
160 msgid = fname, lineno, line
161 msgid = fname, lineno, line
161 if msgid != self._lastseen:
162 if msgid != self._lastseen:
162 print "%s:%d:" % (fname, lineno)
163 print "%s:%d:" % (fname, lineno)
163 print " > %s" % line
164 print " > %s" % line
164 self._lastseen = msgid
165 self._lastseen = msgid
165 print " " + msg
166 print " " + msg
166
167
167 _defaultlogger = norepeatlogger()
168 _defaultlogger = norepeatlogger()
168
169
169 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False):
170 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None, warnings=False):
170 """checks style and portability of a given file
171 """checks style and portability of a given file
171
172
172 :f: filepath
173 :f: filepath
173 :logfunc: function used to report error
174 :logfunc: function used to report error
174 logfunc(filename, linenumber, linecontent, errormessage)
175 logfunc(filename, linenumber, linecontent, errormessage)
175 :maxerr: number of error to display before arborting.
176 :maxerr: number of error to display before arborting.
176 Set to None (default) to report all errors
177 Set to None (default) to report all errors
177
178
178 return True if no error is found, False otherwise.
179 return True if no error is found, False otherwise.
179 """
180 """
180 result = True
181 result = True
181 for name, match, filters, pats in checks:
182 for name, match, filters, pats in checks:
182 fc = 0
183 fc = 0
183 if not re.match(match, f):
184 if not re.match(match, f):
184 continue
185 continue
185 pre = post = open(f).read()
186 pre = post = open(f).read()
186 if "no-" + "check-code" in pre:
187 if "no-" + "check-code" in pre:
187 break
188 break
188 for p, r in filters:
189 for p, r in filters:
189 post = re.sub(p, r, post)
190 post = re.sub(p, r, post)
190 # print post # uncomment to show filtered version
191 # print post # uncomment to show filtered version
191 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
192 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
192 for n, l in z:
193 for n, l in z:
193 if "check-code" + "-ignore" in l[0]:
194 if "check-code" + "-ignore" in l[0]:
194 continue
195 continue
195 for p, msg in pats:
196 for p, msg in pats:
196 if not warnings and msg.startswith("warning"):
197 if not warnings and msg.startswith("warning"):
197 continue
198 continue
198 if re.search(p, l[1]):
199 if re.search(p, l[1]):
199 logfunc(f, n + 1, l[0], msg)
200 logfunc(f, n + 1, l[0], msg)
200 fc += 1
201 fc += 1
201 result = False
202 result = False
202 if maxerr is not None and fc >= maxerr:
203 if maxerr is not None and fc >= maxerr:
203 print " (too many errors, giving up)"
204 print " (too many errors, giving up)"
204 break
205 break
205 break
206 break
206 return result
207 return result
207
208
208
209
209 if __name__ == "__main__":
210 if __name__ == "__main__":
210 parser = optparse.OptionParser("%prog [options] [files]")
211 parser = optparse.OptionParser("%prog [options] [files]")
211 parser.add_option("-w", "--warnings", action="store_true",
212 parser.add_option("-w", "--warnings", action="store_true",
212 help="include warning-level checks")
213 help="include warning-level checks")
213 parser.add_option("-p", "--per-file", type="int",
214 parser.add_option("-p", "--per-file", type="int",
214 help="max warnings per file")
215 help="max warnings per file")
215
216
216 parser.set_defaults(per_file=15, warnings=False)
217 parser.set_defaults(per_file=15, warnings=False)
217 (options, args) = parser.parse_args()
218 (options, args) = parser.parse_args()
218
219
219 if len(args) == 0:
220 if len(args) == 0:
220 check = glob.glob("*")
221 check = glob.glob("*")
221 else:
222 else:
222 check = args
223 check = args
223
224
224 for f in check:
225 for f in check:
225 checkfile(f, maxerr=options.per_file, warnings=options.warnings)
226 checkfile(f, maxerr=options.per_file, warnings=options.warnings)
General Comments 0
You need to be logged in to leave comments. Login now