]>
Commit | Line | Data |
---|---|---|
21eac2da | 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 | ||
80885f4f | 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) | |
21eac2da | 51 | |
80885f4f | 52 | import_pip3(('termcolor', 'colored'), |
53 | ('unidiff', 'PatchSet')) | |
21eac2da | 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 | ||
feedb53d | 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 | ||
21eac2da | 114 | class TrailingWhitespaceCheck: |
115 | def __init__(self): | |
116 | self.re = re.compile('(\s+)$') | |
117 | ||
118 | def check(self, filename, lineno, line): | |
2377d250 | 119 | assert(len(line) == 0 or line[-1] != '\n') |
21eac2da | 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 | ||
2377d250 | 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 | ||
feedb53d | 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 | ||
5d952fcd | 265 | def check_GNU_style_file(file, file_encoding, format): |
21eac2da | 266 | checks = [LineLengthCheck(), SpacesCheck(), TrailingWhitespaceCheck(), |
267 | SentenceSeparatorCheck(), SentenceEndOfCommentCheck(), | |
268 | SentenceDotEndCheck(), FunctionParenthesisCheck(), | |
269 | SquareBracketCheck(), ClosingParenthesisCheck(), | |
feedb53d | 270 | BracesOnSeparateLineCheck(), TrailinigOperatorCheck(), |
271 | SpacesAndTabsMixedCheck()] | |
21eac2da | 272 | errors = [] |
273 | ||
5d952fcd | 274 | patch = PatchSet(file, encoding=file_encoding) |
21eac2da | 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: | |
2377d250 | 287 | line_chomp = line.value.replace('\n', '') |
288 | e = check.check(t, line.target_line_no, line_chomp) | |
21eac2da | 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() |