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 11from __future__ import print_function 12from __future__ import unicode_literals 13 14import argparse 15import io 16import os 17import re 18import sys 19 20# Adapts the module's CMakelist file. Returns 'True' if it could add a new 21# entry and 'False' if the entry already existed. 22def adapt_cmake(module_path, check_name_camel): 23 filename = os.path.join(module_path, 'CMakeLists.txt') 24 25 # The documentation files are encoded using UTF-8, however on Windows the 26 # default encoding might be different (e.g. CP-1252). To make sure UTF-8 is 27 # always used, use `io.open(filename, mode, encoding='utf8')` for reading and 28 # writing files here and elsewhere. 29 with io.open(filename, 'r', encoding='utf8') as f: 30 lines = f.readlines() 31 32 cpp_file = check_name_camel + '.cpp' 33 34 # Figure out whether this check already exists. 35 for line in lines: 36 if line.strip() == cpp_file: 37 return False 38 39 print('Updating %s...' % filename) 40 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 41 cpp_found = False 42 file_added = False 43 for line in lines: 44 cpp_line = line.strip().endswith('.cpp') 45 if (not file_added) and (cpp_line or cpp_found): 46 cpp_found = True 47 if (line.strip() > cpp_file) or (not cpp_line): 48 f.write(' ' + cpp_file + '\n') 49 file_added = True 50 f.write(line) 51 52 return True 53 54 55# Adds a header for the new check. 56def write_header(module_path, module, namespace, check_name, check_name_camel): 57 filename = os.path.join(module_path, check_name_camel) + '.h' 58 print('Creating %s...' % filename) 59 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 60 header_guard = ('LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_' + module.upper() + '_' 61 + check_name_camel.upper() + '_H') 62 f.write('//===--- ') 63 f.write(os.path.basename(filename)) 64 f.write(' - clang-tidy ') 65 f.write('-' * max(0, 42 - len(os.path.basename(filename)))) 66 f.write('*- C++ -*-===//') 67 f.write(""" 68// 69// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 70// See https://llvm.org/LICENSE.txt for license information. 71// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 72// 73//===----------------------------------------------------------------------===// 74 75#ifndef %(header_guard)s 76#define %(header_guard)s 77 78#include "../ClangTidyCheck.h" 79 80namespace clang { 81namespace tidy { 82namespace %(namespace)s { 83 84/// FIXME: Write a short description. 85/// 86/// For the user-facing documentation see: 87/// http://clang.llvm.org/extra/clang-tidy/checks/%(module)s/%(check_name)s.html 88class %(check_name_camel)s : public ClangTidyCheck { 89public: 90 %(check_name_camel)s(StringRef Name, ClangTidyContext *Context) 91 : ClangTidyCheck(Name, Context) {} 92 void registerMatchers(ast_matchers::MatchFinder *Finder) override; 93 void check(const ast_matchers::MatchFinder::MatchResult &Result) override; 94}; 95 96} // namespace %(namespace)s 97} // namespace tidy 98} // namespace clang 99 100#endif // %(header_guard)s 101""" % {'header_guard': header_guard, 102 'check_name_camel': check_name_camel, 103 'check_name': check_name, 104 'module': module, 105 'namespace': namespace}) 106 107 108# Adds the implementation of the new check. 109def write_implementation(module_path, module, namespace, check_name_camel): 110 filename = os.path.join(module_path, check_name_camel) + '.cpp' 111 print('Creating %s...' % filename) 112 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 113 f.write('//===--- ') 114 f.write(os.path.basename(filename)) 115 f.write(' - clang-tidy ') 116 f.write('-' * max(0, 51 - len(os.path.basename(filename)))) 117 f.write('-===//') 118 f.write(""" 119// 120// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 121// See https://llvm.org/LICENSE.txt for license information. 122// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 123// 124//===----------------------------------------------------------------------===// 125 126#include "%(check_name)s.h" 127#include "clang/AST/ASTContext.h" 128#include "clang/ASTMatchers/ASTMatchFinder.h" 129 130using namespace clang::ast_matchers; 131 132namespace clang { 133namespace tidy { 134namespace %(namespace)s { 135 136void %(check_name)s::registerMatchers(MatchFinder *Finder) { 137 // FIXME: Add matchers. 138 Finder->addMatcher(functionDecl().bind("x"), this); 139} 140 141void %(check_name)s::check(const MatchFinder::MatchResult &Result) { 142 // FIXME: Add callback implementation. 143 const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x"); 144 if (!MatchedDecl->getIdentifier() || MatchedDecl->getName().startswith("awesome_")) 145 return; 146 diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome") 147 << MatchedDecl; 148 diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note) 149 << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_"); 150} 151 152} // namespace %(namespace)s 153} // namespace tidy 154} // namespace clang 155""" % {'check_name': check_name_camel, 156 'module': module, 157 'namespace': namespace}) 158 159 160# Returns the source filename that implements the module. 161def get_module_filename(module_path, module): 162 modulecpp = list(filter( 163 lambda p: p.lower() == module.lower() + 'tidymodule.cpp', 164 os.listdir(module_path)))[0] 165 return os.path.join(module_path, modulecpp) 166 167 168# Modifies the module to include the new check. 169def adapt_module(module_path, module, check_name, check_name_camel): 170 filename = get_module_filename(module_path, module) 171 with io.open(filename, 'r', encoding='utf8') as f: 172 lines = f.readlines() 173 174 print('Updating %s...' % filename) 175 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 176 header_added = False 177 header_found = False 178 check_added = False 179 check_fq_name = module + '-' + check_name 180 check_decl = (' CheckFactories.registerCheck<' + check_name_camel + 181 '>(\n "' + check_fq_name + '");\n') 182 183 lines = iter(lines) 184 try: 185 while True: 186 line = next(lines) 187 if not header_added: 188 match = re.search('#include "(.*)"', line) 189 if match: 190 header_found = True 191 if match.group(1) > check_name_camel: 192 header_added = True 193 f.write('#include "' + check_name_camel + '.h"\n') 194 elif header_found: 195 header_added = True 196 f.write('#include "' + check_name_camel + '.h"\n') 197 198 if not check_added: 199 if line.strip() == '}': 200 check_added = True 201 f.write(check_decl) 202 else: 203 match = re.search('registerCheck<(.*)> *\( *(?:"([^"]*)")?', line) 204 prev_line = None 205 if match: 206 current_check_name = match.group(2) 207 if current_check_name is None: 208 # If we didn't find the check name on this line, look on the 209 # next one. 210 prev_line = line 211 line = next(lines) 212 match = re.search(' *"([^"]*)"', line) 213 if match: 214 current_check_name = match.group(1) 215 if current_check_name > check_fq_name: 216 check_added = True 217 f.write(check_decl) 218 if prev_line: 219 f.write(prev_line) 220 f.write(line) 221 except StopIteration: 222 pass 223 224 225# Adds a release notes entry. 226def add_release_notes(module_path, module, check_name): 227 check_name_dashes = module + '-' + check_name 228 filename = os.path.normpath(os.path.join(module_path, 229 '../../docs/ReleaseNotes.rst')) 230 with io.open(filename, 'r', encoding='utf8') as f: 231 lines = f.readlines() 232 233 lineMatcher = re.compile('New checks') 234 nextSectionMatcher = re.compile('New check aliases') 235 checkMatcher = re.compile('- New :doc:`(.*)') 236 237 print('Updating %s...' % filename) 238 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 239 note_added = False 240 header_found = False 241 add_note_here = False 242 243 for line in lines: 244 if not note_added: 245 match = lineMatcher.match(line) 246 match_next = nextSectionMatcher.match(line) 247 match_check = checkMatcher.match(line) 248 if match_check: 249 last_check = match_check.group(1) 250 if last_check > check_name_dashes: 251 add_note_here = True 252 253 if match_next: 254 add_note_here = True 255 256 if match: 257 header_found = True 258 f.write(line) 259 continue 260 261 if line.startswith('^^^^'): 262 f.write(line) 263 continue 264 265 if header_found and add_note_here: 266 if not line.startswith('^^^^'): 267 f.write("""- New :doc:`%s 268 <clang-tidy/checks/%s/%s>` check. 269 270 FIXME: add release notes. 271 272""" % (check_name_dashes, module, check_name)) 273 note_added = True 274 275 f.write(line) 276 277 278# Adds a test for the check. 279def write_test(module_path, module, check_name, test_extension): 280 check_name_dashes = module + '-' + check_name 281 filename = os.path.normpath(os.path.join( 282 module_path, '..', '..', 'test', 'clang-tidy', 'checkers', 283 module, check_name + '.' + test_extension)) 284 print('Creating %s...' % filename) 285 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 286 f.write("""// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t 287 288// FIXME: Add something that triggers the check here. 289void f(); 290// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s] 291 292// FIXME: Verify the applied fix. 293// * Make the CHECK patterns specific enough and try to make verified lines 294// unique to avoid incorrect matches. 295// * Use {{}} for regular expressions. 296// CHECK-FIXES: {{^}}void awesome_f();{{$}} 297 298// FIXME: Add something that doesn't trigger the check here. 299void awesome_f2(); 300""" % {'check_name_dashes': check_name_dashes}) 301 302 303def get_actual_filename(dirname, filename): 304 if not os.path.isdir(dirname): 305 return '' 306 name = os.path.join(dirname, filename) 307 if (os.path.isfile(name)): 308 return name 309 caselessname = filename.lower() 310 for file in os.listdir(dirname): 311 if (file.lower() == caselessname): 312 return os.path.join(dirname, file) 313 return '' 314 315 316# Recreates the list of checks in the docs/clang-tidy/checks directory. 317def update_checks_list(clang_tidy_path): 318 docs_dir = os.path.join(clang_tidy_path, '../docs/clang-tidy/checks') 319 filename = os.path.normpath(os.path.join(docs_dir, 'list.rst')) 320 # Read the content of the current list.rst file 321 with io.open(filename, 'r', encoding='utf8') as f: 322 lines = f.readlines() 323 # Get all existing docs 324 doc_files = [] 325 for subdir in list(filter(lambda s: not s.endswith('.rst') and not s.endswith('.py'), 326 os.listdir(docs_dir))): 327 for file in filter(lambda s: s.endswith('.rst'), os.listdir(os.path.join(docs_dir, subdir))): 328 doc_files.append([subdir, file]) 329 doc_files.sort() 330 331 # We couldn't find the source file from the check name, so try to find the 332 # class name that corresponds to the check in the module file. 333 def filename_from_module(module_name, check_name): 334 module_path = os.path.join(clang_tidy_path, module_name) 335 if not os.path.isdir(module_path): 336 return '' 337 module_file = get_module_filename(module_path, module_name) 338 if not os.path.isfile(module_file): 339 return '' 340 with io.open(module_file, 'r') as f: 341 code = f.read() 342 full_check_name = module_name + '-' + check_name 343 name_pos = code.find('"' + full_check_name + '"') 344 if name_pos == -1: 345 return '' 346 stmt_end_pos = code.find(';', name_pos) 347 if stmt_end_pos == -1: 348 return '' 349 stmt_start_pos = code.rfind(';', 0, name_pos) 350 if stmt_start_pos == -1: 351 stmt_start_pos = code.rfind('{', 0, name_pos) 352 if stmt_start_pos == -1: 353 return '' 354 stmt = code[stmt_start_pos+1:stmt_end_pos] 355 matches = re.search('registerCheck<([^>:]*)>\(\s*"([^"]*)"\s*\)', stmt) 356 if matches and matches[2] == full_check_name: 357 class_name = matches[1] 358 if '::' in class_name: 359 parts = class_name.split('::') 360 class_name = parts[-1] 361 class_path = os.path.join(clang_tidy_path, module_name, '..', *parts[0:-1]) 362 else: 363 class_path = os.path.join(clang_tidy_path, module_name) 364 return get_actual_filename(class_path, class_name + '.cpp') 365 366 return '' 367 368 # Examine code looking for a c'tor definition to get the base class name. 369 def get_base_class(code, check_file): 370 check_class_name = os.path.splitext(os.path.basename(check_file))[0] 371 ctor_pattern = check_class_name + '\([^:]*\)\s*:\s*([A-Z][A-Za-z0-9]*Check)\(' 372 matches = re.search('\s+' + check_class_name + '::' + ctor_pattern, code) 373 374 # The constructor might be inline in the header. 375 if not matches: 376 header_file = os.path.splitext(check_file)[0] + '.h' 377 if not os.path.isfile(header_file): 378 return '' 379 with io.open(header_file, encoding='utf8') as f: 380 code = f.read() 381 matches = re.search(' ' + ctor_pattern, code) 382 383 if matches and matches[1] != 'ClangTidyCheck': 384 return matches[1] 385 return '' 386 387 # Some simple heuristics to figure out if a check has an autofix or not. 388 def has_fixits(code): 389 for needle in ['FixItHint', 'ReplacementText', 'fixit', 390 'TransformerClangTidyCheck']: 391 if needle in code: 392 return True 393 return False 394 395 # Try to figure out of the check supports fixits. 396 def has_auto_fix(check_name): 397 dirname, _, check_name = check_name.partition('-') 398 399 check_file = get_actual_filename(os.path.join(clang_tidy_path, dirname), 400 get_camel_check_name(check_name) + '.cpp') 401 if not os.path.isfile(check_file): 402 # Some older checks don't end with 'Check.cpp' 403 check_file = get_actual_filename(os.path.join(clang_tidy_path, dirname), 404 get_camel_name(check_name) + '.cpp') 405 if not os.path.isfile(check_file): 406 # Some checks aren't in a file based on the check name. 407 check_file = filename_from_module(dirname, check_name) 408 if not check_file or not os.path.isfile(check_file): 409 return '' 410 411 with io.open(check_file, encoding='utf8') as f: 412 code = f.read() 413 if has_fixits(code): 414 return ' "Yes"' 415 416 base_class = get_base_class(code, check_file) 417 if base_class: 418 base_file = os.path.join(clang_tidy_path, dirname, base_class + '.cpp') 419 if os.path.isfile(base_file): 420 with io.open(base_file, encoding='utf8') as f: 421 code = f.read() 422 if has_fixits(code): 423 return ' "Yes"' 424 425 return '' 426 427 def process_doc(doc_file): 428 check_name = doc_file[0] + '-' + doc_file[1].replace('.rst', '') 429 430 with io.open(os.path.join(docs_dir, *doc_file), 'r', encoding='utf8') as doc: 431 content = doc.read() 432 match = re.search('.*:orphan:.*', content) 433 434 if match: 435 # Orphan page, don't list it. 436 return '', '' 437 438 match = re.search('.*:http-equiv=refresh: \d+;URL=(.*).html(.*)', 439 content) 440 # Is it a redirect? 441 return check_name, match 442 443 def format_link(doc_file): 444 check_name, match = process_doc(doc_file) 445 if not match and check_name: 446 return ' `%(check_name)s <%(module)s/%(check)s.html>`_,%(autofix)s\n' % { 447 'check_name': check_name, 448 'module': doc_file[0], 449 'check': doc_file[1].replace('.rst', ''), 450 'autofix': has_auto_fix(check_name) 451 } 452 else: 453 return '' 454 455 def format_link_alias(doc_file): 456 check_name, match = process_doc(doc_file) 457 if match and check_name: 458 module = doc_file[0] 459 check_file = doc_file[1].replace('.rst', '') 460 if match.group(1) == 'https://clang.llvm.org/docs/analyzer/checkers': 461 title = 'Clang Static Analyzer ' + check_file 462 # Preserve the anchor in checkers.html from group 2. 463 target = match.group(1) + '.html' + match.group(2) 464 autofix = '' 465 else: 466 redirect_parts = re.search('^\.\./([^/]*)/([^/]*)$', match.group(1)) 467 title = redirect_parts[1] + '-' + redirect_parts[2] 468 target = redirect_parts[1] + '/' + redirect_parts[2] + '.html' 469 autofix = has_auto_fix(title) 470 471 # The checker is just a redirect. 472 return ' `%(check_name)s <%(module)s/%(check_file)s.html>`_, `%(title)s <%(target)s>`_,%(autofix)s\n' % { 473 'check_name': check_name, 474 'module': module, 475 'check_file': check_file, 476 'target': target, 477 'title': title, 478 'autofix': autofix 479 } 480 return '' 481 482 checks = map(format_link, doc_files) 483 checks_alias = map(format_link_alias, doc_files) 484 485 print('Updating %s...' % filename) 486 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 487 for line in lines: 488 f.write(line) 489 if line.strip() == '.. csv-table::': 490 # We dump the checkers 491 f.write(' :header: "Name", "Offers fixes"\n\n') 492 f.writelines(checks) 493 # and the aliases 494 f.write('\n\n') 495 f.write('.. csv-table:: Aliases..\n') 496 f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n') 497 f.writelines(checks_alias) 498 break 499 500 501# Adds a documentation for the check. 502def write_docs(module_path, module, check_name): 503 check_name_dashes = module + '-' + check_name 504 filename = os.path.normpath(os.path.join( 505 module_path, '../../docs/clang-tidy/checks/', module, check_name + '.rst')) 506 print('Creating %s...' % filename) 507 with io.open(filename, 'w', encoding='utf8', newline='\n') as f: 508 f.write(""".. title:: clang-tidy - %(check_name_dashes)s 509 510%(check_name_dashes)s 511%(underline)s 512 513FIXME: Describe what patterns does the check detect and why. Give examples. 514""" % {'check_name_dashes': check_name_dashes, 515 'underline': '=' * len(check_name_dashes)}) 516 517 518def get_camel_name(check_name): 519 return ''.join(map(lambda elem: elem.capitalize(), 520 check_name.split('-'))) 521 522 523def get_camel_check_name(check_name): 524 return get_camel_name(check_name) + 'Check' 525 526 527def main(): 528 language_to_extension = { 529 'c': 'c', 530 'c++': 'cpp', 531 'objc': 'm', 532 'objc++': 'mm', 533 } 534 parser = argparse.ArgumentParser() 535 parser.add_argument( 536 '--update-docs', 537 action='store_true', 538 help='just update the list of documentation files, then exit') 539 parser.add_argument( 540 '--language', 541 help='language to use for new check (defaults to c++)', 542 choices=language_to_extension.keys(), 543 default='c++', 544 metavar='LANG') 545 parser.add_argument( 546 'module', 547 nargs='?', 548 help='module directory under which to place the new tidy check (e.g., misc)') 549 parser.add_argument( 550 'check', 551 nargs='?', 552 help='name of new tidy check to add (e.g. foo-do-the-stuff)') 553 args = parser.parse_args() 554 555 if args.update_docs: 556 update_checks_list(os.path.dirname(sys.argv[0])) 557 return 558 559 if not args.module or not args.check: 560 print('Module and check must be specified.') 561 parser.print_usage() 562 return 563 564 module = args.module 565 check_name = args.check 566 check_name_camel = get_camel_check_name(check_name) 567 if check_name.startswith(module): 568 print('Check name "%s" must not start with the module "%s". Exiting.' % ( 569 check_name, module)) 570 return 571 clang_tidy_path = os.path.dirname(sys.argv[0]) 572 module_path = os.path.join(clang_tidy_path, module) 573 574 if not adapt_cmake(module_path, check_name_camel): 575 return 576 577 # Map module names to namespace names that don't conflict with widely used top-level namespaces. 578 if module == 'llvm': 579 namespace = module + '_check' 580 else: 581 namespace = module 582 583 write_header(module_path, module, namespace, check_name, check_name_camel) 584 write_implementation(module_path, module, namespace, check_name_camel) 585 adapt_module(module_path, module, check_name, check_name_camel) 586 add_release_notes(module_path, module, check_name) 587 test_extension = language_to_extension.get(args.language) 588 write_test(module_path, module, check_name, test_extension) 589 write_docs(module_path, module, check_name) 590 update_checks_list(clang_tidy_path) 591 print('Done. Now it\'s your turn!') 592 593 594if __name__ == '__main__': 595 main() 596