comparison clang-tools-extra/clang-tidy/add_new_check.py @ 150:1d019706d866

LLVM10
author anatofuz
date Thu, 13 Feb 2020 15:10:13 +0900
parents
children 0572611fdcc8
comparison
equal deleted inserted replaced
147:c2174574ed3a 150:1d019706d866
1 #!/usr/bin/env python
2 #
3 #===- add_new_check.py - clang-tidy check generator ----------*- python -*--===#
4 #
5 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6 # See https://llvm.org/LICENSE.txt for license information.
7 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8 #
9 #===------------------------------------------------------------------------===#
10
11 from __future__ import print_function
12
13 import argparse
14 import os
15 import re
16 import sys
17
18 # Adapts the module's CMakelist file. Returns 'True' if it could add a new entry
19 # and 'False' if the entry already existed.
20 def adapt_cmake(module_path, check_name_camel):
21 filename = os.path.join(module_path, 'CMakeLists.txt')
22 with open(filename, 'r') as f:
23 lines = f.readlines()
24
25 cpp_file = check_name_camel + '.cpp'
26
27 # Figure out whether this check already exists.
28 for line in lines:
29 if line.strip() == cpp_file:
30 return False
31
32 print('Updating %s...' % filename)
33 with open(filename, 'w') as f:
34 cpp_found = False
35 file_added = False
36 for line in lines:
37 cpp_line = line.strip().endswith('.cpp')
38 if (not file_added) and (cpp_line or cpp_found):
39 cpp_found = True
40 if (line.strip() > cpp_file) or (not cpp_line):
41 f.write(' ' + cpp_file + '\n')
42 file_added = True
43 f.write(line)
44
45 return True
46
47
48 # Adds a header for the new check.
49 def write_header(module_path, module, namespace, check_name, check_name_camel):
50 check_name_dashes = module + '-' + check_name
51 filename = os.path.join(module_path, check_name_camel) + '.h'
52 print('Creating %s...' % filename)
53 with open(filename, 'w') as f:
54 header_guard = ('LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_' + module.upper() + '_'
55 + check_name_camel.upper() + '_H')
56 f.write('//===--- ')
57 f.write(os.path.basename(filename))
58 f.write(' - clang-tidy ')
59 f.write('-' * max(0, 42 - len(os.path.basename(filename))))
60 f.write('*- C++ -*-===//')
61 f.write("""
62 //
63 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
64 // See https://llvm.org/LICENSE.txt for license information.
65 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66 //
67 //===----------------------------------------------------------------------===//
68
69 #ifndef %(header_guard)s
70 #define %(header_guard)s
71
72 #include "../ClangTidyCheck.h"
73
74 namespace clang {
75 namespace tidy {
76 namespace %(namespace)s {
77
78 /// FIXME: Write a short description.
79 ///
80 /// For the user-facing documentation see:
81 /// http://clang.llvm.org/extra/clang-tidy/checks/%(check_name_dashes)s.html
82 class %(check_name)s : public ClangTidyCheck {
83 public:
84 %(check_name)s(StringRef Name, ClangTidyContext *Context)
85 : ClangTidyCheck(Name, Context) {}
86 void registerMatchers(ast_matchers::MatchFinder *Finder) override;
87 void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
88 };
89
90 } // namespace %(namespace)s
91 } // namespace tidy
92 } // namespace clang
93
94 #endif // %(header_guard)s
95 """ % {'header_guard': header_guard,
96 'check_name': check_name_camel,
97 'check_name_dashes': check_name_dashes,
98 'module': module,
99 'namespace': namespace})
100
101
102 # Adds the implementation of the new check.
103 def write_implementation(module_path, module, namespace, check_name_camel):
104 filename = os.path.join(module_path, check_name_camel) + '.cpp'
105 print('Creating %s...' % filename)
106 with open(filename, 'w') as f:
107 f.write('//===--- ')
108 f.write(os.path.basename(filename))
109 f.write(' - clang-tidy ')
110 f.write('-' * max(0, 51 - len(os.path.basename(filename))))
111 f.write('-===//')
112 f.write("""
113 //
114 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
115 // See https://llvm.org/LICENSE.txt for license information.
116 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
117 //
118 //===----------------------------------------------------------------------===//
119
120 #include "%(check_name)s.h"
121 #include "clang/AST/ASTContext.h"
122 #include "clang/ASTMatchers/ASTMatchFinder.h"
123
124 using namespace clang::ast_matchers;
125
126 namespace clang {
127 namespace tidy {
128 namespace %(namespace)s {
129
130 void %(check_name)s::registerMatchers(MatchFinder *Finder) {
131 // FIXME: Add matchers.
132 Finder->addMatcher(functionDecl().bind("x"), this);
133 }
134
135 void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
136 // FIXME: Add callback implementation.
137 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
138 if (MatchedDecl->getName().startswith("awesome_"))
139 return;
140 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
141 << MatchedDecl;
142 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note)
143 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
144 }
145
146 } // namespace %(namespace)s
147 } // namespace tidy
148 } // namespace clang
149 """ % {'check_name': check_name_camel,
150 'module': module,
151 'namespace': namespace})
152
153
154 # Modifies the module to include the new check.
155 def adapt_module(module_path, module, check_name, check_name_camel):
156 modulecpp = list(filter(
157 lambda p: p.lower() == module.lower() + 'tidymodule.cpp',
158 os.listdir(module_path)))[0]
159 filename = os.path.join(module_path, modulecpp)
160 with open(filename, 'r') as f:
161 lines = f.readlines()
162
163 print('Updating %s...' % filename)
164 with open(filename, 'w') as f:
165 header_added = False
166 header_found = False
167 check_added = False
168 check_fq_name = module + '-' + check_name
169 check_decl = (' CheckFactories.registerCheck<' + check_name_camel +
170 '>(\n "' + check_fq_name + '");\n')
171
172 lines = iter(lines)
173 try:
174 while True:
175 line = lines.next()
176 if not header_added:
177 match = re.search('#include "(.*)"', line)
178 if match:
179 header_found = True
180 if match.group(1) > check_name_camel:
181 header_added = True
182 f.write('#include "' + check_name_camel + '.h"\n')
183 elif header_found:
184 header_added = True
185 f.write('#include "' + check_name_camel + '.h"\n')
186
187 if not check_added:
188 if line.strip() == '}':
189 check_added = True
190 f.write(check_decl)
191 else:
192 match = re.search('registerCheck<(.*)> *\( *(?:"([^"]*)")?', line)
193 prev_line = None
194 if match:
195 current_check_name = match.group(2)
196 if current_check_name is None:
197 # If we didn't find the check name on this line, look on the
198 # next one.
199 prev_line = line
200 line = lines.next()
201 match = re.search(' *"([^"]*)"', line)
202 if match:
203 current_check_name = match.group(1)
204 if current_check_name > check_fq_name:
205 check_added = True
206 f.write(check_decl)
207 if prev_line:
208 f.write(prev_line)
209 f.write(line)
210 except StopIteration:
211 pass
212
213
214 # Adds a release notes entry.
215 def add_release_notes(module_path, module, check_name):
216 check_name_dashes = module + '-' + check_name
217 filename = os.path.normpath(os.path.join(module_path,
218 '../../docs/ReleaseNotes.rst'))
219 with open(filename, 'r') as f:
220 lines = f.readlines()
221
222 lineMatcher = re.compile('New checks')
223 nextSectionMatcher = re.compile('New check aliases')
224 checkerMatcher = re.compile('- New :doc:`(.*)')
225
226 print('Updating %s...' % filename)
227 with open(filename, 'w') as f:
228 note_added = False
229 header_found = False
230 next_header_found = False
231 add_note_here = False
232
233 for line in lines:
234 if not note_added:
235 match = lineMatcher.match(line)
236 match_next = nextSectionMatcher.match(line)
237 match_checker = checkerMatcher.match(line)
238 if match_checker:
239 last_checker = match_checker.group(1)
240 if last_checker > check_name_dashes:
241 add_note_here = True
242
243 if match_next:
244 next_header_found = True
245 add_note_here = True
246
247 if match:
248 header_found = True
249 f.write(line)
250 continue
251
252 if line.startswith('^^^^'):
253 f.write(line)
254 continue
255
256 if header_found and add_note_here:
257 if not line.startswith('^^^^'):
258 f.write("""- New :doc:`%s
259 <clang-tidy/checks/%s>` check.
260
261 FIXME: add release notes.
262
263 """ % (check_name_dashes, check_name_dashes))
264 note_added = True
265
266 f.write(line)
267
268
269 # Adds a test for the check.
270 def write_test(module_path, module, check_name, test_extension):
271 check_name_dashes = module + '-' + check_name
272 filename = os.path.normpath(os.path.join(module_path, '../../test/clang-tidy/checkers',
273 check_name_dashes + '.' + test_extension))
274 print('Creating %s...' % filename)
275 with open(filename, 'w') as f:
276 f.write("""// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
277
278 // FIXME: Add something that triggers the check here.
279 void f();
280 // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
281
282 // FIXME: Verify the applied fix.
283 // * Make the CHECK patterns specific enough and try to make verified lines
284 // unique to avoid incorrect matches.
285 // * Use {{}} for regular expressions.
286 // CHECK-FIXES: {{^}}void awesome_f();{{$}}
287
288 // FIXME: Add something that doesn't trigger the check here.
289 void awesome_f2();
290 """ % {'check_name_dashes': check_name_dashes})
291
292
293 # Recreates the list of checks in the docs/clang-tidy/checks directory.
294 def update_checks_list(clang_tidy_path):
295 docs_dir = os.path.join(clang_tidy_path, '../docs/clang-tidy/checks')
296 filename = os.path.normpath(os.path.join(docs_dir, 'list.rst'))
297 # Read the content of the current list.rst file
298 with open(filename, 'r') as f:
299 lines = f.readlines()
300 # Get all existing docs
301 doc_files = list(filter(lambda s: s.endswith('.rst') and s != 'list.rst',
302 os.listdir(docs_dir)))
303 doc_files.sort()
304
305 def has_auto_fix(check_name):
306 dirname, _, check_name = check_name.partition("-")
307
308 checkerCode = os.path.join(dirname, get_camel_name(check_name)) + ".cpp"
309
310 if not os.path.isfile(checkerCode):
311 return ""
312
313 with open(checkerCode) as f:
314 code = f.read()
315 if 'FixItHint' in code or "ReplacementText" in code or "fixit" in code:
316 # Some simple heuristics to figure out if a checker has an autofix or not.
317 return ' "Yes"'
318 return ""
319
320 def process_doc(doc_file):
321 check_name = doc_file.replace('.rst', '')
322
323 with open(os.path.join(docs_dir, doc_file), 'r') as doc:
324 content = doc.read()
325 match = re.search('.*:orphan:.*', content)
326
327 if match:
328 # Orphan page, don't list it.
329 return '', ''
330
331 match = re.search('.*:http-equiv=refresh: \d+;URL=(.*).html.*',
332 content)
333 # Is it a redirect?
334 return check_name, match
335
336 def format_link(doc_file):
337 check_name, match = process_doc(doc_file)
338 if not match and check_name:
339 return ' `%(check)s <%(check)s.html>`_,%(autofix)s\n' % {
340 'check': check_name,
341 'autofix': has_auto_fix(check_name)
342 }
343 else:
344 return ''
345
346 def format_link_alias(doc_file):
347 check_name, match = process_doc(doc_file)
348 if match and check_name:
349 if match.group(1) == 'https://clang.llvm.org/docs/analyzer/checkers':
350 title_redirect = 'Clang Static Analyzer'
351 else:
352 title_redirect = match.group(1)
353 # The checker is just a redirect.
354 return ' `%(check)s <%(check)s.html>`_, `%(title)s <%(target)s.html>`_,%(autofix)s\n' % {
355 'check': check_name,
356 'target': match.group(1),
357 'title': title_redirect,
358 'autofix': has_auto_fix(match.group(1))
359 }
360 return ''
361
362 checks = map(format_link, doc_files)
363 checks_alias = map(format_link_alias, doc_files)
364
365 print('Updating %s...' % filename)
366 with open(filename, 'w') as f:
367 for line in lines:
368 f.write(line)
369 if line.strip() == ".. csv-table::":
370 # We dump the checkers
371 f.write(' :header: "Name", "Offers fixes"\n\n')
372 f.writelines(checks)
373 # and the aliases
374 f.write('\n\n')
375 f.write('.. csv-table:: Aliases..\n')
376 f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
377 f.writelines(checks_alias)
378 break
379
380
381 # Adds a documentation for the check.
382 def write_docs(module_path, module, check_name):
383 check_name_dashes = module + '-' + check_name
384 filename = os.path.normpath(os.path.join(
385 module_path, '../../docs/clang-tidy/checks/', check_name_dashes + '.rst'))
386 print('Creating %s...' % filename)
387 with open(filename, 'w') as f:
388 f.write(""".. title:: clang-tidy - %(check_name_dashes)s
389
390 %(check_name_dashes)s
391 %(underline)s
392
393 FIXME: Describe what patterns does the check detect and why. Give examples.
394 """ % {'check_name_dashes': check_name_dashes,
395 'underline': '=' * len(check_name_dashes)})
396
397
398 def get_camel_name(check_name):
399 return ''.join(map(lambda elem: elem.capitalize(),
400 check_name.split('-'))) + 'Check'
401
402
403 def main():
404 language_to_extension = {
405 'c': 'c',
406 'c++': 'cpp',
407 'objc': 'm',
408 'objc++': 'mm',
409 }
410 parser = argparse.ArgumentParser()
411 parser.add_argument(
412 '--update-docs',
413 action='store_true',
414 help='just update the list of documentation files, then exit')
415 parser.add_argument(
416 '--language',
417 help='language to use for new check (defaults to c++)',
418 choices=language_to_extension.keys(),
419 default='c++',
420 metavar='LANG')
421 parser.add_argument(
422 'module',
423 nargs='?',
424 help='module directory under which to place the new tidy check (e.g., misc)')
425 parser.add_argument(
426 'check',
427 nargs='?',
428 help='name of new tidy check to add (e.g. foo-do-the-stuff)')
429 args = parser.parse_args()
430
431 if args.update_docs:
432 update_checks_list(os.path.dirname(sys.argv[0]))
433 return
434
435 if not args.module or not args.check:
436 print('Module and check must be specified.')
437 parser.print_usage()
438 return
439
440 module = args.module
441 check_name = args.check
442 check_name_camel = get_camel_name(check_name)
443 if check_name.startswith(module):
444 print('Check name "%s" must not start with the module "%s". Exiting.' % (
445 check_name, module))
446 return
447 clang_tidy_path = os.path.dirname(sys.argv[0])
448 module_path = os.path.join(clang_tidy_path, module)
449
450 if not adapt_cmake(module_path, check_name_camel):
451 return
452
453 # Map module names to namespace names that don't conflict with widely used top-level namespaces.
454 if module == 'llvm':
455 namespace = module + '_check'
456 else:
457 namespace = module
458
459 write_header(module_path, module, namespace, check_name, check_name_camel)
460 write_implementation(module_path, module, namespace, check_name_camel)
461 adapt_module(module_path, module, check_name, check_name_camel)
462 add_release_notes(module_path, module, check_name)
463 test_extension = language_to_extension.get(args.language)
464 write_test(module_path, module, check_name, test_extension)
465 write_docs(module_path, module, check_name)
466 update_checks_list(clang_tidy_path)
467 print('Done. Now it\'s your turn!')
468
469
470 if __name__ == '__main__':
471 main()