Mercurial > hg > CbC > CbC_llvm
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() |