clang_tidy.py 5.4 KB

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