##// END OF EJS Templates
check-code: improve quote detection regexp, add tests
Benoit Boissinot -
r10722:c4fb2103 default
parent child Browse files
Show More
@@ -1,206 +1,205 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 sys, re, glob
10 import sys, re, glob
11
11
12 def repquote(m):
12 def repquote(m):
13 t = re.sub(r"\w", "x", m.group(2))
13 t = re.sub(r"\w", "x", m.group('text'))
14 t = re.sub(r"[^\sx]", "o", t)
14 t = re.sub(r"[^\sx]", "o", t)
15 return m.group(1) + t + m.group(1)
15 return m.group('quote') + t + m.group('quote')
16
16
17 def repcomment(m):
17 def repcomment(m):
18 return m.group(1) + "#" * len(m.group(2))
18 return m.group(1) + "#" * len(m.group(2))
19
19
20 def repccomment(m):
20 def repccomment(m):
21 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
21 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
22 return m.group(1) + t + "*/"
22 return m.group(1) + t + "*/"
23
23
24 def repcallspaces(m):
24 def repcallspaces(m):
25 t = re.sub(r"\n\s+", "\n", m.group(2))
25 t = re.sub(r"\n\s+", "\n", m.group(2))
26 return m.group(1) + t
26 return m.group(1) + t
27
27
28 def repinclude(m):
28 def repinclude(m):
29 return m.group(1) + "<foo>"
29 return m.group(1) + "<foo>"
30
30
31 def rephere(m):
31 def rephere(m):
32 t = re.sub(r"\S", "x", m.group(2))
32 t = re.sub(r"\S", "x", m.group(2))
33 return m.group(1) + t
33 return m.group(1) + t
34
34
35
35
36 testpats = [
36 testpats = [
37 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
37 (r'(pushd|popd)', "don't use 'pushd' or 'popd', use 'cd'"),
38 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
38 (r'\W\$?\(\([^\)]*\)\)', "don't use (()) or $(()), use 'expr'"),
39 (r'^function', "don't use 'function', use old style"),
39 (r'^function', "don't use 'function', use old style"),
40 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
40 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
41 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
41 (r'echo.*\\n', "don't use 'echo \\n', use printf"),
42 (r'^diff.*-\w*N', "don't use 'diff -N'"),
42 (r'^diff.*-\w*N', "don't use 'diff -N'"),
43 (r'(^| )wc[^|]*$', "filter wc output"),
43 (r'(^| )wc[^|]*$', "filter wc output"),
44 (r'head -c', "don't use 'head -c', use 'dd'"),
44 (r'head -c', "don't use 'head -c', use 'dd'"),
45 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
45 (r'ls.*-\w*R', "don't use 'ls -R', use 'find'"),
46 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
46 (r'printf.*\\\d\d\d', "don't use 'printf \NNN', use Python"),
47 (r'printf.*\\x', "don't use printf \\x, use Python"),
47 (r'printf.*\\x', "don't use printf \\x, use Python"),
48 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
48 (r'\$\(.*\)', "don't use $(expr), use `expr`"),
49 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
49 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
50 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
50 (r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
51 "use egrep for extended grep syntax"),
51 "use egrep for extended grep syntax"),
52 (r'/bin/', "don't use explicit paths for tools"),
52 (r'/bin/', "don't use explicit paths for tools"),
53 (r'\$PWD', "don't use $PWD, use `pwd`"),
53 (r'\$PWD', "don't use $PWD, use `pwd`"),
54 (r'[^\n]\Z', "no trailing newline"),
54 (r'[^\n]\Z', "no trailing newline"),
55 (r'export.*=', "don't export and assign at once"),
55 (r'export.*=', "don't export and assign at once"),
56 ]
56 ]
57
57
58 testfilters = [
58 testfilters = [
59 (r"( *)(#([^\n]*\S)?)", repcomment),
59 (r"( *)(#([^\n]*\S)?)", repcomment),
60 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
60 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
61 ]
61 ]
62
62
63 pypats = [
63 pypats = [
64 (r'^\s*\t', "don't use tabs"),
64 (r'^\s*\t', "don't use tabs"),
65 (r'\S;\s*\n', "semicolon"),
65 (r'\S;\s*\n', "semicolon"),
66 (r'\w,\w', "missing whitespace after ,"),
66 (r'\w,\w', "missing whitespace after ,"),
67 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
67 (r'\w[+/*\-<>]\w', "missing whitespace in expression"),
68 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
68 (r'^\s+\w+=\w+[^,)]$', "missing whitespace in assignment"),
69 (r'.{85}', "line too long"),
69 (r'.{85}', "line too long"),
70 (r'[^\n]\Z', "no trailing newline"),
70 (r'[^\n]\Z', "no trailing newline"),
71 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
71 # (r'^\s+[^_ ][^_. ]+_[^_]+\s*=', "don't use underbars in identifiers"),
72 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
72 # (r'\w*[a-z][A-Z]\w*\s*=', "don't use camelcase in identifiers"),
73 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
73 (r'^\s*(if|while|def|class|except|try)\s[^[]*:\s*[^\]#\s]+',
74 "linebreak after :"),
74 "linebreak after :"),
75 (r'class\s[^(]:', "old-style class, use class foo(object)"),
75 (r'class\s[^(]:', "old-style class, use class foo(object)"),
76 (r'^\s+del\(', "del isn't a function"),
76 (r'^\s+del\(', "del isn't a function"),
77 (r'^\s+except\(', "except isn't a function"),
77 (r'^\s+except\(', "except isn't a function"),
78 (r',]', "unneeded trailing ',' in list"),
78 (r',]', "unneeded trailing ',' in list"),
79 # (r'class\s[A-Z][^\(]*\((?!Exception)',
79 # (r'class\s[A-Z][^\(]*\((?!Exception)',
80 # "don't capitalize non-exception classes"),
80 # "don't capitalize non-exception classes"),
81 # (r'in range\(', "use xrange"),
81 # (r'in range\(', "use xrange"),
82 # (r'^\s*print\s+', "avoid using print in core and extensions"),
82 # (r'^\s*print\s+', "avoid using print in core and extensions"),
83 (r'[\x80-\xff]', "non-ASCII character literal"),
83 (r'[\x80-\xff]', "non-ASCII character literal"),
84 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
84 (r'("\')\.format\(', "str.format() not available in Python 2.4"),
85 (r'^\s*with\s+', "with not available in Python 2.4"),
85 (r'^\s*with\s+', "with not available in Python 2.4"),
86 (r'^\s*(any|all)\(', "any/all not available in Python 2.4"),
86 (r'^\s*(any|all)\(', "any/all not available in Python 2.4"),
87 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
87 (r'if\s.*\selse', "if ... else form not available in Python 2.4"),
88 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
88 (r'([\(\[]\s\S)|(\S\s[\)\]])', "gratuitous whitespace in () or []"),
89 # (r'\s\s=', "gratuitous whitespace before ="),
89 # (r'\s\s=', "gratuitous whitespace before ="),
90 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
90 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
91 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s', "missing whitespace around operator"),
91 (r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=)\s', "missing whitespace around operator"),
92 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
92 (r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=)\S', "missing whitespace around operator"),
93 (r'[^+=*!<>&| -](\s=|=\s)[^= ]', "wrong whitespace around ="),
93 (r'[^+=*!<>&| -](\s=|=\s)[^= ]', "wrong whitespace around ="),
94 (r'raise Exception', "don't raise generic exceptions"),
94 (r'raise Exception', "don't raise generic exceptions"),
95 (r'ui\.(status|progress|write|note)\([\'\"]x', "unwrapped ui message"),
95 (r'ui\.(status|progress|write|note)\([\'\"]x', "unwrapped ui message"),
96 ]
96 ]
97
97
98 pyfilters = [
98 pyfilters = [
99 (r'''(?<!")(")(([^"\n]|\\")+)"(?!")''', repquote),
99 (r"""(?msx)(?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
100 (r"""(?<!')(')(([^'\n]|\\')+)'(?!')""", repquote),
100 (?P<text>(.*?))
101 (r"""(''')(([^']|\\'|'{1,2}(?!'))*)'''""", repquote),
101 (?<!\\)(?P=quote)""", repquote),
102 (r'''(""")(([^"]|\\"|"{1,2}(?!"))*)"""''', repquote),
103 (r"( *)(#([^\n]*\S)?)", repcomment),
102 (r"( *)(#([^\n]*\S)?)", repcomment),
104 ]
103 ]
105
104
106 cpats = [
105 cpats = [
107 (r'//', "don't use //-style comments"),
106 (r'//', "don't use //-style comments"),
108 (r'^ ', "don't use spaces to indent"),
107 (r'^ ', "don't use spaces to indent"),
109 (r'\S\t', "don't use tabs except for indent"),
108 (r'\S\t', "don't use tabs except for indent"),
110 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
109 (r'(\S\s+|^\s+)\n', "trailing whitespace"),
111 (r'.{85}', "line too long"),
110 (r'.{85}', "line too long"),
112 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
111 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
113 (r'return\(', "return is not a function"),
112 (r'return\(', "return is not a function"),
114 (r' ;', "no space before ;"),
113 (r' ;', "no space before ;"),
115 (r'\w+\* \w+', "use int *foo, not int* foo"),
114 (r'\w+\* \w+', "use int *foo, not int* foo"),
116 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
115 (r'\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
117 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
116 (r'\S+ (\+\+|--)', "use foo++, not foo ++"),
118 (r'\w,\w', "missing whitespace after ,"),
117 (r'\w,\w', "missing whitespace after ,"),
119 (r'\w[+/*]\w', "missing whitespace in expression"),
118 (r'\w[+/*]\w', "missing whitespace in expression"),
120 (r'^#\s+\w', "use #foo, not # foo"),
119 (r'^#\s+\w', "use #foo, not # foo"),
121 (r'[^\n]\Z', "no trailing newline"),
120 (r'[^\n]\Z', "no trailing newline"),
122 ]
121 ]
123
122
124 cfilters = [
123 cfilters = [
125 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
124 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
126 (r'''(?<!")(")(([^"]|\\")+"(?!"))''', repquote),
125 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
127 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
126 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
128 (r'(\()([^)]+\))', repcallspaces),
127 (r'(\()([^)]+\))', repcallspaces),
129 ]
128 ]
130
129
131 checks = [
130 checks = [
132 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
131 ('python', r'.*\.(py|cgi)$', pyfilters, pypats),
133 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
132 ('test script', r'(.*/)?test-[^.~]*$', testfilters, testpats),
134 ('c', r'.*\.c$', cfilters, cpats),
133 ('c', r'.*\.c$', cfilters, cpats),
135 ]
134 ]
136
135
137 class norepeatlogger(object):
136 class norepeatlogger(object):
138 def __init__(self):
137 def __init__(self):
139 self._lastseen = None
138 self._lastseen = None
140
139
141 def log(self, fname, lineno, line, msg):
140 def log(self, fname, lineno, line, msg):
142 """print error related a to given line of a given file.
141 """print error related a to given line of a given file.
143
142
144 The faulty line will also be printed but only once in the case
143 The faulty line will also be printed but only once in the case
145 of multiple errors.
144 of multiple errors.
146
145
147 :fname: filename
146 :fname: filename
148 :lineno: line number
147 :lineno: line number
149 :line: actual content of the line
148 :line: actual content of the line
150 :msg: error message
149 :msg: error message
151 """
150 """
152 msgid = fname, lineno, line
151 msgid = fname, lineno, line
153 if msgid != self._lastseen:
152 if msgid != self._lastseen:
154 print "%s:%d:" % (fname, lineno)
153 print "%s:%d:" % (fname, lineno)
155 print " > %s" % line
154 print " > %s" % line
156 self._lastseen = msgid
155 self._lastseen = msgid
157 print " " + msg
156 print " " + msg
158
157
159 _defaultlogger = norepeatlogger()
158 _defaultlogger = norepeatlogger()
160
159
161 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None):
160 def checkfile(f, logfunc=_defaultlogger.log, maxerr=None):
162 """checks style and portability of a given file
161 """checks style and portability of a given file
163
162
164 :f: filepath
163 :f: filepath
165 :logfunc: function used to report error
164 :logfunc: function used to report error
166 logfunc(filename, linenumber, linecontent, errormessage)
165 logfunc(filename, linenumber, linecontent, errormessage)
167 :maxerr: number of error to display before arborting.
166 :maxerr: number of error to display before arborting.
168 Set to None (default) to report all errors
167 Set to None (default) to report all errors
169
168
170 return True if no error is found, False otherwise.
169 return True if no error is found, False otherwise.
171 """
170 """
172 result = True
171 result = True
173 for name, match, filters, pats in checks:
172 for name, match, filters, pats in checks:
174 fc = 0
173 fc = 0
175 if not re.match(match, f):
174 if not re.match(match, f):
176 continue
175 continue
177 pre = post = open(f).read()
176 pre = post = open(f).read()
178 if "no-" + "check-code" in pre:
177 if "no-" + "check-code" in pre:
179 break
178 break
180 for p, r in filters:
179 for p, r in filters:
181 post = re.sub(p, r, post)
180 post = re.sub(p, r, post)
182 # print post # uncomment to show filtered version
181 # print post # uncomment to show filtered version
183 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
182 z = enumerate(zip(pre.splitlines(), post.splitlines(True)))
184 for n, l in z:
183 for n, l in z:
185 if "check-code" + "-ignore" in l[0]:
184 if "check-code" + "-ignore" in l[0]:
186 continue
185 continue
187 for p, msg in pats:
186 for p, msg in pats:
188 if re.search(p, l[1]):
187 if re.search(p, l[1]):
189 logfunc(f, n+1, l[0], msg)
188 logfunc(f, n+1, l[0], msg)
190 fc += 1
189 fc += 1
191 result = False
190 result = False
192 if maxerr is not None and fc >= maxerr:
191 if maxerr is not None and fc >= maxerr:
193 print " (too many errors, giving up)"
192 print " (too many errors, giving up)"
194 break
193 break
195 break
194 break
196 return result
195 return result
197
196
198
197
199 if __name__ == "__main__":
198 if __name__ == "__main__":
200 if len(sys.argv) == 1:
199 if len(sys.argv) == 1:
201 check = glob.glob("*")
200 check = glob.glob("*")
202 else:
201 else:
203 check = sys.argv[1:]
202 check = sys.argv[1:]
204
203
205 for f in check:
204 for f in check:
206 checkfile(f, maxerr=15)
205 checkfile(f, maxerr=15)
@@ -1,16 +1,24 b''
1 #!/bin/sh
1 #!/bin/sh
2 #cd `dirname $0`
2 #cd `dirname $0`
3 cat > correct.py <<EOF
3 cat > correct.py <<EOF
4 def toto(arg1, arg2):
4 def toto(arg1, arg2):
5 del arg2
5 del arg2
6 return (5 + 6, 9)
6 return (5 + 6, 9)
7 EOF
7 EOF
8
8
9 cat > wrong.py <<EOF
9 cat > wrong.py <<EOF
10 def toto( arg1, arg2):
10 def toto( arg1, arg2):
11 del(arg2)
11 del(arg2)
12 return ( 5+6, 9)
12 return ( 5+6, 9)
13 EOF
13 EOF
14
14
15 cat > quote.py <<EOF
16 (''' ( 4x5 )
17 but """\''' and finally''',
18 """let's fool checkpatch""", '1+2',
19 '"""', 42+1, """and
20 ( 4-1 ) """, "( 1+1 )\" and ")
21 EOF
22
15 check_code=`dirname $0`/../contrib/check-code.py
23 check_code=`dirname $0`/../contrib/check-code.py
16 ${check_code} ./wrong.py ./correct.py
24 ${check_code} ./wrong.py ./correct.py ./quote.py
@@ -1,10 +1,13 b''
1 ./wrong.py:1:
1 ./wrong.py:1:
2 > def toto( arg1, arg2):
2 > def toto( arg1, arg2):
3 gratuitous whitespace in () or []
3 gratuitous whitespace in () or []
4 ./wrong.py:2:
4 ./wrong.py:2:
5 > del(arg2)
5 > del(arg2)
6 del isn't a function
6 del isn't a function
7 ./wrong.py:3:
7 ./wrong.py:3:
8 > return ( 5+6, 9)
8 > return ( 5+6, 9)
9 missing whitespace in expression
9 missing whitespace in expression
10 gratuitous whitespace in () or []
10 gratuitous whitespace in () or []
11 ./quote.py:4:
12 > '"""', 42+1, """and
13 missing whitespace in expression
General Comments 0
You need to be logged in to leave comments. Login now