3 # Copyright (C) 2017-2024 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(r
'\ \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(r
'(\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(r
'\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(r
'\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(r
'\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(r
'\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(r
'\w\s+(\[)')
184 def check(self
, filename
, lineno
, line
):
185 if filename
.endswith('.md'):
188 m
= self
.re
.search(line
)
190 return CheckError(filename
, lineno
,
191 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
192 'there should be no space before a left square bracket',
195 class ClosingParenthesisCheck
:
197 self
.re
= re
.compile(r
'\S\s+(\))')
199 def check(self
, filename
, lineno
, line
):
200 m
= self
.re
.search(line
)
202 return CheckError(filename
, lineno
,
203 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
204 'there should be no space before closing parenthesis',
207 class BracesOnSeparateLineCheck
:
208 # This will give false positives for C99 compound literals.
211 self
.re
= re
.compile(r
'(\)|else)\s*({)')
213 def check(self
, filename
, lineno
, line
):
214 m
= self
.re
.search(line
)
216 return CheckError(filename
, lineno
,
217 line
[:m
.start(2)] + error_string(m
.group(2)) + line
[m
.end(2):],
218 'braces should be on a separate line', m
.start(2))
220 class TrailinigOperatorCheck
:
222 regex
= r
'^\s.*(([^a-zA-Z_]\*)|([-%<=&|^?])|([^*]/)|([^:][+]))$'
223 self
.re
= re
.compile(regex
)
225 def check(self
, filename
, lineno
, line
):
226 m
= self
.re
.search(line
)
228 return CheckError(filename
, lineno
,
229 line
[:m
.start(1)] + error_string(m
.group(1)) + line
[m
.end(1):],
230 'trailing operator', m
.start(1))
232 class LineLengthTest(unittest
.TestCase
):
234 self
.check
= LineLengthCheck()
236 def test_line_length_check_basic(self
):
237 r
= self
.check
.check('foo', 123, self
.check
.limit
* 'a' + ' = 123;')
238 self
.assertIsNotNone(r
)
239 self
.assertEqual('foo', r
.filename
)
240 self
.assertEqual(80, r
.column
)
241 self
.assertEqual(r
.console_error
,
242 self
.check
.limit
* 'a' + error_string(' = 123;'))
244 class TrailingWhitespaceTest(unittest
.TestCase
):
246 self
.check
= TrailingWhitespaceCheck()
248 def test_trailing_whitespace_check_basic(self
):
249 r
= self
.check
.check('foo', 123, 'a = 123;')
251 r
= self
.check
.check('foo', 123, 'a = 123; ')
252 self
.assertIsNotNone(r
)
253 r
= self
.check
.check('foo', 123, 'a = 123;\t')
254 self
.assertIsNotNone(r
)
256 class SpacesAndTabsMixedTest(unittest
.TestCase
):
258 self
.check
= SpacesAndTabsMixedCheck()
260 def test_trailing_whitespace_check_basic(self
):
261 r
= self
.check
.check('foo', 123, ' \ta = 123;')
262 self
.assertEqual('foo', r
.filename
)
263 self
.assertEqual(0, r
.column
)
264 self
.assertIsNotNone(r
.console_error
)
265 r
= self
.check
.check('foo', 123, ' \t a = 123;')
266 self
.assertIsNotNone(r
.console_error
)
267 r
= self
.check
.check('foo', 123, '\t a = 123;')
270 def check_GNU_style_file(file, format
):
271 checks
= [LineLengthCheck(), SpacesCheck(), TrailingWhitespaceCheck(),
272 SentenceSeparatorCheck(), SentenceEndOfCommentCheck(),
273 SentenceDotEndCheck(), FunctionParenthesisCheck(),
274 SquareBracketCheck(), ClosingParenthesisCheck(),
275 BracesOnSeparateLineCheck(), TrailinigOperatorCheck(),
276 SpacesAndTabsMixedCheck()]
279 patch
= PatchSet(file)
281 for pfile
in patch
.added_files
+ patch
.modified_files
:
282 t
= pfile
.target_file
.lstrip('b/')
283 # Skip testsuite files
284 if 'testsuite' in t
or t
.endswith('.py'):
290 if line
.is_added
and line
.target_line_no
!= None:
292 line_chomp
= line
.value
.replace('\n', '')
293 e
= check
.check(t
, line
.target_line_no
, line_chomp
)
297 if format
== 'stdio':
298 fn
= lambda x
: x
.error_message
300 for (k
, errors
) in groupby(sorted(errors
, key
= fn
), fn
):
301 errors
= list(errors
)
302 print('=== ERROR type #%d: %s (%d error(s)) ==='
303 % (i
, k
, len(errors
)))
306 print(e
.error_location () + e
.console_error
)
309 exit(0 if len(errors
) == 0 else 1)
310 elif format
== 'quickfix':
312 with
open(f
, 'w+') as qf
:
314 qf
.write('%s%s\n' % (e
.error_location(), e
.error_message
))
318 print('%d error(s) written to %s file.' % (len(errors
), f
))
323 if __name__
== '__main__':