From ab3d528bfeca90275691a2141f796ce697fba011 Mon Sep 17 00:00:00 2001 From: Mats Nilsson Date: Wed, 12 Feb 2025 14:12:20 +0100 Subject: [PATCH] Add clang-format and ruff --- .clang-format | 21 ++- .github/workflows/clang-format.yaml | 45 ----- .github/workflows/code-checks.yaml | 12 +- .github/workflows/format.yml | 19 +++ format.py | 248 ++++++++++++++++++++++++++++ 5 files changed, 289 insertions(+), 56 deletions(-) delete mode 100644 .github/workflows/clang-format.yaml create mode 100644 .github/workflows/format.yml create mode 100755 format.py diff --git a/.clang-format b/.clang-format index 9d159247d..1c6125f2f 100644 --- a/.clang-format +++ b/.clang-format @@ -1,2 +1,19 @@ -DisableFormat: true -SortIncludes: false +BasedOnStyle: Mozilla +InsertNewlineAtEOF: true +UseTab: Always +IndentWidth: 4 +TabWidth: 4 +ColumnLimit: 120 +AccessModifierOffset: -4 +AlignConsecutiveAssignments: Consecutive +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: Empty +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +BraceWrapping: + SplitEmptyFunction: false +BreakBeforeBraces: Custom +BreakConstructorInitializers: AfterColon +Cpp11BracedListStyle: true +FixNamespaceComments: true +PackConstructorInitializers: Never diff --git a/.github/workflows/clang-format.yaml b/.github/workflows/clang-format.yaml deleted file mode 100644 index 87ca862a8..000000000 --- a/.github/workflows/clang-format.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: clang-tidy-review - -# You can be more specific, but it currently only works on pull requests -on: - workflow_call: - inputs: - target_files: - description: 'The list of files to be checked by clang-tidy' - required: true - default: '' - type: string - -jobs: - clang-format: - runs-on: ubuntu-latest - container: ros:humble - steps: - - name: Check out repository - uses: actions/checkout@v4 - with: - submodules: recursive - - - name: Install clang-format - run: | - sudo apt-get -yqq update - sudo apt-get -yqq install clang-format - shell: bash - - - name: Run clang-format style check. - id: clang-format - env: - ALL_CHANGED_FILES: ${{ inputs.target_files }} - run: | - mkdir /tmp/clang-format-result - for file_path in ${ALL_CHANGED_FILES}; do - folder_name="${file_path%/*}/" - mkdir -p /tmp/clang-format-result/$folder_name - clang-format ${file_path} > /tmp/clang-format-result/${file_path} || true - done - - - name: Upload clang-format result - uses: actions/upload-artifact@v4 - with: - name: clang-format-result - path: /tmp/clang-format-result/ diff --git a/.github/workflows/code-checks.yaml b/.github/workflows/code-checks.yaml index af98cabd9..242e05d8c 100644 --- a/.github/workflows/code-checks.yaml +++ b/.github/workflows/code-checks.yaml @@ -8,7 +8,7 @@ jobs: file-changes: runs-on: ubuntu-latest - outputs: + outputs: all: ${{ steps.changed-files.outputs.all }} cpp: ${{ steps.cpp-files.outputs.CPP }} steps: @@ -37,12 +37,12 @@ jobs: done echo "Target files: $cpp_files" - { + { echo 'CPP<> $GITHUB_OUTPUT - + - name: Print all files run: | echo ${{ steps.cpp-files.outputs.CPP }} @@ -52,12 +52,6 @@ jobs: needs: [build, file-changes] with: target_files: ${{ needs.file-changes.outputs.cpp }} - - clang-format: - uses: ./.github/workflows/clang-format.yaml - needs: [file-changes] - with: - target_files: ${{ needs.file-changes.outputs.cpp }} cppcheck: uses: ./.github/workflows/cppcheck.yaml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 000000000..8834ce72c --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,19 @@ +name: format + +on: + pull_request: + branches: [ "dev" ] + +jobs: + build: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch + run: git fetch origin dev + + - name: CheckFormat + run: ./format.py --check diff --git a/format.py b/format.py new file mode 100755 index 000000000..b5a74aafd --- /dev/null +++ b/format.py @@ -0,0 +1,248 @@ +#!/usr/bin/python3 +# +# To enable formatting in VS Code install the following extensions: +# $ code --install-extension charliermarsh.ruff && code --install-extension xaver.clang-format +# Add the following to ~/.config/Code/User/settings.json +# +# "[python]": { +# "editor.defaultFormatter": "charliermarsh.ruff", +# "editor.codeActionsOnSave": { +# "source.fixAll": "always", +# } +# }, +# "ruff.path": [ +# "/home//.cache/ruff-x86_64-unknown-linux-gnu/ruff" +# ], +# "ruff.lint.extendSelect": [ +# "I" +# ], +# "editor.formatOnSave": true, +# "clang-format.executable": "/home//.cache/clang-format", +# "files.insertFinalNewline": true, +# "files.trimFinalNewlines": true, +# "files.trimTrailingWhitespace": true, +# +# If you have problems: +# 1. make sure you have no other extensions trying to format cpp files. +# 2. you have selected a Python interpreter. + +import argparse +import hashlib +import json +import os +import stat +import subprocess +import sys +import tarfile +from pathlib import Path + +import requests + + +def sha256sum(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def download_if_incorrect_hash(path, url, sha256): + updated_file = False + path.parent.mkdir(parents=True, exist_ok=True) + if (not path.is_file()) or (sha256sum(path) != sha256): + r = requests.get(url, allow_redirects=True) + with open(path, "wb") as f: + f.write(r.content) + updated_file = True + assert sha256sum(path) == sha256 + return updated_file + + +def download_and_unpack_if_incorrect_hash(path, url, sha256): + if download_if_incorrect_hash(path, url, sha256): + with tarfile.open(path, "r:gz") as f: + f.extractall(path=path.parent) + + +def make_file_runnable(path): + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + + +def is_git_index_dirty(): + p = subprocess.run( + ["git", "status", "-s"], + text=True, + stdout=subprocess.PIPE, + check=True, + ) + files = [line for line in p.stdout.splitlines() if "M " in line or "A " in line] + return len(files) + + +def git_touched_files(): + p = subprocess.run( + ["git", "merge-base", "HEAD", "origin/dev"], + text=True, + check=True, + stdout=subprocess.PIPE, + ) + d = subprocess.run( + ["git", "diff", "--name-only", f"{p.stdout.strip()}..HEAD"], + text=True, + check=True, + stdout=subprocess.PIPE, + ) + return d.stdout.splitlines() + + +def git_ls_files(): + p = subprocess.run( + ["git", "ls-files"], + text=True, + check=True, + stdout=subprocess.PIPE, + ) + + return p.stdout.splitlines() + + +def cache_folder(): + return Path("~/.cache/rise").expanduser() + + +def clang_format(files, check): + cpp_files = [f for f in files if "." in f and f.rsplit(".")[1] in ["hpp", "cpp"]] + if len(cpp_files) == 0: + return [] + + path = cache_folder() / "clang-format" + url = "https://github.com/cpp-linter/clang-tools-static-binaries/releases/download/master-67c95218/clang-format-19_linux-amd64" + sha256 = "6ede4977469da4325bb7109916e41c067f9e01fa3bddd3c90090c8609d8e364e" + + download_if_incorrect_hash(path, url, sha256) + make_file_runnable(path) + + problematic_files = [] + p = subprocess.run( + [path, "--Werror", "--dry-run", *cpp_files], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + problematic_files = list( + set( + line.split(":")[0] + for line in p.stderr.splitlines() + if "-Wclang-format-violations" in line + ) + ) + + if len(problematic_files) > 0 and not check: + p = subprocess.run( + [path, "-i", "--verbose", *problematic_files], + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + return problematic_files + + +def ruff(files, check): + files = [f for f in files if "." in f and f.rsplit(".")[1] == "py"] + if len(files) == 0: + return [] + + tar_path = cache_folder() / "ruff-x86_64-unknown-linux-gnu.tar.gz" + url = "https://github.com/astral-sh/ruff/releases/download/0.9.6/ruff-x86_64-unknown-linux-gnu.tar.gz" + sha256 = "bed850f15d4d5aaaef2b6a131bfecd5b9d7d3191596249d07e576bd9fd37078e" + download_and_unpack_if_incorrect_hash(tar_path, url, sha256) + ruff_binary = cache_folder() / "ruff-x86_64-unknown-linux-gnu/ruff" + make_file_runnable(ruff_binary) + + format_cmd = [ruff_binary, "format"] + p = subprocess.run( + [*format_cmd, "--check", *files], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + unformatted_files = [ + line.replace("Would reformat: ", "") + for line in p.stdout.splitlines() + if "Would reformat" in line + ] + if len(unformatted_files) > 0 and not check: + p = subprocess.run( + [*format_cmd, *unformatted_files], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + lint_cmd = [ruff_binary, "check", "--extend-select=I"] + p = subprocess.run( + [*lint_cmd, "--output-format=json", *files], + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + problematic_files = [ + str(Path(f["filename"]).relative_to(Path.cwd())) for f in json.loads(p.stdout) + ] + + if len(problematic_files) == 0: + return unformatted_files + + if not check: + lint_cmd.append("--fix") + subprocess.run([*lint_cmd, *files], text=True) + + return list(set(problematic_files) | set(unformatted_files)) + + +def filter_files_to_ignore(files): + ignore = ("atos/modules/ObjectControl/inc/sml.hpp",) + return [f for f in files if f not in ignore] + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--check", action="store_true", help="Don't apply any changes.") + parser.add_argument("--all-files", action="store_true", help="Format all files.") + + args = parser.parse_args() + + if is_git_index_dirty(): + print( + "git index contain unstaged files, add and commit your changes before attempting linting." + ) + return 1 + + if args.all_files: + files = filter_files_to_ignore(git_ls_files()) + else: + files = filter_files_to_ignore(git_touched_files()) + + problematic_files = [] + problematic_files.extend(ruff(files, args.check)) + problematic_files.extend(clang_format(files, args.check)) + + if len(problematic_files) > 0: + if args.check: + print( + "\n\nThe following files needs formatting and/or contains problems:\n", + "\n ".join(sorted(problematic_files)), + ) + else: + print( + "\n\nThe following files were formated and/or contains problems:\n", + "\n ".join(sorted(problematic_files)), + ) + return 1 + else: + return 0 + + +if __name__ == "__main__": + sys.exit(main())