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