/ test / script / custom-linting-rules
custom-linting-rules
  1  #!/usr/bin/env bash
  2  
  3  set -e
  4  set -u
  5  
  6  # This Bash script implements custom sanity checks for scripts beyond what
  7  # Vint covers, which are easy to check with regex.
  8  
  9  # A flag for automatically fixing some errors.
 10  FIX_ERRORS=0
 11  RETURN_CODE=0
 12  
 13  function print_help() {
 14      echo "Usage: test/script/custom-linting-rules [--fix] [DIRECTORY]" 1>&2
 15      echo 1>&2
 16      echo "  -h, --help    Print this help text" 1>&2
 17      echo "      --fix     Automatically fix some errors" 1>&2
 18      exit 1
 19  }
 20  
 21  while [ $# -ne 0 ]; do
 22      case $1 in
 23      -h) ;& --help)
 24          print_help
 25      ;;
 26      --fix)
 27          FIX_ERRORS=1
 28          shift
 29      ;;
 30      --)
 31          shift
 32          break
 33      ;;
 34      -?*)
 35          echo "Invalid argument: $1" 1>&2
 36          exit 1
 37      ;;
 38      *)
 39          break
 40      ;;
 41      esac
 42  done
 43  
 44  if [ $# -eq 0 ] || [ -z "$1" ]; then
 45      print_help
 46  fi
 47  
 48  shopt -s globstar
 49  
 50  directories=("$@")
 51  
 52  check_errors() {
 53      regex="$1"
 54      message="$2"
 55      include_arg=''
 56      exclude_arg=''
 57  
 58      if [ $# -gt 2 ]; then
 59          include_arg="--include $3"
 60      fi
 61  
 62      if [ $# -gt 3 ]; then
 63          shift
 64          shift
 65          shift
 66  
 67          while (( "$#" )); do
 68            exclude_arg="$exclude_arg --exclude $1"
 69            shift
 70          done
 71      fi
 72  
 73      for directory in "${directories[@]}"; do
 74          # shellcheck disable=SC2086
 75          while read -r; do
 76              line=$(cut -d ":" -f2 <<< "$REPLY")
 77  
 78              if ((line > 1)); then
 79                  line=$((line - 1))
 80                  file=$(cut -d ":" -f1 <<< "$REPLY")
 81  
 82                  if sed -n "${line},${line}p" $file | grep -q '^ *" *no-custom-checks$'; then
 83                      continue
 84                  fi
 85              fi
 86  
 87              RETURN_CODE=1
 88              echo "$REPLY $message"
 89          done < <(grep -H -n "$regex" $include_arg $exclude_arg "$directory"/**/*.vim \
 90              | grep -v 'no-custom-checks' \
 91              | grep -o '^[^:]\+:[0-9]\+' \
 92              | sed 's:^\./::')
 93      done
 94  }
 95  
 96  if (( FIX_ERRORS )); then
 97      for directory in "${directories[@]}"; do
 98          sed -i "s/^\(function.*)\) *$/\1 abort/" "$directory"/**/*.vim
 99          sed -i "s/shellescape(/ale#Escape(/" "$directory"/**/*.vim
100          sed -i 's/==#/is#/g' "$directory"/**/*.vim
101          sed -i 's/==?/is?/g' "$directory"/**/*.vim
102          sed -i 's/!=#/isnot#/g' "$directory"/**/*.vim
103          sed -i 's/!=?/isnot?/g' "$directory"/**/*.vim
104          # Improving type checks.
105          sed -i $'s/\\(==.\\?\\|is\\) type([\'"]\+)/is v:t_string/g' "$directory"/**/*.vim
106          sed -i 's/\(==.\?\|is\) type([0-9]\+)/is v:t_number/g' "$directory"/**/*.vim
107          sed -i 's/\(==.\?\|is\) type(\[\])/is v:t_list/g' "$directory"/**/*.vim
108          sed -i 's/\(==.\?\|is\) type({})/is v:t_dict/g' "$directory"/**/*.vim
109          sed -i 's/\(==.\?\|is\) type(function([^)]\+))/is v:t_func/g' "$directory"/**/*.vim
110          sed -i $'s/\\(!=.\\?\\|isnot\\) type([\'"]\+)/isnot v:t_string/g' "$directory"/**/*.vim
111          sed -i 's/\(!=.\?\|isnot\) type([0-9]\+)/isnot v:t_number/g' "$directory"/**/*.vim
112          sed -i 's/\(!=.\?\|isnot\) type(\[\])/isnot v:t_list/g' "$directory"/**/*.vim
113          sed -i 's/\(!=.\?\|isnot\) type({})/isnot v:t_dict/g' "$directory"/**/*.vim
114          sed -i 's/\(!=.\?\|isnot\) type(function([^)]\+))/isnot v:t_func/g' "$directory"/**/*.vim
115      done
116  fi
117  
118  # The arguments are: regex, explanation, [filename_filter], [list, of, exclusions]
119  check_errors \
120      '^function.*) *$' \
121      'Function without abort keyword (See :help except-compat)'
122  check_errors '^function[^!]' 'function without !'
123  check_errors ' \+$' 'Trailing whitespace'
124  check_errors '^ * end\?i\? *$' 'Write endif, not en, end, or endi'
125  check_errors '^  [^ ]' 'Use four spaces, not two spaces'
126  check_errors $'\t' 'Use four spaces, not tabs'
127  # This check should prevent people from using a particular inconsistent name.
128  check_errors 'let g:ale_\w\+_\w\+_args =' 'Name your option g:ale_<filetype>_<lintername>_options instead'
129  check_errors 'shellescape(' 'Use ale#Escape instead of shellescape'
130  check_errors 'simplify(' 'Use ale#path#Simplify instead of simplify'
131  check_errors 'tempname(' 'Use ale#util#Tempname instead of tempname'
132  check_errors 'getcurpos(' "Use getpos('.') instead of getcurpos() if you don't need curswant, to avoid a bug that changes curswant"
133  check_errors "expand(['\"]%" "Use expand('#' . a:buffer . '...') instead. You might get a filename for the wrong buffer."
134  check_errors 'getcwd()' "Do not use getcwd(), as it could run from the wrong buffer. Use expand('#' . a:buffer . ':p:h') instead."
135  check_errors '==#' "Use 'is#' instead of '==#'. 0 ==# 'foobar' is true"
136  check_errors '==?' "Use 'is?' instead of '==?'. 0 ==? 'foobar' is true"
137  check_errors '!=#' "Use 'isnot#' instead of '!=#'. 0 !=# 'foobar' is false"
138  check_errors '!=?' "Use 'isnot?' instead of '!=?'. 0 !=? 'foobar' is false"
139  check_errors '^ *:\?echo' "Stray echo line. Ignore with \" no-custom-checks if needed"
140  check_errors '^ *:\?redir' 'User execute() instead of redir'
141  # Exclusions for grandfathered-in exceptions
142  exclusions="clojure/clj_kondo.vim elixir/elixir_ls.vim go/golangci_lint.vim swift/swiftformat.vim"
143  # shellcheck disable=SC2086
144  check_errors $'name.:.*\'[a-z_]*[^a-z_0-9][a-z_0-9]*\',$' 'Use snake_case names for linters' '*/ale_linters/*' $exclusions
145  # Checks for improving type checks.
146  check_errors $'\\(==.\\?\\|is\\) type([\'"]\+)' "Use 'is v:t_string' instead"
147  check_errors '\(==.\?\|is\) type([0-9]\+)' "Use 'is v:t_number' instead"
148  check_errors '\(==.\?\|is\) type(\[\])' "Use 'is v:t_list' instead"
149  check_errors '\(==.\?\|is\) type({})' "Use 'is v:t_dict' instead"
150  check_errors '\(==.\?\|is\) type(function([^)]\+))' "Use 'is v:t_func' instead"
151  check_errors $'\\(!=.\\?\\|isnot\\) type([\'"]\+)' "Use 'isnot v:t_string' instead"
152  check_errors '\(!=.\?\|isnot\) type([0-9]\+)' "Use 'isnot v:t_number' instead"
153  check_errors '\(!=.\?\|isnot\) type(\[\])' "Use 'isnot v:t_list' instead"
154  check_errors '\(!=.\?\|isnot\) type({})' "Use 'isnot v:t_dict' instead"
155  check_errors '\(!=.\?\|isnot\) type(function([^)]\+))' "Use 'isnot v:t_func' instead"
156  
157  # Run a Python script to find lines that require padding around them.  For
158  # users without Python installed, we'll skip these checks. GitHub Actions will
159  # run the script.
160  if command -v python > /dev/null; then
161      if ! test/script/block-padding-checker "$directory"/**/*.vim; then
162          RETURN_CODE=1
163      fi
164  fi
165  
166  exit $RETURN_CODE