111
|
1 #!/usr/bin/env python3
|
|
2 #
|
|
3 # Checks some of the GNU style formatting rules in a set of patches.
|
|
4 # The script is a rewritten of the same bash script and should eventually
|
|
5 # replace the former script.
|
|
6 #
|
|
7 # This file is part of GCC.
|
|
8 #
|
|
9 # GCC is free software; you can redistribute it and/or modify it under
|
|
10 # the terms of the GNU General Public License as published by the Free
|
|
11 # Software Foundation; either version 3, or (at your option) any later
|
|
12 # version.
|
|
13 #
|
|
14 # GCC is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
15 # WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
16 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
17 # for more details.
|
|
18 #
|
|
19 # You should have received a copy of the GNU General Public License
|
|
20 # along with GCC; see the file COPYING3. If not see
|
|
21 # <http://www.gnu.org/licenses/>. */
|
|
22 #
|
|
23 # The script requires python packages, which can be installed via pip3
|
|
24 # like this:
|
|
25 # $ pip3 install unidiff termcolor
|
|
26
|
|
27 import sys
|
|
28 import re
|
|
29 import unittest
|
|
30
|
|
31 def import_pip3(*args):
|
|
32 missing=[]
|
|
33 for (module, names) in args:
|
|
34 try:
|
|
35 lib = __import__(module)
|
|
36 except ImportError:
|
|
37 missing.append(module)
|
|
38 continue
|
|
39 if not isinstance(names, list):
|
|
40 names=[names]
|
|
41 for name in names:
|
|
42 globals()[name]=getattr(lib, name)
|
|
43 if len(missing) > 0:
|
|
44 missing_and_sep = ' and '.join(missing)
|
|
45 missing_space_sep = ' '.join(missing)
|
|
46 print('%s %s missing (run: pip3 install %s)'
|
|
47 % (missing_and_sep,
|
|
48 ("module is" if len(missing) == 1 else "modules are"),
|
|
49 missing_space_sep))
|
|
50 exit(3)
|
|
51
|
|
52 import_pip3(('termcolor', 'colored'),
|
|
53 ('unidiff', 'PatchSet'))
|
|
54
|
|
55 from itertools import *
|
|
56
|
|
57 ws_char = '█'
|
|
58 ts = 8
|
|
59
|
|
60 def error_string(s):
|
|
61 return colored(s, 'red', attrs = ['bold'])
|
|
62
|
|
63 class CheckError:
|
|
64 def __init__(self, filename, lineno, console_error, error_message,
|
|
65 column = -1):
|
|
66 self.filename = filename
|
|
67 self.lineno = lineno
|
|
68 self.console_error = console_error
|
|
69 self.error_message = error_message
|
|
70 self.column = column
|
|
71
|
|
72 def error_location(self):
|
|
73 return '%s:%d:%d:' % (self.filename, self.lineno,
|
|
74 self.column if self.column != -1 else -1)
|
|
75
|
|
76 class LineLengthCheck:
|
|
77 def __init__(self):
|
|
78 self.limit = 80
|
|
79 self.expanded_tab = ' ' * ts
|
|
80
|
|
81 def check(self, filename, lineno, line):
|
|
82 line_expanded = line.replace('\t', self.expanded_tab)
|
|
83 if len(line_expanded) > self.limit:
|
|
84 return CheckError(filename, lineno,
|
|
85 line_expanded[:self.limit]
|
|
86 + error_string(line_expanded[self.limit:]),
|
|
87 'lines should not exceed 80 characters', self.limit)
|
|
88
|
|
89 return None
|
|
90
|
|
91 class SpacesCheck:
|
|
92 def __init__(self):
|
|
93 self.expanded_tab = ' ' * ts
|
|
94
|
|
95 def check(self, filename, lineno, line):
|
|
96 i = line.find(self.expanded_tab)
|
|
97 if i != -1:
|
|
98 return CheckError(filename, lineno,
|
|
99 line.replace(self.expanded_tab, error_string(ws_char * ts)),
|
|
100 'blocks of 8 spaces should be replaced with tabs', i)
|
|
101
|
145
|
102 class SpacesAndTabsMixedCheck:
|
|
103 def __init__(self):
|
|
104 self.re = re.compile('\ \t')
|
|
105
|
|
106 def check(self, filename, lineno, line):
|
|
107 stripped = line.lstrip()
|
|
108 start = line[:len(line) - len(stripped)]
|
|
109 if self.re.search(line):
|
|
110 return CheckError(filename, lineno,
|
|
111 error_string(start.replace('\t', ws_char * ts)) + line[len(start):],
|
|
112 'a space should not precede a tab', 0)
|
|
113
|
111
|
114 class TrailingWhitespaceCheck:
|
|
115 def __init__(self):
|
|
116 self.re = re.compile('(\s+)$')
|
|
117
|
|
118 def check(self, filename, lineno, line):
|
|
119 assert(len(line) == 0 or line[-1] != '\n')
|
|
120 m = self.re.search(line)
|
|
121 if m != None:
|
|
122 return CheckError(filename, lineno,
|
|
123 line[:m.start(1)] + error_string(ws_char * len(m.group(1)))
|
|
124 + line[m.end(1):],
|
|
125 'trailing whitespace', m.start(1))
|
|
126
|
|
127 class SentenceSeparatorCheck:
|
|
128 def __init__(self):
|
|
129 self.re = re.compile('\w\.(\s|\s{3,})\w')
|
|
130
|
|
131 def check(self, filename, lineno, line):
|
|
132 m = self.re.search(line)
|
|
133 if m != None:
|
|
134 return CheckError(filename, lineno,
|
|
135 line[:m.start(1)] + error_string(ws_char * len(m.group(1)))
|
|
136 + line[m.end(1):],
|
|
137 'dot, space, space, new sentence', m.start(1))
|
|
138
|
|
139 class SentenceEndOfCommentCheck:
|
|
140 def __init__(self):
|
|
141 self.re = re.compile('\w\.(\s{0,1}|\s{3,})\*/')
|
|
142
|
|
143 def check(self, filename, lineno, line):
|
|
144 m = self.re.search(line)
|
|
145 if m != None:
|
|
146 return CheckError(filename, lineno,
|
|
147 line[:m.start(1)] + error_string(ws_char * len(m.group(1)))
|
|
148 + line[m.end(1):],
|
|
149 'dot, space, space, end of comment', m.start(1))
|
|
150
|
|
151 class SentenceDotEndCheck:
|
|
152 def __init__(self):
|
|
153 self.re = re.compile('\w(\s*\*/)')
|
|
154
|
|
155 def check(self, filename, lineno, line):
|
|
156 m = self.re.search(line)
|
|
157 if m != None:
|
|
158 return CheckError(filename, lineno,
|
|
159 line[:m.start(1)] + error_string(m.group(1)) + line[m.end(1):],
|
|
160 'dot, space, space, end of comment', m.start(1))
|
|
161
|
|
162 class FunctionParenthesisCheck:
|
|
163 # TODO: filter out GTY stuff
|
|
164 def __init__(self):
|
|
165 self.re = re.compile('\w(\s{2,})?(\()')
|
|
166
|
|
167 def check(self, filename, lineno, line):
|
|
168 if '#define' in line:
|
|
169 return None
|
|
170
|
|
171 m = self.re.search(line)
|
|
172 if m != None:
|
|
173 return CheckError(filename, lineno,
|
|
174 line[:m.start(2)] + error_string(m.group(2)) + line[m.end(2):],
|
|
175 'there should be exactly one space between function name ' \
|
|
176 'and parenthesis', m.start(2))
|
|
177
|
|
178 class SquareBracketCheck:
|
|
179 def __init__(self):
|
|
180 self.re = re.compile('\w\s+(\[)')
|
|
181
|
|
182 def check(self, filename, lineno, line):
|
|
183 m = self.re.search(line)
|
|
184 if m != None:
|
|
185 return CheckError(filename, lineno,
|
|
186 line[:m.start(1)] + error_string(m.group(1)) + line[m.end(1):],
|
|
187 'there should be no space before a left square bracket',
|
|
188 m.start(1))
|
|
189
|
|
190 class ClosingParenthesisCheck:
|
|
191 def __init__(self):
|
|
192 self.re = re.compile('\S\s+(\))')
|
|
193
|
|
194 def check(self, filename, lineno, line):
|
|
195 m = self.re.search(line)
|
|
196 if m != None:
|
|
197 return CheckError(filename, lineno,
|
|
198 line[:m.start(1)] + error_string(m.group(1)) + line[m.end(1):],
|
|
199 'there should be no space before closing parenthesis',
|
|
200 m.start(1))
|
|
201
|
|
202 class BracesOnSeparateLineCheck:
|
|
203 # This will give false positives for C99 compound literals.
|
|
204
|
|
205 def __init__(self):
|
|
206 self.re = re.compile('(\)|else)\s*({)')
|
|
207
|
|
208 def check(self, filename, lineno, line):
|
|
209 m = self.re.search(line)
|
|
210 if m != None:
|
|
211 return CheckError(filename, lineno,
|
|
212 line[:m.start(2)] + error_string(m.group(2)) + line[m.end(2):],
|
|
213 'braces should be on a separate line', m.start(2))
|
|
214
|
|
215 class TrailinigOperatorCheck:
|
|
216 def __init__(self):
|
|
217 regex = '^\s.*(([^a-zA-Z_]\*)|([-%<=&|^?])|([^*]/)|([^:][+]))$'
|
|
218 self.re = re.compile(regex)
|
|
219
|
|
220 def check(self, filename, lineno, line):
|
|
221 m = self.re.search(line)
|
|
222 if m != None:
|
|
223 return CheckError(filename, lineno,
|
|
224 line[:m.start(1)] + error_string(m.group(1)) + line[m.end(1):],
|
|
225 'trailing operator', m.start(1))
|
|
226
|
|
227 class LineLengthTest(unittest.TestCase):
|
|
228 def setUp(self):
|
|
229 self.check = LineLengthCheck()
|
|
230
|
|
231 def test_line_length_check_basic(self):
|
|
232 r = self.check.check('foo', 123, self.check.limit * 'a' + ' = 123;')
|
|
233 self.assertIsNotNone(r)
|
|
234 self.assertEqual('foo', r.filename)
|
|
235 self.assertEqual(80, r.column)
|
|
236 self.assertEqual(r.console_error,
|
|
237 self.check.limit * 'a' + error_string(' = 123;'))
|
|
238
|
|
239 class TrailingWhitespaceTest(unittest.TestCase):
|
|
240 def setUp(self):
|
|
241 self.check = TrailingWhitespaceCheck()
|
|
242
|
|
243 def test_trailing_whitespace_check_basic(self):
|
|
244 r = self.check.check('foo', 123, 'a = 123;')
|
|
245 self.assertIsNone(r)
|
|
246 r = self.check.check('foo', 123, 'a = 123; ')
|
|
247 self.assertIsNotNone(r)
|
|
248 r = self.check.check('foo', 123, 'a = 123;\t')
|
|
249 self.assertIsNotNone(r)
|
|
250
|
145
|
251 class SpacesAndTabsMixedTest(unittest.TestCase):
|
|
252 def setUp(self):
|
|
253 self.check = SpacesAndTabsMixedCheck()
|
|
254
|
|
255 def test_trailing_whitespace_check_basic(self):
|
|
256 r = self.check.check('foo', 123, ' \ta = 123;')
|
|
257 self.assertEqual('foo', r.filename)
|
|
258 self.assertEqual(0, r.column)
|
|
259 self.assertIsNotNone(r.console_error)
|
|
260 r = self.check.check('foo', 123, ' \t a = 123;')
|
|
261 self.assertIsNotNone(r.console_error)
|
|
262 r = self.check.check('foo', 123, '\t a = 123;')
|
|
263 self.assertIsNone(r)
|
|
264
|
111
|
265 def check_GNU_style_file(file, file_encoding, format):
|
|
266 checks = [LineLengthCheck(), SpacesCheck(), TrailingWhitespaceCheck(),
|
|
267 SentenceSeparatorCheck(), SentenceEndOfCommentCheck(),
|
|
268 SentenceDotEndCheck(), FunctionParenthesisCheck(),
|
|
269 SquareBracketCheck(), ClosingParenthesisCheck(),
|
145
|
270 BracesOnSeparateLineCheck(), TrailinigOperatorCheck(),
|
|
271 SpacesAndTabsMixedCheck()]
|
111
|
272 errors = []
|
|
273
|
|
274 patch = PatchSet(file, encoding=file_encoding)
|
|
275
|
|
276 for pfile in patch.added_files + patch.modified_files:
|
|
277 t = pfile.target_file.lstrip('b/')
|
|
278 # Skip testsuite files
|
|
279 if 'testsuite' in t:
|
|
280 continue
|
|
281
|
|
282 for hunk in pfile:
|
|
283 delta = 0
|
|
284 for line in hunk:
|
|
285 if line.is_added and line.target_line_no != None:
|
|
286 for check in checks:
|
|
287 line_chomp = line.value.replace('\n', '')
|
|
288 e = check.check(t, line.target_line_no, line_chomp)
|
|
289 if e != None:
|
|
290 errors.append(e)
|
|
291
|
|
292 if format == 'stdio':
|
|
293 fn = lambda x: x.error_message
|
|
294 i = 1
|
|
295 for (k, errors) in groupby(sorted(errors, key = fn), fn):
|
|
296 errors = list(errors)
|
|
297 print('=== ERROR type #%d: %s (%d error(s)) ==='
|
|
298 % (i, k, len(errors)))
|
|
299 i += 1
|
|
300 for e in errors:
|
|
301 print(e.error_location () + e.console_error)
|
|
302 print()
|
|
303
|
|
304 exit(0 if len(errors) == 0 else 1)
|
|
305 elif format == 'quickfix':
|
|
306 f = 'errors.err'
|
|
307 with open(f, 'w+') as qf:
|
|
308 for e in errors:
|
|
309 qf.write('%s%s\n' % (e.error_location(), e.error_message))
|
|
310 if len(errors) == 0:
|
|
311 exit(0)
|
|
312 else:
|
|
313 print('%d error(s) written to %s file.' % (len(errors), f))
|
|
314 exit(1)
|
|
315 else:
|
|
316 assert False
|
|
317
|
|
318 if __name__ == '__main__':
|
|
319 unittest.main()
|