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 # ==-------------------------------------------------------------------------==#
15 from functools
import cached_property
18 from github
import IssueComment
, PullRequest
22 COMMENT_TAG
= "<!--LLVM CODE FORMAT COMMENT: {fmt}-->"
27 def comment_tag(self
) -> str:
28 return self
.COMMENT_TAG
.replace("fmt", self
.name
)
31 def instructions(self
) -> str:
32 raise NotImplementedError()
35 self
, changed_files
: list[str], args
: argparse
.Namespace
37 raise NotImplementedError()
39 def pr_comment_text_for_diff(self
, diff
: str) -> str:
41 :warning: {self.friendly_name}, {self.name} found issues in your code. :warning:
45 You can test this locally with the following command:
56 View the diff from {self.name} here.
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
:
75 self
, comment_text
: str, args
: argparse
.Namespace
, create_new
: bool
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
)
84 existing_comment
.edit(comment_text
)
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
)
92 :white_check_mark: With the latest revision this PR passed the {self.friendly_name}.
94 self
.update_pr(comment_text
, args
, create_new
=False)
97 comment_text
= self
.pr_comment_text_for_diff(diff
)
98 self
.update_pr(comment_text
, args
, create_new
=True)
101 # The formatter failed but didn't output a diff (e.g. some sort of
102 # infrastructure failure).
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)
110 class ClangFormatHelper(FormatHelper
):
111 name
= "clang-format"
112 friendly_name
= "C/C++ code formatter"
115 def instructions(self
) -> str:
116 return " ".join(self
.cf_cmd
)
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}")
129 def filter_changed_files(self
, changed_files
: list[str]) -> list[str]:
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
139 self
, changed_files
: list[str], args
: argparse
.Namespace
141 cpp_files
= self
.filter_changed_files(changed_files
)
151 print(f
"Running: {' '.join(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")
161 sys
.stdout
.write(proc
.stdout
.decode("utf-8"))
165 class DarkerFormatHelper(FormatHelper
):
167 friendly_name
= "Python code formatter"
170 def instructions(self
) -> str:
171 return " ".join(self
.darker_cmd
)
173 def filter_changed_files(self
, changed_files
: list[str]) -> list[str]:
175 for path
in changed_files
:
176 name
, ext
= os
.path
.splitext(path
)
178 filtered_files
.append(path
)
180 return filtered_files
183 self
, changed_files
: list[str], args
: argparse
.Namespace
185 py_files
= self
.filter_changed_files(changed_files
)
193 f
"{args.start_rev}..{args.end_rev}",
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")
205 sys
.stdout
.write(proc
.stdout
.decode("utf-8"))
209 ALL_FORMATTERS
= (DarkerFormatHelper(), ClangFormatHelper())
211 if __name__
== "__main__":
212 parser
= argparse
.ArgumentParser()
214 "--token", type=str, required
=True, help="GitHub authentiation token"
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)
227 help="Compute changes from this revision.",
230 "--end-rev", type=str, required
=True, help="Compute changes to this revision"
235 help="Comma separated list of files that has been changed",
238 args
= parser
.parse_args()
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)}")