Run DCE after a LoopFlatten test to reduce spurious output [nfc]
[llvm-project.git] / llvm / utils / git / code-format-helper.py
blobc45bb4d935d478a9e0f0643b6ba3a2427ceb97b6
1 #!/usr/bin/env python3
3 # ====- code-format-helper, runs code formatters from the ci --*- python -*--==#
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-exception
9 # ==-------------------------------------------------------------------------==#
11 import argparse
12 import os
13 import subprocess
14 import sys
15 from functools import cached_property
17 import github
18 from github import IssueComment, PullRequest
21 class FormatHelper:
22 COMMENT_TAG = "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
23 name: str
24 friendly_name: str
26 @property
27 def comment_tag(self) -> str:
28 return self.COMMENT_TAG.replace("fmt", self.name)
30 @property
31 def instructions(self) -> str:
32 raise NotImplementedError()
34 def format_run(
35 self, changed_files: list[str], args: argparse.Namespace
36 ) -> str | None:
37 raise NotImplementedError()
39 def pr_comment_text_for_diff(self, diff: str) -> str:
40 return f"""
41 :warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
43 <details>
44 <summary>
45 You can test this locally with the following command:
46 </summary>
48 ``````````bash
49 {self.instructions}
50 ``````````
52 </details>
54 <details>
55 <summary>
56 View the diff from {self.name} here.
57 </summary>
59 ``````````diff
60 {diff}
61 ``````````
63 </details>
64 """
66 def find_comment(
67 self, pr: PullRequest.PullRequest
68 ) -> IssueComment.IssueComment | None:
69 for comment in pr.as_issue().get_comments():
70 if self.comment_tag in comment.body:
71 return comment
72 return None
74 def update_pr(
75 self, comment_text: str, args: argparse.Namespace, create_new: bool
76 ) -> None:
77 repo = github.Github(args.token).get_repo(args.repo)
78 pr = repo.get_issue(args.issue_number).as_pull_request()
80 comment_text = self.comment_tag + "\n\n" + comment_text
82 existing_comment = self.find_comment(pr)
83 if existing_comment:
84 existing_comment.edit(comment_text)
85 elif create_new:
86 pr.as_issue().create_comment(comment_text)
88 def run(self, changed_files: list[str], args: argparse.Namespace) -> bool:
89 diff = self.format_run(changed_files, args)
90 if diff is None:
91 comment_text = f"""
92 :white_check_mark: With the latest revision this PR passed the {self.friendly_name}.
93 """
94 self.update_pr(comment_text, args, create_new=False)
95 return True
96 elif len(diff) > 0:
97 comment_text = self.pr_comment_text_for_diff(diff)
98 self.update_pr(comment_text, args, create_new=True)
99 return False
100 else:
101 # The formatter failed but didn't output a diff (e.g. some sort of
102 # infrastructure failure).
103 comment_text = f"""
104 :warning: The {self.friendly_name} failed without printing a diff. Check the logs for stderr output. :warning:
106 self.update_pr(comment_text, args, create_new=False)
107 return False
110 class ClangFormatHelper(FormatHelper):
111 name = "clang-format"
112 friendly_name = "C/C++ code formatter"
114 @property
115 def instructions(self) -> str:
116 return " ".join(self.cf_cmd)
118 @cached_property
119 def libcxx_excluded_files(self) -> list[str]:
120 with open("libcxx/utils/data/ignore_format.txt", "r") as ifd:
121 return [excl.strip() for excl in ifd.readlines()]
123 def should_be_excluded(self, path: str) -> bool:
124 if path in self.libcxx_excluded_files:
125 print(f"{self.name}: Excluding file {path}")
126 return True
127 return False
129 def filter_changed_files(self, changed_files: list[str]) -> list[str]:
130 filtered_files = []
131 for path in changed_files:
132 _, ext = os.path.splitext(path)
133 if ext in (".cpp", ".c", ".h", ".hpp", ".hxx", ".cxx"):
134 if not self.should_be_excluded(path):
135 filtered_files.append(path)
136 return filtered_files
138 def format_run(
139 self, changed_files: list[str], args: argparse.Namespace
140 ) -> str | None:
141 cpp_files = self.filter_changed_files(changed_files)
142 if not cpp_files:
143 return None
144 cf_cmd = [
145 "git-clang-format",
146 "--diff",
147 args.start_rev,
148 args.end_rev,
149 "--",
150 ] + cpp_files
151 print(f"Running: {' '.join(cf_cmd)}")
152 self.cf_cmd = cf_cmd
153 proc = subprocess.run(cf_cmd, capture_output=True)
154 sys.stdout.write(proc.stderr.decode("utf-8"))
156 if proc.returncode != 0:
157 # formatting needed, or the command otherwise failed
158 print(f"error: {self.name} exited with code {proc.returncode}")
159 return proc.stdout.decode("utf-8")
160 else:
161 sys.stdout.write(proc.stdout.decode("utf-8"))
162 return None
165 class DarkerFormatHelper(FormatHelper):
166 name = "darker"
167 friendly_name = "Python code formatter"
169 @property
170 def instructions(self) -> str:
171 return " ".join(self.darker_cmd)
173 def filter_changed_files(self, changed_files: list[str]) -> list[str]:
174 filtered_files = []
175 for path in changed_files:
176 name, ext = os.path.splitext(path)
177 if ext == ".py":
178 filtered_files.append(path)
180 return filtered_files
182 def format_run(
183 self, changed_files: list[str], args: argparse.Namespace
184 ) -> str | None:
185 py_files = self.filter_changed_files(changed_files)
186 if not py_files:
187 return None
188 darker_cmd = [
189 "darker",
190 "--check",
191 "--diff",
192 "-r",
193 f"{args.start_rev}..{args.end_rev}",
194 ] + py_files
195 print(f"Running: {' '.join(darker_cmd)}")
196 self.darker_cmd = darker_cmd
197 proc = subprocess.run(darker_cmd, capture_output=True)
198 sys.stdout.write(proc.stderr.decode("utf-8"))
200 if proc.returncode != 0:
201 # formatting needed, or the command otherwise failed
202 print(f"error: {self.name} exited with code {proc.returncode}")
203 return proc.stdout.decode("utf-8")
204 else:
205 sys.stdout.write(proc.stdout.decode("utf-8"))
206 return None
209 ALL_FORMATTERS = (DarkerFormatHelper(), ClangFormatHelper())
211 if __name__ == "__main__":
212 parser = argparse.ArgumentParser()
213 parser.add_argument(
214 "--token", type=str, required=True, help="GitHub authentiation token"
216 parser.add_argument(
217 "--repo",
218 type=str,
219 default=os.getenv("GITHUB_REPOSITORY", "llvm/llvm-project"),
220 help="The GitHub repository that we are working with in the form of <owner>/<repo> (e.g. llvm/llvm-project)",
222 parser.add_argument("--issue-number", type=int, required=True)
223 parser.add_argument(
224 "--start-rev",
225 type=str,
226 required=True,
227 help="Compute changes from this revision.",
229 parser.add_argument(
230 "--end-rev", type=str, required=True, help="Compute changes to this revision"
232 parser.add_argument(
233 "--changed-files",
234 type=str,
235 help="Comma separated list of files that has been changed",
238 args = parser.parse_args()
240 changed_files = []
241 if args.changed_files:
242 changed_files = args.changed_files.split(",")
244 failed_formatters = []
245 for fmt in ALL_FORMATTERS:
246 if not fmt.run(changed_files, args):
247 failed_formatters.append(fmt.name)
249 if len(failed_formatters) > 0:
250 print(f"error: some formatters failed: {' '.join(failed_formatters)}")
251 sys.exit(1)