aboutsummaryrefslogtreecommitdiff
path: root/scripts/pvscheck.sh
blob: c09e8c4555af959bb1fafd251f0b577ea7ec6857 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
#!/bin/sh

# Assume that "local" is available.
# shellcheck disable=SC2039

set -e
# Note: -u causes problems with posh, it barks at “undefined” $@ when no
# arguments provided.
test -z "$POSH_VERSION" && set -u

log_info() {
  >&2 printf "pvscheck.sh: %s\n" "$@"
}

get_jobs_num() {
  if [ -n "${TRAVIS:-}" ] ; then
    # HACK: /proc/cpuinfo on Travis CI is misleading, so hardcode 1.
    echo 1
  else
    echo $(( $(grep -c "^processor" /proc/cpuinfo) + 1 ))
  fi
}

help() {
  echo 'Usage:'
  echo '  pvscheck.sh [--pvs URL] [--deps] [--environment-cc]'
  echo '              [target-directory [branch]]'
  echo '  pvscheck.sh [--pvs URL] [--recheck] [--environment-cc] [--update]'
  echo '              [target-directory]'
  echo '  pvscheck.sh [--pvs URL] --only-analyse [target-directory]'
  echo '  pvscheck.sh [--pvs URL] --pvs-install {target-directory}'
  echo '  pvscheck.sh --patch [--only-build]'
  echo
  echo '    --pvs: Fetch pvs-studio from URL.'
  echo
  echo '    --pvs detect: Auto-detect latest version (by scraping viva64.com).'
  echo
  echo '    --deps: (for regular run) Use top-level Makefile and build deps.'
  echo '            Without this it assumes all dependencies are already'
  echo '            installed.'
  echo
  echo '    --environment-cc: (for regular run and --recheck) Do not export'
  echo '                      CC=clang. Build is still run with CFLAGS=-O0.'
  echo
  echo '    --only-build: (for --patch) Only patch files in ./build directory.'
  echo
  echo '    --pvs-install: Only install PVS-studio to the specified location.'
  echo
  echo '    --patch: patch sources in the current directory.'
  echo '             Does not patch already patched files.'
  echo '             Does not run analysis.'
  echo
  echo '    --recheck: run analysis on a prepared target directory.'
  echo
  echo '    --update: when rechecking first do a pull.'
  echo
  echo '    --only-analyse: run analysis on a prepared target directory '
  echo '                    without building Neovim.'
  echo
  echo '    target-directory: Directory where build should occur.'
  echo '                      Default: ../neovim-pvs'
  echo
  echo '    branch: Branch to check.'
  echo '            Default: master.'
}

getopts_error() {
  local msg="$1" ; shift
  local do_help=
  if test "$msg" = "--help" ; then
    msg="$1" ; shift
    do_help=1
  fi
  printf '%s\n' "$msg" >&2
  if test -n "$do_help" ; then
    printf '\n' >&2
    help >&2
  fi
  echo 'return 1'
  return 1
}

# Usage `eval "$(getopts_long long_defs -- positionals_defs -- "$@")"`
#
# long_defs: list of pairs of arguments like `longopt action`.
# positionals_defs: list of arguments like `action`.
#
# `action` is a space-separated commands:
#
#   store_const [const] [varname] [default]
#     Store constant [const] (default 1) (note: eval’ed) if argument is present
#     (long options only). Assumes long option accepts no arguments.
#   store [varname] [default]
#     Store value. Assumes long option needs an argument.
#   run {func} [varname] [default]
#     Run function {func} and store its output to the [varname]. Assumes no
#     arguments accepted (long options only).
#   modify {func} [varname] [default]
#     Like run, but assumes a single argument, passed to function {func} as $1.
#
#   All actions stores empty value if neither [varname] nor [default] are
#   present. [default] is evaled by top-level `eval`, so be careful. Also note
#   that no arguments may contain spaces, including [default] and [const].
getopts_long() {
  local positional=
  local opt_bases=""
  while test $# -gt 0 ; do
    local arg="$1" ; shift
    local opt_base=
    local act=
    local opt_name=
    if test -z "$positional" ; then
      if test "$arg" = "--" ; then
        positional=0
        continue
      fi
      act="$1" ; shift
      opt_name="$(echo "$arg" | tr '-' '_')"
      opt_base="longopt_$opt_name"
    else
      if test "$arg" = "--" ; then
        break
      fi
      : $(( positional+=1 ))
      act="$arg"
      opt_name="arg_$positional"
      opt_base="positional_$positional"
    fi
    opt_bases="$opt_bases $opt_base"
    eval "local varname_$opt_base=$opt_name"
    local i=0
    for act_subarg in $act ; do
      eval "local act_$(( i+=1 ))_$opt_base=\"\$act_subarg\""
    done
  done
  # Process options
  local positional=0
  local force_positional=
  while test $# -gt 0 ; do
    local argument="$1" ; shift
    local opt_base=
    local has_equal=
    local equal_arg=
    local is_positional=
    if test "$argument" = "--" ; then
      force_positional=1
      continue
    elif test -z "$force_positional" && test "${argument#--}" != "$argument"
    then
      local opt_name="${argument#--}"
      local opt_name_striparg="${opt_name%%=*}"
      if test "$opt_name" = "$opt_name_striparg" ; then
        has_equal=0
      else
        has_equal=1
        equal_arg="${argument#*=}"
        opt_name="$opt_name_striparg"
      fi
      # Use trailing x to prevent stripping newlines
      opt_name="$(printf '%sx' "$opt_name" | tr '-' '_')"
      opt_name="${opt_name%x}"
      if test -n "$(printf '%sx' "$opt_name" | tr -d 'a-z_')" ; then
        getopts_error "Option contains invalid characters: $opt_name"
      fi
      opt_base="longopt_$opt_name"
    else
      : $(( positional+=1 ))
      opt_base="positional_$positional"
      is_positional=1
    fi
    if test -n "$opt_base" ; then
      eval "local occurred_$opt_base=1"

      eval "local act_1=\"\${act_1_$opt_base:-}\""
      eval "local varname=\"\${varname_$opt_base:-}\""
      local need_val=
      local func=
      case "$act_1" in
        (store_const)
          eval "local const=\"\${act_2_${opt_base}:-1}\""
          eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
          printf 'local %s=%s\n' "$varname" "$const"
          ;;
        (store)
          eval "varname=\"\${act_2_${opt_base}:-$varname}\""
          need_val=1
          ;;
        (run)
          eval "func=\"\${act_2_${opt_base}}\""
          eval "varname=\"\${act_3_${opt_base}:-$varname}\""
          printf 'local %s="$(%s)"\n' "$varname" "$func"
          ;;
        (modify)
          eval "func=\"\${act_2_${opt_base}}\""
          eval "varname=\"\${act_3_${opt_base}:-$varname}\""
          need_val=1
          ;;
        ("")
          getopts_error --help "Wrong argument: $argument"
          ;;
      esac
      if test -n "$need_val" ; then
        local val=
        if test -z "$is_positional" ; then
          if test $has_equal = 1 ; then
            val="$equal_arg"
          else
            if test $# -eq 0 ; then
              getopts_error "Missing argument for $opt_name"
            fi
            val="$1" ; shift
          fi
        else
          val="$argument"
        fi
        local escaped_val="'$(printf "%s" "$val" | sed "s/'/'\\\\''/g")'"
        case "$act_1" in
          (store)
            printf 'local %s=%s\n' "$varname" "$escaped_val"
            ;;
          (modify)
            printf 'local %s="$(%s %s)"\n' "$varname" "$func" "$escaped_val"
            ;;
        esac
      fi
    fi
  done
  # Print default values when no values were provided
  local opt_base=
  for opt_base in $opt_bases ; do
    eval "local occurred=\"\${occurred_$opt_base:-}\""
    if test -n "$occurred" ; then
      continue
    fi
    eval "local act_1=\"\$act_1_$opt_base\""
    eval "local varname=\"\$varname_$opt_base\""
    case "$act_1" in
      (store)
        eval "local varname=\"\${act_2_${opt_base}:-$varname}\""
        eval "local default=\"\${act_3_${opt_base}:-}\""
        printf 'local %s=%s\n' "$varname" "$default"
        ;;
      (store_const|run|modify)
        eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
        eval "local default=\"\${act_4_${opt_base}:-}\""
        printf 'local %s=%s\n' "$varname" "$default"
        ;;
    esac
  done
}

get_pvs_comment() {
  local tgt="$1" ; shift

  cat > "$tgt/pvs-comment" << EOF
// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com

EOF
}

install_pvs() {(
  local tgt="$1" ; shift
  local pvs_url="$1" ; shift

  cd "$tgt"

  if test -d pvs-studio ; then
    log_info 'install_pvs: "pvs-studio" directory already exists, skipping install'
    return 0
  fi

  mkdir pvs-studio
  cd pvs-studio

  curl -L -o pvs-studio.tar.gz "$pvs_url"
  tar xzf pvs-studio.tar.gz
  rm pvs-studio.tar.gz
  local pvsdir="$(find . -maxdepth 1 -mindepth 1)"
  find "$pvsdir" -maxdepth 1 -mindepth 1 -exec mv '{}' . \;
  rmdir "$pvsdir"
)}

create_compile_commands() {(
  local tgt="$1" ; shift
  local deps="$1" ; shift
  local environment_cc="$1" ; shift

  if test -z "$environment_cc" ; then
    export CC=clang
  fi
  export CFLAGS=' -O0 '

  if test -z "$deps" ; then
    mkdir -p "$tgt/build"
    (
      cd "$tgt/build"

      cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="$PWD/root"
      make -j"$(get_jobs_num)"
    )
  else
    (
      cd "$tgt"

      make -j"$(get_jobs_num)" CMAKE_EXTRA_FLAGS=" -DCMAKE_INSTALL_PREFIX=$PWD/root -DCMAKE_BUILD_TYPE=Debug "
    )
  fi
  find "$tgt/build/src/nvim/auto" -name '*.test-include.c' -delete
)}

# Warning: realdir below only cares about directories unlike realpath.
#
# realpath is not available in Ubuntu trusty yet.
realdir() {(
  local dir="$1"
  local add=""
  while ! cd "$dir" 2>/dev/null ; do
    add="${dir##*/}/$add"
    local new_dir="${dir%/*}"
    if test "$new_dir" = "$dir" ; then
      return 1
    fi
    dir="$new_dir"
  done
  printf '%s\n' "$PWD/$add"
)}

patch_sources() {(
  local tgt="$1" ; shift
  local only_bulid="${1}" ; shift

  get_pvs_comment "$tgt"

  local sh_script='
    pvs_comment="$(cat pvs-comment ; echo -n EOS)"
    filehead="$(head -c $(( ${#pvs_comment} - 3 )) "$1" ; echo -n EOS)"
    if test "x$filehead" != "x$pvs_comment" ; then
      cat pvs-comment "$1" > "$1.tmp"
      mv "$1.tmp" "$1"
    fi
  '

  cd "$tgt"

  if test "$only_build" != "--only-build" ; then
    find \
      src/nvim test/functional/fixtures test/unit/fixtures \
      -name '*.c' \
      -exec /bin/sh -c "$sh_script" - '{}' \;
  fi

  find \
    build/src/nvim/auto build/config \
    -name '*.c' -not -name '*.test-include.c' \
    -exec /bin/sh -c "$sh_script" - '{}' \;

  rm pvs-comment
)}

run_analysis() {(
  local tgt="$1" ; shift

  cd "$tgt"

  # pvs-studio-analyzer exits with a non-zero exit code when there are detected
  # errors, so ignore its return
  pvs-studio-analyzer \
    analyze \
      --threads "$(get_jobs_num)" \
      --output-file PVS-studio.log \
      --file build/compile_commands.json \
      --sourcetree-root . || true

  rm -rf PVS-studio.{xml,err,tsk,html.d}
  local plog_args="PVS-studio.log --srcRoot . --excludedCodes V011"
  plog-converter $plog_args --renderTypes xml       --output PVS-studio.xml
  plog-converter $plog_args --renderTypes errorfile --output PVS-studio.err
  plog-converter $plog_args --renderTypes tasklist  --output PVS-studio.tsk
  plog-converter $plog_args --renderTypes fullhtml  --output PVS-studio.html.d
)}

detect_url() {
  local url="${1:-detect}"
  if test "$url" = detect ; then
    curl --silent -L 'https://www.viva64.com/en/pvs-studio-download/' \
    | grep -o 'https\{0,1\}://[^"<>]\{1,\}/pvs-studio[^/"<>]*-x86_64\.tgz' \
    || echo FAILED
  else
    printf '%s' "$url"
  fi
}

do_check() {
  local tgt="$1" ; shift
  local branch="$1" ; shift
  local pvs_url="$1" ; shift
  local deps="$1" ; shift
  local environment_cc="$1" ; shift

  if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
    pvs_url="$(detect_url detect)"
    if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
      echo "failed to auto-detect PVS URL"
      exit 1
    fi
    echo "Auto-detected PVS URL: ${pvs_url}"
  fi

  git clone --branch="$branch" . "$tgt"

  install_pvs "$tgt" "$pvs_url"

  do_recheck "$tgt" "$deps" "$environment_cc" ""
}

do_recheck() {
  local tgt="$1" ; shift
  local deps="$1" ; shift
  local environment_cc="$1" ; shift
  local update="$1" ; shift

  if test -n "$update" ; then
    (
      cd "$tgt"
      local branch="$(git rev-parse --abbrev-ref HEAD)"
      git checkout --detach
      git fetch -f origin "${branch}:${branch}"
      git checkout -f "$branch"
    )
  fi

  create_compile_commands "$tgt" "$deps" "$environment_cc"

  do_analysis "$tgt"
}

do_analysis() {
  local tgt="$1" ; shift

  if test -d "$tgt/pvs-studio" ; then
    local saved_pwd="$PWD"
    cd "$tgt/pvs-studio"
    export PATH="$PWD/bin${PATH+:}${PATH}"
    cd "$saved_pwd"
  fi

  run_analysis "$tgt"
}

main() {
  eval "$(
    getopts_long \
      help store_const \
      pvs 'modify detect_url pvs_url' \
      patch store_const \
      only-build 'store_const --only-build' \
      recheck store_const \
      only-analyse store_const \
      pvs-install store_const \
      deps store_const \
      environment-cc store_const \
      update store_const \
      -- \
      'modify realdir tgt "$PWD/../neovim-pvs"' \
      'store branch master' \
      -- "$@"
  )"

  if test -n "$help" ; then
    help
    return 0
  fi

  if test -n "$patch" ; then
    patch_sources "$tgt" "$only_build"
  elif test -n "$pvs_install" ; then
    install_pvs "$tgt" "$pvs_url"
  elif test -n "$recheck" ; then
    do_recheck "$tgt" "$deps" "$environment_cc" "$update"
  elif test -n "$only_analyse" ; then
    do_analysis "$tgt"
  else
    do_check "$tgt" "$branch" "$pvs_url" "$deps" "$environment_cc"
  fi
}

main "$@"