-
Notifications
You must be signed in to change notification settings - Fork 15.3k
[Github][CI] Add doc8 for clang-tidy documentation formatting
#168827
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
9552857
8fc6300
3a5c436
27c30ab
75acf3b
09a61e4
f3cb4bb
342cae4
f8a1c23
f553d45
2ded50d
a462bbe
29ebebe
f6eac19
da783fc
8760922
5a5538c
51cb509
c112202
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,7 +30,7 @@ jobs: | |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| with: | ||
| fetch-depth: 2 | ||
|
|
||
| - name: Get changed files | ||
| id: changed-files | ||
| uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0 | ||
|
|
@@ -39,14 +39,14 @@ jobs: | |
| skip_initial_fetch: true | ||
| base_sha: 'HEAD~1' | ||
| sha: 'HEAD' | ||
|
|
||
| - name: Listed files | ||
| env: | ||
| CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} | ||
| run: | | ||
| echo "Changed files:" | ||
| echo "$CHANGED_FILES" | ||
|
|
||
| # TODO: create special mapping for 'codegen' targets, for now build predefined set | ||
| # TODO: add entrypoint in 'compute_projects.py' that only adds a project and its direct dependencies | ||
| - name: Configure and CodeGen | ||
|
|
@@ -71,25 +71,43 @@ jobs: | |
| -DLLVM_INCLUDE_TESTS=OFF \ | ||
| -DCLANG_INCLUDE_TESTS=OFF \ | ||
| -DCMAKE_BUILD_TYPE=Release | ||
|
|
||
| ninja -C build \ | ||
| clang-tablegen-targets \ | ||
| genconfusable # for "ConfusableIdentifierCheck.h" | ||
|
|
||
| - name: Run code linter | ||
| - name: Install linter dependencies | ||
| run: | | ||
| pip install doc8 --break-system-packages | ||
| echo "$HOME/.local/bin" >> $GITHUB_PATH | ||
|
|
||
| - name: Run clang-tidy linter | ||
| env: | ||
| GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} | ||
| run: | | ||
| echo "[]" > comments && | ||
| python3 llvm/utils/git/code-lint-helper.py \ | ||
| --linter clang-tidy \ | ||
| --token ${{ secrets.GITHUB_TOKEN }} \ | ||
| --issue-number $GITHUB_PR_NUMBER \ | ||
| --start-rev HEAD~1 \ | ||
| --end-rev HEAD \ | ||
| --verbose \ | ||
| --changed-files "$CHANGED_FILES" | ||
|
|
||
|
|
||
| - name: Run doc8 linter | ||
| env: | ||
| GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| run: | | ||
| python3 llvm/utils/git/code-lint-helper.py \ | ||
| --linter doc8 \ | ||
| --token ${{ secrets.GITHUB_TOKEN }} \ | ||
| --issue-number $GITHUB_PR_NUMBER \ | ||
| --start-rev HEAD~1 \ | ||
| --end-rev HEAD \ | ||
| --verbose | ||
|
||
|
|
||
| - name: Upload results | ||
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||
| if: always() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,6 +34,8 @@ class LintArgs: | |
| issue_number: int = 0 | ||
| build_path: str = "build" | ||
| clang_tidy_binary: str = "clang-tidy" | ||
| doc8_binary: str = "doc8" | ||
| linter: str = None | ||
|
|
||
| def __init__(self, args: argparse.Namespace = None) -> None: | ||
| if not args is None: | ||
|
|
@@ -46,9 +48,12 @@ def __init__(self, args: argparse.Namespace = None) -> None: | |
| self.verbose = args.verbose | ||
| self.build_path = args.build_path | ||
| self.clang_tidy_binary = args.clang_tidy_binary | ||
| self.doc8_binary = args.doc8_binary | ||
| self.linter = args.linter | ||
|
|
||
|
|
||
| COMMENT_TAG = "<!--LLVM CODE LINT COMMENT: clang-tidy-->" | ||
| COMMENT_TAG_CLANG_TIDY = "<!--LLVM CODE LINT COMMENT: clang-tidy-->" | ||
| COMMENT_TAG_DOC8 = "<!--LLVM CODE LINT COMMENT: doc8-->" | ||
|
|
||
|
|
||
| def get_instructions(cpp_files: List[str]) -> str: | ||
|
|
@@ -135,13 +140,27 @@ def create_comment_text(warning: str, cpp_files: List[str]) -> str: | |
| """ | ||
|
|
||
|
|
||
| def find_comment(pr: any) -> any: | ||
| def find_comment(pr: any, args: LintArgs) -> any: | ||
| linter_tag = get_comment_tag(args.linter) | ||
| other_linter = "doc8" if args.linter == "clang-tidy" else "clang-tidy" | ||
| other_tag = get_comment_tag(other_linter) | ||
|
|
||
| for comment in pr.as_issue().get_comments(): | ||
| if COMMENT_TAG in comment.body: | ||
| body = comment.body | ||
| if linter_tag in body and other_tag not in body: | ||
| # Found a comment that is exclusively for this linter. | ||
| return comment | ||
| return None | ||
|
|
||
|
|
||
| def get_comment_tag(linter: str) -> str: | ||
| if linter == "clang-tidy": | ||
| return COMMENT_TAG_CLANG_TIDY | ||
| elif linter == "doc8": | ||
| return COMMENT_TAG_DOC8 | ||
| raise ValueError(f"Unknown linter: {linter}") | ||
|
|
||
|
|
||
| def create_comment( | ||
| comment_text: str, args: LintArgs, create_new: bool | ||
| ) -> Optional[dict]: | ||
|
|
@@ -150,9 +169,10 @@ def create_comment( | |
| repo = github.Github(args.token).get_repo(args.repo) | ||
| pr = repo.get_issue(args.issue_number).as_pull_request() | ||
|
|
||
| comment_text = COMMENT_TAG + "\n\n" + comment_text | ||
| comment_tag = get_comment_tag(args.linter) | ||
| comment_text = comment_tag + "\n\n" + comment_text | ||
|
|
||
| existing_comment = find_comment(pr) | ||
| existing_comment = find_comment(pr, args) | ||
|
|
||
| comment = None | ||
| if create_new or existing_comment: | ||
|
|
@@ -215,7 +235,125 @@ def run_clang_tidy(changed_files: List[str], args: LintArgs) -> Optional[str]: | |
| return clean_clang_tidy_output(proc.stdout.strip()) | ||
|
|
||
|
|
||
| def run_linter(changed_files: List[str], args: LintArgs) -> tuple[bool, Optional[dict]]: | ||
| def clean_doc8_output(output: str) -> Optional[str]: | ||
| if not output: | ||
| return None | ||
|
|
||
| lines = output.split("\n") | ||
| cleaned_lines = [] | ||
| in_summary = False | ||
|
|
||
| for line in lines: | ||
| if line.startswith("Scanning...") or line.startswith("Validating..."): | ||
| continue | ||
| if line.startswith("========"): | ||
| in_summary = True | ||
| continue | ||
| if in_summary: | ||
| continue | ||
| if line.strip(): | ||
| cleaned_lines.append(line) | ||
vbvictor marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| if cleaned_lines: | ||
| return "\n".join(cleaned_lines) | ||
| return None | ||
|
|
||
|
|
||
| def get_doc8_instructions() -> str: | ||
| # TODO: use git diff | ||
| return "doc8 ./clang-tools-extra/docs/clang-tidy/checks/" | ||
|
||
|
|
||
|
|
||
| def create_doc8_comment_text(doc8_output: str) -> str: | ||
| instructions = get_doc8_instructions() | ||
| return f""" | ||
| :warning: Documentation linter doc8 found issues in your code. :warning: | ||
|
|
||
| <details> | ||
| <summary> | ||
| You can test this locally with the following command: | ||
| </summary> | ||
|
|
||
| ```bash | ||
| {instructions} | ||
| ``` | ||
|
|
||
| </details> | ||
|
|
||
| <details> | ||
| <summary> | ||
| View the output from doc8 here. | ||
| </summary> | ||
|
|
||
| ``` | ||
| {doc8_output} | ||
| ``` | ||
|
|
||
| </details> | ||
| """ | ||
|
|
||
|
|
||
| def run_doc8(args: LintArgs) -> tuple[int, Optional[str]]: | ||
| doc8_cmd = [args.doc8_binary, "./clang-tools-extra/docs/clang-tidy/checks/"] | ||
|
|
||
| if args.verbose: | ||
| print(f"Running doc8: {' '.join(doc8_cmd)}") | ||
|
|
||
| proc = subprocess.run( | ||
| doc8_cmd, | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE, | ||
| text=True, | ||
| check=False, | ||
| ) | ||
|
|
||
| cleaned_output = clean_doc8_output(proc.stdout.strip()) | ||
| if proc.returncode != 0 and cleaned_output is None: | ||
| # Infrastructure failure | ||
| return proc.returncode, proc.stderr.strip() | ||
|
|
||
| return proc.returncode, cleaned_output | ||
|
|
||
|
|
||
| def run_doc8_linter(args: LintArgs) -> tuple[bool, Optional[dict]]: | ||
| returncode, result = run_doc8(args) | ||
| should_update_gh = args.token is not None and args.repo is not None | ||
| comment = None | ||
|
|
||
| if returncode == 0: | ||
| if should_update_gh: | ||
| comment_text = ( | ||
| ":white_check_mark: With the latest revision " | ||
| "this PR passed the documentation linter." | ||
| ) | ||
| comment = create_comment(comment_text, args, create_new=False) | ||
| return True, comment | ||
| else: | ||
| if should_update_gh: | ||
| if result: | ||
| comment_text = create_doc8_comment_text(result) | ||
| comment = create_comment(comment_text, args, create_new=True) | ||
| else: | ||
| comment_text = ( | ||
| ":warning: The documentation linter failed without printing " | ||
| "an output. Check the logs for output. :warning:" | ||
| ) | ||
| comment = create_comment(comment_text, args, create_new=False) | ||
| else: | ||
| if result: | ||
| print( | ||
| "Warning: Documentation linter, doc8 detected " | ||
| "some issues with your code..." | ||
| ) | ||
| print(result) | ||
| else: | ||
| print("Warning: Documentation linter, doc8 failed to run.") | ||
| return False, comment | ||
|
|
||
|
|
||
| def run_clang_tidy_linter( | ||
| changed_files: List[str], args: LintArgs | ||
| ) -> tuple[bool, Optional[dict]]: | ||
| changed_files = [arg for arg in changed_files if "third-party" not in arg] | ||
|
|
||
| cpp_files = filter_changed_files(changed_files) | ||
|
|
@@ -255,6 +393,13 @@ def run_linter(changed_files: List[str], args: LintArgs) -> tuple[bool, Optional | |
|
|
||
| if __name__ == "__main__": | ||
| parser = argparse.ArgumentParser() | ||
| parser.add_argument( | ||
| "--linter", | ||
| type=str, | ||
| choices=["clang-tidy", "doc8"], | ||
|
||
| required=True, | ||
| help="The linter to run.", | ||
| ) | ||
| parser.add_argument( | ||
| "--token", type=str, required=True, help="GitHub authentication token" | ||
| ) | ||
|
|
@@ -291,39 +436,57 @@ def run_linter(changed_files: List[str], args: LintArgs) -> tuple[bool, Optional | |
| default="clang-tidy", | ||
| help="Path to clang-tidy binary", | ||
| ) | ||
| parser.add_argument( | ||
| "--doc8-binary", | ||
| type=str, | ||
| default="doc8", | ||
| help="Path to doc8 binary", | ||
| ) | ||
| parser.add_argument( | ||
| "--verbose", action="store_true", default=True, help="Verbose output" | ||
| ) | ||
|
|
||
| parsed_args = parser.parse_args() | ||
| args = LintArgs(parsed_args) | ||
|
|
||
| changed_files = [] | ||
| if args.changed_files: | ||
| changed_files = args.changed_files.split(",") | ||
|
|
||
| if args.verbose: | ||
| print(f"got changed files: {changed_files}") | ||
|
|
||
| if args.verbose: | ||
| print("running linter clang-tidy") | ||
| print(f"running linter {args.linter}") | ||
|
|
||
| success, comment = run_linter(changed_files, args) | ||
| success, comment = False, None | ||
| if args.linter == "clang-tidy": | ||
| changed_files = [] | ||
| if args.changed_files: | ||
| changed_files = args.changed_files.split(",") | ||
| if args.verbose: | ||
| print(f"got changed files: {changed_files}") | ||
| success, comment = run_clang_tidy_linter(changed_files, args) | ||
| elif args.linter == "doc8": | ||
| success, comment = run_doc8_linter(args) | ||
|
|
||
| if not success: | ||
| if args.verbose: | ||
| print("linter clang-tidy failed") | ||
| print(f"linter {args.linter} failed") | ||
|
|
||
| # Write comments file if we have a comment | ||
| if comment: | ||
| import json | ||
| if args.verbose: | ||
| print(f"linter clang-tidy has comment: {comment}") | ||
| print(f"linter {args.linter} has comment: {comment}") | ||
|
|
||
| with open("comments", "w") as f: | ||
| import json | ||
| existing_comments = [] | ||
| if os.path.exists("comments"): | ||
| with open("comments", "r") as f: | ||
| try: | ||
| existing_comments = json.load(f) | ||
| except json.JSONDecodeError: | ||
| # File might be empty or invalid, start fresh | ||
| pass | ||
|
|
||
| json.dump([comment], f) | ||
| existing_comments.append(comment) | ||
|
|
||
| with open("comments", "w") as f: | ||
| json.dump(existing_comments, f) | ||
|
|
||
| if not success: | ||
| print("error: some linters failed: clang-tidy") | ||
| print(f"error: linter {args.linter} failed") | ||
| sys.exit(1) | ||
Uh oh!
There was an error while loading. Please reload this page.