clang_tidy.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import argparse
  2. import contextlib
  3. import json
  4. import os
  5. import re
  6. import shutil
  7. import sys
  8. import tempfile
  9. import subprocess
  10. import yaml
  11. def setup_script(args):
  12. global tidy_config_validation
  13. sys.path.append(os.path.dirname(args.config_validation_script))
  14. import tidy_config_validation
  15. def parse_args():
  16. parser = argparse.ArgumentParser()
  17. parser.add_argument("--testing-src", required=True)
  18. parser.add_argument("--clang-tidy-bin", required=True)
  19. parser.add_argument("--config-validation-script", required=True)
  20. parser.add_argument("--ymake-python", required=True)
  21. parser.add_argument("--tidy-json", required=True)
  22. parser.add_argument("--source-root", required=True)
  23. parser.add_argument("--build-root", required=True)
  24. parser.add_argument("--default-config-file", required=True)
  25. parser.add_argument("--project-config-file", required=True)
  26. parser.add_argument("--export-fixes", required=True)
  27. parser.add_argument("--checks", required=False, default="")
  28. parser.add_argument("--header-filter", required=False, default=None)
  29. return parser.parse_known_args()
  30. def generate_compilation_database(clang_cmd, source_root, filename, path):
  31. compile_database = [
  32. {
  33. "file": filename,
  34. "command": subprocess.list2cmdline(clang_cmd),
  35. "directory": source_root,
  36. }
  37. ]
  38. compilation_database_json = os.path.join(path, "compile_commands.json")
  39. with open(compilation_database_json, "w") as afile:
  40. json.dump(compile_database, afile)
  41. return compilation_database_json
  42. @contextlib.contextmanager
  43. def gen_tmpdir():
  44. path = tempfile.mkdtemp()
  45. yield path
  46. shutil.rmtree(path)
  47. @contextlib.contextmanager
  48. def gen_tmpfile():
  49. _, path = tempfile.mkstemp()
  50. yield path
  51. os.remove(path)
  52. def load_profile(path):
  53. if os.path.exists(path):
  54. files = os.listdir(path)
  55. if len(files) == 1:
  56. with open(os.path.join(path, files[0])) as afile:
  57. return json.load(afile)["profile"]
  58. elif len(files) > 1:
  59. return {
  60. "error": "found several profile files: {}".format(files),
  61. }
  62. return {
  63. "error": "profile file is missing",
  64. }
  65. def load_fixes(path):
  66. if os.path.exists(path):
  67. with open(path, 'r') as afile:
  68. return afile.read()
  69. else:
  70. return ""
  71. def is_generated(testing_src, build_root):
  72. return testing_src.startswith(build_root)
  73. def generate_outputs(output_json):
  74. output_obj = os.path.splitext(output_json)[0] + ".o"
  75. open(output_obj, "w").close()
  76. open(output_json, "w").close()
  77. def filter_configs(result_config, filtered_config):
  78. with open(result_config, 'r') as afile:
  79. input_config = yaml.safe_load(afile)
  80. result_config = tidy_config_validation.filter_config(input_config)
  81. with open(filtered_config, 'w') as afile:
  82. yaml.safe_dump(result_config, afile)
  83. def main():
  84. args, clang_cmd = parse_args()
  85. setup_script(args)
  86. clang_tidy_bin = args.clang_tidy_bin
  87. output_json = args.tidy_json
  88. generate_outputs(output_json)
  89. if is_generated(args.testing_src, args.build_root):
  90. return
  91. if args.header_filter is None:
  92. header_filter = r"^" + re.escape(os.path.dirname(args.testing_src)) + r".*" # .pb.h files will be excluded because they are not in source_root
  93. else:
  94. header_filter = r"^(" + args.header_filter + r").*"
  95. with gen_tmpdir() as profile_tmpdir, gen_tmpdir() as db_tmpdir, gen_tmpfile() as fixes_file, gen_tmpdir() as config_dir:
  96. result_config_file = args.default_config_file
  97. if args.project_config_file != args.default_config_file:
  98. result_config = os.path.join(config_dir, "result_tidy_config.yaml")
  99. filtered_config = os.path.join(config_dir, "filtered_tidy_config.yaml")
  100. filter_configs(args.project_config_file, filtered_config)
  101. result_config_file = tidy_config_validation.merge_tidy_configs(base_config_path=args.default_config_file, additional_config_path=filtered_config, result_config_path=result_config)
  102. compile_command_path = generate_compilation_database(clang_cmd, args.source_root, args.testing_src, db_tmpdir)
  103. cmd = [
  104. clang_tidy_bin,
  105. args.testing_src,
  106. "-p",
  107. compile_command_path,
  108. "--warnings-as-errors",
  109. "*",
  110. "--config-file",
  111. result_config_file,
  112. "--header-filter",
  113. header_filter,
  114. "--use-color",
  115. "--enable-check-profile",
  116. "--store-check-profile={}".format(profile_tmpdir),
  117. ]
  118. if args.export_fixes == "yes":
  119. cmd += ["--export-fixes", fixes_file]
  120. if args.checks:
  121. cmd += ["--checks", args.checks]
  122. res = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  123. out, err = res.communicate()
  124. exit_code = res.returncode
  125. profile = load_profile(profile_tmpdir)
  126. testing_src = os.path.relpath(args.testing_src, args.source_root)
  127. tidy_fixes = load_fixes(fixes_file)
  128. with open(output_json, "wb") as afile:
  129. json.dump(
  130. {
  131. "file": testing_src,
  132. "exit_code": exit_code,
  133. "profile": profile,
  134. "stderr": err,
  135. "stdout": out,
  136. "fixes": tidy_fixes,
  137. },
  138. afile,
  139. )
  140. if __name__ == "__main__":
  141. main()