From da03cbeccb4474e374dfc67c1208e1becb403e08 Mon Sep 17 00:00:00 2001 From: Michael Richters Date: Tue, 5 Apr 2022 12:46:42 -0500 Subject: [PATCH] Replace clang-format wrapper script with something more configurable This python script is more configurable than the shell script that it replaces, and makes it clearer when looking at the makefile what it's acting on. Signed-off-by: Michael Richters --- Makefile | 12 +- bin/format-code.py | 271 +++++++++++++++++++++++++++++++++++++++++++++ bin/format-code.sh | 29 ++--- 3 files changed, 289 insertions(+), 23 deletions(-) create mode 100755 bin/format-code.py diff --git a/Makefile b/Makefile index 29a3464f..25246a54 100644 --- a/Makefile +++ b/Makefile @@ -106,10 +106,18 @@ find-filename-conflicts: .PHONY: format check-formatting cpplint cpplint-noisy shellcheck smoke-examples find-filename-conflicts prepare-virtual checkout-platform adjust-git-timestamps docker-bash docker-simulator-tests run-tests simulator-tests setup format: - bin/format-code.sh + bin/format-code.py \ + --exclude-dir 'testing/googletest' \ + --exclude-file 'generated-testcase.cpp' \ + src plugins examples testing check-formatting: - bin/format-code.sh --check + bin/format-code.py \ + --exclude-dir 'testing/googletest' \ + --exclude-file 'generated-testcase.cpp' \ + --check \ + --verbose \ + src plugins examples testing cpplint-noisy: -bin/cpplint.py --filter=-legal/copyright,-build/include,-readability/namespace,-whitespace/line_length,-runtime/references --recursive --extensions=cpp,h,ino src examples diff --git a/bin/format-code.py b/bin/format-code.py new file mode 100755 index 00000000..a71c19ba --- /dev/null +++ b/bin/format-code.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# Copyright (c) 2022 Michael Richters + +# This is free and unencumbered software released into the public domain. + +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. + +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +# For more information, please refer to +# ------------------------------------------------------------------------------ +"""This script runs clang-format on Kaleidoscope's codebase.""" + +import argparse +import glob +import logging +import os +import re +import subprocess +import sys + + +# ============================================================================== +def parse_args(args): + """Parse command line parameters + + Args: + args (List[str]): command line parameters as list of strings + (for example ``["--help"]``). + + Returns: + :obj:`argparse.Namespace`: command line parameters namespace + """ + parser = argparse.ArgumentParser( + description=""" + Recursively search specified directories and format source files with + clang-format. By default, it operates on Arduino C++ source files with + extensions: *.{cpp,h,hpp,inc,ino}.""") + parser.add_argument( + '-v', + '--verbose', + dest='loglevel', + help="Verbose output", + action='store_const', + const=logging.INFO, + ) + parser.add_argument( + '-q', + '--quiet', + dest='loglevel', + help="Suppress all non-error output", + action='store_const', + const=logging.ERROR, + ) + parser.add_argument( + '-X', + '--exclude-dir', + metavar="", + dest='exclude_dirs', + help="Exclude dir from search (path relative to the pwd)", + action='append', + default=[], + ) + parser.add_argument( + '-x', + '--exclude-file', + metavar="", + dest='exclude_files', + help="Exclude (base name only, not a full path) from formatting", + action='append', + default=[], + ) + parser.add_argument( + '-e', + '--regex', + metavar="", + dest='src_re_str', + help="Regular expression for matching source file names", + default=r'\.(cpp|h|hpp|inc|ino)$', + ) + parser.add_argument( + '-f', + '--force', + action='store_true', + help="Format code even if there are unstaged changes", + ) + parser.add_argument( + '--check', + action='store_true', + help="Check for changes after formatting", + ) + parser.add_argument( + 'targets', + metavar="", + nargs='+', + help="""A list of files and/or directories to search for source files to format""", + ) + return parser.parse_args(args) + + +# ============================================================================== +def setup_logging(loglevel): + """Setup basic logging + + Args: + loglevel (int): minimum loglevel for emitting messages + """ + logformat = "%(message)s" + logging.basicConfig( + level=loglevel, + stream=sys.stdout, + format=logformat, + datefmt="", + ) + return logging.getLogger() + + +# ============================================================================== +def format_code(path, opts, clang_format_cmd): + """Run clang-format on a directory.""" + logging.info("Formatting code in %s...", path) + + src_regex = re.compile(opts.src_re_str) + + src_files = [] + + for root, dirs, files in os.walk(path): + for exclude_path in opts.exclude_dirs: + exclude_path = exclude_path.rstrip(os.path.sep) + if os.path.dirname(exclude_path) == root: + exclude_dir = os.path.basename(exclude_path) + if exclude_dir in dirs: + dirs.remove(exclude_dir) + + for name in files: + if name in opts.exclude_files: + continue + if src_regex.search(name): + src_files.append(os.path.join(root, name)) + + proc = subprocess.run(clang_format_cmd + src_files) + if proc.returncode != 0: + logging.error("Error: clang-format returned non-zero status: %s", proc.returncode) + return + + +# ============================================================================== +def build_file_list(path, src_regex): + """Docstring""" + + # If the specified path is a filename, return it (as a list), regardless of + # whether or not it matches the regex. + if os.path.isfile(path): + return [path] + + # If the specified path is not valid, just return an empty list. + if not os.path.isdir(path): + return [] + + # The specified path is a directory, so we search recursively for files + # contained therein that match the specified regular expression. + source_files = [] + for root, dirs, files in os.walk(path): + # First, ignore all dotfiles (and directories). + dotfiles = set(glob.glob('.*')) + dirs = set(dirs) - dotfiles + files = set(dirs) - dotfiles + + # Check for a list of file glob patterns that should be excluded. + if IWYU_IGNORE_FILE in files: + with open(os.path.join(root, IWYU_IGNORE_FILE)) as f: + for pattern in f.read().splitlines(): + matches = set(glob.glob(os.path.join(root, pattern))) + dirs = set(dirs) - matches + files = set(files) - matches + + # Add all matching files to the list of source files to be formatted. + for f in filter(src_regex.search, files): + source_files.append(os.path.join(root, f)) + + return source_files + + +# ============================================================================== +def warn_if_output(byte_str, msg): + """Convert a string of bytes to a UTF-8 string, break it on newlines. If + there is any output, print a warning message, followed by each line, + indented by four spaces.""" + lines = byte_str.decode('utf-8').splitlines() + if '' in lines: + lines.remove('') + if len(lines) > 0: + logging.warning('%s', msg) + for line in lines: + logging.warning(' %s', line) + return + + +# ============================================================================== +def main(cli_args): + """Parse command-line arguments and format source files.""" + args = parse_args(cli_args) + if args.loglevel is None: + args.loglevel = logging.WARNING + setup_logging(args.loglevel) + + clang_format = os.getenv('CLANG_FORMAT_CMD') + if clang_format is None: + clang_format = 'clang-format' + + clang_format_cmd = [clang_format, '-i'] + + git_diff_cmd = ['git', 'diff', '--exit-code'] + + proc = subprocess.run(git_diff_cmd + ['--name-only'], capture_output=True) + if proc.returncode != 0: + warn_if_output(proc.stdout, 'Warning: you have unstaged changes to these files:') + if not args.force: + logging.warning( + 'Formatting aborted. Stage your changes or use --force to override.') + sys.exit(proc.returncode) + + if args.loglevel >= logging.WARNING: + git_diff_cmd.append('--quiet') + elif args.loglevel <= logging.INFO: + git_diff_cmd.append('--name-only') + + for path in args.targets: + format_code(path, args, clang_format_cmd) + + if args.check: + logging.warning('Checking for changes made by the formatter...') + + proc = subprocess.run(git_diff_cmd + ['--cached'], capture_output=True) + if proc.returncode != 0: + logging.warning( + 'Warning: Your working tree has staged changes. ' + + 'Committed changes might not pass this check.') + warn_if_output(proc.stdout, 'The following files have unstaged changes:') + + proc = subprocess.run(git_diff_cmd, capture_output=True) + if proc.returncode != 0: + warn_if_output(proc.stdout, 'The following files have changes:') + logging.error( + 'Check failed: Please commit formatting changes before submitting.') + sys.exit(proc.returncode) + return + + +# ============================================================================== +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/bin/format-code.sh b/bin/format-code.sh index e81dda2e..db0cc137 100755 --- a/bin/format-code.sh +++ b/bin/format-code.sh @@ -1,25 +1,12 @@ #!/usr/bin/env bash -# Allow the caller to specify a particular version of clang-format to use: -: "${CLANG_FORMAT_CMD:=clang-format}" +: "${KALEIDOSCOPE_DIR:=$(pwd)}" +cd "${KALEIDOSCOPE_DIR}" || exit 1 -# Find all *.cpp and *.h files, except those in `testing/googletest/` and files -# generated by testcase scripts, and run `clang-format` on them: -find ./* -type f \( -name '*.h' -o -name '*.cpp' \) \ - -not \( -path './testing/googletest/*' -o -name 'generated-testcase.cpp' \) \ - -print0 | \ - xargs -0 "${CLANG_FORMAT_CMD}" -i +: "${VERBOSE:=}" -# If we get the `--check` option, return an error if there are any changes to -# the git working tree after running `clang-format`: -if [[ $1 == '--check' ]]; then - - if ! git diff --quiet; then - cat >&2 <