Skip to content
This repository has been archived by the owner on Dec 25, 2022. It is now read-only.

Introduce meta checks #96

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
repos:
- repo: local
hooks:
- id: meta-lint-ci
name: Running Meta/lint-ci.sh to ensure changes will pass linting on CI
entry: bash Meta/lint-ci.sh
stages: [ commit ]
language: system

- id: meta-lint-commit
name: Lint commit message to ensure it will pass the commit linting on CI
entry: Meta/lint-commit.sh
stages: [ commit-msg ]
language: system
67 changes: 67 additions & 0 deletions Meta/check-newlines-at-eof.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3

import os
import re
import subprocess
import sys


RE_RELEVANT_FILE_EXTENSION = re.compile('\\.(cpp|h|gml|html|js|css|sh|py|json|txt)$')


def should_check_file(filename):
if not RE_RELEVANT_FILE_EXTENSION.search(filename):
return False
if filename.endswith('.txt'):
return 'CMake' in filename
return True


def find_files_here_or_argv():
if len(sys.argv) > 1:
raw_list = sys.argv[1:]
else:
process = subprocess.run(["git", "ls-files"], check=True, capture_output=True)
raw_list = process.stdout.decode().strip('\n').split('\n')

return filter(should_check_file, raw_list)


def run():
"""Check files checked in to git for trailing newlines at end of file."""
no_newline_at_eof_errors = []
blank_lines_at_eof_errors = []

did_fail = False
for filename in find_files_here_or_argv():
with open(filename, "r") as f:
f.seek(0, os.SEEK_END)

f.seek(f.tell() - 1, os.SEEK_SET)
if f.read(1) != '\n':
did_fail = True
no_newline_at_eof_errors.append(filename)
continue

while True:
f.seek(f.tell() - 2, os.SEEK_SET)
char = f.read(1)
if not char.isspace():
break
if char == '\n':
did_fail = True
blank_lines_at_eof_errors.append(filename)
break

if no_newline_at_eof_errors:
print("Files with no newline at the end:", " ".join(no_newline_at_eof_errors))
if blank_lines_at_eof_errors:
print("Files that have blank lines at the end:", " ".join(blank_lines_at_eof_errors))

if did_fail:
sys.exit(1)


if __name__ == '__main__':
os.chdir(os.path.dirname(__file__) + "/..")
run()
86 changes: 86 additions & 0 deletions Meta/check-style.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env python3

import os
import re
import subprocess
import sys

# Ensure copyright headers match this format and are followed by a blank line:
# /*
# * Copyright (c) YYYY(-YYYY), Whatever
# * ... more of these ...
# *
# * SPDX-License-Identifier: BSD-2-Clause
# */
GOOD_LICENSE_HEADER_PATTERN = re.compile(
'^/\\*\n' +
'( \\* Copyright \\(c\\) [0-9]{4}(-[0-9]{4})?, .*\n)+' +
' \\*\n' +
' \\* SPDX-License-Identifier: BSD-2-Clause\n' +
' \\*/\n' +
'\n')
LICENSE_HEADER_CHECK_EXCLUDES = {}

# We check that "#pragma once" is present
PRAGMA_ONCE_STRING = '#pragma once'
PRAGMA_ONCE_CHECK_EXCLUDES = {}

# We make sure that there's a blank line before and after pragma once
GOOD_PRAGMA_ONCE_PATTERN = re.compile('(^|\\S\n\n)#pragma once(\n\n\\S.|$)')


def should_check_file(filename):
if not filename.endswith('.cpp') and not filename.endswith('.h'):
return False
return True


def find_files_here_or_argv():
if len(sys.argv) > 1:
raw_list = sys.argv[1:]
else:
process = subprocess.run(["git", "ls-files"], check=True, capture_output=True)
raw_list = process.stdout.decode().strip('\n').split('\n')

return filter(should_check_file, raw_list)


def run():
errors_license = []
errors_pragma_once_bad = []
errors_pragma_once_missing = []

for filename in find_files_here_or_argv():
with open(filename, "r") as f:
file_content = f.read()
if not any(filename.startswith(forbidden_prefix) for forbidden_prefix in LICENSE_HEADER_CHECK_EXCLUDES):
if not GOOD_LICENSE_HEADER_PATTERN.search(file_content):
errors_license.append(filename)
if filename.endswith('.h'):
if any(filename.startswith(forbidden_prefix) for forbidden_prefix in PRAGMA_ONCE_CHECK_EXCLUDES):
# File was excluded
pass
elif GOOD_PRAGMA_ONCE_PATTERN.search(file_content):
# Excellent, the formatting is correct.
pass
elif PRAGMA_ONCE_STRING in file_content:
# Bad, the '#pragma once' is present but it's formatted wrong.
errors_pragma_once_bad.append(filename)
else:
# Bad, the '#pragma once' is missing completely.
errors_pragma_once_missing.append(filename)

if errors_license:
print("Files with bad licenses:", " ".join(errors_license))
if errors_pragma_once_missing:
print("Files without #pragma once:", " ".join(errors_pragma_once_missing))
if errors_pragma_once_bad:
print("Files with a bad #pragma once:", " ".join(errors_pragma_once_bad))

if errors_license or errors_pragma_once_missing or errors_pragma_once_bad:
sys.exit(1)


if __name__ == '__main__':
os.chdir(os.path.dirname(__file__) + "/..")
run()
36 changes: 36 additions & 0 deletions Meta/lint-ci.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash

set -e

script_path=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
cd "${script_path}/.." || exit 1

RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color

FAILURES=0

set +e

for cmd in \
Meta/check-newlines-at-eof.py \
Meta/check-style.py; do
echo "Running ${cmd}"
if time "${cmd}" "$@"; then
echo -e "[${GREEN}OK${NC}]: ${cmd}"
else
echo -e "[${RED}FAIL${NC}]: ${cmd}"
((FAILURES+=1))
fi
done

echo "Running Meta/lint-clang-format.sh"
if time Meta/lint-clang-format.sh --overwrite-inplace "$@" && git diff --exit-code; then
echo -e "[${GREEN}OK${NC}]: Meta/lint-clang-format.sh"
else
echo -e "[${RED}FAIL${NC}]: Meta/lint-clang-format.sh"
((FAILURES+=1))
fi

exit "${FAILURES}"
54 changes: 54 additions & 0 deletions Meta/lint-clang-format.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash

set -e

script_path=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
cd "${script_path}/.." || exit 1

if [ "$#" -eq "1" ]; then
mapfile -t files < <(
git ls-files -- \
'*.cpp' \
'*.h'
)
else
files=()
for file in "${@:2}"; do
if [[ "${file}" == *".cpp" || "${file}" == *".h" ]]; then
files+=("${file}")
fi
done
fi

if (( ${#files[@]} )); then
CLANG_FORMAT=false
if command -v clang-format-14 >/dev/null 2>&1 ; then
CLANG_FORMAT=clang-format-14
elif command -v clang-format >/dev/null 2>&1 ; then
CLANG_FORMAT=clang-format
if ! "${CLANG_FORMAT}" --version | awk '{ if (substr($NF, 1, index($NF, ".") - 1) < 14) exit 1; }'; then
echo "You are using '$("${CLANG_FORMAT}" --version)', which appears to not be clang-format 14 or later."
echo "It is very likely that the resulting changes are not what you wanted."
fi
else
echo "clang-format-14 is not available, but C or C++ files need linting! Either skip this script, or install clang-format-14."
echo "(If you install a package 'clang-format', please make sure it's version 14 or later.)"
exit 1
fi

if [ "$#" -gt "0" ] && [ "--overwrite-inplace" = "$1" ] ; then
true # The only way to run this script.
else
# Note that this branch also covers --help, -h, -help, -?, etc.
echo "USAGE: $0 --overwrite-inplace"
echo "The argument is necessary to make you aware that this *will* overwrite your local files."
exit 1
fi

echo "Using ${CLANG_FORMAT}"

"${CLANG_FORMAT}" -style=file -i "${files[@]}"
echo "Maybe some files have changed. Sorry, but clang-format doesn't indicate what happened."
else
echo "No .cpp or .h files to check."
fi
61 changes: 61 additions & 0 deletions Meta/lint-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash

# the file containing the commit message is passed as the first argument
commit_file="$1"
commit_message=$(cat "$commit_file")

error() {
echo -e "\033[0;31m$1:\033[0m"
echo "$commit_message"
exit 1
}

# fail if the commit message contains windows style line breaks (carriage returns)
if grep -q -U $'\x0D' "$commit_file"; then
error "Commit message contains CRLF line breaks (only unix-style LF linebreaks are allowed)"
fi

line_number=0
while read -r line; do
# break on git cut line, used by git commit --verbose
if [[ "$line" == "# ------------------------ >8 ------------------------" ]]; then
break
fi

# ignore comment lines
[[ "$line" =~ ^#.* ]] && continue
# ignore overlong 'fixup!' commit descriptions
[[ "$line" =~ ^fixup!\ .* ]] && continue

((line_number += 1))
line_length=${#line}

if [[ $line_number -eq 2 ]] && [[ $line_length -ne 0 ]]; then
error "Empty line between commit title and body is missing"
fi

category_pattern='^(Revert "|\S+: )'
if [[ $line_number -eq 1 ]] && (echo "$line" | grep -E -v -q "$category_pattern"); then
error "Missing category in commit title (if this is a fix up of a previous commit, it should be squashed)"
fi

title_case_pattern="^\S.*?: [A-Z0-9]"
if [[ $line_number -eq 1 ]] && (echo "$line" | grep -E -v -q "$title_case_pattern"); then
error "First word of commit after the subsystem is not capitalized"
fi

if [[ $line_number -eq 1 ]] && [[ "$line" =~ \.$ ]]; then
error "Commit title ends in a period"
fi

url_pattern="([a-z]+:\/\/)?(([a-zA-Z0-9_]|-)+\.)+[a-z]{2,}(:\d+)?([a-zA-Z_0-9@:%\+.~\?&\/=]|-)+"
if [[ $line_length -gt 72 ]] && (echo "$line" | grep -E -v -q "$url_pattern"); then
error "Commit message lines are too long (maximum allowed is 72 characters)"
fi

if [[ "$line" == "Signed-off-by: "* ]]; then
error "Commit body contains a Signed-off-by tag"
fi

done <"$commit_file"
exit 0