330 lines · python
1#!/usr/bin/env python32#3# ====- clang-tidy-helper, runs clang-tidy from the ci --*- 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-exception8#9# ==-------------------------------------------------------------------------==#10"""A helper script to run clang-tidy linter in GitHub actions11 12This script is run by GitHub actions to ensure that the code in PR's conform to13the coding style of LLVM. The canonical source of this script is in the LLVM14source tree under llvm/utils/git.15 16You can learn more about the LLVM coding style on llvm.org:17https://llvm.org/docs/CodingStandards.html18"""19 20import argparse21import os22import subprocess23import sys24from typing import List, Optional25 26 27class LintArgs:28 start_rev: str = None29 end_rev: str = None30 repo: str = None31 changed_files: List[str] = []32 token: str = None33 verbose: bool = True34 issue_number: int = 035 build_path: str = "build"36 clang_tidy_binary: str = "clang-tidy"37 38 def __init__(self, args: argparse.Namespace = None) -> None:39 if not args is None:40 self.start_rev = args.start_rev41 self.end_rev = args.end_rev42 self.repo = args.repo43 self.token = args.token44 self.changed_files = args.changed_files45 self.issue_number = args.issue_number46 self.verbose = args.verbose47 self.build_path = args.build_path48 self.clang_tidy_binary = args.clang_tidy_binary49 50 51COMMENT_TAG = "<!--LLVM CODE LINT COMMENT: clang-tidy-->"52 53 54def get_instructions(cpp_files: List[str]) -> str:55 files_str = " ".join(cpp_files)56 return f"""57git diff -U0 origin/main...HEAD -- {files_str} |58python3 clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py \\59 -path build -p1 -quiet"""60 61 62def clean_clang_tidy_output(output: str) -> Optional[str]:63 """64 - Remove 'Running clang-tidy in X threads...' line65 - Remove 'N warnings generated.' line66 - Strip leading workspace path from file paths67 """68 if not output or output == "No relevant changes found.":69 return None70 71 lines = output.split("\n")72 cleaned_lines = []73 74 for line in lines:75 if line.startswith("Running clang-tidy in") or line.endswith("generated."):76 continue77 78 # Remove everything up to rightmost "llvm-project/" for correct files names79 idx = line.rfind("llvm-project/")80 if idx != -1:81 line = line[idx + len("llvm-project/") :]82 83 cleaned_lines.append(line)84 85 if cleaned_lines:86 return "\n".join(cleaned_lines)87 return None88 89 90# TODO: Add more rules when enabling other projects to use clang-tidy in CI.91def should_lint_file(filepath: str) -> bool:92 return filepath.startswith("clang-tools-extra/clang-tidy/")93 94 95def filter_changed_files(changed_files: List[str]) -> List[str]:96 filtered_files = []97 for filepath in changed_files:98 _, ext = os.path.splitext(filepath)99 if ext not in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx"):100 continue101 if not should_lint_file(filepath):102 continue103 if os.path.exists(filepath):104 filtered_files.append(filepath)105 106 return filtered_files107 108 109def create_comment_text(warning: str, cpp_files: List[str]) -> str:110 instructions = get_instructions(cpp_files)111 return f"""112:warning: C/C++ code linter clang-tidy found issues in your code. :warning:113 114<details>115<summary>116You can test this locally with the following command:117</summary>118 119```bash120{instructions}121```122 123</details>124 125<details>126<summary>127View the output from clang-tidy here.128</summary>129 130```131{warning}132```133 134</details>135"""136 137 138def find_comment(pr: any) -> any:139 for comment in pr.as_issue().get_comments():140 if COMMENT_TAG in comment.body:141 return comment142 return None143 144 145def create_comment(146 comment_text: str, args: LintArgs, create_new: bool147) -> Optional[dict]:148 import github149 150 repo = github.Github(args.token).get_repo(args.repo)151 pr = repo.get_issue(args.issue_number).as_pull_request()152 153 comment_text = COMMENT_TAG + "\n\n" + comment_text154 155 existing_comment = find_comment(pr)156 157 comment = None158 if create_new or existing_comment:159 comment = {"body": comment_text}160 if existing_comment:161 comment["id"] = existing_comment.id162 return comment163 164 165def run_clang_tidy(changed_files: List[str], args: LintArgs) -> Optional[str]:166 if not changed_files:167 print("no c/c++ files found")168 return None169 170 git_diff_cmd = [171 "git",172 "diff",173 "-U0",174 f"{args.start_rev}...{args.end_rev}",175 "--",176 ] + changed_files177 178 diff_proc = subprocess.run(179 git_diff_cmd,180 stdout=subprocess.PIPE,181 stderr=subprocess.PIPE,182 text=True,183 check=False,184 )185 186 if diff_proc.returncode != 0:187 print(f"Git diff failed: {diff_proc.stderr}")188 return None189 190 diff_content = diff_proc.stdout191 if not diff_content.strip():192 print("No diff content found")193 return None194 195 tidy_diff_cmd = [196 "clang-tools-extra/clang-tidy/tool/clang-tidy-diff.py",197 "-path",198 args.build_path,199 "-p1",200 "-quiet",201 ]202 203 if args.verbose:204 print(f"Running clang-tidy-diff: {' '.join(tidy_diff_cmd)}")205 206 proc = subprocess.run(207 tidy_diff_cmd,208 input=diff_content,209 stdout=subprocess.PIPE,210 stderr=subprocess.PIPE,211 text=True,212 check=False,213 )214 215 return clean_clang_tidy_output(proc.stdout.strip())216 217 218def run_linter(changed_files: List[str], args: LintArgs) -> tuple[bool, Optional[dict]]:219 changed_files = [arg for arg in changed_files if "third-party" not in arg]220 221 cpp_files = filter_changed_files(changed_files)222 223 tidy_result = run_clang_tidy(cpp_files, args)224 should_update_gh = args.token is not None and args.repo is not None225 226 comment = None227 if tidy_result is None:228 if should_update_gh:229 comment_text = (230 ":white_check_mark: With the latest revision "231 "this PR passed the C/C++ code linter."232 )233 comment = create_comment(comment_text, args, create_new=False)234 return True, comment235 elif len(tidy_result) > 0:236 if should_update_gh:237 comment_text = create_comment_text(tidy_result, cpp_files)238 comment = create_comment(comment_text, args, create_new=True)239 else:240 print(241 "Warning: C/C++ code linter, clang-tidy detected "242 "some issues with your code..."243 )244 return False, comment245 else:246 # The linter failed but didn't output a result (e.g. some sort of247 # infrastructure failure).248 comment_text = (249 ":warning: The C/C++ code linter failed without printing "250 "an output. Check the logs for output. :warning:"251 )252 comment = create_comment(comment_text, args, create_new=False)253 return False, comment254 255 256if __name__ == "__main__":257 parser = argparse.ArgumentParser()258 parser.add_argument(259 "--token", type=str, required=True, help="GitHub authentication token"260 )261 parser.add_argument("--issue-number", type=int, required=True)262 parser.add_argument(263 "--repo",264 type=str,265 default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),266 help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",267 )268 parser.add_argument(269 "--start-rev",270 type=str,271 required=True,272 help="Compute changes from this revision.",273 )274 parser.add_argument(275 "--end-rev", type=str, required=True, help="Compute changes to this revision"276 )277 parser.add_argument(278 "--changed-files",279 type=str,280 help="Comma separated list of files that has been changed",281 )282 parser.add_argument(283 "--build-path",284 type=str,285 default="build",286 help="Path to build directory with compile_commands.json",287 )288 parser.add_argument(289 "--clang-tidy-binary",290 type=str,291 default="clang-tidy",292 help="Path to clang-tidy binary",293 )294 parser.add_argument(295 "--verbose", action="store_true", default=True, help="Verbose output"296 )297 298 parsed_args = parser.parse_args()299 args = LintArgs(parsed_args)300 301 changed_files = []302 if args.changed_files:303 changed_files = args.changed_files.split(",")304 305 if args.verbose:306 print(f"got changed files: {changed_files}")307 308 if args.verbose:309 print("running linter clang-tidy")310 311 success, comment = run_linter(changed_files, args)312 313 if not success:314 if args.verbose:315 print("linter clang-tidy failed")316 317 # Write comments file if we have a comment318 if comment:319 if args.verbose:320 print(f"linter clang-tidy has comment: {comment}")321 322 with open("comments", "w") as f:323 import json324 325 json.dump([comment], f)326 327 if not success:328 print("error: some linters failed: clang-tidy")329 sys.exit(1)330