github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/tools/lint (about) 1 #!/bin/bash 2 # This scipt lints files for common errors. 3 # 4 # For go files, it runs gofmt and go vet, and optionally golint and 5 # gocyclo, if they are installed. 6 # 7 # For shell files, it runs shfmt. If you don't have that installed, you can get 8 # it with: 9 # curl -fsSLo shfmt https://github.com/mvdan/sh/releases/download/v1.3.0/shfmt_v1.3.0_linux_amd64 10 # chmod +x shfmt 11 # 12 # With no arguments, it lints the current files staged 13 # for git commit. Or you can pass it explicit filenames 14 # (or directories) and it will lint them. 15 # 16 # To use this script automatically, run: 17 # ln -s ../../bin/lint .git/hooks/pre-commit 18 19 set -e 20 21 LINT_IGNORE_FILE=${LINT_IGNORE_FILE:-".lintignore"} 22 23 IGNORE_LINT_COMMENT= 24 IGNORE_SPELLINGS= 25 PARALLEL= 26 while true; do 27 case "$1" in 28 -nocomment) 29 IGNORE_LINT_COMMENT=1 30 shift 1 31 ;; 32 -notestpackage) 33 # NOOP, still accepted for backwards compatibility 34 shift 1 35 ;; 36 -ignorespelling) 37 IGNORE_SPELLINGS="$2,$IGNORE_SPELLINGS" 38 shift 2 39 ;; 40 -p) 41 PARALLEL=1 42 shift 1 43 ;; 44 *) 45 break 46 ;; 47 esac 48 done 49 50 spell_check() { 51 local filename="$1" 52 local lint_result=0 53 54 # misspell is completely optional. If you don't like it 55 # don't have it installed. 56 if ! type misspell >/dev/null 2>&1; then 57 return $lint_result 58 fi 59 60 if ! misspell -error -i "$IGNORE_SPELLINGS" "${filename}"; then 61 lint_result=1 62 fi 63 64 return $lint_result 65 } 66 67 lint_go() { 68 # This function is called on a whole directory containing Go files 69 local filename="$1" 70 local lint_result=0 71 72 if [ -n "$(gofmt -s -l "${filename}")" ]; then 73 lint_result=1 74 echo "${filename}: run gofmt -s -w ${filename}" 75 fi 76 77 go vet "${filename}" || lint_result=$? 78 79 # golint is completely optional. If you don't like it 80 # don't have it installed. 81 if type golint >/dev/null 2>&1; then 82 # golint doesn't set an exit code it seems 83 if [ -z "$IGNORE_LINT_COMMENT" ]; then 84 lintoutput=$(golint "${filename}") 85 else 86 lintoutput=$(golint "${filename}" | grep -vE 'comment|dot imports|ALL_CAPS') 87 fi 88 if [ -n "$lintoutput" ]; then 89 lint_result=1 90 echo "$lintoutput" 91 fi 92 fi 93 94 # gocyclo is completely optional. If you don't like it 95 # don't have it installed. Also never blocks a commit, 96 # it just warns. 97 if type gocyclo >/dev/null 2>&1; then 98 gocyclo -over 25 "${filename}" | while read -r line; do 99 echo "${filename}": higher than 25 cyclomatic complexity - "${line}" 100 done 101 fi 102 103 return $lint_result 104 } 105 106 lint_sh() { 107 local filename="$1" 108 local lint_result=0 109 110 # Skip shfmt validation, if not installed 111 if type shfmt >/dev/null 2>&1; then 112 if ! diff -u "${filename}" <(shfmt -i 4 "${filename}"); then 113 lint_result=1 114 echo "${filename}: run shfmt -i 4 -w ${filename}" 115 fi 116 fi 117 118 # the shellcheck is completely optional. If you don't like it 119 # don't have it installed. 120 if type shellcheck >/dev/null 2>&1; then 121 shellcheck "${filename}" || lint_result=1 122 fi 123 124 return $lint_result 125 } 126 127 lint_tf() { 128 local filename="$1" 129 local lint_result=0 130 131 if ! diff -u <(hclfmt "${filename}") "${filename}"; then 132 lint_result=1 133 echo "${filename}: run hclfmt -w ${filename}" 134 fi 135 136 return $lint_result 137 } 138 139 lint_md() { 140 local filename="$1" 141 local lint_result=0 142 143 for i in '=======' '>>>>>>>'; do 144 if grep -q "${i}" "${filename}"; then 145 lint_result=1 146 echo "${filename}: bad merge/rebase!" 147 fi 148 done 149 150 return $lint_result 151 } 152 153 lint_py() { 154 local filename="$1" 155 local lint_result=0 156 157 if yapf --diff "${filename}" | grep -qE '^[+-]'; then 158 lint_result=1 159 echo "${filename} needs reformatting. Run: yapf --in-place ${filename}" 160 else 161 # Only run flake8 if yapf passes, since they pick up a lot of similar issues 162 flake8 "${filename}" || lint_result=1 163 fi 164 165 return $lint_result 166 } 167 168 lint() { 169 filename="$1" 170 ext="${filename##*\.}" 171 local lint_result=0 172 173 # Don't lint deleted files 174 if [ ! -f "$filename" ]; then 175 return 176 fi 177 178 # Don't lint specific files 179 case "$(basename "${filename}")" in 180 static.go) return ;; 181 coverage.html) return ;; 182 *.pb.go) return ;; 183 esac 184 185 mimetype=$(file --mime-type "${filename}" | awk '{print $2}') 186 187 case "$mimetype.$ext" in 188 text/x-shellscript.*) lint_sh "${filename}" || lint_result=1 ;; 189 *.go) ;; # done at directory level 190 *.tf) lint_tf "${filename}" || lint_result=1 ;; 191 *.md) lint_md "${filename}" || lint_result=1 ;; 192 *.py) lint_py "${filename}" || lint_result=1 ;; 193 esac 194 195 # we don't want to spell check tar balls, binaries, Makefile and json files 196 case "$mimetype.$ext" in 197 *.tar | *.gz | *.json) ;; 198 *.req | *.key | *.pem | *.crt) ;; 199 application/x-executable.*) ;; 200 text/x-makefile.*) ;; 201 *) spell_check "${filename}" || lint_result=1 ;; 202 esac 203 204 return $lint_result 205 } 206 207 lint_files() { 208 local lint_result=0 209 while read -r filename; do 210 lint "${filename}" || lint_result=1 211 done 212 return $lint_result 213 } 214 215 matches_any() { 216 local filename="$1" 217 local patterns="$2" 218 while read -r pattern; do 219 # shellcheck disable=SC2053 220 # Use the [[ operator without quotes on $pattern 221 # in order to "glob" the provided filename: 222 [[ "$filename" == $pattern ]] && return 0 223 done <<<"$patterns" 224 return 1 225 } 226 227 filter_out() { 228 local patterns_file="$1" 229 if [ -n "$patterns_file" ] && [ -r "$patterns_file" ]; then 230 local patterns 231 patterns=$(sed '/^#.*$/d ; /^\s*$/d' "$patterns_file") # Remove blank lines and comments before we start iterating. 232 [ -n "$DEBUG" ] && echo >&2 "> Filters:" && echo >&2 "$patterns" 233 local filtered_out=() 234 while read -r filename; do 235 matches_any "$filename" "$patterns" && filtered_out+=("$filename") || echo "$filename" 236 done 237 [ -n "$DEBUG" ] && echo >&2 "> Files filtered out (i.e. NOT linted):" && printf >&2 '%s\n' "${filtered_out[@]}" 238 else 239 cat # No patterns provided: simply propagate stdin to stdout. 240 fi 241 } 242 243 lint_directory() { 244 local dirname="$1" 245 local lint_result=0 246 # This test is just checking if there are any Go files in the directory 247 if compgen -G "$dirname/*.go" >/dev/null; then 248 lint_go "${dirname}" || lint_result=1 249 fi 250 ls $dirname/* | filter_out "$LINT_IGNORE_FILE" | lint_files 251 return $lint_result 252 } 253 254 lint_directories() { 255 local lint_result=0 256 while read -r dirname; do 257 lint_directory "${dirname}" || lint_result=1 258 done 259 exit $lint_result 260 } 261 262 list_directories() { 263 if [ $# -gt 0 ]; then 264 find "$@" \( -name vendor -o -name .git -o -name .cache -o -name .pkg \) -prune -o -type d 265 fi 266 } 267 268 if [ $# = 1 ] && [ -f "$1" ]; then 269 lint "$1" 270 else 271 list_directories "$@" | lint_directories 272 fi