summaryrefslogtreecommitdiff
path: root/clang-tools-extra/test/clang-tidy/check_clang_tidy.py
blob: 46fe0f6c8af3264e3c46d3bb18f315ef14074aaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/env python
#
#===- check_clang_tidy.py - ClangTidy Test Helper ------------*- python -*--===#
#
#                     The LLVM Compiler Infrastructure
#
# This file is distributed under the University of Illinois Open Source
# License. See LICENSE.TXT for details.
#
#===------------------------------------------------------------------------===#

r"""
ClangTidy Test Helper
=====================

This script runs clang-tidy in fix mode and verify fixes, messages or both.

Usage:
  check_clang_tidy.py [-resource-dir=<resource-dir>] \
    [-assume-filename=<file-with-source-extension>] \
    [-check-suffix=<file-check-suffix>] \
    <source-file> <check-name> <temp-file> \
    -- [optional clang-tidy arguments]

Example:
  // RUN: %check_clang_tidy %s llvm-include-order %t -- -- -isystem %S/Inputs
"""

import argparse
import os
import re
import subprocess
import sys


def write_file(file_name, text):
  with open(file_name, 'w') as f:
    f.write(text)
    f.truncate()

def main():
  parser = argparse.ArgumentParser()
  parser.add_argument('-expect-clang-tidy-error', action='store_true')
  parser.add_argument('-resource-dir')
  parser.add_argument('-assume-filename')
  parser.add_argument('-check-suffix', default='')
  parser.add_argument('input_file_name')
  parser.add_argument('check_name')
  parser.add_argument('temp_file_name')

  args, extra_args = parser.parse_known_args()

  resource_dir = args.resource_dir
  assume_file_name = args.assume_filename
  input_file_name = args.input_file_name
  check_name = args.check_name
  temp_file_name = args.temp_file_name
  expect_clang_tidy_error = args.expect_clang_tidy_error

  file_name_with_extension = assume_file_name or input_file_name
  _, extension = os.path.splitext(file_name_with_extension)
  if extension not in ['.c', '.hpp', '.m', '.mm']:
    extension = '.cpp'
  temp_file_name = temp_file_name + extension

  clang_tidy_extra_args = extra_args
  if len(clang_tidy_extra_args) == 0:
    clang_tidy_extra_args = ['--']
    if extension in ['.cpp', '.hpp', '.mm']:
      clang_tidy_extra_args.append('--std=c++11')
    if extension in ['.m', '.mm']:
      clang_tidy_extra_args.extend(
          ['-fobjc-abi-version=2', '-fobjc-arc'])

  if args.check_suffix and not re.match('^[A-Z0-9\-]+$', args.check_suffix):
    sys.exit('Only A..Z, 0..9 and "-" are allowed in check suffix, but "%s" was given' % (args.check_suffix))

  file_check_suffix = ('-' + args.check_suffix) if args.check_suffix else ''
  check_fixes_prefix = 'CHECK-FIXES' + file_check_suffix
  check_messages_prefix = 'CHECK-MESSAGES' + file_check_suffix
  check_notes_prefix = 'CHECK-NOTES' + file_check_suffix

  # Tests should not rely on STL being available, and instead provide mock
  # implementations of relevant APIs.
  clang_tidy_extra_args.append('-nostdinc++')

  if resource_dir is not None:
    clang_tidy_extra_args.append('-resource-dir=%s' % resource_dir)

  with open(input_file_name, 'r') as input_file:
    input_text = input_file.read()

  has_check_fixes = check_fixes_prefix in input_text
  has_check_messages = check_messages_prefix in input_text
  has_check_notes = check_notes_prefix in input_text

  if not has_check_fixes and not has_check_messages and not has_check_notes:
    sys.exit('%s, %s or %s not found in the input' % (check_fixes_prefix,
             check_messages_prefix, check_notes_prefix) )

  if has_check_notes and has_check_messages:
    sys.exit('Please use either CHECK-NOTES or CHECK-MESSAGES but not both')

  # Remove the contents of the CHECK lines to avoid CHECKs matching on
  # themselves.  We need to keep the comments to preserve line numbers while
  # avoiding empty lines which could potentially trigger formatting-related
  # checks.
  cleaned_test = re.sub('// *CHECK-[A-Z0-9\-]*:[^\r\n]*', '//', input_text)

  write_file(temp_file_name, cleaned_test)

  original_file_name = temp_file_name + ".orig"
  write_file(original_file_name, cleaned_test)

  args = ['clang-tidy', temp_file_name, '-fix', '--checks=-*,' + check_name] + \
        clang_tidy_extra_args
  if expect_clang_tidy_error:
    args.insert(0, 'not')
  print('Running ' + repr(args) + '...')
  try:
    clang_tidy_output = \
        subprocess.check_output(args, stderr=subprocess.STDOUT).decode()
  except subprocess.CalledProcessError as e:
    print('clang-tidy failed:\n' + e.output.decode())
    raise

  print('------------------------ clang-tidy output -----------------------\n' +
        clang_tidy_output +
        '\n------------------------------------------------------------------')

  try:
    diff_output = subprocess.check_output(
        ['diff', '-u', original_file_name, temp_file_name],
        stderr=subprocess.STDOUT)
  except subprocess.CalledProcessError as e:
    diff_output = e.output

  print('------------------------------ Fixes -----------------------------\n' +
        diff_output.decode() +
        '\n------------------------------------------------------------------')

  if has_check_fixes:
    try:
      subprocess.check_output(
          ['FileCheck', '-input-file=' + temp_file_name, input_file_name,
           '-check-prefix=' + check_fixes_prefix, '-strict-whitespace'],
          stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
      print('FileCheck failed:\n' + e.output.decode())
      raise

  if has_check_messages:
    messages_file = temp_file_name + '.msg'
    write_file(messages_file, clang_tidy_output)
    try:
      subprocess.check_output(
          ['FileCheck', '-input-file=' + messages_file, input_file_name,
           '-check-prefix=' + check_messages_prefix,
           '-implicit-check-not={{warning|error}}:'],
          stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
      print('FileCheck failed:\n' + e.output.decode())
      raise

  if has_check_notes:
    notes_file = temp_file_name + '.notes'
    filtered_output = [line for line in clang_tidy_output.splitlines()
                       if not "note: FIX-IT applied suggested changes" in line]
    write_file(notes_file, '\n'.join(filtered_output))
    try:
      subprocess.check_output(
          ['FileCheck', '-input-file=' + notes_file, input_file_name,
           '-check-prefix=' + check_notes_prefix,
           '-implicit-check-not={{note|warning|error}}:'],
          stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
      print('FileCheck failed:\n' + e.output.decode())
      raise

if __name__ == '__main__':
  main()