-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpagurus-check
More file actions
executable file
·324 lines (291 loc) · 12 KB
/
pagurus-check
File metadata and controls
executable file
·324 lines (291 loc) · 12 KB
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
#!/bin/bash
# pagurus-check — Whole-project C borrow-checking with the pagurus Clang plugin.
#
# SYNOPSIS
# pagurus-check [OPTIONS] [FILE...]
# pagurus-check [OPTIONS] --dir=DIR
# pagurus-check [OPTIONS] --compile-db=compile_commands.json
#
# OPTIONS
# -p PATH, --plugin=PATH pagurus_plugin.so location
# [default: ./build/pagurus_plugin.so]
# -C CMD, --clang=CMD Clang executable
# [default: clang]
# -f FLAGS,--cflags=FLAGS Extra clang flags (e.g. "-DFOO -Iinclude")
# -d DIR, --dir=DIR Scan DIR recursively for *.c files
# -b FILE, --compile-db=FILE JSON compilation database produced by
# `bear make` or `intercept-build make`;
# per-file include paths and defines are
# extracted automatically
# -j N, --jobs=N Parallel jobs [default: 1]
# --dry-run Dry-run mode: report findings but do NOT
# write .pagurus.c output files
# --ir-pass Also enable the LLVM IR analysis pass
# (-fpass-plugin=, requires -g -O0 in CFLAGS)
# -h, --help Show this help and exit
#
# DEPENDENCIES
# pagurus_plugin.so — the built plugin (see README for build instructions)
# clang — same major version as the plugin was built against
# Standard POSIX tools: bash ≥ 4, awk, find, grep (no python, perl, or jq)
#
# EXIT STATUS
# 0 All files pass (no E0xx errors reported)
# 1 One or more files have borrow-check errors
#
# EXAMPLES
# # Check two files with an include directory:
# pagurus-check --cflags="-Iinclude" src/widget.c src/main.c
#
# # Check every .c file under src/:
# pagurus-check --dir=src --cflags="-Iinclude -DNDEBUG"
#
# # Use a compilation database generated by `bear make`:
# bear make
# pagurus-check --compile-db=compile_commands.json
#
# # Dry-run on a whole project with 4 parallel jobs:
# pagurus-check --dry-run --jobs=4 --dir=src
set -euo pipefail
# ── Defaults ─────────────────────────────────────────────────────────────────
PLUGIN="${PAGURUS_PLUGIN:-./build/pagurus_plugin.so}"
CLANG="${PAGURUS_CLANG:-clang}"
EXTRA_CFLAGS="${PAGURUS_CFLAGS:-}"
JOBS=1
DRY_RUN=0
IR_PASS=0
SCAN_DIR=""
COMPILE_DB=""
EXPLICIT_FILES=()
# ── Argument parsing ──────────────────────────────────────────────────────────
usage() {
sed -n '/^# SYNOPSIS/,/^[^#]/{ /^# /{ s/^# \{0,1\}//; p } }' "$0"
exit 0
}
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
--dry-run) DRY_RUN=1 ; shift ;;
--ir-pass) IR_PASS=1 ; shift ;;
-p) PLUGIN="$2" ; shift 2 ;;
--plugin=*) PLUGIN="${1#--plugin=}" ; shift ;;
-C) CLANG="$2" ; shift 2 ;;
--clang=*) CLANG="${1#--clang=}" ; shift ;;
-f) EXTRA_CFLAGS="$2" ; shift 2 ;;
--cflags=*) EXTRA_CFLAGS="${1#--cflags=}" ; shift ;;
-d) SCAN_DIR="$2" ; shift 2 ;;
--dir=*) SCAN_DIR="${1#--dir=}" ; shift ;;
-b) COMPILE_DB="$2" ; shift 2 ;;
--compile-db=*) COMPILE_DB="${1#--compile-db=}" ; shift ;;
-j) JOBS="$2" ; shift 2 ;;
--jobs=*) JOBS="${1#--jobs=}" ; shift ;;
--) shift ; EXPLICIT_FILES+=("$@") ; break ;;
-*) echo "pagurus-check: unknown option '$1'" >&2 ; exit 1 ;;
*) EXPLICIT_FILES+=("$1") ; shift ;;
esac
done
# ── Validate plugin ───────────────────────────────────────────────────────────
if [[ ! -f "$PLUGIN" ]]; then
echo "pagurus-check: plugin not found: $PLUGIN" >&2
echo " Build with: mkdir -p build && cd build && cmake .. && make" >&2
echo " Or set --plugin=PATH or PAGURUS_PLUGIN env var." >&2
exit 1
fi
# ── Build flags ───────────────────────────────────────────────────────────────
PLUGIN_FLAGS=("-fplugin=$PLUGIN")
if [[ $DRY_RUN -eq 1 ]]; then
PLUGIN_FLAGS+=(-Xclang -plugin-arg-pagurus -Xclang dry-run)
fi
if [[ $IR_PASS -eq 1 ]]; then
PLUGIN_FLAGS+=("-fpass-plugin=$PLUGIN" -g -O0)
fi
# ── Collect files ─────────────────────────────────────────────────────────────
# Source files and their per-file extra flags are stored as parallel arrays.
ALL_FILES=()
declare -A FILE_FLAGS # file → extra flags string (from compile-db)
if [[ -n "$COMPILE_DB" ]]; then
if [[ ! -f "$COMPILE_DB" ]]; then
echo "pagurus-check: compile-db not found: $COMPILE_DB" >&2
exit 1
fi
# Parse compile_commands.json with awk — no python3 required.
#
# Handles both formats produced by bear and cmake:
# "command" : string — single shell command string (bear, cmake default)
# "arguments": array — pre-split argument list (cmake with newer generators)
#
# Known limitation: arguments or paths containing spaces are not supported
# in "command" string format (space is used as the token separator).
# The "arguments" array format (one token per line in JSON) handles spaces
# correctly. In practice, bear and cmake generate absolute paths with no
# spaces, so this limitation rarely matters for C projects.
#
# Per-entry flags: compiler name, -c, -o/-oFOO, -MF/-MT/-MMD/-MQ/-MP,
# and source files (.c/.cc/.cpp/.cxx) are stripped; everything else is kept.
_db_base_dir=$(cd "$(dirname "$COMPILE_DB")" && pwd)
awk_out=$(awk -v base_dir="$_db_base_dir" '
# Strip compilation-only flags from a space-separated command string;
# returns the surviving flags (include paths, defines, feature flags, …).
function strip_cmd(cmd, i, n, words, w, skip, flags) {
n = split(cmd, words, " ")
flags = ""; skip = 0
for (i = 2; i <= n; i++) { # i=1 is the compiler name
w = words[i]
if (skip) { skip = 0; continue }
if (w == "-c" || w == "-MMD" || w == "-MD" ||
w == "-MP" || w == "-MG") { continue }
if (w == "-o" || w == "-MF" || w == "-MT" || w == "-MQ") {
skip = 1; continue
}
if (w ~ /^-o.+/) { continue } # -ofoo
if (w ~ /\.(c|cc|cpp|cxx)$/) { continue } # source file
flags = (flags == "") ? w : flags " " w
}
return flags
}
# Resolve a path against dir (both from the JSON) then base_dir.
# "file" and "directory" in real compile_commands.json are almost always
# absolute; this handles the uncommon relative case.
function resolve(path, dir) {
if (path ~ /^\//) return path
if (dir != "" && dir ~ /^\//) return dir "/" path
return base_dir "/" path
}
BEGIN { file=""; dir=""; cmd=""; in_args=0 }
/^[[:space:]]*\{/ { file=""; dir=""; cmd=""; in_args=0 }
/"directory"[[:space:]]*:/ && !in_args {
s = $0
sub(/.*"directory"[[:space:]]*:[[:space:]]*"/, "", s)
sub(/".*$/, "", s)
dir = s
}
/"file"[[:space:]]*:/ && !in_args {
s = $0
sub(/.*"file"[[:space:]]*:[[:space:]]*"/, "", s)
sub(/".*$/, "", s)
file = s
}
# "command": "..." — single string; unescape \" inside the value
/"command"[[:space:]]*:/ && !in_args {
s = $0
sub(/.*"command"[[:space:]]*:[[:space:]]*"/, "", s)
sub(/"[[:space:]]*,?[[:space:]]*$/, "", s)
gsub(/\\"/, "\"", s)
cmd = s
}
# "arguments": [ ... ] — collect each quoted token into a space-joined string
/"arguments"[[:space:]]*:[[:space:]]*\[/ { in_args=1; cmd="" }
in_args && /^[[:space:]]*\]/ { in_args=0 }
in_args && /"[^"]*"/ {
s = $0
sub(/^[[:space:]]*"/, "", s)
sub(/".*$/, "", s)
gsub(/\\"/, "\"", s)
cmd = (cmd == "") ? s : cmd " " s
}
/^[[:space:]]*\}/ && file != "" {
abs = resolve(file, dir)
flags = strip_cmd(cmd)
if (!seen[abs]++) print abs "\t" flags
file=""; dir=""; cmd=""; in_args=0
}
' "$COMPILE_DB")
while IFS=$'\t' read -r src flags; do
[[ -z "$src" ]] && continue
ALL_FILES+=("$src")
FILE_FLAGS["$src"]="$flags"
done <<< "$awk_out"
elif [[ -n "$SCAN_DIR" ]]; then
if [[ ! -d "$SCAN_DIR" ]]; then
echo "pagurus-check: directory not found: $SCAN_DIR" >&2
exit 1
fi
while IFS= read -r f; do
ALL_FILES+=("$f")
done < <(find "$SCAN_DIR" -name '*.c' ! -name '*.pagurus.c' | sort)
else
ALL_FILES=("${EXPLICIT_FILES[@]}")
fi
if [[ ${#ALL_FILES[@]} -eq 0 ]]; then
echo "pagurus-check: no source files to check." >&2
echo " Pass files directly, use --dir=DIR, or --compile-db=FILE." >&2
exit 1
fi
# ── Worker function (checks a single file) ────────────────────────────────────
check_one() {
local src="$1"
local db_flags="${FILE_FLAGS[$src]:-}"
# Merge: compile-db flags take precedence; EXTRA_CFLAGS are appended last.
# shellcheck disable=SC2086
local all_flags=($db_flags $EXTRA_CFLAGS)
local output
output=$("$CLANG" "${PLUGIN_FLAGS[@]}" "${all_flags[@]}" -c "$src" \
-o /dev/null 2>&1 || true)
# Extract pagurus diagnostics (lines containing E0xx[).
local diags
diags=$(echo "$output" | grep -E 'E[0-9]{3}\[' || true)
if [[ -n "$diags" ]]; then
# Print with a blank line separator for readability when parallel.
printf '%s\n' "$output"
return 1
fi
return 0
}
export -f check_one
export CLANG DRY_RUN IR_PASS EXTRA_CFLAGS PLUGIN
# Note: PLUGIN_FLAGS is a bash array; subshells (()) inherit it directly.
# ── Dispatch (sequential or parallel) ────────────────────────────────────────
ERRORS=0
PASS=0
echo "pagurus-check: analysing ${#ALL_FILES[@]} file(s) with plugin $PLUGIN"
if [[ $DRY_RUN -eq 1 ]]; then
echo " mode: dry-run (no .pagurus.c files written)"
else
echo " mode: compile (writes .pagurus.c alongside each source)"
fi
if [[ "$JOBS" -le 1 ]]; then
for src in "${ALL_FILES[@]}"; do
if check_one "$src"; then
PASS=$((PASS+1))
else
ERRORS=$((ERRORS+1))
fi
done
else
# Parallel: keep up to JOBS workers active at once; each writes a status file.
TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
idx=0
pids=()
for src in "${ALL_FILES[@]}"; do
out_file="$TMP_DIR/$(printf '%06d' $idx)"
idx=$((idx+1))
(
if check_one "$src"; then
echo "0" > "${out_file}.status"
else
echo "1" > "${out_file}.status"
fi
) &
pids+=($!)
# When we have JOBS active workers, wait for the oldest to finish before
# launching more — this gives continuous rather than batch parallelism.
if (( ${#pids[@]} >= JOBS )); then
wait "${pids[0]}"
pids=("${pids[@]:1}")
fi
done
wait
for status_file in "$TMP_DIR"/*.status; do
[[ -f "$status_file" ]] || continue
s=$(cat "$status_file")
if [[ "$s" -eq 0 ]]; then
PASS=$((PASS+1))
else
ERRORS=$((ERRORS+1))
fi
done
fi
echo ""
echo "pagurus-check: $PASS passed, $ERRORS failed."
[[ "$ERRORS" -eq 0 ]]