3 # Copyright (C) 2017-2023 Free Software Foundation, Inc.
5 # Checks some of the GNU style formatting rules in a set of patches.
6 # The script is a rewritten of the same bash script and should eventually
7 # replace the former script.
9 # This file is part of GCC.
11 # GCC is free software; you can redistribute it and/or modify it under
12 # the terms of the GNU General Public License as published by the Free
13 # Software Foundation; either version 3, or (at your option) any later
16 # GCC is distributed in the hope that it will be useful, but WITHOUT ANY
17 # WARRANTY; without even the implied warranty of MERCHANTABILITY or
18 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 # You should have received a copy of the GNU General Public License
22 # along with GCC; see the file COPYING3. If not see
23 # <http://www.gnu.org/licenses/>. */
25 # The script requires python packages, which can be installed via pip3
27 # $ pip3 install unidiff termcolor
33 def import_pip3(*args
):
35 for (module
, names
) in args
:
37 lib
= __import__(module
)
39 missing
.append(module
)
41 if not isinstance(names
, list):
44 globals()[name
]=getattr(lib
, name
)
46 missing_and_sep
= ' and '.join(missing
)
47 missing_space_sep
= ' '.join(missing
)
48 print('%s %s missing (run: pip3 install %s)'
50 ("module is" if len(missing
) == 1 else "modules are"),
54 import_pip3(('termcolor', 'colored'),
55 ('unidiff', 'PatchSet'))
57 from itertools
import *
63 return colored(s
, 'red', attrs
= ['bold'])
66 def __init__(self
, filename
, lineno
, console_error
, error_message
,
68 self
.filename
= filename
70 self
.console_error
= console_error
71 self
.error_message
= error_message
74 def error_location(self
):
75 return '%s:%d:%d:' % (self
.filename
, self
.lineno
,
76 self
.column
if self
.column
!= -1 else -1)
78 class LineLengthCheck
:
81 self
.expanded_tab
= ' ' * ts
83 def check(self
, filename
, lineno
, line
):
84 line_expanded
= line
.replace('\t', self
.expanded_tab
)
85 if len(line_expanded
) > self
.limit
:
86 return CheckError(filename
, lineno
,
87 line_expanded
[:self
.limit
]
88 + error_string(line_expanded
[self
.limit
:]),
89 'lines should not exceed 80 characters', self
.limit
)
95 self
.expanded_tab
= ' ' * ts
97 def check(self
, filename
, lineno
, line
):
98 i
= line
.find(self
.expanded_tab
)
100 return CheckError(filename
, lineno
,
101 line
.replace(self
.expanded_tab
, error_string(ws_char
* ts
)),
102 'blocks of 8 spaces should be replaced with tabs', i
)
104 class SpacesAndTabsMixedCheck
:
106 self
.re
= re
.compile('\ \t')
108 def check(self
, filename
, lineno
, line
):
109 stripped
= line
.lstrip()
110 start
= line
[:len(line
) - len(stripped
)]
111 if self
.re
.search(line
):
112 return CheckError(filename
, lineno
,
113 error_string(start
.replace('\t', ws_char
* ts
)) + line
[len(start
):],
114 'a space should not precede a tab', 0)
116 class TrailingWhitespaceCheck
:
118 self
.re
= re
.compile('(\s+)$')
120 def check(self
, filename
, lineno
, line
):
121 assert(len(line
) == 0 or line
[-1] != '\n')
122 m
= self
.re
.search(line
)
124 return CheckError(filename
, lineno
,
125 line
[:m
.start(1)] + error_string(ws_char
* len(m
.group(1)))
127 'trailing whitespace', m
.start(1))
129 class SentenceSeparatorCheck
:
131 self
.re
= re
.compile('\w\.(\s|\s{3,})\w')
133 def check(self
, filename
, lineno
, line
):
134 m
= self
.re
.search(line
)
136 return CheckError(filename
, lineno
,
137 line
[:m
.start(1)] + error_string(ws_char
* len(m
.group(1)))
139 'dot, space, space, new sentence', m
.start(1))
141 class SentenceEndOfCommentCheck
:
143 self
.re
= re
.compile('\w\.(\s{0,1}|\s{3,})\*/')
145 def check(self
, filename
, lineno
, line
):
146 m
= self
.re
.search(line
)
148 return CheckError(filename
, lineno
,
149 line
[:m
.start(1)] + error_string(ws_char
* len(m
.group(1)))
151 'dot, space, space, end of comment', m
.start(1))
153 class SentenceDotEndCheck
:
155 self
.re
= re
.compile('\w(\s*\*/)')
157 def check(self
, filename
, lineno
, line
):
158 m
= self
.re
.search(line
)
160 return CheckError(filename
, lineno
,
161 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
162 'dot, space, space, end of comment', m
.start(1))
164 class FunctionParenthesisCheck
:
165 # TODO: filter out GTY stuff
167 self
.re
= re
.compile('\w(\s{2,})?(\()')
169 def check(self
, filename
, lineno
, line
):
170 if '#define' in line
:
173 m
= self
.re
.search(line
)
175 return CheckError(filename
, lineno
,
176 line
[:m
.start(2)] + error_string(m
.group(2)) + line
[m
.end(2):],
177 'there should be exactly one space between function name ' \
178 'and parenthesis', m
.start(2))
180 class SquareBracketCheck
:
182 self
.re
= re
.compile('\w\s+(\[)')
184 def check(self
, filename
, lineno
, line
):
185 m
= self
.re
.search(line
)
187 return CheckError(filename
, lineno
,
188 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
189 'there should be no space before a left square bracket',
192 class ClosingParenthesisCheck
:
194 self
.re
= re
.compile('\S\s+(\))')
196 def check(self
, filename
, lineno
, line
):
197 m
= self
.re
.search(line
)
199 return CheckError(filename
, lineno
,
200 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
201 'there should be no space before closing parenthesis',
204 class BracesOnSeparateLineCheck
:
205 # This will give false positives for C99 compound literals.
208 self
.re
= re
.compile('(\)|else)\s*({)')
210 def check(self
, filename
, lineno
, line
):
211 m
= self
.re
.search(line
)
213 return CheckError(filename
, lineno
,
214 line
[:m
.start(2)] + error_string(m
.group(2)) + line
[m
.end(2):],
215 'braces should be on a separate line', m
.start(2))
217 class TrailinigOperatorCheck
:
219 regex
= '^\s.*(([^a-zA-Z_]\*)|([-%<=&|^?])|([^*]/)|([^:][+]))$'
220 self
.re
= re
.compile(regex
)
222 def check(self
, filename
, lineno
, line
):
223 m
= self
.re
.search(line
)
225 return CheckError(filename
, lineno
,
226 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
227 'trailing operator', m
.start(1))
229 class LineLengthTest(unittest
.TestCase
):
231 self
.check
= LineLengthCheck()
233 def test_line_length_check_basic(self
):
234 r
= self
.check
.check('foo', 123, self
.check
.limit
* 'a' + ' = 123;')
235 self
.assertIsNotNone(r
)
236 self
.assertEqual('foo', r
.filename
)
237 self
.assertEqual(80, r
.column
)
238 self
.assertEqual(r
.console_error
,
239 self
.check
.limit
* 'a' + error_string(' = 123;'))
241 class TrailingWhitespaceTest(unittest
.TestCase
):
243 self
.check
= TrailingWhitespaceCheck()
245 def test_trailing_whitespace_check_basic(self
):
246 r
= self
.check
.check('foo', 123, 'a = 123;')
248 r
= self
.check
.check('foo', 123, 'a = 123; ')
249 self
.assertIsNotNone(r
)
250 r
= self
.check
.check('foo', 123, 'a = 123;\t')
251 self
.assertIsNotNone(r
)
253 class SpacesAndTabsMixedTest(unittest
.TestCase
):
255 self
.check
= SpacesAndTabsMixedCheck()
257 def test_trailing_whitespace_check_basic(self
):
258 r
= self
.check
.check('foo', 123, ' \ta = 123;')
259 self
.assertEqual('foo', r
.filename
)
260 self
.assertEqual(0, r
.column
)
261 self
.assertIsNotNone(r
.console_error
)
262 r
= self
.check
.check('foo', 123, ' \t a = 123;')
263 self
.assertIsNotNone(r
.console_error
)
264 r
= self
.check
.check('foo', 123, '\t a = 123;')
267 def check_GNU_style_file(file, format
):
268 checks
= [LineLengthCheck(), SpacesCheck(), TrailingWhitespaceCheck(),
269 SentenceSeparatorCheck(), SentenceEndOfCommentCheck(),
270 SentenceDotEndCheck(), FunctionParenthesisCheck(),
271 SquareBracketCheck(), ClosingParenthesisCheck(),
272 BracesOnSeparateLineCheck(), TrailinigOperatorCheck(),
273 SpacesAndTabsMixedCheck()]
276 patch
= PatchSet(file)
278 for pfile
in patch
.added_files
+ patch
.modified_files
:
279 t
= pfile
.target_file
.lstrip('b/')
280 # Skip testsuite files
281 if 'testsuite' in t
or t
.endswith('.py'):
287 if line
.is_added
and line
.target_line_no
!= None:
289 line_chomp
= line
.value
.replace('\n', '')
290 e
= check
.check(t
, line
.target_line_no
, line_chomp
)
294 if format
== 'stdio':
295 fn
= lambda x
: x
.error_message
297 for (k
, errors
) in groupby(sorted(errors
, key
= fn
), fn
):
298 errors
= list(errors
)
299 print('=== ERROR type #%d: %s (%d error(s)) ==='
300 % (i
, k
, len(errors
)))
303 print(e
.error_location () + e
.console_error
)
306 exit(0 if len(errors
) == 0 else 1)
307 elif format
== 'quickfix':
309 with
open(f
, 'w+') as qf
:
311 qf
.write('%s%s\n' % (e
.error_location(), e
.error_message
))
315 print('%d error(s) written to %s file.' % (len(errors
), f
))
320 if __name__
== '__main__':