xref: /openbmc/openbmc-build-scripts/scripts/format-code.sh (revision c1513c88966408b8df6d261276976e6820d4cd64)
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] "
14    echo "                      [<path>]"
15    echo
16    echo "Format and lint a repository."
17    echo
18    echo "Arguments:"
19    echo "    path           Path to git repository (default to pwd)"
20}
21
22eval set -- "$(getopt -o 'h' --long 'help' -n 'format-code.sh' -- "$@")"
23while true; do
24    case "$1" in
25        '-h'|'--help')
26            display_help && exit 0
27            ;;
28
29        '--')
30            shift
31            break
32            ;;
33
34        *)
35            echo "unknown option: $1"
36            display_help && exit 1
37            ;;
38    esac
39done
40
41# Detect tty and set nicer colors.
42if [ -t 1 ]; then
43    BLUE="\e[34m"
44    GREEN="\e[32m"
45    NORMAL="\e[0m"
46    RED="\e[31m"
47    YELLOW="\e[33m"
48else # non-tty, no escapes.
49    BLUE=""
50    GREEN=""
51    NORMAL=""
52    RED=""
53    YELLOW=""
54fi
55
56# Path to default config files for linters.
57CONFIG_PATH="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/config"
58
59# Find repository root for `pwd` or $1.
60if [ -z "$1" ]; then
61    DIR="$(git rev-parse --show-toplevel || pwd)"
62else
63    DIR="$(git -C "$1" rev-parse --show-toplevel)"
64fi
65if [ ! -d "$DIR/.git" ]; then
66    echo "${RED}Error:${NORMAL} Directory ($DIR) does not appear to be a git repository"
67    exit 1
68fi
69
70cd "${DIR}"
71echo -e "    ${BLUE}Formatting code under${NORMAL} $DIR"
72
73ALL_OPERATIONS=( \
74        commit_gitlint \
75        commit_spelling \
76        clang_format \
77        eslint \
78        pycodestyle \
79        shellcheck \
80    )
81
82function do_commit_spelling() {
83    if [ ! -e .git/COMMIT_EDITMSG ]; then
84        return
85    fi
86    echo -e "    ${BLUE}Running codespell${NORMAL}"
87
88    # Run the codespell with openbmc spcific spellings on the patchset
89    echo "openbmc-dictionary - misspelling count >> "
90    sed "s/Signed-off-by.*//" .git/COMMIT_EDITMSG | \
91        codespell -D "${CONFIG_PATH}/openbmc-spelling.txt" -d --count -
92
93    # Run the codespell with generic dictionary on the patchset
94    echo "generic-dictionary - misspelling count >> "
95    sed "s/Signed-off-by.*//" .git/COMMIT_EDITMSG | \
96        codespell --builtin clear,rare,en-GB_to_en-US -d --count -
97}
98
99function do_commit_gitlint() {
100    echo -e "    ${BLUE}Running gitlint${NORMAL}"
101    # Check for commit message issues
102    gitlint \
103        --extra-path "${CONFIG_PATH}/gitlint/" \
104        --config "${CONFIG_PATH}/.gitlint"
105}
106
107function do_eslint() {
108    if [[ -f ".eslintignore" ]]; then
109        ESLINT_IGNORE="--ignore-path .eslintignore"
110    elif [[ -f ".gitignore" ]]; then
111        ESLINT_IGNORE="--ignore-path .gitignore"
112    fi
113
114    # Get the eslint configuration from the repository
115    if [[ -f ".eslintrc.json" ]]; then
116        echo -e "    ${BLUE}Running eslint${NORMAL}"
117        ESLINT_RC="-c .eslintrc.json"
118    else
119        echo -e "    ${BLUE}Running eslint using ${YELLOW}the global config${NORMAL}"
120        ESLINT_RC="--no-eslintrc -c ${CONFIG_PATH}/eslint-global-config.json"
121    fi
122
123    ESLINT_COMMAND="eslint . ${ESLINT_IGNORE} ${ESLINT_RC} \
124               --ext .json --format=stylish \
125               --resolve-plugins-relative-to /usr/local/lib/node_modules \
126               --no-error-on-unmatched-pattern"
127
128    # Print eslint command
129    echo "$ESLINT_COMMAND"
130    # Run eslint
131    $ESLINT_COMMAND
132}
133
134function do_pycodestyle() {
135    if [[ -f "setup.cfg" ]]; then
136        echo -e "    ${BLUE}Running pycodestyle${NORMAL}"
137        pycodestyle --show-source --exclude=subprojects .
138        rc=$?
139        if [[ ${rc} -ne 0 ]]; then
140            exit ${rc}
141        fi
142    fi
143}
144
145function do_shellcheck() {
146    # If .shellcheck exists, stop on error.  Otherwise, allow pass.
147    if [[ -f ".shellcheck" ]]; then
148        local shellcheck_allowfail="false"
149    else
150        local shellcheck_allowfail="true"
151    fi
152
153    # Run shellcheck on any shell-script.
154    shell_scripts="$(git ls-files | xargs -n1 file -0 | \
155    grep -a "shell script" | cut -d '' -f 1)"
156    if [ -n "${shell_scripts}" ]; then
157        echo -e "    ${BLUE}Running shellcheck${NORMAL}"
158    fi
159    for script in ${shell_scripts}; do
160        shellcheck --color=never -x "${script}" || ${shellcheck_allowfail}
161    done
162}
163
164
165do_clang_format() {
166    # Allow called scripts to know which clang format we are using
167    export CLANG_FORMAT="clang-format"
168    IGNORE_FILE=".clang-ignore"
169    declare -a IGNORE_LIST
170
171    if [[ -f "${IGNORE_FILE}" ]]; then
172        readarray -t IGNORE_LIST < "${IGNORE_FILE}"
173    fi
174
175    ignorepaths=""
176    ignorefiles=""
177
178    for path in "${IGNORE_LIST[@]}"; do
179        # Check for comment, line starting with space, or zero-length string.
180        # Checking for [[:space:]] checks all options.
181        if [[ -z "${path}" ]] || [[ "${path}" =~ ^(#|[[:space:]]).*$ ]]; then
182            continue
183        fi
184
185        # All paths must start with ./ for find's path prune expectation.
186        if [[ "${path}" =~ ^\.\/.+$ ]]; then
187            ignorepaths+=" ${path}"
188        else
189            ignorefiles+=" ${path}"
190        fi
191    done
192
193    searchfiles=""
194    while read -r path; do
195        # skip ignorefiles
196        if [[ $ignorefiles == *"$(basename "${path}")"* ]]; then
197            continue
198        fi
199
200        skip=false
201        #skip paths in ingorepaths
202        for pathname in $ignorepaths; do
203            if [[ "./${path}" == "${pathname}"* ]]; then
204                skip=true
205                break
206            fi
207        done
208
209        if [ "$skip" = true ]; then
210            continue
211        fi
212        # shellcheck disable=2089
213        searchfiles+="\"./${path}\" "
214
215        # Get C and C++ files managed by git and skip the mako files
216    done <<<"$(git ls-files | grep -e '\.[ch]pp$' -e '\.[ch]$' | grep -v '\.mako\.')"
217
218    if [[ -f ".clang-format" ]]; then
219        echo -e "    ${BLUE}Running clang-format${NORMAL}"
220        # shellcheck disable=SC2090 disable=SC2086
221        echo ${searchfiles} | xargs "${CLANG_FORMAT}" -i
222    fi
223
224}
225
226for op in "${ALL_OPERATIONS[@]}"; do
227    "do_$op"
228done
229
230echo -e "    ${BLUE}Result differences...${NORMAL}"
231if ! git --no-pager diff --exit-code ; then
232    echo -e "Format: ${RED}FAILED${NORMAL}"
233    exit 1
234else
235    echo -e "Format: ${GREEN}PASSED${NORMAL}"
236fi
237
238# Sometimes your situation is terrible enough that you need the flexibility.
239# For example, phosphor-mboxd.
240if [[ -f "format-code.sh" ]]; then
241    ./format-code.sh
242    git --no-pager diff --exit-code
243fi
244