From ba4576d4907e1f3df5a94e441b07c42968bd43ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Tue, 14 Nov 2023 10:57:49 +0100 Subject: [PATCH] ci: Add uncrustify config and checker Based on what we added to phoc recently --- .gitlab-ci/check-style.py | 169 ++++++++++++++++++++++++++++++++++++++ .gitlab-ci/uncrustify.cfg | 109 ++++++++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100755 .gitlab-ci/check-style.py create mode 100644 .gitlab-ci/uncrustify.cfg diff --git a/.gitlab-ci/check-style.py b/.gitlab-ci/check-style.py new file mode 100755 index 0000000..454ddca --- /dev/null +++ b/.gitlab-ci/check-style.py @@ -0,0 +1,169 @@ +#!/bin/env python3 +# +# Based on check-style.py by +# Carlos Garnacho + +import argparse +import os +import re +import subprocess +import sys +import tempfile + +# Path relative to this script +uncrustify_cfg = '.gitlab-ci/uncrustify.cfg' + + +def run_diff(sha): + proc = subprocess.run( + ["git", "diff", "-U0", "--function-context", sha, "HEAD"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + ) + return proc.stdout.strip().splitlines() + + +def find_chunks(diff): + file_entry_re = re.compile(r'^\+\+\+ b/(.*)$') + diff_chunk_re = re.compile(r'^@@ -\d+,\d+ \+(\d+),(\d+)') + file = None + chunks = [] + + for line in diff: + match = file_entry_re.match(line) + if match: + file = match.group(1) + + match = diff_chunk_re.match(line) + if match: + start = int(match.group(1)) + len = int(match.group(2)) + end = start + len + + if len > 0 and (file.endswith('.c') or file.endswith('.h') or file.endswith('.vala')): + chunks.append({'file': file, 'start': start, 'end': end}) + + return chunks + + +def reformat_chunks(chunks, rewrite, dry_run): + # Creates temp file with INDENT-ON/OFF comments + def create_temp_file(file, start, end): + with open(file) as f: + tmp = tempfile.NamedTemporaryFile() + if start > 1: + tmp.write(b'/** *INDENT-OFF* **/\n') + for i, line in enumerate(f, start=1): + if i == start - 1: + tmp.write(b'/** *INDENT-ON* **/\n') + + tmp.write(bytes(line, 'utf-8')) + if i == end - 1: + tmp.write(b'/** *INDENT-OFF* **/\n') + tmp.seek(0) + return tmp + + # Removes uncrustify INDENT-ON/OFF helper comments + def remove_indent_comments(output): + tmp = tempfile.NamedTemporaryFile() + for line in output: + if line != b'/** *INDENT-OFF* **/\n' and line != b'/** *INDENT-ON* **/\n': + tmp.write(line) + + tmp.seek(0) + return tmp + + changed = None + for chunk in chunks: + # Add INDENT-ON/OFF comments + tmp = create_temp_file(chunk['file'], chunk['start'], chunk['end']) + + # uncrustify chunk + proc = subprocess.run( + ["uncrustify", "-c", uncrustify_cfg, "-f", tmp.name], + stdout=subprocess.PIPE, + ) + reindented = proc.stdout.splitlines(keepends=True) + if proc.returncode != 0: + continue + + tmp.close() + + # Remove INDENT-ON/OFF comments + formatted = remove_indent_comments(reindented) + + if dry_run is True: + # Show changes + proc = subprocess.run( + ["diff", "-up", "--color=always", chunk['file'], formatted.name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + ) + diff = proc.stdout + if diff != '': + output = re.sub('\t', '↦\t', diff) + print(output) + changed = True + else: + # Apply changes + diff = subprocess.run( + ["diff", "-up", chunk['file'], formatted.name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + subprocess.run(["patch", chunk['file']], input=diff.stdout) + + formatted.close() + + return changed + + +def main(argv): + parser = argparse.ArgumentParser(description='Check code style. Needs uncrustify installed.') + parser.add_argument('--sha', metavar='SHA', type=str, + help='SHA for the commit to compare HEAD with') + parser.add_argument('--dry-run', '-d', type=bool, + action=argparse.BooleanOptionalAction, + help='Only print changes to stdout, do not change code') + parser.add_argument('--rewrite', '-r', type=bool, + action=argparse.BooleanOptionalAction, + help='Whether to amend the result to the last commit (e.g. \'git rebase --exec "%(prog)s -r"\')') + + if not os.path.exists('.git'): + print("Not in toplevel of a git repository", fille=sys.stderr) + return 1 + + args = parser.parse_args(argv) + sha = args.sha or 'HEAD^' + + diff = run_diff(sha) + chunks = find_chunks(diff) + changed = reformat_chunks(chunks, args.rewrite, args.dry_run) + + if args.dry_run is not True and args.rewrite is True: + proc = subprocess.run(["git", "add", "-p"]) + if proc.returncode == 0: + # Commit the added changes as a squash commit + subprocess.run( + ["git", "commit", "--squash", "HEAD", "-C", "HEAD"], + stdout=subprocess.DEVNULL) + # Delete the unapplied changes + subprocess.run(["git", "reset", "--hard"], stdout=subprocess.DEVNULL) + return 0 + elif args.dry_run is True and changed is True: + print(f""" +Issue the following commands in your local tree to apply the suggested changes: + + $ git rebase {sha} --exec "./.gitlab-ci/check-style.py -r" + $ git rebase --autosquash {sha} + +Don't trust uncrustify unconditionally. +""") + return 1 + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/.gitlab-ci/uncrustify.cfg b/.gitlab-ci/uncrustify.cfg new file mode 100644 index 0000000..50be00b --- /dev/null +++ b/.gitlab-ci/uncrustify.cfg @@ -0,0 +1,109 @@ +# Indent by two spaces +indent_columns = 2 +# No tabs +indent_with_tabs = 0 +# Line length +code_width = 100 + +# +# Keywords and operators +# +# Add between 'do' and '{'. +sp_do_brace_open = add +# Add space between '}' and 'while'. +sp_brace_close_while = add +# Add 'while' and '('. +sp_while_paren_open = add +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = add +# Ternary operator +sp_cond_ternary_short = remove +# Remove newline between 'struct and '{'. +nl_struct_brace = remove +# Remove newline between 'if' and '{'. +nl_if_brace = remove +# Remove newline between '}' and 'else'. +nl_brace_else = remove +# Remove newline between 'else' and '{'. +nl_else_brace = remove +# Remove newline between 'else' and 'if'. +nl_else_if = remove +# Add or remove newline between 'for' and '{'. +nl_for_brace = remove +# Treat iterators as for loops: +set FOR wl_list_for_each wl_list_for_each_reverse wl_list_for_each_safe + +# +# Function declarations, defintions and calls +# +# Add space between function name and '(' on function declaration. +sp_func_proto_paren = add +# Add or remove space between function name and '()' on function declaration +# without parameters. +sp_func_proto_paren_empty = add +# Add space between function name and '(' on function definition. +sp_func_def_paren = add +# Add or remove space between function name and '(' on function calls. +sp_func_call_paren = add +# Whether to force indentation of function definitions to start in column 1. +indent_func_def_force_col1 = true +# Add newline between return type and function name in a function definition. +nl_func_type_name = add +# Add newline between function signature and '{'. +nl_fdef_brace = add +# Whether to align variable definitions in prototypes and functions. +align_func_params = true +# The span for aligning function prototypes. +align_func_proto_span = 8 +# Add space between 'decltype(...)' and word. +sp_after_decltype = add +# Add or remove space after a pointer star '*', if followed by a function +# prototype or function definition. +sp_after_ptr_star_func = remove +# Add or remove newline between a function call's ')' and '{', as in +# 'list_for_each(item, &list) { }'. +nl_fcall_brace = add + +# +# Typedefs +# +# Add space between '}' and the name of a typedef on the same line. +sp_brace_typedef = add + +# +# Comments +# +# Add space after the opening of a C++ comment, i.e. '// A' vs. '//A'. +sp_cmt_cpp_start = add + +# +# Preprocessor +# +# Add or remove space between #else or #endif and a trailing comment. +sp_endif_cmt = add + +# Newlines at the start and end of the file. +nl_start_of_file = remove +nl_end_of_file = add +nl_end_of_file_min = 1 + +# +# Variable definitions +# +# How to align the '*' in variable definitions. +# +# 0: Part of the type 'void * foo;' (default) +# 1: Part of the variable 'void *foo;' +# 2: Dangling 'void *foo;' +# Dangling: the '*' will not be taken into account when aligning. +align_var_def_star_style = 2 +# Same for typedefs +align_typedef_star_style = 2 +# The gap for aligning struct/union member definitions. +align_var_struct_gap = 1 +# The span for aligning struct/union member definitions. +align_var_struct_span = 8 +# Remove space between pointer stars '*'. +sp_between_ptr_star = remove +# Add space before '(' of control statements ('if', 'for', 'switch', 'while', etc.) +sp_before_sparen = add