xref: /llvm-project/llvm/utils/git/code-format-helper.py (revision c84f5a9e00c02e6a4349846ed59ec85154b65e3f)
1a1177b0bSTobias Hieta#!/usr/bin/env python3
2a1177b0bSTobias Hieta#
3bd3e8eb6STobias Hieta# ====- code-format-helper, runs code formatters from the ci or in a hook --*- python -*--==#
4a1177b0bSTobias Hieta#
5a1177b0bSTobias Hieta# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6a1177b0bSTobias Hieta# See https://llvm.org/LICENSE.txt for license information.
7a1177b0bSTobias Hieta# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8a1177b0bSTobias Hieta#
9bd3e8eb6STobias Hieta# ==--------------------------------------------------------------------------------------==#
10a1177b0bSTobias Hieta
11a1177b0bSTobias Hietaimport argparse
12a1177b0bSTobias Hietaimport os
1319bc2823SNuno Lopesimport re
1419bc2823SNuno Lopesimport shlex
15a1177b0bSTobias Hietaimport subprocess
16a1177b0bSTobias Hietaimport sys
17bd3e8eb6STobias Hietafrom typing import List, Optional
18a1177b0bSTobias Hieta
19bd3e8eb6STobias Hieta"""
20bd3e8eb6STobias HietaThis script is run by GitHub actions to ensure that the code in PR's conform to
21bd3e8eb6STobias Hietathe coding style of LLVM. It can also be installed as a pre-commit git hook to
22bd3e8eb6STobias Hietacheck the coding style before submitting it. The canonical source of this script
23bd3e8eb6STobias Hietais in the LLVM source tree under llvm/utils/git.
24bd3e8eb6STobias Hieta
25bd3e8eb6STobias HietaFor C/C++ code it uses clang-format and for Python code it uses darker (which
26bd3e8eb6STobias Hietain turn invokes black).
27bd3e8eb6STobias Hieta
28bd3e8eb6STobias HietaYou can learn more about the LLVM coding style on llvm.org:
29bd3e8eb6STobias Hietahttps://llvm.org/docs/CodingStandards.html
30bd3e8eb6STobias Hieta
31bd3e8eb6STobias HietaYou can install this script as a git hook by symlinking it to the .git/hooks
32bd3e8eb6STobias Hietadirectory:
33bd3e8eb6STobias Hieta
34bd3e8eb6STobias Hietaln -s $(pwd)/llvm/utils/git/code-format-helper.py .git/hooks/pre-commit
35bd3e8eb6STobias Hieta
36bd3e8eb6STobias HietaYou can control the exact path to clang-format or darker with the following
37bd3e8eb6STobias Hietaenvironment variables: $CLANG_FORMAT_PATH and $DARKER_FORMAT_PATH.
38bd3e8eb6STobias Hieta"""
39bd3e8eb6STobias Hieta
40bd3e8eb6STobias Hieta
41bd3e8eb6STobias Hietaclass FormatArgs:
42bd3e8eb6STobias Hieta    start_rev: str = None
43bd3e8eb6STobias Hieta    end_rev: str = None
44bd3e8eb6STobias Hieta    repo: str = None
45bd3e8eb6STobias Hieta    changed_files: List[str] = []
46bd3e8eb6STobias Hieta    token: str = None
47bd3e8eb6STobias Hieta    verbose: bool = True
48a3a8acd9STobias Hieta    issue_number: int = 0
492120f574STom Stellard    write_comment_to_file: bool = False
50bd3e8eb6STobias Hieta
51bd3e8eb6STobias Hieta    def __init__(self, args: argparse.Namespace = None) -> None:
52bd3e8eb6STobias Hieta        if not args is None:
53bd3e8eb6STobias Hieta            self.start_rev = args.start_rev
54bd3e8eb6STobias Hieta            self.end_rev = args.end_rev
55bd3e8eb6STobias Hieta            self.repo = args.repo
56bd3e8eb6STobias Hieta            self.token = args.token
57bd3e8eb6STobias Hieta            self.changed_files = args.changed_files
58a3a8acd9STobias Hieta            self.issue_number = args.issue_number
592120f574STom Stellard            self.write_comment_to_file = args.write_comment_to_file
60a1177b0bSTobias Hieta
61a1177b0bSTobias Hieta
62a1177b0bSTobias Hietaclass FormatHelper:
63a1177b0bSTobias Hieta    COMMENT_TAG = "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
64af253043SRyan Prichard    name: str
65af253043SRyan Prichard    friendly_name: str
662120f574STom Stellard    comment: dict = None
67a1177b0bSTobias Hieta
68a1177b0bSTobias Hieta    @property
69a1177b0bSTobias Hieta    def comment_tag(self) -> str:
70a1177b0bSTobias Hieta        return self.COMMENT_TAG.replace("fmt", self.name)
71a1177b0bSTobias Hieta
72af253043SRyan Prichard    @property
73af253043SRyan Prichard    def instructions(self) -> str:
74af253043SRyan Prichard        raise NotImplementedError()
75af253043SRyan Prichard
76bd3e8eb6STobias Hieta    def has_tool(self) -> bool:
77bd3e8eb6STobias Hieta        raise NotImplementedError()
78bd3e8eb6STobias Hieta
79bd3e8eb6STobias Hieta    def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
80af253043SRyan Prichard        raise NotImplementedError()
81a1177b0bSTobias Hieta
8256cadac8SRyan Prichard    def pr_comment_text_for_diff(self, diff: str) -> str:
83a1177b0bSTobias Hieta        return f"""
84a1177b0bSTobias Hieta:warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
85a1177b0bSTobias Hieta
86a1177b0bSTobias Hieta<details>
87a1177b0bSTobias Hieta<summary>
88a1177b0bSTobias HietaYou can test this locally with the following command:
89a1177b0bSTobias Hieta</summary>
90a1177b0bSTobias Hieta
91a1177b0bSTobias Hieta``````````bash
92a1177b0bSTobias Hieta{self.instructions}
93a1177b0bSTobias Hieta``````````
94a1177b0bSTobias Hieta
95a1177b0bSTobias Hieta</details>
96a1177b0bSTobias Hieta
97a1177b0bSTobias Hieta<details>
98a1177b0bSTobias Hieta<summary>
99a1177b0bSTobias HietaView the diff from {self.name} here.
100a1177b0bSTobias Hieta</summary>
101a1177b0bSTobias Hieta
102a1177b0bSTobias Hieta``````````diff
103a1177b0bSTobias Hieta{diff}
104a1177b0bSTobias Hieta``````````
105a1177b0bSTobias Hieta
106a1177b0bSTobias Hieta</details>
107a1177b0bSTobias Hieta"""
108a1177b0bSTobias Hieta
109bd3e8eb6STobias Hieta    # TODO: any type should be replaced with the correct github type, but it requires refactoring to
110bd3e8eb6STobias Hieta    # not require the github module to be installed everywhere.
111bd3e8eb6STobias Hieta    def find_comment(self, pr: any) -> any:
112a1177b0bSTobias Hieta        for comment in pr.as_issue().get_comments():
113a1177b0bSTobias Hieta            if self.comment_tag in comment.body:
114a1177b0bSTobias Hieta                return comment
115a1177b0bSTobias Hieta        return None
116a1177b0bSTobias Hieta
117bd3e8eb6STobias Hieta    def update_pr(self, comment_text: str, args: FormatArgs, create_new: bool) -> None:
118bd3e8eb6STobias Hieta        import github
119bd3e8eb6STobias Hieta        from github import IssueComment, PullRequest
120bd3e8eb6STobias Hieta
121a1177b0bSTobias Hieta        repo = github.Github(args.token).get_repo(args.repo)
122a1177b0bSTobias Hieta        pr = repo.get_issue(args.issue_number).as_pull_request()
123a1177b0bSTobias Hieta
12456cadac8SRyan Prichard        comment_text = self.comment_tag + "\n\n" + comment_text
125a1177b0bSTobias Hieta
126a1177b0bSTobias Hieta        existing_comment = self.find_comment(pr)
1272120f574STom Stellard
1282120f574STom Stellard        if args.write_comment_to_file:
129de917dc2STom Stellard            if create_new or existing_comment:
1302120f574STom Stellard                self.comment = {"body": comment_text}
1312120f574STom Stellard            if existing_comment:
1322120f574STom Stellard                self.comment["id"] = existing_comment.id
1332120f574STom Stellard            return
1342120f574STom Stellard
135a1177b0bSTobias Hieta        if existing_comment:
13656cadac8SRyan Prichard            existing_comment.edit(comment_text)
13756cadac8SRyan Prichard        elif create_new:
13856cadac8SRyan Prichard            pr.as_issue().create_comment(comment_text)
139a1177b0bSTobias Hieta
140bd3e8eb6STobias Hieta    def run(self, changed_files: List[str], args: FormatArgs) -> bool:
141a81a7b99SMircea Trofin        changed_files = [arg for arg in changed_files if "third-party" not in arg]
142a1177b0bSTobias Hieta        diff = self.format_run(changed_files, args)
143bd3e8eb6STobias Hieta        should_update_gh = args.token is not None and args.repo is not None
144bd3e8eb6STobias Hieta
14556cadac8SRyan Prichard        if diff is None:
146bd3e8eb6STobias Hieta            if should_update_gh:
147fc3eed1bSAiden Grossman                comment_text = (
148fc3eed1bSAiden Grossman                    ":white_check_mark: With the latest revision "
149fc3eed1bSAiden Grossman                    f"this PR passed the {self.friendly_name}."
150fc3eed1bSAiden Grossman                )
15156cadac8SRyan Prichard                self.update_pr(comment_text, args, create_new=False)
15256cadac8SRyan Prichard            return True
15356cadac8SRyan Prichard        elif len(diff) > 0:
154bd3e8eb6STobias Hieta            if should_update_gh:
15556cadac8SRyan Prichard                comment_text = self.pr_comment_text_for_diff(diff)
15656cadac8SRyan Prichard                self.update_pr(comment_text, args, create_new=True)
157bd3e8eb6STobias Hieta            else:
158bd3e8eb6STobias Hieta                print(
159fc3eed1bSAiden Grossman                    f"Warning: {self.friendly_name}, {self.name} detected "
160fc3eed1bSAiden Grossman                    "some issues with your code formatting..."
161bd3e8eb6STobias Hieta                )
162a1177b0bSTobias Hieta            return False
163a1177b0bSTobias Hieta        else:
16456cadac8SRyan Prichard            # The formatter failed but didn't output a diff (e.g. some sort of
16556cadac8SRyan Prichard            # infrastructure failure).
166fc3eed1bSAiden Grossman            comment_text = (
167fc3eed1bSAiden Grossman                f":warning: The {self.friendly_name} failed without printing "
168fc3eed1bSAiden Grossman                "a diff. Check the logs for stderr output. :warning:"
169fc3eed1bSAiden Grossman            )
17056cadac8SRyan Prichard            self.update_pr(comment_text, args, create_new=False)
17156cadac8SRyan Prichard            return False
172a1177b0bSTobias Hieta
173a1177b0bSTobias Hieta
174a1177b0bSTobias Hietaclass ClangFormatHelper(FormatHelper):
175a1177b0bSTobias Hieta    name = "clang-format"
176a1177b0bSTobias Hieta    friendly_name = "C/C++ code formatter"
177a1177b0bSTobias Hieta
178a1177b0bSTobias Hieta    @property
179af253043SRyan Prichard    def instructions(self) -> str:
180a1177b0bSTobias Hieta        return " ".join(self.cf_cmd)
181a1177b0bSTobias Hieta
1823e28e1ecSLouis Dionne    def should_include_extensionless_file(self, path: str) -> bool:
1833e28e1ecSLouis Dionne        return path.startswith("libcxx/include")
1843e28e1ecSLouis Dionne
185bd3e8eb6STobias Hieta    def filter_changed_files(self, changed_files: List[str]) -> List[str]:
186a1177b0bSTobias Hieta        filtered_files = []
187a1177b0bSTobias Hieta        for path in changed_files:
188a1177b0bSTobias Hieta            _, ext = os.path.splitext(path)
1893e28e1ecSLouis Dionne            if ext in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx", ".inc", ".cppm"):
190a1177b0bSTobias Hieta                filtered_files.append(path)
1913e28e1ecSLouis Dionne            elif ext == "" and self.should_include_extensionless_file(path):
1923e28e1ecSLouis Dionne                filtered_files.append(path)
193a1177b0bSTobias Hieta        return filtered_files
194a1177b0bSTobias Hieta
195bd3e8eb6STobias Hieta    @property
196bd3e8eb6STobias Hieta    def clang_fmt_path(self) -> str:
197bd3e8eb6STobias Hieta        if "CLANG_FORMAT_PATH" in os.environ:
198bd3e8eb6STobias Hieta            return os.environ["CLANG_FORMAT_PATH"]
199bd3e8eb6STobias Hieta        return "git-clang-format"
200bd3e8eb6STobias Hieta
201bd3e8eb6STobias Hieta    def has_tool(self) -> bool:
202bd3e8eb6STobias Hieta        cmd = [self.clang_fmt_path, "-h"]
203bd3e8eb6STobias Hieta        proc = None
204bd3e8eb6STobias Hieta        try:
205bd3e8eb6STobias Hieta            proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
206bd3e8eb6STobias Hieta        except:
207bd3e8eb6STobias Hieta            return False
208bd3e8eb6STobias Hieta        return proc.returncode == 0
209bd3e8eb6STobias Hieta
210bd3e8eb6STobias Hieta    def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
211a1177b0bSTobias Hieta        cpp_files = self.filter_changed_files(changed_files)
212a1177b0bSTobias Hieta        if not cpp_files:
213af253043SRyan Prichard            return None
214bd3e8eb6STobias Hieta
215bd3e8eb6STobias Hieta        cf_cmd = [self.clang_fmt_path, "--diff"]
216bd3e8eb6STobias Hieta
217bd3e8eb6STobias Hieta        if args.start_rev and args.end_rev:
218bd3e8eb6STobias Hieta            cf_cmd.append(args.start_rev)
219bd3e8eb6STobias Hieta            cf_cmd.append(args.end_rev)
220bd3e8eb6STobias Hieta
221b3c450d4SLouis Dionne        # Gather the extension of all modified files and pass them explicitly to git-clang-format.
222b3c450d4SLouis Dionne        # This prevents git-clang-format from applying its own filtering rules on top of ours.
223b3c450d4SLouis Dionne        extensions = set()
224b3c450d4SLouis Dionne        for file in cpp_files:
225b3c450d4SLouis Dionne            _, ext = os.path.splitext(file)
226b3c450d4SLouis Dionne            extensions.add(
227b3c450d4SLouis Dionne                ext.strip(".")
228b3c450d4SLouis Dionne            )  # Exclude periods since git-clang-format takes extensions without them
229b3c450d4SLouis Dionne        cf_cmd.append("--extensions")
230b3c450d4SLouis Dionne        cf_cmd.append(",".join(extensions))
231b3c450d4SLouis Dionne
232bd3e8eb6STobias Hieta        cf_cmd.append("--")
233bd3e8eb6STobias Hieta        cf_cmd += cpp_files
234bd3e8eb6STobias Hieta
235bd3e8eb6STobias Hieta        if args.verbose:
236a1177b0bSTobias Hieta            print(f"Running: {' '.join(cf_cmd)}")
237a1177b0bSTobias Hieta        self.cf_cmd = cf_cmd
238bd3e8eb6STobias Hieta        proc = subprocess.run(cf_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
23956cadac8SRyan Prichard        sys.stdout.write(proc.stderr.decode("utf-8"))
240a1177b0bSTobias Hieta
24156cadac8SRyan Prichard        if proc.returncode != 0:
24256cadac8SRyan Prichard            # formatting needed, or the command otherwise failed
243bd3e8eb6STobias Hieta            if args.verbose:
24456cadac8SRyan Prichard                print(f"error: {self.name} exited with code {proc.returncode}")
24556d0e8ccSAiden Grossman                # Print the diff in the log so that it is viewable there
24656d0e8ccSAiden Grossman                print(proc.stdout.decode("utf-8"))
247a1177b0bSTobias Hieta            return proc.stdout.decode("utf-8")
24856cadac8SRyan Prichard        else:
249a1177b0bSTobias Hieta            return None
250a1177b0bSTobias Hieta
251a1177b0bSTobias Hieta
252a1177b0bSTobias Hietaclass DarkerFormatHelper(FormatHelper):
253a1177b0bSTobias Hieta    name = "darker"
254a1177b0bSTobias Hieta    friendly_name = "Python code formatter"
255a1177b0bSTobias Hieta
256a1177b0bSTobias Hieta    @property
257af253043SRyan Prichard    def instructions(self) -> str:
258a1177b0bSTobias Hieta        return " ".join(self.darker_cmd)
259a1177b0bSTobias Hieta
260bd3e8eb6STobias Hieta    def filter_changed_files(self, changed_files: List[str]) -> List[str]:
261a1177b0bSTobias Hieta        filtered_files = []
262a1177b0bSTobias Hieta        for path in changed_files:
263a1177b0bSTobias Hieta            name, ext = os.path.splitext(path)
264a1177b0bSTobias Hieta            if ext == ".py":
265a1177b0bSTobias Hieta                filtered_files.append(path)
266a1177b0bSTobias Hieta
267a1177b0bSTobias Hieta        return filtered_files
268a1177b0bSTobias Hieta
269bd3e8eb6STobias Hieta    @property
270bd3e8eb6STobias Hieta    def darker_fmt_path(self) -> str:
271bd3e8eb6STobias Hieta        if "DARKER_FORMAT_PATH" in os.environ:
272bd3e8eb6STobias Hieta            return os.environ["DARKER_FORMAT_PATH"]
273bd3e8eb6STobias Hieta        return "darker"
274bd3e8eb6STobias Hieta
275bd3e8eb6STobias Hieta    def has_tool(self) -> bool:
276bd3e8eb6STobias Hieta        cmd = [self.darker_fmt_path, "--version"]
277bd3e8eb6STobias Hieta        proc = None
278bd3e8eb6STobias Hieta        try:
279bd3e8eb6STobias Hieta            proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
280bd3e8eb6STobias Hieta        except:
281bd3e8eb6STobias Hieta            return False
282bd3e8eb6STobias Hieta        return proc.returncode == 0
283bd3e8eb6STobias Hieta
284bd3e8eb6STobias Hieta    def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
285a1177b0bSTobias Hieta        py_files = self.filter_changed_files(changed_files)
286a1177b0bSTobias Hieta        if not py_files:
287af253043SRyan Prichard            return None
288a1177b0bSTobias Hieta        darker_cmd = [
289bd3e8eb6STobias Hieta            self.darker_fmt_path,
290a1177b0bSTobias Hieta            "--check",
291a1177b0bSTobias Hieta            "--diff",
292bd3e8eb6STobias Hieta        ]
293bd3e8eb6STobias Hieta        if args.start_rev and args.end_rev:
294bd3e8eb6STobias Hieta            darker_cmd += ["-r", f"{args.start_rev}...{args.end_rev}"]
295bd3e8eb6STobias Hieta        darker_cmd += py_files
296bd3e8eb6STobias Hieta        if args.verbose:
297a1177b0bSTobias Hieta            print(f"Running: {' '.join(darker_cmd)}")
298a1177b0bSTobias Hieta        self.darker_cmd = darker_cmd
299bd3e8eb6STobias Hieta        proc = subprocess.run(
300bd3e8eb6STobias Hieta            darker_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
301bd3e8eb6STobias Hieta        )
302bd3e8eb6STobias Hieta        if args.verbose:
30356cadac8SRyan Prichard            sys.stdout.write(proc.stderr.decode("utf-8"))
304a1177b0bSTobias Hieta
30556cadac8SRyan Prichard        if proc.returncode != 0:
30656cadac8SRyan Prichard            # formatting needed, or the command otherwise failed
307bd3e8eb6STobias Hieta            if args.verbose:
30856cadac8SRyan Prichard                print(f"error: {self.name} exited with code {proc.returncode}")
30956d0e8ccSAiden Grossman                # Print the diff in the log so that it is viewable there
31056d0e8ccSAiden Grossman                print(proc.stdout.decode("utf-8"))
311a1177b0bSTobias Hieta            return proc.stdout.decode("utf-8")
31256cadac8SRyan Prichard        else:
31356cadac8SRyan Prichard            sys.stdout.write(proc.stdout.decode("utf-8"))
314a1177b0bSTobias Hieta            return None
315a1177b0bSTobias Hieta
316a1177b0bSTobias Hieta
31719bc2823SNuno Lopesclass UndefGetFormatHelper(FormatHelper):
31819bc2823SNuno Lopes    name = "undef deprecator"
31919bc2823SNuno Lopes    friendly_name = "undef deprecator"
32019bc2823SNuno Lopes
32119bc2823SNuno Lopes    @property
32219bc2823SNuno Lopes    def instructions(self) -> str:
32319bc2823SNuno Lopes        return " ".join(shlex.quote(c) for c in self.cmd)
32419bc2823SNuno Lopes
32519bc2823SNuno Lopes    def filter_changed_files(self, changed_files: List[str]) -> List[str]:
32619bc2823SNuno Lopes        filtered_files = []
32719bc2823SNuno Lopes        for path in changed_files:
32819bc2823SNuno Lopes            _, ext = os.path.splitext(path)
32919bc2823SNuno Lopes            if ext in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx", ".inc", ".cppm", ".ll"):
33019bc2823SNuno Lopes                filtered_files.append(path)
33119bc2823SNuno Lopes        return filtered_files
33219bc2823SNuno Lopes
33319bc2823SNuno Lopes    def has_tool(self) -> bool:
33419bc2823SNuno Lopes        return True
33519bc2823SNuno Lopes
33619bc2823SNuno Lopes    def pr_comment_text_for_diff(self, diff: str) -> str:
33719bc2823SNuno Lopes        return f"""
33819bc2823SNuno Lopes:warning: {self.name} found issues in your code. :warning:
33919bc2823SNuno Lopes
34019bc2823SNuno Lopes<details>
34119bc2823SNuno Lopes<summary>
34219bc2823SNuno LopesYou can test this locally with the following command:
34319bc2823SNuno Lopes</summary>
34419bc2823SNuno Lopes
34519bc2823SNuno Lopes``````````bash
34619bc2823SNuno Lopes{self.instructions}
34719bc2823SNuno Lopes``````````
34819bc2823SNuno Lopes
34919bc2823SNuno Lopes</details>
35019bc2823SNuno Lopes
35119bc2823SNuno Lopes{diff}
35219bc2823SNuno Lopes"""
35319bc2823SNuno Lopes
35419bc2823SNuno Lopes    def format_run(self, changed_files: List[str], args: FormatArgs) -> Optional[str]:
35519bc2823SNuno Lopes        files = self.filter_changed_files(changed_files)
35619bc2823SNuno Lopes
35719bc2823SNuno Lopes        # Use git to find files that have had a change in the number of undefs
35819bc2823SNuno Lopes        regex = "([^a-zA-Z0-9#_-]undef[^a-zA-Z0-9_-]|UndefValue::get)"
35919bc2823SNuno Lopes        cmd = ["git", "diff", "-U0", "--pickaxe-regex", "-S", regex]
36019bc2823SNuno Lopes
36119bc2823SNuno Lopes        if args.start_rev and args.end_rev:
36219bc2823SNuno Lopes            cmd.append(args.start_rev)
36319bc2823SNuno Lopes            cmd.append(args.end_rev)
36419bc2823SNuno Lopes
36519bc2823SNuno Lopes        cmd += files
36619bc2823SNuno Lopes        self.cmd = cmd
36719bc2823SNuno Lopes
36819bc2823SNuno Lopes        if args.verbose:
36919bc2823SNuno Lopes            print(f"Running: {self.instructions}")
37019bc2823SNuno Lopes
37119bc2823SNuno Lopes        proc = subprocess.run(
37219bc2823SNuno Lopes            cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8"
37319bc2823SNuno Lopes        )
37419bc2823SNuno Lopes        sys.stdout.write(proc.stderr)
37519bc2823SNuno Lopes        stdout = proc.stdout
37619bc2823SNuno Lopes
37719bc2823SNuno Lopes        files = []
37819bc2823SNuno Lopes        # Split the diff so we have one array entry per file.
37919bc2823SNuno Lopes        # Each file is prefixed like:
38019bc2823SNuno Lopes        # diff --git a/file b/file
38119bc2823SNuno Lopes        for file in re.split("^diff --git ", stdout, 0, re.MULTILINE):
382*c84f5a9eSAiden Grossman            # We skip checking in MIR files as undef is a valid token and not
383*c84f5a9eSAiden Grossman            # going away.
384*c84f5a9eSAiden Grossman            if file.endswith(".mir"):
385*c84f5a9eSAiden Grossman                continue
38619bc2823SNuno Lopes            # search for additions of undef
3870c629206SNuno Lopes            if re.search(r"^[+](?!\s*#\s*).*(\bundef\b|UndefValue::get)", file, re.MULTILINE):
38819bc2823SNuno Lopes                files.append(re.match("a/([^ ]+)", file.splitlines()[0])[1])
38919bc2823SNuno Lopes
39019bc2823SNuno Lopes        if not files:
39119bc2823SNuno Lopes            return None
39219bc2823SNuno Lopes
39319bc2823SNuno Lopes        files = "\n".join(" - " + f for f in files)
39419bc2823SNuno Lopes        report = f"""
39519bc2823SNuno LopesThe following files introduce new uses of undef:
39619bc2823SNuno Lopes{files}
39719bc2823SNuno Lopes
39819bc2823SNuno Lopes[Undef](https://llvm.org/docs/LangRef.html#undefined-values) is now deprecated and should only be used in the rare cases where no replacement is possible. For example, a load of uninitialized memory yields `undef`. You should use `poison` values for placeholders instead.
39919bc2823SNuno Lopes
40019bc2823SNuno LopesIn tests, avoid using `undef` and having tests that trigger undefined behavior. If you need an operand with some unimportant value, you can add a new argument to the function and use that instead.
40119bc2823SNuno Lopes
40219bc2823SNuno LopesFor example, this is considered a bad practice:
40319bc2823SNuno Lopes```llvm
40419bc2823SNuno Lopesdefine void @fn() {{
40519bc2823SNuno Lopes  ...
40619bc2823SNuno Lopes  br i1 undef, ...
40719bc2823SNuno Lopes}}
40819bc2823SNuno Lopes```
40919bc2823SNuno Lopes
41019bc2823SNuno LopesPlease use the following instead:
41119bc2823SNuno Lopes```llvm
41219bc2823SNuno Lopesdefine void @fn(i1 %cond) {{
41319bc2823SNuno Lopes  ...
41419bc2823SNuno Lopes  br i1 %cond, ...
41519bc2823SNuno Lopes}}
41619bc2823SNuno Lopes```
41719bc2823SNuno Lopes
41819bc2823SNuno LopesPlease refer to the [Undefined Behavior Manual](https://llvm.org/docs/UndefinedBehavior.html) for more information.
41919bc2823SNuno Lopes"""
42019bc2823SNuno Lopes        if args.verbose:
42119bc2823SNuno Lopes            print(f"error: {self.name} failed")
42219bc2823SNuno Lopes            print(report)
42319bc2823SNuno Lopes        return report
42419bc2823SNuno Lopes
42519bc2823SNuno Lopes
42619bc2823SNuno LopesALL_FORMATTERS = (DarkerFormatHelper(), ClangFormatHelper(), UndefGetFormatHelper())
427a1177b0bSTobias Hieta
428bd3e8eb6STobias Hieta
429bd3e8eb6STobias Hietadef hook_main():
430bd3e8eb6STobias Hieta    # fill out args
431bd3e8eb6STobias Hieta    args = FormatArgs()
432c4aa8384SMehdi Amini    args.verbose = os.getenv("FORMAT_HOOK_VERBOSE", False)
433bd3e8eb6STobias Hieta
434bd3e8eb6STobias Hieta    # find the changed files
435bd3e8eb6STobias Hieta    cmd = ["git", "diff", "--cached", "--name-only", "--diff-filter=d"]
436bd3e8eb6STobias Hieta    proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
437bd3e8eb6STobias Hieta    output = proc.stdout.decode("utf-8")
438bd3e8eb6STobias Hieta    for line in output.splitlines():
439bd3e8eb6STobias Hieta        args.changed_files.append(line)
440bd3e8eb6STobias Hieta
441bd3e8eb6STobias Hieta    failed_fmts = []
442bd3e8eb6STobias Hieta    for fmt in ALL_FORMATTERS:
443bd3e8eb6STobias Hieta        if fmt.has_tool():
444bd3e8eb6STobias Hieta            if not fmt.run(args.changed_files, args):
445bd3e8eb6STobias Hieta                failed_fmts.append(fmt.name)
4462120f574STom Stellard            if fmt.comment:
4472120f574STom Stellard                comments.append(fmt.comment)
448bd3e8eb6STobias Hieta        else:
449bd3e8eb6STobias Hieta            print(f"Couldn't find {fmt.name}, can't check " + fmt.friendly_name.lower())
450bd3e8eb6STobias Hieta
451bd3e8eb6STobias Hieta    if len(failed_fmts) > 0:
452c4aa8384SMehdi Amini        print(
453c4aa8384SMehdi Amini            "Pre-commit format hook failed, rerun with FORMAT_HOOK_VERBOSE=1 environment for verbose output"
454c4aa8384SMehdi Amini        )
455bd3e8eb6STobias Hieta        sys.exit(1)
456bd3e8eb6STobias Hieta
457bd3e8eb6STobias Hieta    sys.exit(0)
458bd3e8eb6STobias Hieta
459bd3e8eb6STobias Hieta
460a1177b0bSTobias Hietaif __name__ == "__main__":
461bd3e8eb6STobias Hieta    script_path = os.path.abspath(__file__)
462bd3e8eb6STobias Hieta    if ".git/hooks" in script_path:
463bd3e8eb6STobias Hieta        hook_main()
464bd3e8eb6STobias Hieta        sys.exit(0)
465bd3e8eb6STobias Hieta
466a1177b0bSTobias Hieta    parser = argparse.ArgumentParser()
467a1177b0bSTobias Hieta    parser.add_argument(
468a1177b0bSTobias Hieta        "--token", type=str, required=True, help="GitHub authentiation token"
469a1177b0bSTobias Hieta    )
470a1177b0bSTobias Hieta    parser.add_argument(
471a1177b0bSTobias Hieta        "--repo",
472a1177b0bSTobias Hieta        type=str,
473a1177b0bSTobias Hieta        default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),
474a1177b0bSTobias Hieta        help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",
475a1177b0bSTobias Hieta    )
476a1177b0bSTobias Hieta    parser.add_argument("--issue-number", type=int, required=True)
477a1177b0bSTobias Hieta    parser.add_argument(
478a1177b0bSTobias Hieta        "--start-rev",
479a1177b0bSTobias Hieta        type=str,
480a1177b0bSTobias Hieta        required=True,
481a1177b0bSTobias Hieta        help="Compute changes from this revision.",
482a1177b0bSTobias Hieta    )
483a1177b0bSTobias Hieta    parser.add_argument(
484a1177b0bSTobias Hieta        "--end-rev", type=str, required=True, help="Compute changes to this revision"
485a1177b0bSTobias Hieta    )
486a1177b0bSTobias Hieta    parser.add_argument(
487a1177b0bSTobias Hieta        "--changed-files",
488a1177b0bSTobias Hieta        type=str,
489a1177b0bSTobias Hieta        help="Comma separated list of files that has been changed",
490a1177b0bSTobias Hieta    )
4912120f574STom Stellard    parser.add_argument(
4922120f574STom Stellard        "--write-comment-to-file",
4932120f574STom Stellard        action="store_true",
4942120f574STom Stellard        help="Don't post comments on the PR, instead write the comments and metadata a file called 'comment'",
4952120f574STom Stellard    )
496a1177b0bSTobias Hieta
497bd3e8eb6STobias Hieta    args = FormatArgs(parser.parse_args())
498a1177b0bSTobias Hieta
499a1177b0bSTobias Hieta    changed_files = []
500a1177b0bSTobias Hieta    if args.changed_files:
501a1177b0bSTobias Hieta        changed_files = args.changed_files.split(",")
502a1177b0bSTobias Hieta
50356cadac8SRyan Prichard    failed_formatters = []
5042120f574STom Stellard    comments = []
505a1177b0bSTobias Hieta    for fmt in ALL_FORMATTERS:
506a1177b0bSTobias Hieta        if not fmt.run(changed_files, args):
50756cadac8SRyan Prichard            failed_formatters.append(fmt.name)
5082120f574STom Stellard        if fmt.comment:
5092120f574STom Stellard            comments.append(fmt.comment)
5102120f574STom Stellard
5112120f574STom Stellard    if len(comments):
5122120f574STom Stellard        with open("comments", "w") as f:
5132120f574STom Stellard            import json
5142120f574STom Stellard
5152120f574STom Stellard            json.dump(comments, f)
516a1177b0bSTobias Hieta
51756cadac8SRyan Prichard    if len(failed_formatters) > 0:
51856cadac8SRyan Prichard        print(f"error: some formatters failed: {' '.join(failed_formatters)}")
51956cadac8SRyan Prichard        sys.exit(1)
520