clang-tools  9.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 # Adapts the module's CMakelist file. Returns 'True' if it could add a new entry
19 # and 'False' if the entry already existed.
20 def adapt_cmake(module_path, check_name_camel):
21  filename = os.path.join(module_path, 'CMakeLists.txt')
22  with open(filename, 'r') as f:
23  lines = f.readlines()
24 
25  cpp_file = check_name_camel + '.cpp'
26 
27  # Figure out whether this check already exists.
28  for line in lines:
29  if line.strip() == cpp_file:
30  return False
31 
32  print('Updating %s...' % filename)
33  with open(filename, 'w') as f:
34  cpp_found = False
35  file_added = False
36  for line in lines:
37  cpp_line = line.strip().endswith('.cpp')
38  if (not file_added) and (cpp_line or cpp_found):
39  cpp_found = True
40  if (line.strip() > cpp_file) or (not cpp_line):
41  f.write(' ' + cpp_file + '\n')
42  file_added = True
43  f.write(line)
44 
45  return True
46 
47 
48 # Adds a header for the new check.
49 def write_header(module_path, module, namespace, check_name, check_name_camel):
50  check_name_dashes = module + '-' + check_name
51  filename = os.path.join(module_path, check_name_camel) + '.h'
52  print('Creating %s...' % filename)
53  with open(filename, 'w') as f:
54  header_guard = ('LLVM_CLANG_TOOLS_EXTRA_CLANG_TIDY_' + module.upper() + '_'
55  + check_name_camel.upper() + '_H')
56  f.write('//===--- ')
57  f.write(os.path.basename(filename))
58  f.write(' - clang-tidy ')
59  f.write('-' * max(0, 42 - len(os.path.basename(filename))))
60  f.write('*- C++ -*-===//')
61  f.write("""
62 //
63 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
64 // See https://llvm.org/LICENSE.txt for license information.
65 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66 //
67 //===----------------------------------------------------------------------===//
68 
69 #ifndef %(header_guard)s
70 #define %(header_guard)s
71 
72 #include "../ClangTidyCheck.h"
73 
74 namespace clang {
75 namespace tidy {
76 namespace %(namespace)s {
77 
78 /// FIXME: Write a short description.
79 ///
80 /// For the user-facing documentation see:
81 /// http://clang.llvm.org/extra/clang-tidy/checks/%(check_name_dashes)s.html
82 class %(check_name)s : public ClangTidyCheck {
83 public:
84  %(check_name)s(StringRef Name, ClangTidyContext *Context)
85  : ClangTidyCheck(Name, Context) {}
86  void registerMatchers(ast_matchers::MatchFinder *Finder) override;
87  void check(const ast_matchers::MatchFinder::MatchResult &Result) override;
88 };
89 
90 } // namespace %(namespace)s
91 } // namespace tidy
92 } // namespace clang
93 
94 #endif // %(header_guard)s
95 """ % {'header_guard': header_guard,
96  'check_name': check_name_camel,
97  'check_name_dashes': check_name_dashes,
98  'module': module,
99  'namespace': namespace})
100 
101 
102 # Adds the implementation of the new check.
103 def write_implementation(module_path, module, namespace, check_name_camel):
104  filename = os.path.join(module_path, check_name_camel) + '.cpp'
105  print('Creating %s...' % filename)
106  with open(filename, 'w') as f:
107  f.write('//===--- ')
108  f.write(os.path.basename(filename))
109  f.write(' - clang-tidy ')
110  f.write('-' * max(0, 51 - len(os.path.basename(filename))))
111  f.write('-===//')
112  f.write("""
113 //
114 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
115 // See https://llvm.org/LICENSE.txt for license information.
116 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
117 //
118 //===----------------------------------------------------------------------===//
119 
120 #include "%(check_name)s.h"
121 #include "clang/AST/ASTContext.h"
122 #include "clang/ASTMatchers/ASTMatchFinder.h"
123 
124 using namespace clang::ast_matchers;
125 
126 namespace clang {
127 namespace tidy {
128 namespace %(namespace)s {
129 
130 void %(check_name)s::registerMatchers(MatchFinder *Finder) {
131  // FIXME: Add matchers.
132  Finder->addMatcher(functionDecl().bind("x"), this);
133 }
134 
135 void %(check_name)s::check(const MatchFinder::MatchResult &Result) {
136  // FIXME: Add callback implementation.
137  const auto *MatchedDecl = Result.Nodes.getNodeAs<FunctionDecl>("x");
138  if (MatchedDecl->getName().startswith("awesome_"))
139  return;
140  diag(MatchedDecl->getLocation(), "function %%0 is insufficiently awesome")
141  << MatchedDecl;
142  diag(MatchedDecl->getLocation(), "insert 'awesome'", DiagnosticIDs::Note)
143  << FixItHint::CreateInsertion(MatchedDecl->getLocation(), "awesome_");
144 }
145 
146 } // namespace %(namespace)s
147 } // namespace tidy
148 } // namespace clang
149 """ % {'check_name': check_name_camel,
150  'module': module,
151  'namespace': namespace})
152 
153 
154 # Modifies the module to include the new check.
155 def adapt_module(module_path, module, check_name, check_name_camel):
156  modulecpp = list(filter(
157  lambda p: p.lower() == module.lower() + 'tidymodule.cpp',
158  os.listdir(module_path)))[0]
159  filename = os.path.join(module_path, modulecpp)
160  with open(filename, 'r') as f:
161  lines = f.readlines()
162 
163  print('Updating %s...' % filename)
164  with open(filename, 'w') as f:
165  header_added = False
166  header_found = False
167  check_added = False
168  check_decl = (' CheckFactories.registerCheck<' + check_name_camel +
169  '>(\n "' + module + '-' + check_name + '");\n')
170 
171  for line in lines:
172  if not header_added:
173  match = re.search('#include "(.*)"', line)
174  if match:
175  header_found = True
176  if match.group(1) > check_name_camel:
177  header_added = True
178  f.write('#include "' + check_name_camel + '.h"\n')
179  elif header_found:
180  header_added = True
181  f.write('#include "' + check_name_camel + '.h"\n')
182 
183  if not check_added:
184  if line.strip() == '}':
185  check_added = True
186  f.write(check_decl)
187  else:
188  match = re.search('registerCheck<(.*)>', line)
189  if match and match.group(1) > check_name_camel:
190  check_added = True
191  f.write(check_decl)
192  f.write(line)
193 
194 
195 # Adds a release notes entry.
196 def add_release_notes(module_path, module, check_name):
197  check_name_dashes = module + '-' + check_name
198  filename = os.path.normpath(os.path.join(module_path,
199  '../../docs/ReleaseNotes.rst'))
200  with open(filename, 'r') as f:
201  lines = f.readlines()
202 
203  lineMatcher = re.compile('Improvements to clang-tidy')
204  nextSectionMatcher = re.compile('Improvements to clang-include-fixer')
205  checkerMatcher = re.compile('- New :doc:`(.*)')
206 
207  print('Updating %s...' % filename)
208  with open(filename, 'w') as f:
209  note_added = False
210  header_found = False
211  next_header_found = False
212  add_note_here = False
213 
214  for line in lines:
215  if not note_added:
216  match = lineMatcher.match(line)
217  match_next = nextSectionMatcher.match(line)
218  match_checker = checkerMatcher.match(line)
219  if match_checker:
220  last_checker = match_checker.group(1)
221  if last_checker > check_name_dashes:
222  add_note_here = True
223 
224  if match_next:
225  next_header_found = True
226  add_note_here = True
227 
228  if match:
229  header_found = True
230  f.write(line)
231  continue
232 
233  if line.startswith('----'):
234  f.write(line)
235  continue
236 
237  if header_found and add_note_here:
238  if not line.startswith('----'):
239  f.write("""- New :doc:`%s
240  <clang-tidy/checks/%s>` check.
241 
242  FIXME: add release notes.
243 
244 """ % (check_name_dashes, check_name_dashes))
245  note_added = True
246 
247  f.write(line)
248 
249 
250 # Adds a test for the check.
251 def write_test(module_path, module, check_name, test_extension):
252  check_name_dashes = module + '-' + check_name
253  filename = os.path.normpath(os.path.join(module_path, '../../test/clang-tidy',
254  check_name_dashes + '.' + test_extension))
255  print('Creating %s...' % filename)
256  with open(filename, 'w') as f:
257  f.write("""// RUN: %%check_clang_tidy %%s %(check_name_dashes)s %%t
258 
259 // FIXME: Add something that triggers the check here.
260 void f();
261 // CHECK-MESSAGES: :[[@LINE-1]]:6: warning: function 'f' is insufficiently awesome [%(check_name_dashes)s]
262 
263 // FIXME: Verify the applied fix.
264 // * Make the CHECK patterns specific enough and try to make verified lines
265 // unique to avoid incorrect matches.
266 // * Use {{}} for regular expressions.
267 // CHECK-FIXES: {{^}}void awesome_f();{{$}}
268 
269 // FIXME: Add something that doesn't trigger the check here.
270 void awesome_f2();
271 """ % {'check_name_dashes': check_name_dashes})
272 
273 
274 # Recreates the list of checks in the docs/clang-tidy/checks directory.
275 def update_checks_list(clang_tidy_path):
276  docs_dir = os.path.join(clang_tidy_path, '../docs/clang-tidy/checks')
277  filename = os.path.normpath(os.path.join(docs_dir, 'list.rst'))
278  with open(filename, 'r') as f:
279  lines = f.readlines()
280  doc_files = list(filter(lambda s: s.endswith('.rst') and s != 'list.rst',
281  os.listdir(docs_dir)))
282  doc_files.sort()
283 
284  def format_link(doc_file):
285  check_name = doc_file.replace('.rst', '')
286  with open(os.path.join(docs_dir, doc_file), 'r') as doc:
287  content = doc.read()
288  match = re.search('.*:orphan:.*', content)
289  if match:
290  return ''
291 
292  match = re.search('.*:http-equiv=refresh: \d+;URL=(.*).html.*',
293  content)
294  if match:
295  return ' %(check)s (redirects to %(target)s) <%(check)s>\n' % {
296  'check': check_name,
297  'target': match.group(1)
298  }
299  return ' %s\n' % check_name
300 
301  checks = map(format_link, doc_files)
302 
303  print('Updating %s...' % filename)
304  with open(filename, 'w') as f:
305  for line in lines:
306  f.write(line)
307  if line.startswith('.. toctree::'):
308  f.writelines(checks)
309  break
310 
311 
312 # Adds a documentation for the check.
313 def write_docs(module_path, module, check_name):
314  check_name_dashes = module + '-' + check_name
315  filename = os.path.normpath(os.path.join(
316  module_path, '../../docs/clang-tidy/checks/', check_name_dashes + '.rst'))
317  print('Creating %s...' % filename)
318  with open(filename, 'w') as f:
319  f.write(""".. title:: clang-tidy - %(check_name_dashes)s
320 
321 %(check_name_dashes)s
322 %(underline)s
323 
324 FIXME: Describe what patterns does the check detect and why. Give examples.
325 """ % {'check_name_dashes': check_name_dashes,
326  'underline': '=' * len(check_name_dashes)})
327 
328 
329 def main():
330  language_to_extension = {
331  'c': 'c',
332  'c++': 'cpp',
333  'objc': 'm',
334  'objc++': 'mm',
335  }
336  parser = argparse.ArgumentParser()
337  parser.add_argument(
338  '--update-docs',
339  action='store_true',
340  help='just update the list of documentation files, then exit')
341  parser.add_argument(
342  '--language',
343  help='language to use for new check (defaults to c++)',
344  choices=language_to_extension.keys(),
345  default='c++',
346  metavar='LANG')
347  parser.add_argument(
348  'module',
349  nargs='?',
350  help='module directory under which to place the new tidy check (e.g., misc)')
351  parser.add_argument(
352  'check',
353  nargs='?',
354  help='name of new tidy check to add (e.g. foo-do-the-stuff)')
355  args = parser.parse_args()
356 
357  if args.update_docs:
358  update_checks_list(os.path.dirname(sys.argv[0]))
359  return
360 
361  if not args.module or not args.check:
362  print('Module and check must be specified.')
363  parser.print_usage()
364  return
365 
366  module = args.module
367  check_name = args.check
368 
369  if check_name.startswith(module):
370  print('Check name "%s" must not start with the module "%s". Exiting.' % (
371  check_name, module))
372  return
373  check_name_camel = ''.join(map(lambda elem: elem.capitalize(),
374  check_name.split('-'))) + 'Check'
375  clang_tidy_path = os.path.dirname(sys.argv[0])
376  module_path = os.path.join(clang_tidy_path, module)
377 
378  if not adapt_cmake(module_path, check_name_camel):
379  return
380 
381  # Map module names to namespace names that don't conflict with widely used top-level namespaces.
382  if module == 'llvm':
383  namespace = module + '_check'
384  else:
385  namespace = module
386 
387  write_header(module_path, module, namespace, check_name, check_name_camel)
388  write_implementation(module_path, module, namespace, check_name_camel)
389  adapt_module(module_path, module, check_name, check_name_camel)
390  add_release_notes(module_path, module, check_name)
391  test_extension = language_to_extension.get(args.language)
392  write_test(module_path, module, check_name, test_extension)
393  write_docs(module_path, module, check_name)
394  update_checks_list(clang_tidy_path)
395  print('Done. Now it\'s your turn!')
396 
397 
398 if __name__ == '__main__':
399  main()
def write_header(module_path, module, namespace, check_name, check_name_camel)
def write_test(module_path, module, check_name, test_extension)
def write_implementation(module_path, module, namespace, check_name_camel)
def write_docs(module_path, module, check_name)
def add_release_notes(module_path, module, check_name)
def update_checks_list(clang_tidy_path)
def adapt_module(module_path, module, check_name, check_name_camel)
static std::string join(ArrayRef< SpecialMemberFunctionsCheck::SpecialMemberFunctionKind > SMFS, llvm::StringRef AndOr)
def adapt_cmake(module_path, check_name_camel)