brintos

brintos / llvm-project-archived public Read only

0
0
Text · 9.1 KiB · 1232f3a Raw
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