xref: /openbmc/openbmc-build-scripts/scripts/format-code.sh (revision 71b73249a30d589c97e15b387d4a18e8e6b795a9)
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=()
92declare -A LINTER_IGNORE=()
93LINTERS_ENABLED=()
94declare -A LINTER_TYPES=()
95
96LINTER_REQUIRE+=([commit_spelling]="codespell")
97LINTER_TYPES+=([commit_spelling]="commit")
98function do_commit_spelling() {
99    # Run the codespell with openbmc spcific spellings on the patchset
100    echo -n "openbmc-dictionary - misspelling count >> "
101    sed "s/Signed-off-by.*//" "$@" | \
102        codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count -
103
104    # Run the codespell with generic dictionary on the patchset
105    echo -n "generic-dictionary - misspelling count >> "
106    sed "s/Signed-off-by.*//" "$@" | \
107        codespell --builtin clear,rare,en-GB_to_en-US -d --count -
108}
109
110LINTER_REQUIRE+=([commit_gitlint]="gitlint")
111LINTER_TYPES+=([commit_gitlint]="commit")
112function do_commit_gitlint() {
113    gitlint --extra-path "${CONFIG_PATH}/gitlint/" \
114        --config "${CONFIG_PATH}/.gitlint"
115}
116
117LINTER_REQUIRE+=([eslint]="eslint;.eslintrc.json;${CONFIG_PATH}/eslint-global-config.json")
118LINTER_IGNORE+=([eslint]=".eslintignore")
119LINTER_TYPES+=([eslint]="json")
120function do_eslint() {
121    eslint --no-eslintrc -c "${LINTER_CONFIG[eslint]}" \
122        --ext .json --format=stylish \
123        --resolve-plugins-relative-to /usr/local/lib/node_modules \
124        --no-error-on-unmatched-pattern "$@"
125}
126
127LINTER_REQUIRE+=([pycodestyle]="pycodestyle;setup.cfg")
128LINTER_TYPES+=([pycodestyle]="python")
129function do_pycodestyle() {
130    pycodestyle --show-source "$@"
131}
132
133LINTER_REQUIRE+=([shellcheck]="shellcheck;.shellcheck")
134LINTER_TYPES+=([shellcheck]="bash;sh")
135function do_shellcheck() {
136    shellcheck --color=never -x "$@"
137}
138
139LINTER_REQUIRE+=([clang_format]="clang-format;.clang-format")
140LINTER_IGNORE+=([clang_format]=".clang-ignore;.clang-format-ignore")
141LINTER_TYPES+=([clang_format]="c;cpp")
142do_clang_format() {
143    "${CLANG_FORMAT}" -i "$@"
144}
145
146function get_file_type()
147{
148    case "$(basename "$1")" in
149            # First to early detect template files.
150        *.in | *.meson) echo "meson-template" && return ;;
151        *.mako | *.mako.*) echo "mako" && return ;;
152
153        *.ac) echo "autoconf" && return ;;
154        *.[ch]) echo "c" && return ;;
155        *.[ch]pp) echo "cpp" &&  return ;;
156        *.json) echo "json" && return ;;
157        *.md) echo "markdown" && return ;;
158        *.py) echo "python" && return ;;
159        *.yaml | *.yml) echo "yaml" && return ;;
160
161            # Special files.
162        .git/COMMIT_EDITMSG) echo "commit" && return ;;
163        meson.build) echo "meson" && return ;;
164    esac
165
166    case "$(file "$1")" in
167        *Bourne-Again\ shell*) echo "bash" && return ;;
168        *C++\ source*) echo "cpp" && return ;;
169        *C\ source*) echo "c" && return ;;
170        *JSON\ data*) echo "json" && return ;;
171        *POSIX\ shell*) echo "sh" && return ;;
172        *Python\ script*) echo "python" && return ;;
173        *zsh\ shell*) echo "zsh" && return ;;
174    esac
175
176    echo "unknown"
177}
178
179function check_linter()
180{
181    TITLE="$1"
182    IFS=";" read -r -a ARGS <<< "$2"
183
184    EXE="${ARGS[0]}"
185    if [ ! -x "${EXE}" ]; then
186        if ! which "${EXE}" > /dev/null 2>&1 ; then
187            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${EXE}"
188            return
189        fi
190    fi
191
192    CONFIG="${ARGS[1]}"
193    FALLBACK="${ARGS[2]}"
194
195    if [ -n "${CONFIG}" ]; then
196        if [ -e "${CONFIG}" ]; then
197            LINTER_CONFIG+=( [${TITLE}]="${CONFIG}" )
198        elif [ -n "${FALLBACK}" ] && [ -e "${FALLBACK}" ]; then
199            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find ${CONFIG}; using ${FALLBACK}"
200            LINTER_CONFIG+=( [${TITLE}]="${FALLBACK}" )
201        else
202            echo -e "    ${YELLOW}${TITLE}:${NORMAL} cannot find config ${CONFIG}"
203            return
204        fi
205    fi
206
207    LINTERS_ENABLED+=( "${TITLE}" )
208}
209
210# Check for a global .linter-ignore file.
211GLOBAL_IGNORE=("cat")
212if [ -e ".linter-ignore" ]; then
213    GLOBAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter" ".linter-ignore")
214fi
215
216# Find all the files in the git repository and organize by type.
217declare -A FILES=()
218if [ -e .git/COMMIT_EDITMSG ]; then
219    FILES+=([commit]=".git/COMMIT_EDITMSG")
220fi
221while read -r file; do
222    ftype="$(get_file_type "$file")"
223    FILES+=([$ftype]="$(echo -ne "$file;${FILES[$ftype]:-}")")
224done < <(git ls-files | "${GLOBAL_IGNORE[@]}")
225
226# For each linter, check if there are an applicable files and if it can
227# be enabled.
228for op in "${LINTERS_ALL[@]}"; do
229    for ftype in ${LINTER_TYPES[$op]//;/ }; do
230        if [[ -v FILES["$ftype"] ]]; then
231            check_linter "$op" "${LINTER_REQUIRE[${op}]}"
232            break
233        fi
234    done
235done
236
237# Call each linter.
238for op in "${LINTERS_ENABLED[@]}"; do
239
240    # Determine the linter-specific ignore file(s).
241    LOCAL_IGNORE=("${CONFIG_PATH}/lib/ignore-filter")
242    if [[ -v LINTER_IGNORE["$op"] ]]; then
243        for ignorefile in ${LINTER_IGNORE["$op"]//;/ } ; do
244            if [ -e "$ignorefile" ]; then
245                LOCAL_IGNORE+=("$ignorefile")
246            fi
247        done
248    fi
249    if [ 1 -eq ${#LOCAL_IGNORE[@]} ]; then
250        LOCAL_IGNORE=("cat")
251    fi
252
253    # Find all the files for this linter, filtering out the ignores.
254    LINTER_FILES=()
255    while read -r file ; do
256        if [ -e "$file" ]; then
257            LINTER_FILES+=("$file")
258        fi
259        done < <(for ftype in ${LINTER_TYPES[$op]//;/ }; do
260            # shellcheck disable=SC2001
261            echo "${FILES["$ftype"]:-}" | sed "s/;/\\n/g"
262    done | "${LOCAL_IGNORE[@]}")
263
264    # Call the linter now with all the files.
265    echo -e "    ${BLUE}Running $op${NORMAL}"
266    "do_$op" "${LINTER_FILES[@]}"
267done
268
269# Check for differences.
270if [ -z "$OPTION_NO_DIFF" ]; then
271    echo -e "    ${BLUE}Result differences...${NORMAL}"
272    if ! git --no-pager diff --exit-code ; then
273        echo -e "Format: ${RED}FAILED${NORMAL}"
274        exit 1
275    else
276        echo -e "Format: ${GREEN}PASSED${NORMAL}"
277    fi
278fi
279
280# Sometimes your situation is terrible enough that you need the flexibility.
281# For example, phosphor-mboxd.
282for formatter in "format-code.sh" "format-code"; do
283    if [[ -x "${formatter}" ]]; then
284        echo -e "    ${BLUE}Calling secondary formatter:${NORMAL} ${formatter}"
285        "./${formatter}"
286        if [ -z "$OPTION_NO_DIFF" ]; then
287            git --no-pager diff --exit-code
288        fi
289    fi
290done
291