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