clang-tools  14.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') 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') 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') 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 # Modifies the module to include the new check.
162 def adapt_module(module_path, module, check_name, check_name_camel):
163  modulecpp = list(filter(
164  lambda p: p.lower() == module.lower() + 'tidymodule.cpp',
165  os.listdir(module_path)))[0]
166  filename = os.path.join(module_path, modulecpp)
167  with io.open(filename, 'r', encoding='utf8') as f:
168  lines = f.readlines()
169 
170  print('Updating %s...' % filename)
171  with io.open(filename, 'w', encoding='utf8') as f:
172  header_added = False
173  header_found = False
174  check_added = False
175  check_fq_name = module + '-' + check_name
176  check_decl = (' CheckFactories.registerCheck<' + check_name_camel +
177  '>(\n "' + check_fq_name + '");\n')
178 
179  lines = iter(lines)
180  try:
181  while True:
182  line = next(lines)
183  if not header_added:
184  match = re.search('#include "(.*)"', line)
185  if match:
186  header_found = True
187  if match.group(1) > check_name_camel:
188  header_added = True
189  f.write('#include "' + check_name_camel + '.h"\n')
190  elif header_found:
191  header_added = True
192  f.write('#include "' + check_name_camel + '.h"\n')
193 
194  if not check_added:
195  if line.strip() == '}':
196  check_added = True
197  f.write(check_decl)
198  else:
199  match = re.search('registerCheck<(.*)> *\( *(?:"([^"]*)")?', line)
200  prev_line = None
201  if match:
202  current_check_name = match.group(2)
203  if current_check_name is None:
204  # If we didn't find the check name on this line, look on the
205  # next one.
206  prev_line = line
207  line = next(lines)
208  match = re.search(' *"([^"]*)"', line)
209  if match:
210  current_check_name = match.group(1)
211  if current_check_name > check_fq_name:
212  check_added = True
213  f.write(check_decl)
214  if prev_line:
215  f.write(prev_line)
216  f.write(line)
217  except StopIteration:
218  pass
219 
220 
221 # Adds a release notes entry.
222 def add_release_notes(module_path, module, check_name):
223  check_name_dashes = module + '-' + check_name
224  filename = os.path.normpath(os.path.join(module_path,
225  '../../docs/ReleaseNotes.rst'))
226  with io.open(filename, 'r', encoding='utf8') as f:
227  lines = f.readlines()
228 
229  lineMatcher = re.compile('New checks')
230  nextSectionMatcher = re.compile('New check aliases')
231  checkMatcher = re.compile('- New :doc:`(.*)')
232 
233  print('Updating %s...' % filename)
234  with io.open(filename, 'w', encoding='utf8') as f:
235  note_added = False
236  header_found = False
237  add_note_here = False
238 
239  for line in lines:
240  if not note_added:
241  match = lineMatcher.match(line)
242  match_next = nextSectionMatcher.match(line)
243  match_check = checkMatcher.match(line)
244  if match_check:
245  last_check = match_check.group(1)
246  if last_check > check_name_dashes:
247  add_note_here = True
248 
249  if match_next:
250  add_note_here = True
251 
252  if match:
253  header_found = True
254  f.write(line)
255  continue
256 
257  if line.startswith('^^^^'):
258  f.write(line)
259  continue
260 
261  if header_found and add_note_here:
262  if not line.startswith('^^^^'):
263  f.write("""- New :doc:`%s
264  <clang-tidy/checks/%s>` check.
265 
266  FIXME: add release notes.
267 
268 """ % (check_name_dashes, check_name_dashes))
269  note_added = True
270 
271  f.write(line)
272 
273 
274 # Adds a test for the check.
275 def write_test(module_path, module, check_name, test_extension):
276  check_name_dashes = module + '-' + check_name
277  filename = os.path.normpath(os.path.join(module_path, '../../test/clang-tidy/checkers',
278  check_name_dashes + '.' + test_extension))
279  print('Creating %s...' % filename)
280  with io.open(filename, 'w', encoding='utf8') as f:
281  f.write("""// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
282 
283 // FIXME: Add something that triggers the check here.
284 void f();
285 // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
286 
287 // FIXME: Verify the applied fix.
288 // * Make the CHECK patterns specific enough and try to make verified lines
289 // unique to avoid incorrect matches.
290 // * Use {{}} for regular expressions.
291 // CHECK-FIXES: {{^}}void awesome_f();{{$}}
292 
293 // FIXME: Add something that doesn't trigger the check here.
294 void awesome_f2();
295 """ % {'check_name_dashes': check_name_dashes})
296 
297 
298 def get_actual_filename(dirname, filename):
299  if not os.path.isdir(dirname):
300  return ""
301  name = os.path.join(dirname, filename)
302  if (os.path.isfile(name)):
303  return name
304  caselessname = filename.lower()
305  for file in os.listdir(dirname):
306  if (file.lower() == caselessname):
307  return os.path.join(dirname, file)
308  return ""
309 
310 
311 # Recreates the list of checks in the docs/clang-tidy/checks directory.
312 def update_checks_list(clang_tidy_path):
313  docs_dir = os.path.join(clang_tidy_path, '../docs/clang-tidy/checks')
314  filename = os.path.normpath(os.path.join(docs_dir, 'list.rst'))
315  # Read the content of the current list.rst file
316  with io.open(filename, 'r', encoding='utf8') as f:
317  lines = f.readlines()
318  # Get all existing docs
319  doc_files = list(filter(lambda s: s.endswith('.rst') and s != 'list.rst',
320  os.listdir(docs_dir)))
321  doc_files.sort()
322 
323  def has_auto_fix(check_name):
324  dirname, _, check_name = check_name.partition("-")
325 
326  checker_code = get_actual_filename(os.path.join(clang_tidy_path, dirname),
327  get_camel_name(check_name) + '.cpp')
328 
329  if not os.path.isfile(checker_code):
330  return ""
331 
332  with io.open(checker_code, encoding='utf8') as f:
333  code = f.read()
334  if 'FixItHint' in code or "ReplacementText" in code or "fixit" in code:
335  # Some simple heuristics to figure out if a checker has an autofix or not.
336  return ' "Yes"'
337  return ""
338 
339  def process_doc(doc_file):
340  check_name = doc_file.replace('.rst', '')
341 
342  with io.open(os.path.join(docs_dir, doc_file), 'r', encoding='utf8') as doc:
343  content = doc.read()
344  match = re.search('.*:orphan:.*', content)
345 
346  if match:
347  # Orphan page, don't list it.
348  return '', ''
349 
350  match = re.search('.*:http-equiv=refresh: \d+;URL=(.*).html.*',
351  content)
352  # Is it a redirect?
353  return check_name, match
354 
355  def format_link(doc_file):
356  check_name, match = process_doc(doc_file)
357  if not match and check_name:
358  return ' `%(check)s <%(check)s.html>`_,%(autofix)s\n' % {
359  'check': check_name,
360  'autofix': has_auto_fix(check_name)
361  }
362  else:
363  return ''
364 
365  def format_link_alias(doc_file):
366  check_name, match = process_doc(doc_file)
367  if match and check_name:
368  if match.group(1) == 'https://clang.llvm.org/docs/analyzer/checkers':
369  title_redirect = 'Clang Static Analyzer'
370  else:
371  title_redirect = match.group(1)
372  # The checker is just a redirect.
373  return ' `%(check)s <%(check)s.html>`_, `%(title)s <%(target)s.html>`_,%(autofix)s\n' % {
374  'check': check_name,
375  'target': match.group(1),
376  'title': title_redirect,
377  'autofix': has_auto_fix(match.group(1))
378  }
379  return ''
380 
381  checks = map(format_link, doc_files)
382  checks_alias = map(format_link_alias, doc_files)
383 
384  print('Updating %s...' % filename)
385  with io.open(filename, 'w', encoding='utf8') as f:
386  for line in lines:
387  f.write(line)
388  if line.strip() == ".. csv-table::":
389  # We dump the checkers
390  f.write(' :header: "Name", "Offers fixes"\n\n')
391  f.writelines(checks)
392  # and the aliases
393  f.write('\n\n')
394  f.write('.. csv-table:: Aliases..\n')
395  f.write(' :header: "Name", "Redirect", "Offers fixes"\n\n')
396  f.writelines(checks_alias)
397  break
398 
399 
400 # Adds a documentation for the check.
401 def write_docs(module_path, module, check_name):
402  check_name_dashes = module + '-' + check_name
403  filename = os.path.normpath(os.path.join(
404  module_path, '../../docs/clang-tidy/checks/', check_name_dashes + '.rst'))
405  print('Creating %s...' % filename)
406  with io.open(filename, 'w', encoding='utf8') as f:
407  f.write(""".. title:: clang-tidy - %(check_name_dashes)s
408 
409 %(check_name_dashes)s
410 %(underline)s
411 
412 FIXME: Describe what patterns does the check detect and why. Give examples.
413 """ % {'check_name_dashes': check_name_dashes,
414  'underline': '=' * len(check_name_dashes)})
415 
416 
417 def get_camel_name(check_name):
418  return ''.join(map(lambda elem: elem.capitalize(),
419  check_name.split('-'))) + 'Check'
420 
421 
422 def main():
423  language_to_extension = {
424  'c': 'c',
425  'c++': 'cpp',
426  'objc': 'm',
427  'objc++': 'mm',
428  }
429  parser = argparse.ArgumentParser()
430  parser.add_argument(
431  '--update-docs',
432  action='store_true',
433  help='just update the list of documentation files, then exit')
434  parser.add_argument(
435  '--language',
436  help='language to use for new check (defaults to c++)',
437  choices=language_to_extension.keys(),
438  default='c++',
439  metavar='LANG')
440  parser.add_argument(
441  'module',
442  nargs='?',
443  help='module directory under which to place the new tidy check (e.g., misc)')
444  parser.add_argument(
445  'check',
446  nargs='?',
447  help='name of new tidy check to add (e.g. foo-do-the-stuff)')
448  args = parser.parse_args()
449 
450  if args.update_docs:
451  update_checks_list(os.path.dirname(sys.argv[0]))
452  return
453 
454  if not args.module or not args.check:
455  print('Module and check must be specified.')
456  parser.print_usage()
457  return
458 
459  module = args.module
460  check_name = args.check
461  check_name_camel = get_camel_name(check_name)
462  if check_name.startswith(module):
463  print('Check name "%s" must not start with the module "%s". Exiting.' % (
464  check_name, module))
465  return
466  clang_tidy_path = os.path.dirname(sys.argv[0])
467  module_path = os.path.join(clang_tidy_path, module)
468 
469  if not adapt_cmake(module_path, check_name_camel):
470  return
471 
472  # Map module names to namespace names that don't conflict with widely used top-level namespaces.
473  if module == 'llvm':
474  namespace = module + '_check'
475  else:
476  namespace = module
477 
478  write_header(module_path, module, namespace, check_name, check_name_camel)
479  write_implementation(module_path, module, namespace, check_name_camel)
480  adapt_module(module_path, module, check_name, check_name_camel)
481  add_release_notes(module_path, module, check_name)
482  test_extension = language_to_extension.get(args.language)
483  write_test(module_path, module, check_name, test_extension)
484  write_docs(module_path, module, check_name)
485  update_checks_list(clang_tidy_path)
486  print('Done. Now it\'s your turn!')
487 
488 
489 if __name__ == '__main__':
490  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:401
add_new_check.get_camel_name
def get_camel_name(check_name)
Definition: add_new_check.py:417
add_new_check.add_release_notes
def add_release_notes(module_path, module, check_name)
Definition: add_new_check.py:222
add_new_check.get_actual_filename
def get_actual_filename(dirname, filename)
Definition: add_new_check.py:298
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:312
add_new_check.adapt_module
def adapt_module(module_path, module, check_name, check_name_camel)
Definition: add_new_check.py:162
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.write_test
def write_test(module_path, module, check_name, test_extension)
Definition: add_new_check.py:275
add_new_check.main
def main()
Definition: add_new_check.py:422