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