xref: /openbmc/openbmc-build-scripts/scripts/format-code.sh (revision 3d29494c295ee8d5fb17c9be0866fa6c10edbbe6)
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] [--disable <tool>]"
14    echo "                      [--list-tools] [<path>]"
15    echo
16    echo "Format and lint a repository."
17    echo
18    echo "Arguments:"
19    echo "    --list-tools      Display available linters and formatters"
20    echo "    --no-diff         Don't show final diff output"
21    echo "    --disable <tool>  Disable linter"
22    echo "    path              Path to git repository (default to pwd)"
23}
24
25LINTERS_ALL=( \
26        commit_gitlint \
27        commit_spelling \
28        clang_format \
29        eslint \
30        pycodestyle \
31        shellcheck \
32    )
33LINTERS_DISABLED=()
34LINTERS_ENABLED=()
35
36eval set -- "$(getopt -o 'h' --long 'help,list-tools,no-diff,disable:' -n 'format-code.sh' -- "$@")"
37while true; do
38    case "$1" in
39        '-h'|'--help')
40            display_help && exit 0
41            ;;
42
43        '--list-tools')
44            echo "Available tools:"
45            for t in "${LINTERS_ALL[@]}"; do
46                echo "    $t"
47            done
48            exit 0
49            ;;
50
51        '--no-diff')
52            OPTION_NO_DIFF=1
53            shift
54            ;;
55
56        '--disable')
57            LINTERS_DISABLED+=("$2")
58            shift && shift
59            ;;
60
61        '--')
62            shift
63            break
64            ;;
65
66        *)
67            echo "unknown option: $1"
68            display_help && exit 1
69            ;;
70    esac
71done
72
73# Detect tty and set nicer colors.
74if [ -t 1 ]; then
75    BLUE="\e[34m"
76    GREEN="\e[32m"
77    NORMAL="\e[0m"
78    RED="\e[31m"
79    YELLOW="\e[33m"
80else # non-tty, no escapes.
81    BLUE=""
82    GREEN=""
83    NORMAL=""
84    RED=""
85    YELLOW=""
86fi
87
88# Allow called scripts to know which clang format we are using
89export CLANG_FORMAT="clang-format"
90
91# Path to default config files for linters.
92CONFIG_PATH="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/config"
93
94# Find repository root for `pwd` or $1.
95if [ -z "$1" ]; then
96    DIR="$(git rev-parse --show-toplevel || pwd)"
97else
98    DIR="$(git -C "$1" rev-parse --show-toplevel)"
99fi
100if [ ! -e "$DIR/.git" ]; then
101    echo -e "${RED}Error:${NORMAL} Directory ($DIR) does not appear to be a git repository"
102    exit 1
103fi
104
105cd "${DIR}"
106echo -e "    ${BLUE}Formatting code under${NORMAL} $DIR"
107
108# Config hashes:
109#   LINTER_REQUIRE - The requirements to run a linter, semi-colon separated.
110#       1. Executable.
111#       2. [optional] Configuration file.
112#       3. [optional] Global fallback configuration file.
113#
114#   LINTER_IGNORE - An optional set of semi-colon separated ignore-files
115#       specific to the linter.
116#
117#   LINTER_TYPES - The file types supported by the linter, semi-colon separated.
118#
119#   LINTER_CONFIG - The config (from LINTER_REQUIRE) chosen for the repository.
120#
121declare -A LINTER_REQUIRE=()
122declare -A LINTER_IGNORE=()
123declare -A LINTER_TYPES=()
124declare -A LINTER_CONFIG=()
125
126LINTER_REQUIRE+=([commit_spelling]="codespell")
127LINTER_TYPES+=([commit_spelling]="commit")
128function do_commit_spelling() {
129    # Run the codespell with openbmc spcific spellings on the patchset
130    echo -n "openbmc-dictionary - misspelling count >> "
131    sed "s/Signed-off-by.*//" "$@" | \
132        codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count -
133
134    # Run the codespell with generic dictionary on the patchset
135    echo -n "generic-dictionary - misspelling count >> "
136    sed "s/Signed-off-by.*//" "$@" | \
137        codespell --builtin clear,rare,en-GB_to_en-US -d --count -
138}
139
140LINTER_REQUIRE+=([commit_gitlint]="gitlint")
141LINTER_TYPES+=([commit_gitlint]="commit")
142function do_commit_gitlint() {
143    gitlint --extra-path "${CONFIG_PATH}/gitlint/" \
144        --config "${CONFIG_PATH}/.gitlint"
145}
146
147LINTER_REQUIRE+=([eslint]="eslint;.eslintrc.json;${CONFIG_PATH}/eslint-global-config.json")
148LINTER_IGNORE+=([eslint]=".eslintignore")
149LINTER_TYPES+=([eslint]="json")
150function do_eslint() {
151    eslint --no-eslintrc -c "${LINTER_CONFIG[eslint]}" \
152        --ext .json --format=stylish \
153        --resolve-plugins-relative-to /usr/local/lib/node_modules \
154        --no-error-on-unmatched-pattern "$@"
155}
156
157LINTER_REQUIRE+=([pycodestyle]="pycodestyle;setup.cfg")
158LINTER_TYPES+=([pycodestyle]="python")
159function do_pycodestyle() {
160    pycodestyle --show-source "$@"
161}
162
163LINTER_REQUIRE+=([shellcheck]="shellcheck;.shellcheck")
164LINTER_TYPES+=([shellcheck]="bash;sh")
165function do_shellcheck() {
166    shellcheck --color=never -x "$@"
167}
168
169LINTER_REQUIRE+=([clang_format]="clang-format;.clang-format")
170LINTER_IGNORE+=([clang_format]=".clang-ignore;.clang-format-ignore")
171LINTER_TYPES+=([clang_format]="c;cpp")
172do_clang_format() {
173    "${CLANG_FORMAT}" -i "$@"
174}
175
176function get_file_type()
177{
178    case "$(basename "$1")" in
179            # First to early detect template files.
180        *.in | *.meson) echo "meson-template" && return ;;
181        *.mako | *.mako.*) echo "mako" && return ;;
182
183        *.ac) echo "autoconf" && return ;;
184        *.[ch]) echo "c" && return ;;
185        *.[ch]pp) echo "cpp" &&  return ;;
186        *.json) echo "json" && return ;;
187        *.md) echo "markdown" && return ;;
188        *.py) echo "python" && return ;;
189        *.yaml | *.yml) echo "yaml" && return ;;
190
191            # Special files.
192        .git/COMMIT_EDITMSG) echo "commit" && return ;;
193        meson.build) echo "meson" && return ;;
194    esac
195
196    case "$(file "$1")" in
197        *Bourne-Again\ shell*) echo "bash" && return ;;
198        *C++\ source*) echo "cpp" && return ;;
199        *C\ source*) echo "c" && return ;;
200        *JSON\ data*) echo "json" && return ;;
201        *POSIX\ shell*) echo "sh" && return ;;
202        *Python\ script*) echo "python" && return ;;
203        *zsh\ shell*) echo "zsh" && return ;;
204    esac
205
206    echo "unknown"
207}
208
209function check_linter()
210{
211    TITLE="$1"
212    IFS=";" read -r -a ARGS <<< "$2"
213
214    if [[ "${LINTERS_DISABLED[*]}" =~ $1 ]]; then
215        return
216    fi
217
218    EXE="${ARGS[0]}"
219    if [ ! -x "${EXE}" ]; then
220        if ! which "${EXE}" > /dev/null 2>&1 ; then
221            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${EXE}"
222            return
223        fi
224    fi
225
226    CONFIG="${ARGS[1]}"
227    FALLBACK="${ARGS[2]}"
228
229    if [ -n "${CONFIG}" ]; then
230        if [ -e "${CONFIG}" ]; then
231            LINTER_CONFIG+=( [${TITLE}]="${CONFIG}" )
232        elif [ -n "${FALLBACK}" ] && [ -e "${FALLBACK}" ]; then
233            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${CONFIG}; using ${FALLBACK}"
234            LINTER_CONFIG+=( [${TITLE}]="${FALLBACK}" )
235        else
236            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find config ${CONFIG}"
237            return
238        fi
239    fi
240
241    LINTERS_ENABLED+=( "${TITLE}" )
242}
243
244# Check for a global .linter-ignore file.
245GLOBAL_IGNORE=("cat")
246if [ -e ".linter-ignore" ]; then
247    GLOBAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter" ".linter-ignore")
248fi
249
250# Find all the files in the git repository and organize by type.
251declare -A FILES=()
252if [ -e .git/COMMIT_EDITMSG ]; then
253    FILES+=([commit]=".git/COMMIT_EDITMSG")
254fi
255while read -r file; do
256    ftype="$(get_file_type "$file")"
257    FILES+=([$ftype]="$(echo -ne "$file;${FILES[$ftype]:-}")")
258done < <(git ls-files | "${GLOBAL_IGNORE[@]}")
259
260# For each linter, check if there are an applicable files and if it can
261# be enabled.
262for op in "${LINTERS_ALL[@]}"; do
263    for ftype in ${LINTER_TYPES[$op]//;/ }; do
264        if [[ -v FILES["$ftype"] ]]; then
265            check_linter "$op" "${LINTER_REQUIRE[${op}]}"
266            break
267        fi
268    done
269done
270
271# Call each linter.
272for op in "${LINTERS_ENABLED[@]}"; do
273
274    # Determine the linter-specific ignore file(s).
275    LOCAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter")
276    if [[ -v LINTER_IGNORE["$op"] ]]; then
277        for ignorefile in ${LINTER_IGNORE["$op"]//;/ } ; do
278            if [ -e "$ignorefile" ]; then
279                LOCAL_IGNORE+=("$ignorefile")
280            fi
281        done
282    fi
283    if [ 1 -eq ${#LOCAL_IGNORE[@]} ]; then
284        LOCAL_IGNORE=("cat")
285    fi
286
287    # Find all the files for this linter, filtering out the ignores.
288    LINTER_FILES=()
289    while read -r file ; do
290        if [ -e "$file" ]; then
291            LINTER_FILES+=("$file")
292        fi
293        done < <(for ftype in ${LINTER_TYPES[$op]//;/ }; do
294            # shellcheck disable=SC2001
295            echo "${FILES["$ftype"]:-}" | sed "s/;/\\n/g"
296    done | "${LOCAL_IGNORE[@]}")
297
298    # Call the linter now with all the files.
299    echo -e "    ${BLUE}Running $op${NORMAL}"
300    "do_$op" "${LINTER_FILES[@]}"
301done
302
303# Check for differences.
304if [ -z "$OPTION_NO_DIFF" ]; then
305    echo -e "    ${BLUE}Result differences...${NORMAL}"
306    if ! git --no-pager diff --exit-code ; then
307        echo -e "Format: ${RED}FAILED${NORMAL}"
308        exit 1
309    else
310        echo -e "Format: ${GREEN}PASSED${NORMAL}"
311    fi
312fi
313
314# Sometimes your situation is terrible enough that you need the flexibility.
315# For example, phosphor-mboxd.
316for formatter in "format-code.sh" "format-code"; do
317    if [[ -x "${formatter}" ]]; then
318        echo -e "    ${BLUE}Calling secondary formatter:${NORMAL} ${formatter}"
319        "./${formatter}"
320        if [ -z "$OPTION_NO_DIFF" ]; then
321            git --no-pager diff --exit-code
322        fi
323    fi
324done
325