]>
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 | ||
102 | class TrailingWhitespaceCheck: | |
103 | def __init__(self): | |
104 | self.re = re.compile('(\s+)$') | |
105 | ||
106 | def check(self, filename, lineno, line): | |
2377d250 | 107 | assert(len(line) == 0 or line[-1] != '\n') |
21eac2da | 108 | m = self.re.search(line) |
109 | if m != None: | |
110 | return CheckError(filename, lineno, | |
111 | line[:m.start(1)] + error_string(ws_char * len(m.group(1))) | |
112 | + line[m.end(1):], | |
113 | 'trailing whitespace', m.start(1)) | |
114 | ||
115 | class SentenceSeparatorCheck: | |
116 | def __init__(self): | |
117 | self.re = re.compile('\w\.(\s|\s{3,})\w') | |
118 | ||
119 | def check(self, filename, lineno, line): | |
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 | 'dot, space, space, new sentence', m.start(1)) | |
126 | ||
127 | class SentenceEndOfCommentCheck: | |
128 | def __init__(self): | |
129 | self.re = re.compile('\w\.(\s{0,1}|\s{3,})\*/') | |
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, end of comment', m.start(1)) | |
138 | ||
139 | class SentenceDotEndCheck: | |
140 | def __init__(self): | |
141 | self.re = re.compile('\w(\s*\*/)') | |
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(m.group(1)) + line[m.end(1):], | |
148 | 'dot, space, space, end of comment', m.start(1)) | |
149 | ||
150 | class FunctionParenthesisCheck: | |
151 | # TODO: filter out GTY stuff | |
152 | def __init__(self): | |
153 | self.re = re.compile('\w(\s{2,})?(\()') | |
154 | ||
155 | def check(self, filename, lineno, line): | |
156 | if '#define' in line: | |
157 | return None | |
158 | ||
159 | m = self.re.search(line) | |
160 | if m != None: | |
161 | return CheckError(filename, lineno, | |
162 | line[:m.start(2)] + error_string(m.group(2)) + line[m.end(2):], | |
163 | 'there should be exactly one space between function name ' \ | |
164 | 'and parenthesis', m.start(2)) | |
165 | ||
166 | class SquareBracketCheck: | |
167 | def __init__(self): | |
168 | self.re = re.compile('\w\s+(\[)') | |
169 | ||
170 | def check(self, filename, lineno, line): | |
171 | m = self.re.search(line) | |
172 | if m != None: | |
173 | return CheckError(filename, lineno, | |
174 | line[:m.start(1)] + error_string(m.group(1)) + line[m.end(1):], | |
175 | 'there should be no space before a left square bracket', | |
176 | m.start(1)) | |
177 | ||
178 | class ClosingParenthesisCheck: | |
179 | def __init__(self): | |
180 | self.re = re.compile('\S\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 closing parenthesis', | |
188 | m.start(1)) | |
189 | ||
190 | class BracesOnSeparateLineCheck: | |
191 | # This will give false positives for C99 compound literals. | |
192 | ||
193 | def __init__(self): | |
194 | self.re = re.compile('(\)|else)\s*({)') | |
195 | ||
196 | def check(self, filename, lineno, line): | |
197 | m = self.re.search(line) | |
198 | if m != None: | |
199 | return CheckError(filename, lineno, | |
200 | line[:m.start(2)] + error_string(m.group(2)) + line[m.end(2):], | |
201 | 'braces should be on a separate line', m.start(2)) | |
202 | ||
203 | class TrailinigOperatorCheck: | |
204 | def __init__(self): | |
205 | regex = '^\s.*(([^a-zA-Z_]\*)|([-%<=&|^?])|([^*]/)|([^:][+]))$' | |
206 | self.re = re.compile(regex) | |
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(1)] + error_string(m.group(1)) + line[m.end(1):], | |
213 | 'trailing operator', m.start(1)) | |
214 | ||
215 | class LineLengthTest(unittest.TestCase): | |
216 | def setUp(self): | |
217 | self.check = LineLengthCheck() | |
218 | ||
219 | def test_line_length_check_basic(self): | |
220 | r = self.check.check('foo', 123, self.check.limit * 'a' + ' = 123;') | |
221 | self.assertIsNotNone(r) | |
222 | self.assertEqual('foo', r.filename) | |
223 | self.assertEqual(80, r.column) | |
224 | self.assertEqual(r.console_error, | |
225 | self.check.limit * 'a' + error_string(' = 123;')) | |
226 | ||
2377d250 | 227 | class TrailingWhitespaceTest(unittest.TestCase): |
228 | def setUp(self): | |
229 | self.check = TrailingWhitespaceCheck() | |
230 | ||
231 | def test_trailing_whitespace_check_basic(self): | |
232 | r = self.check.check('foo', 123, 'a = 123;') | |
233 | self.assertIsNone(r) | |
234 | r = self.check.check('foo', 123, 'a = 123; ') | |
235 | self.assertIsNotNone(r) | |
236 | r = self.check.check('foo', 123, 'a = 123;\t') | |
237 | self.assertIsNotNone(r) | |
238 | ||
5d952fcd | 239 | def check_GNU_style_file(file, file_encoding, format): |
21eac2da | 240 | checks = [LineLengthCheck(), SpacesCheck(), TrailingWhitespaceCheck(), |
241 | SentenceSeparatorCheck(), SentenceEndOfCommentCheck(), | |
242 | SentenceDotEndCheck(), FunctionParenthesisCheck(), | |
243 | SquareBracketCheck(), ClosingParenthesisCheck(), | |
244 | BracesOnSeparateLineCheck(), TrailinigOperatorCheck()] | |
245 | errors = [] | |
246 | ||
5d952fcd | 247 | patch = PatchSet(file, encoding=file_encoding) |
21eac2da | 248 | |
249 | for pfile in patch.added_files + patch.modified_files: | |
250 | t = pfile.target_file.lstrip('b/') | |
251 | # Skip testsuite files | |
252 | if 'testsuite' in t: | |
253 | continue | |
254 | ||
255 | for hunk in pfile: | |
256 | delta = 0 | |
257 | for line in hunk: | |
258 | if line.is_added and line.target_line_no != None: | |
259 | for check in checks: | |
2377d250 | 260 | line_chomp = line.value.replace('\n', '') |
261 | e = check.check(t, line.target_line_no, line_chomp) | |
21eac2da | 262 | if e != None: |
263 | errors.append(e) | |
264 | ||
265 | if format == 'stdio': | |
266 | fn = lambda x: x.error_message | |
267 | i = 1 | |
268 | for (k, errors) in groupby(sorted(errors, key = fn), fn): | |
269 | errors = list(errors) | |
270 | print('=== ERROR type #%d: %s (%d error(s)) ===' | |
271 | % (i, k, len(errors))) | |
272 | i += 1 | |
273 | for e in errors: | |
274 | print(e.error_location () + e.console_error) | |
275 | print() | |
276 | ||
277 | exit(0 if len(errors) == 0 else 1) | |
278 | elif format == 'quickfix': | |
279 | f = 'errors.err' | |
280 | with open(f, 'w+') as qf: | |
281 | for e in errors: | |
282 | qf.write('%s%s\n' % (e.error_location(), e.error_message)) | |
283 | if len(errors) == 0: | |
284 | exit(0) | |
285 | else: | |
286 | print('%d error(s) written to %s file.' % (len(errors), f)) | |
287 | exit(1) | |
288 | else: | |
289 | assert False | |
290 | ||
291 | if __name__ == '__main__': | |
292 | unittest.main() |