#!/bin/bash # # This file is distributed under the MIT License. See LICENSE.md for details. # set -euo pipefail function print_usage() { cat > /dev/stderr << END_USAGE_TEXT SYNOPSIS $0 [] [FILES...] DESCRIPTION Checks that the files specified in FILES... respect the rev.ng coding conventions, and prints all the violations. If the FILES... arguments are missing, checks that all the C++ source files in the project that have already been added to the git repository project respect the rev.ng coding conventions. OPTIONS --help Prints this help and exit --force-format In addition to checking the rev.ng coding conventions on the specified files, uses clang-format to try to enforce the coding conventions automatically. If this fails, exit with failure, otherwise exit with success. WARNING: using this option will overwrite your files, make sure to backup important stuff. --use-local-clang-format-file By default, clang-format is executed with a hard-coded configuration that should apply to all the projects under the rev.ng umbrella. When this option is passed, the hard-coded configuration is ignored, and clang-format looks for configuration in a .clang-format file in the current directory or in its parent directories. This allows to use different clang-format configuration on per-project basis, in cases where the hard-coded configuration does not fit (e.g Qt projects where Qt coding conventions are a better fit). See clang-format documentation for more details. --print-clang-format-config Print the clang-format configuration to stdout, ignoring all the other arguments except for --use-local-clang-format-file. The conventions are not checked. --commit-range Check files changed in the specified commit range --HEAD Alias for '--commit-range HEAD^..HEAD' --all Check all files instead of only changed ones FILES List of filenames of the files for which you want to check the rev.ng coding conventions. RETURN VALUES On success exit code is 0. On failure, i.e. if there is at least one file that is not respecting the coding conventions, exit code is 1. END_USAGE_TEXT } SCRIPT_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FORCE_FORMAT="0" USE_CLANG_FORMAT_FILE="0" PRINT_CLANG_FORMAT_CONFIG="0" GIT_LS_FILES="git ls-files -m --exclude-standard" while [[ $# -gt 0 ]]; do key="$1" case $key in --print-clang-format-config) PRINT_CLANG_FORMAT_CONFIG="1" shift # past argument ;; --force-format) FORCE_FORMAT="1" shift # past argument ;; --use-local-clang-format-file) USE_CLANG_FORMAT_FILE="1" shift # past argument ;; --commit-range) shift # get next argument GIT_LS_FILES="eval git log --pretty='format:' --name-only '$1'" shift # past argument ;; --HEAD) GIT_LS_FILES="eval git log --pretty='format:' --name-only 'HEAD^..HEAD'" shift # past argument ;; --all) GIT_LS_FILES="git ls-files" shift # past argument ;; --help) print_usage exit 0 ;; -*) echo "Error: unrecognized option $key" > /dev/stderr print_usage exit 1 ;; *) break ;; esac done # Retrieve files by extension or shebang # $1: extension to match # $2: shebang to match function find_by_ext_or_shebang() { local EXT_FILES SHEBANG_FILES FILES FILE readarray -t EXT_FILES < <(grep "\.${1}$" <<< "$ALL_FILES_N") SHEBANG_FILES=() for FILE in "${ALL_FILES[@]}"; do if head -n1 "$FILE" | grep -qE "^#!.*${2}"; then SHEBANG_FILES+=("$FILE") fi done FILES=("${EXT_FILES[@]}" "${SHEBANG_FILES[@]}") if [[ ${#FILES[@]} -eq 0 ]]; then return fi printf "%s\n" "${FILES[@]}" | sort -u } # cat, but print header if input is not empty # stdin: lines to print # $1: Header to print if stdin is not empty function cat_header() { local INPUT INPUT="$(cat -)" if echo "$INPUT" | grep -vqz '^\s*$'; then echo "$1" # shellcheck disable=SC2001 sed 's;^; ;' <<< "$INPUT" fi } # Checks that all file names are proper function check_file_names() { local FILENAME FILE readarray -d '' FILENAME < <($GIT_LS_FILES -z) for FILE in "${FILENAME[@]}"; do if echo -n "$FILE" | grep -q '\s'; then echo "ERROR, file '$FILE' has whitespace in filename/path, bailing" exit 1 fi done } CLANG_FORMAT_STYLE_FILE="$SCRIPT_PATH/clang-format-style-file" if [[ ! -e "$CLANG_FORMAT_STYLE_FILE" ]]; then CLANG_FORMAT_STYLE_FILE="$SCRIPT_PATH/../../share/revng/clang-format-style-file" if [[ ! -e "$CLANG_FORMAT_STYLE_FILE" ]]; then echo "Can't find clang-format-style-file" > /dev/stderr exit 1 fi fi CLANG_FORMAT_STYLE=$(cat "$CLANG_FORMAT_STYLE_FILE") if [[ "$USE_CLANG_FORMAT_FILE" -ne 0 ]]; then CLANG_FORMAT_STYLE="file" fi # If the user passed the --print-clang-format-config, dump the config and exit # with success. if [[ "$PRINT_CLANG_FORMAT_CONFIG" -ne 0 ]]; then clang-format --dry-run -style="$CLANG_FORMAT_STYLE" --dump-config exit 0 fi check_file_names if [[ $# -eq 0 ]]; then readarray -t ALL_FILES_TMP < <($GIT_LS_FILES) if [[ "${#ALL_FILES_TMP[@]}" -eq 0 ]]; then exit 0; fi else ALL_FILES_TMP=("$@") fi ALL_FILES=() for FILE in "${ALL_FILES_TMP[@]}"; do if [[ -e $FILE ]]; then ALL_FILES+=("$FILE") fi done ALL_FILES_N=$(printf '%s\n' "${ALL_FILES[@]}") readarray -t FILES < <(grep -E '\.(c|cc|cpp|h|hpp)$' <<< "$ALL_FILES_N") FILES_N=$(printf '%s\n' "${FILES[@]}") readarray -t PYTHON_FILES < <(find_by_ext_or_shebang py python) readarray -t BASH_FILES < <(find_by_ext_or_shebang sh bash) readarray -t CMAKE_FILES < <(grep -i cmake <<< "$ALL_FILES_N") # Run clang-format on FILES function run_clang_format() { if [[ ${#FILES[@]} -eq 0 ]]; then return; fi if [[ "$FORCE_FORMAT" -ne 0 ]]; then clang-format -style="$CLANG_FORMAT_STYLE" -i "${FILES[@]}" || true else clang-format --dry-run -style="$CLANG_FORMAT_STYLE" -i "${FILES[@]}" fi } # Run black function run_black() { if [[ ${#PYTHON_FILES[@]} -eq 0 ]]; then return; fi if [[ "$FORCE_FORMAT" -ne 0 ]]; then black -q -l 100 "${PYTHON_FILES[@]}" else black -q --check -l 100 "${PYTHON_FILES[@]}" fi } # Run flake8 function run_flake8() { if [[ ${#PYTHON_FILES[@]} -eq 0 ]]; then return; fi flake8 --config "$SCRIPT_PATH/flake8-config" "${PYTHON_FILES[@]}" } # Run mypy function run_mypy() { # mypy will complain that there are multiple __main__ files # in the python/scripts directory, manually exclude them # Will be fixed by https://github.com/python/mypy/pull/12543/files readarray -t MYPY_FILES < <(printf '%s\n' "${PYTHON_FILES[@]}" | grep -vF 'python/scripts') if [[ ${#MYPY_FILES[@]} -eq 0 ]]; then return; fi mypy --scripts-are-modules --ignore-missing-imports --no-error-summary \ "${MYPY_FILES[@]}" } # Run isort function run_isort() { if [[ ${#PYTHON_FILES[@]} -eq 0 ]]; then return; fi if [[ "$FORCE_FORMAT" -ne 0 ]]; then isort --settings-file "$SCRIPT_PATH/isort.cfg" "${PYTHON_FILES[@]}" else isort --settings-file "$SCRIPT_PATH/isort.cfg" --diff "${PYTHON_FILES[@]}" fi } # Run cmake-format function run_cmake_format() { if [[ ${#CMAKE_FILES[@]} -eq 0 ]]; then return; fi if [[ "$FORCE_FORMAT" -ne 0 ]]; then cmake-format -i "${CMAKE_FILES[@]}" -l error else cmake-format --check "${CMAKE_FILES[@]}" -l error fi } # Run shellcheck + check for set -euo pipefail function run_bash_check() { local NO_SET_FILES if [[ ${#BASH_FILES[@]} -eq 0 ]]; then return; fi shellcheck "${BASH_FILES[@]}" NO_SET_FILES=() for FILE in "${BASH_FILES[@]}"; do if ! head -n 10 "$FILE" | grep -qFe 'set -euo pipefail'; then NO_SET_FILES+=("$FILE") fi done if [[ ${#NO_SET_FILES[@]} -gt 0 ]]; then echo "There are script files without 'set -euo pipefail':" printf ' %s\n' "${NO_SET_FILES[@]}" fi } # Check that all files have a license header function run_license_check() { local LICENSE_FILES NO_LICENSE_FILES readarray -t LICENSE_FILES < <(grep -Ev \ -e '\.txt$' \ -e '\.md$' \ -e '\.rst$' \ -e '.gitignore' \ -e 'LICENSE.*' \ -e 'pyproject.toml' \ -e 'clang-format-style-file' \ -e '.clang-tidy' \ -e '\.dot$' \ -e 'Doxyfile.in' \ <<< "$ALL_FILES_N" ) NO_LICENSE_FILES=() for FILE in "${LICENSE_FILES[@]}"; do if ! head -n 10 "$FILE" | grep -qF 'See LICENSE.md for details'; then NO_LICENSE_FILES+=("$FILE") fi done if [[ ${#NO_LICENSE_FILES[@]} -gt 0 ]]; then echo "There are files without a license header:" printf ' %s\n' "${NO_LICENSE_FILES[@]}" fi } # Run revng-specific checks on files function run_revng_checks() { if [[ ${#FILES[@]} -eq 0 ]]; then return; fi GREP="grep -Hn --color=always" # Check for lines longer than 80 columns $GREP -E '^.{81,}$' "${FILES[@]}" | cat_header "There are lines longer than 80 characters:" # Things should never match for REGEXP in '\(--> 0\)' ';;' '^\s*->.*;$' 'Twine [^&]'; do $GREP "$REGEXP" "${FILES[@]}" done | cat_header "Found snippets that should not be present:" # Things should never match (except in support.c) readarray -t FILTERED_FILES < <(grep -v \ -e '^runtime/support\.c$' \ -e '^lib/Support/Assert\.cpp$' \ <<< "$FILES_N") for REGEXP in '\babort(' '\bassert(' 'assert(false' 'llvm_unreachable'; do $GREP "$REGEXP" "${FILTERED_FILES[@]}" done | cat_header "Use revng_{assert,check,abort,unreachable}:" # Things should never be at the end of a line for REGEXP in '::' '<' 'RegisterPass.*>' '} else' '\bopt\b.*>'; do $GREP "$REGEXP\$" "${FILES[@]}" done | cat_header "Found snippets that shouldn't be at the end of a line:" # Includes should never use <..> except for C++ standard includes readarray -t CXX_FILES < <(grep -v \ -e '^runtime/support\.h$' \ -e '^include/revng/Runtime/.*$' \ -e '^include/revng/PipelineC/.*$' \ -e '^include/revng/Support/Assert\.h$' \ -e '^include/revng/Support/ClassSentinel\.h$' \ -e '\.c$' \ <<< "$FILES_N" ) if [[ ${#CXX_FILES[@]} -ne 0 ]]; then cat <($GREP "^\s*#include <.*\.hpp>" "${FILES[@]}") \ <($GREP "^\s*#include <.*\.h>" "${CXX_FILES[@]}") \ | cat_header "Includes should never user <..> except for C++ standard includes:" else $GREP "^\s*#include <.*\.hpp>" "${FILES[@]}" | \ cat_header "Includes should never user <..> except for C++ standard includes:" fi # Parenthesis at the end of line (except for raw strings) $GREP "(\$" "${FILES[@]}" | grep -v 'R"LLVM.*(' | cat_header "Parenthesis at the end of line:" # Ban whitespaces at the end of a line $GREP " \$" "${FILES[@]}" | cat_header "Whitespace at the end of line:" # Ban tabs altogether $GREP -P "\t" "${FILES[@]}" | cat_header "Tabs present:" # Ensure all files end with a newline for FILE in "${ALL_FILES[@]}"; do if [[ -s "$FILE" && ! $(tail -c 1 "$FILE" | base64) == "Cg==" ]]; then echo "$FILE does not end with a newline" fi done | cat_header "Files that don't end in a newline:" # Things should never be at the beginning of a line REGEXPS=('\*>' '/[^/\*]' ':[^:\(]*)' '==' '\!=' '<[^<]' '>' '>=' '<=' '//\s*WIP' '#if\s*[01]') for REGEXP in "${REGEXPS[@]}"; do $GREP "^\s*$REGEXP" "${FILES[@]}" done | cat_header "Found snippets that should never be at the beginning of a line:" # Check there are no static functions in C++ header files for FILE in "${CXX_FILES[@]}"; do if [[ $FILE == *h ]]; then $GREP -H '^static\b[^=]*$' "$FILE" | cat head -n1 "$FILE" | grep -E '^#pragma once$' > /dev/null || \ echo "$FILE: header does not start with #pragma once" fi done } # Replace fd #1 (stdout) with process substitution so that after the exec any # write to it will also get written into $TMP_FILE TMP_FILE=$(mktemp) exec 3>&1 1> >(tee "$TMP_FILE" > /dev/stdout) function cleanup() { trap - SIGINT SIGTERM ERR EXIT rm "$TMP_FILE" } trap cleanup SIGINT SIGTERM ERR EXIT EXIT_CODE=0 run_clang_format || EXIT_CODE=$? run_black || EXIT_CODE=$? run_isort || EXIT_CODE=$? run_flake8 || EXIT_CODE=$? run_mypy || EXIT_CODE=$? run_cmake_format || EXIT_CODE=$? run_bash_check || EXIT_CODE=$? run_license_check || EXIT_CODE=$? run_revng_checks || EXIT_CODE=$? if [[ $(wc -c < "$TMP_FILE") -ne 0 || $EXIT_CODE -ne 0 ]]; then exit 1 fi