xref: /openbmc/openbmc-build-scripts/scripts/format-code.sh (revision d535ecb4460e7d6f1bdeae8bd5ab3f7633740119)
1#!/bin/bash
2set -e
3
4# This script reformats source files using various formatters and linters.
5#
6# Files are changed in-place, so make sure you don't have anything open in an
7# editor, and you may want to commit before formatting in case of awryness.
8#
9# This must be run on a clean repository to succeed
10#
11function display_help()
12{
13    echo "usage: format-code.sh [-h | --help] [--no-diff]"
14    echo "                      [<path>]"
15    echo
16    echo "Format and lint a repository."
17    echo
18    echo "Arguments:"
19    echo "    --no-diff      Don't show final diff output"
20    echo "    path           Path to git repository (default to pwd)"
21}
22
23eval set -- "$(getopt -o 'h' --long 'help,no-diff' -n 'format-code.sh' -- "$@")"
24while true; do
25    case "$1" in
26        '-h'|'--help')
27            display_help && exit 0
28            ;;
29
30        '--no-diff')
31            OPTION_NO_DIFF=1
32            shift
33            ;;
34
35        '--')
36            shift
37            break
38            ;;
39
40        *)
41            echo "unknown option: $1"
42            display_help && exit 1
43            ;;
44    esac
45done
46
47# Detect tty and set nicer colors.
48if [ -t 1 ]; then
49    BLUE="\e[34m"
50    GREEN="\e[32m"
51    NORMAL="\e[0m"
52    RED="\e[31m"
53    YELLOW="\e[33m"
54else # non-tty, no escapes.
55    BLUE=""
56    GREEN=""
57    NORMAL=""
58    RED=""
59    YELLOW=""
60fi
61
62# Allow called scripts to know which clang format we are using
63export CLANG_FORMAT="clang-format"
64
65# Path to default config files for linters.
66CONFIG_PATH="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/config"
67
68# Find repository root for `pwd` or $1.
69if [ -z "$1" ]; then
70    DIR="$(git rev-parse --show-toplevel || pwd)"
71else
72    DIR="$(git -C "$1" rev-parse --show-toplevel)"
73fi
74if [ ! -e "$DIR/.git" ]; then
75    echo -e "${RED}Error:${NORMAL} Directory ($DIR) does not appear to be a git repository"
76    exit 1
77fi
78
79cd "${DIR}"
80echo -e "    ${BLUE}Formatting code under${NORMAL} $DIR"
81
82LINTERS_ALL=( \
83        commit_gitlint \
84        commit_spelling \
85        clang_format \
86        eslint \
87        pycodestyle \
88        shellcheck \
89    )
90declare -A LINTER_REQUIRE=()
91declare -A LINTER_CONFIG=()
92LINTERS_ENABLED=()
93
94LINTER_REQUIRE+=([commit_spelling]="codespell")
95function do_commit_spelling() {
96    if [ ! -e .git/COMMIT_EDITMSG ]; then
97        return
98    fi
99    echo -e "    ${BLUE}Running codespell${NORMAL}"
100
101    # Run the codespell with openbmc spcific spellings on the patchset
102    echo "openbmc-dictionary - misspelling count >> "
103    sed "s/Signed-off-by.*//" .git/COMMIT_EDITMSG | \
104        codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count -
105
106    # Run the codespell with generic dictionary on the patchset
107    echo "generic-dictionary - misspelling count >> "
108    sed "s/Signed-off-by.*//" .git/COMMIT_EDITMSG | \
109        codespell --builtin clear,rare,en-GB_to_en-US -d --count -
110}
111
112LINTER_REQUIRE+=([commit_gitlint]="gitlint")
113function do_commit_gitlint() {
114    echo -e "    ${BLUE}Running gitlint${NORMAL}"
115    # Check for commit message issues
116    gitlint \
117        --extra-path "${CONFIG_PATH}/gitlint/" \
118        --config "${CONFIG_PATH}/.gitlint"
119}
120
121LINTER_REQUIRE+=([eslint]="eslint;.eslintrc.json;${CONFIG_PATH}/eslint-global-config.json")
122function do_eslint() {
123    echo -e "    ${BLUE}Running eslint${NORMAL}"
124
125    if [[ -f ".eslintignore" ]]; then
126        ESLINT_IGNORE="--ignore-path=.eslintignore"
127    elif [[ -f ".gitignore" ]]; then
128        ESLINT_IGNORE="--ignore-path=.gitignore"
129    fi
130
131    eslint . "${ESLINT_IGNORE}" --no-eslintrc -c "${LINTER_CONFIG[eslint]}" \
132        --ext .json --format=stylish \
133        --resolve-plugins-relative-to /usr/local/lib/node_modules \
134        --no-error-on-unmatched-pattern
135}
136
137LINTER_REQUIRE+=([pycodestyle]="pycodestyle;setup.cfg")
138function do_pycodestyle() {
139    echo -e "    ${BLUE}Running pycodestyle${NORMAL}"
140    pycodestyle --show-source --exclude=subprojects .
141    rc=$?
142    if [[ ${rc} -ne 0 ]]; then
143        exit ${rc}
144    fi
145}
146
147LINTER_REQUIRE+=([shellcheck]="shellcheck;.shellcheck")
148function do_shellcheck() {
149    # Run shellcheck on any shell-script.
150    shell_scripts="$(git ls-files | xargs -n1 file -0 | \
151    grep -a "shell script" | cut -d '' -f 1)"
152    if [ -n "${shell_scripts}" ]; then
153        echo -e "    ${BLUE}Running shellcheck${NORMAL}"
154    fi
155    for script in ${shell_scripts}; do
156        shellcheck --color=never -x "${script}"
157    done
158}
159
160LINTER_REQUIRE+=([clang_format]="clang-format;.clang-format")
161do_clang_format() {
162
163    echo -e "    ${BLUE}Running clang-format${NORMAL}"
164    files=$(git ls-files | \
165        grep -e '\.[ch]pp$' -e '\.[ch]$' | \
166        grep -v '\.mako\.')
167
168    if [ -e .clang-ignore ]; then
169        files=$("${CONFIG_PATH}/lib/ignore-filter" .clang-ignore <<< "${files}")
170    fi
171
172    xargs "${CLANG_FORMAT}" -i <<< "${files}"
173}
174
175function check_linter()
176{
177    TITLE="$1"
178    IFS=";" read -r -a ARGS <<< "$2"
179
180    EXE="${ARGS[0]}"
181    if [ ! -x "${EXE}" ]; then
182        if ! which "${EXE}" > /dev/null 2>&1 ; then
183            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${EXE}"
184            return
185        fi
186    fi
187
188    CONFIG="${ARGS[1]}"
189    FALLBACK="${ARGS[2]}"
190
191    if [ -n "${CONFIG}" ]; then
192        if [ -e "${CONFIG}" ]; then
193            LINTER_CONFIG+=( [${TITLE}]="${CONFIG}" )
194        elif [ -n "${FALLBACK}" ] && [ -e "${FALLBACK}" ]; then
195            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${CONFIG}; using ${FALLBACK}"
196            LINTER_CONFIG+=( [${TITLE}]="${FALLBACK}" )
197        else
198            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find config ${CONFIG}"
199            return
200        fi
201    fi
202
203    LINTERS_ENABLED+=( "${TITLE}" )
204}
205
206for op in "${LINTERS_ALL[@]}"; do
207    check_linter "$op" "${LINTER_REQUIRE[${op}]}"
208done
209
210for op in "${LINTERS_ENABLED[@]}"; do
211    "do_$op"
212done
213
214if [ -z "$OPTION_NO_DIFF" ]; then
215    echo -e "    ${BLUE}Result differences...${NORMAL}"
216    if ! git --no-pager diff --exit-code ; then
217        echo -e "Format: ${RED}FAILED${NORMAL}"
218        exit 1
219    else
220        echo -e "Format: ${GREEN}PASSED${NORMAL}"
221    fi
222fi
223
224# Sometimes your situation is terrible enough that you need the flexibility.
225# For example, phosphor-mboxd.
226for formatter in "format-code.sh" "format-code"; do
227    if [[ -x "${formatter}" ]]; then
228        echo -e "    ${BLUE}Calling secondary formatter:${NORMAL} ${formatter}"
229        "./${formatter}"
230        if [ -z "$OPTION_NO_DIFF" ]; then
231            git --no-pager diff --exit-code
232        fi
233    fi
234done
235