clang-tools  11.0.0
add_new_check.py
Go to the documentation of this file.
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 
19 # Adapts the module's CMakelist file. Returns 'True' if it could add a new
20 # entry and 'False' if the entry already existed.
21 def 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.
50 def 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 
75 namespace clang {
76 namespace tidy {
77 namespace %(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
83 class %(check_name)s : public ClangTidyCheck {
84 public:
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.
104 def 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 
125 using namespace clang::ast_matchers;
126 
127 namespace clang {
128 namespace tidy {
129 namespace %(namespace)s {
130 
131 void %(check_name)s::registerMatchers(MatchFinder *Finder) {
132  // FIXME: Add matchers.
133  Finder->addMatcher(functionDecl().bind("x"), this);
134 }
135 
136 void %(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->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.
156 def 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.
216 def 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.
269 def 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.
278 void 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.
288 void awesome_f2();
289 """ % {'check_name_dashes': check_name_dashes})
290 
291 
292 def 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.
306 def 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.
395 def 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 
406 FIXME: 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 
411 def get_camel_name(check_name):
412  return ''.join(map(lambda elem: elem.capitalize(),
413  check_name.split('-'))) + 'Check'
414 
415 
416 def 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 
483 if __name__ == '__main__':
484  main()
add_new_check.write_implementation
def write_implementation(module_path, module, namespace, check_name_camel)
Definition: add_new_check.py:104
add_new_check.write_docs
def write_docs(module_path, module, check_name)
Definition: add_new_check.py:395
add_new_check.get_camel_name
def get_camel_name(check_name)
Definition: add_new_check.py:411
add_new_check.add_release_notes
def add_release_notes(module_path, module, check_name)
Definition: add_new_check.py:216
add_new_check.get_actual_filename
def get_actual_filename(dirname, filename)
Definition: add_new_check.py:292
clang::tidy::cppcoreguidelines::join
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
Definition: SpecialMemberFunctionsCheck.cpp:83
add_new_check.adapt_cmake
def adapt_cmake(module_path, check_name_camel)
Definition: add_new_check.py:21
add_new_check.update_checks_list
def update_checks_list(clang_tidy_path)
Definition: add_new_check.py:306
add_new_check.adapt_module
def adapt_module(module_path, module, check_name, check_name_camel)
Definition: add_new_check.py:156
add_new_check.write_header
def write_header(module_path, module, namespace, check_name, check_name_camel)
Definition: add_new_check.py:50
add_new_check.write_test
def write_test(module_path, module, check_name, test_extension)
Definition: add_new_check.py:269
add_new_check.main
def main()
Definition: add_new_check.py:416