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