mirror of
https://github.com/revng/revng
synced 2026-06-21 14:07:57 +00:00
439 lines
13 KiB
Bash
Executable File
439 lines
13 KiB
Bash
Executable File
#!/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 [<options>] [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 <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
|