diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/clang-tools-extra/clang-tidy/add_new_check.py	Thu Feb 13 15:10:13 2020 +0900
@@ -0,0 +1,471 @@
+#!/usr/bin/env python
+#
+#===- add_new_check.py - clang-tidy check generator ----------*- python -*--===#
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+#===------------------------------------------------------------------------===#
+
+from __future__ import print_function
+
+import argparse
+import os
+import re
+import sys
+
+# Adapts the module's CMakelist file. Returns 'True' if it could add a new entry
+# and 'False' if the entry already existed.
+def adapt_cmake(module_path, check_name_camel):
+  filename = os.path.join(module_path, 'CMakeLists.txt')
+  with open(filename, 'r') as f:
+    lines = f.readlines()
+
+  cpp_file = check_name_camel + '.cpp'
+
+  # Figure out whether this check already exists.
+  for line in lines:
+    if line.strip() == cpp_file:
+      return False
+
+  print('Updating %s...' % filename)
+  with open(filename, 'w') as f:
+    cpp_found = False
+    file_added = False
+    for line in lines:
+      cpp_line = line.strip().endswith('.cpp')
+      if (not file_added) and (cpp_line or cpp_found):
+        cpp_found = True
+        if (line.strip() > cpp_file) or (not cpp_line):
+          f.write('  ' + cpp_file + '\n')
+          file_added = True
+      f.write(line)
+
+  return True
+
+
+# Adds a header for the new check.
+def write_header(module_path, module, namespace, check_name, check_name_camel):
+  check_name_dashes = module + '-' + check_name
+  filename = os.path.join(module_path, check_name_camel) + '.h'
+  print('Creating %s...' % filename)
+  with open(filename, 'w') as f:
+    header_guard = ('LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_' + module.upper() + '_'
+                    + check_name_camel.upper() + '_H')
+    f.write('//===--- ')
+    f.write(os.path.basename(filename))
+    f.write(' - clang-tidy ')
+    f.write('-' * max(0, 42 - len(os.path.basename(filename))))
+    f.write('*- C++ -*-===//')
+    f.write("""
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef %(header_guard)s
+#define %(header_guard)s
+
+#include "../ClangTidyCheck.h"
+
+namespace clang {
+namespace tidy {
+namespace %(namespace)s {
+
+/// FIXME: Write a short description.
+///
+/// For the user-facing documentation see:
+/// http://clang.llvm.org/extra/clang-tidy/checks/%(check_name_dashes)s.html
+class %(check_name)s : public ClangTidyCheck {
+public:
+  %(check_name)s(StringRef Name, ClangTidyContext *Context)
+      : ClangTidyCheck(Name, Context) {}
+  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
+  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
+};
+
+} // namespace %(namespace)s
+} // namespace tidy
+} // namespace clang
+
+#endif // %(header_guard)s
+""" % {'header_guard': header_guard,
+       'check_name': check_name_camel,
+       'check_name_dashes': check_name_dashes,
+       'module': module,
+       'namespace': namespace})
+
+
+# Adds the implementation of the new check.
+def write_implementation(module_path, module, namespace, check_name_camel):
+  filename = os.path.join(module_path, check_name_camel) + '.cpp'
+  print('Creating %s...' % filename)
+  with open(filename, 'w') as f:
+    f.write('//===--- ')
+    f.write(os.path.basename(filename))
+    f.write(' - clang-tidy ')
+    f.write('-' * max(0, 51 - len(os.path.basename(filename))))
+    f.write('-===//')
+    f.write("""
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "%(check_name)s.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+using namespace clang::ast_matchers;
+
+namespace clang {
+namespace tidy {
+namespace %(namespace)s {
+
+void %(check_name)s::registerMatchers(MatchFinder *Finder) {
+  // FIXME: Add matchers.
+  Finder->addMatcher(functionDecl().bind("x"), this);
+}
+
+void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
+  // FIXME: Add callback implementation.
+  const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
+  if (MatchedDecl->getName().startswith("awesome_"))
+    return;
+  diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
+      << MatchedDecl;
+  diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note)
+      << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
+}
+
+} // namespace %(namespace)s
+} // namespace tidy
+} // namespace clang
+""" % {'check_name': check_name_camel,
+       'module': module,
+       'namespace': namespace})
+
+
+# Modifies the module to include the new check.
+def adapt_module(module_path, module, check_name, check_name_camel):
+  modulecpp = list(filter(
+      lambda p: p.lower() == module.lower() + 'tidymodule.cpp',
+      os.listdir(module_path)))[0]
+  filename = os.path.join(module_path, modulecpp)
+  with open(filename, 'r') as f:
+    lines = f.readlines()
+
+  print('Updating %s...' % filename)
+  with open(filename, 'w') as f:
+    header_added = False
+    header_found = False
+    check_added = False
+    check_fq_name = module + '-' + check_name
+    check_decl = ('    CheckFactories.registerCheck<' + check_name_camel +
+                  '>(\n        "' + check_fq_name + '");\n')
+
+    lines = iter(lines)
+    try:
+      while True:
+        line = lines.next()
+        if not header_added:
+          match = re.search('#include "(.*)"', line)
+          if match:
+            header_found = True
+            if match.group(1) > check_name_camel:
+              header_added = True
+              f.write('#include "' + check_name_camel + '.h"\n')
+          elif header_found:
+            header_added = True
+            f.write('#include "' + check_name_camel + '.h"\n')
+
+        if not check_added:
+          if line.strip() == '}':
+            check_added = True
+            f.write(check_decl)
+          else:
+            match = re.search('registerCheck<(.*)> *\( *(?:"([^"]*)")?', line)
+            prev_line = None
+            if match:
+              current_check_name = match.group(2)
+              if current_check_name is None:
+                # If we didn't find the check name on this line, look on the
+                # next one.
+                prev_line = line
+                line = lines.next()
+                match = re.search(' *"([^"]*)"', line)
+                if match:
+                  current_check_name = match.group(1)
+              if current_check_name > check_fq_name:
+                check_added = True
+                f.write(check_decl)
+              if prev_line:
+                f.write(prev_line)
+        f.write(line)
+    except StopIteration:
+      pass
+
+
+# Adds a release notes entry.
+def add_release_notes(module_path, module, check_name):
+  check_name_dashes = module + '-' + check_name
+  filename = os.path.normpath(os.path.join(module_path,
+                                           '../../docs/ReleaseNotes.rst'))
+  with open(filename, 'r') as f:
+    lines = f.readlines()
+
+  lineMatcher = re.compile('New checks')
+  nextSectionMatcher = re.compile('New check aliases')
+  checkerMatcher = re.compile('- New :doc:`(.*)')
+
+  print('Updating %s...' % filename)
+  with open(filename, 'w') as f:
+    note_added = False
+    header_found = False
+    next_header_found = False
+    add_note_here = False
+
+    for line in lines:
+      if not note_added:
+        match = lineMatcher.match(line)
+        match_next = nextSectionMatcher.match(line)
+        match_checker = checkerMatcher.match(line)
+        if match_checker:
+          last_checker = match_checker.group(1)
+          if last_checker > check_name_dashes:
+            add_note_here = True
+
+        if match_next:
+          next_header_found = True
+          add_note_here = True
+
+        if match:
+          header_found = True
+          f.write(line)
+          continue
+
+        if line.startswith('^^^^'):
+          f.write(line)
+          continue
+
+        if header_found and add_note_here:
+          if not line.startswith('^^^^'):
+            f.write("""- New :doc:`%s
+  <clang-tidy/checks/%s>` check.
+
+  FIXME: add release notes.
+
+""" % (check_name_dashes, check_name_dashes))
+            note_added = True
+
+      f.write(line)
+
+
+# Adds a test for the check.
+def write_test(module_path, module, check_name, test_extension):
+  check_name_dashes = module + '-' + check_name
+  filename = os.path.normpath(os.path.join(module_path, '../../test/clang-tidy/checkers',
+                                           check_name_dashes + '.' + test_extension))
+  print('Creating %s...' % filename)
+  with open(filename, 'w') as f:
+    f.write("""// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
+
+// FIXME: Add something that triggers the check here.
+void f();
+// CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
+
+// FIXME: Verify the applied fix.
+//   * Make the CHECK patterns specific enough and try to make verified lines
+//     unique to avoid incorrect matches.
+//   * Use {{}} for regular expressions.
+// CHECK-FIXES: {{^}}void awesome_f();{{$}}
+
+// FIXME: Add something that doesn't trigger the check here.
+void awesome_f2();
+""" % {'check_name_dashes': check_name_dashes})
+
+
+# Recreates the list of checks in the docs/clang-tidy/checks directory.
+def update_checks_list(clang_tidy_path):
+  docs_dir = os.path.join(clang_tidy_path, '../docs/clang-tidy/checks')
+  filename = os.path.normpath(os.path.join(docs_dir, 'list.rst'))
+  # Read the content of the current list.rst file
+  with open(filename, 'r') as f:
+    lines = f.readlines()
+  # Get all existing docs
+  doc_files = list(filter(lambda s: s.endswith('.rst') and s != 'list.rst',
+                     os.listdir(docs_dir)))
+  doc_files.sort()
+
+  def has_auto_fix(check_name):
+    dirname, _, check_name = check_name.partition("-")
+
+    checkerCode = os.path.join(dirname, get_camel_name(check_name)) + ".cpp"
+
+    if not os.path.isfile(checkerCode):
+      return ""
+
+    with open(checkerCode) as f:
+      code = f.read()
+      if 'FixItHint' in code or "ReplacementText" in code or "fixit" in code:
+        # Some simple heuristics to figure out if a checker has an autofix or not.
+        return ' "Yes"'
+    return ""
+
+  def process_doc(doc_file):
+    check_name = doc_file.replace('.rst', '')
+
+    with open(os.path.join(docs_dir, doc_file), 'r') as doc:
+      content = doc.read()
+      match = re.search('.*:orphan:.*', content)
+
+      if match:
+        # Orphan page, don't list it.
+        return '', ''
+
+      match = re.search('.*:http-equiv=refresh: \d+;URL=(.*).html.*',
+                        content)
+      # Is it a redirect?
+      return check_name, match
+
+  def format_link(doc_file):
+    check_name, match = process_doc(doc_file)
+    if not match and check_name:
+      return '   `%(check)s <%(check)s.html>`_,%(autofix)s\n' % {
+        'check': check_name,
+        'autofix': has_auto_fix(check_name)
+      }
+    else:
+      return ''
+
+  def format_link_alias(doc_file):
+    check_name, match = process_doc(doc_file)
+    if match and check_name:
+      if match.group(1) == 'https://clang.llvm.org/docs/analyzer/checkers':
+        title_redirect = 'Clang Static Analyzer'
+      else:
+        title_redirect = match.group(1)
+      # The checker is just a redirect.
+      return '   `%(check)s <%(check)s.html>`_, `%(title)s <%(target)s.html>`_,%(autofix)s\n' % {
+        'check': check_name,
+        'target': match.group(1),
+        'title': title_redirect,
+        'autofix': has_auto_fix(match.group(1))
+      }
+    return ''
+
+  checks = map(format_link, doc_files)
+  checks_alias = map(format_link_alias, doc_files)
+
+  print('Updating %s...' % filename)
+  with open(filename, 'w') as f:
+    for line in lines:
+      f.write(line)
+      if line.strip() == ".. csv-table::":
+        # We dump the checkers
+        f.write('   :header: "Name", "Offers fixes"\n\n')
+        f.writelines(checks)
+        # and the aliases
+        f.write('\n\n')
+        f.write('.. csv-table:: Aliases..\n')
+        f.write('   :header: "Name", "Redirect", "Offers fixes"\n\n')
+        f.writelines(checks_alias)
+        break
+
+
+# Adds a documentation for the check.
+def write_docs(module_path, module, check_name):
+  check_name_dashes = module + '-' + check_name
+  filename = os.path.normpath(os.path.join(
+      module_path, '../../docs/clang-tidy/checks/', check_name_dashes + '.rst'))
+  print('Creating %s...' % filename)
+  with open(filename, 'w') as f:
+    f.write(""".. title:: clang-tidy - %(check_name_dashes)s
+
+%(check_name_dashes)s
+%(underline)s
+
+FIXME: Describe what patterns does the check detect and why. Give examples.
+""" % {'check_name_dashes': check_name_dashes,
+       'underline': '=' * len(check_name_dashes)})
+
+
+def get_camel_name(check_name):
+  return ''.join(map(lambda elem: elem.capitalize(),
+                     check_name.split('-'))) + 'Check'
+
+
+def main():
+  language_to_extension = {
+      'c': 'c',
+      'c++': 'cpp',
+      'objc': 'm',
+      'objc++': 'mm',
+  }
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--update-docs',
+      action='store_true',
+      help='just update the list of documentation files, then exit')
+  parser.add_argument(
+      '--language',
+      help='language to use for new check (defaults to c++)',
+      choices=language_to_extension.keys(),
+      default='c++',
+      metavar='LANG')
+  parser.add_argument(
+      'module',
+      nargs='?',
+      help='module directory under which to place the new tidy check (e.g., misc)')
+  parser.add_argument(
+      'check',
+      nargs='?',
+      help='name of new tidy check to add (e.g. foo-do-the-stuff)')
+  args = parser.parse_args()
+
+  if args.update_docs:
+    update_checks_list(os.path.dirname(sys.argv[0]))
+    return
+
+  if not args.module or not args.check:
+    print('Module and check must be specified.')
+    parser.print_usage()
+    return
+
+  module = args.module
+  check_name = args.check
+  check_name_camel = get_camel_name(check_name)
+  if check_name.startswith(module):
+    print('Check name "%s" must not start with the module "%s". Exiting.' % (
+        check_name, module))
+    return
+  clang_tidy_path = os.path.dirname(sys.argv[0])
+  module_path = os.path.join(clang_tidy_path, module)
+
+  if not adapt_cmake(module_path, check_name_camel):
+    return
+
+  # Map module names to namespace names that don't conflict with widely used top-level namespaces.
+  if module == 'llvm':
+    namespace = module + '_check'
+  else:
+    namespace = module
+
+  write_header(module_path, module, namespace, check_name, check_name_camel)
+  write_implementation(module_path, module, namespace, check_name_camel)
+  adapt_module(module_path, module, check_name, check_name_camel)
+  add_release_notes(module_path, module, check_name)
+  test_extension = language_to_extension.get(args.language)
+  write_test(module_path, module, check_name, test_extension)
+  write_docs(module_path, module, check_name)
+  update_checks_list(clang_tidy_path)
+  print('Done. Now it\'s your turn!')
+
+
+if __name__ == '__main__':
+  main()