panicgraph 0.2.0

Reports which functions can panic, why, and through what call path.
Documentation
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
//! What the analysis reports for a crate whose panics are known.
//!
//! This is the only test that runs the compiler driver, so it is what keeps
//! the two halves honest: the reachability of a body is decided against the
//! settings of the build in front of it, and a check the arguments settle is
//! not a panic. Both directions matter. Dropping a check that can fail would
//! make a clean report meaningless, and keeping one that cannot fail is the
//! noise the tool exists to remove.

mod support;

use crate::support::analyse_fixture;

/// The panic each function must be reported with.
const MUST_PANIC: &[(&str, &str)] = &[
    ("must_index", "index"),
    ("must_divide", "divide-by-zero"),
    ("must_remainder", "remainder-by-zero"),
    ("must_unwrap", "unwrap"),
    ("must_assert", "explicit"),
    ("must_slice_tail", "index"),
    ("must_copy", "explicit"),
    ("must_assert_generic", "explicit"),
    ("must_assert_false", "explicit"),
    ("must_divide_misguarded", "divide-by-zero"),
    ("must_divide_inverted_guard", "divide-by-zero"),
    ("must_divide_once_of_two", "divide-by-zero"),
    ("must_divide_narrowed", "divide-by-zero"),
    ("must_push", "capacity-overflow"),
    ("must_push", "alloc-failure"),
    ("must_rethrow", "explicit"),
    ("must_lock", "poison"),
    ("must_write", "fmt"),
    ("must_rc_clone", "refcount-overflow"),
    ("must_slice_str", "str-boundary"),
    ("must_borrow", "borrow"),
    ("must_dyn", "dyn-call"),
    ("must_foreign", "foreign"),
    ("must_catch_abort", "alloc-failure"),
    ("must_index_off_by_one", "index"),
    ("must_index_wrong_slice", "index"),
    ("must_modulo_signed", "index"),
    ("must_fn_ptr", "fn-pointer"),
    ("must_generic", "generic-bound"),
    ("must_dyn_speak", "dyn-call"),
    ("must_zeroed_ref", "explicit"),
    ("must_panic_literal", "explicit"),
    ("must_zeroed_chain", "generic-bound"),
    ("must_divide_by_max_zero", "divide-by-zero"),
    ("must_divide_by_min", "divide-by-zero"),
    ("must_index_past_length_guard", "index"),
    ("must_modulo_length", "remainder-by-zero"),
    ("must_masked_index_offset", "index"),
    ("must_be_below", "explicit"),
    ("must_pass_unchecked_limit", "explicit"),
    ("Cursor::must_field_written", "index"),
    ("Cursor::must_field_after_call", "index"),
    ("Cursor::must_field_of_other", "index"),
    ("must_wide_index", "index"),
    ("must_unwrap_argument", "unwrap"),
    ("must_two_lengths", "index"),
    ("must_unwrap_wrong_arm", "unwrap"),
    ("must_match_panic", "explicit"),
    ("must_nonnull_of_argument", "unwrap"),
    ("must_generic_size_divide", "divide-by-zero"),
    ("must_take_indexed", "index"),
    ("must_pass_unguarded", "index"),
    ("Window::must_window_of_other", "index"),
    ("must_shifted_index", "index"),
    ("must_shift_by_runtime", "index"),
    ("must_signed_shift_index", "index"),
    ("must_shifted_left_index", "index"),
    ("must_or_divide", "divide-by-zero"),
    ("must_or_index", "index"),
    ("must_xor_index", "index"),
    ("must_divided_index", "index"),
    ("must_remainder_by_bounded", "index"),
    ("must_leading_zeros_index", "index"),
    ("must_range_loop_of_other", "index"),
    ("must_option_carries_index", "index"),
    ("must_copy_two_slices", "index"),
    ("must_second_last_of_guarded", "index"),
    ("must_take_four", "explicit"),
    ("must_index_after_shrink", "index"),
    ("must_loop_past_the_end", "index"),
    ("must_chunks_of_a_size", "explicit"),
    ("must_step_by_a_size", "explicit"),
    ("must_split_unguarded", "explicit"),
    ("must_convert_runtime", "remainder-by-zero"),
    ("must_copy_into_prefix", "index"),
    ("must_copy_guarded_on_other", "explicit"),
    ("must_index_of_equal_lengths", "index"),
    ("must_nonzero_of_anything", "unwrap"),
    ("must_clamped_to_larger", "index"),
    ("must_prefix_unguarded", "index"),
    ("must_guard_within_a_loose_guard", "index"),
    ("must_guard_within_a_guard_of_other", "index"),
    ("must_countdown_from_the_length", "index"),
    ("must_fill_past_the_end", "index"),
    ("must_middle_of_one", "index"),
    ("must_offset_at_the_end", "index"),
    ("must_prefix_of_the_longer", "index"),
    ("must_split_two_past", "index"),
    ("must_shift_from_the_length", "index"),
    ("must_inner_of_other", "index"),
    ("must_inner_after_move", "index"),
    ("must_char_high_bits", "index"),
    ("must_copy_prefix_of_other", "explicit"),
    ("must_copy_two_lengths", "explicit"),
    ("must_modulo_at_most_guard", "remainder-by-zero"),
    ("must_table_of_threes", "index"),
    ("must_scan_by_two", "index"),
    ("must_drop_beside_a_guard", "index"),
    ("must_deque_range", "index"),
    ("must_atomic_load", "explicit"),
    ("must_masked_switch", "explicit"),
    ("must_vector_index", "index"),
    ("must_index_after_le", "index"),
    ("must_short_constant_table", "index"),
    ("sixteenth_of", "index"),
    ("must_reach_under_wrapping_guard", "index"),
    ("must_reach_past_guard", "index"),
    ("must_window_past_sixteen", "index"),
    ("must_guard_across_borrow", "index"),
    ("must_range_without_order", "index"),
    ("must_index_under_loose_chain", "index"),
    ("must_after_call_that_cannot_return", "explicit"),
    ("at_most_ten", "explicit"),
    ("must_always_stub", "explicit"),
];

/// The panics each function must *not* be reported with.
///
/// A check the analysis can settle has to go even where the same function
/// keeps another that it cannot.
const MUST_NOT_PANIC: &[(&str, &str)] = &[
    ("must_divide_once_of_two", "remainder-by-zero"),
    ("must_lock", "unwrap"),
    ("must_write", "unwrap"),
    ("must_not_catch_explicit", "explicit"),
    ("must_dyn_speak", "explicit"),
    ("must_modulo_length", "index"),
    ("must_copy_into_prefix", "explicit"),
    ("must_after_call_that_cannot_return", "index"),
];

/// The functions that must be reported with nothing at all in a release
/// build, on top of the ones that must also stay clean in a debug build.
///
/// The two lists are disjoint so that a name is written once: the debug
/// list is a part of the clean set rather than a copy of part of it, and
/// the two cannot drift apart.
const MUST_BE_CLEAN_IN_RELEASE: &[&str] = &[
    "clean_fold",
    "clean_count_zeros",
    "clean_sum_by_get",
    "clean_assert_true",
    "clean_guarded_widening",
    "clean_zeroed_int",
    "clean_divide_by_min_plus_one",
    "clean_index_after_empty_guard",
    "clean_index_after_length_guard",
    "clean_masked_index_offset",
    "clean_nonnull_of_place",
    "clean_guard_before_call",
    "Window::clean_window_read",
    "clean_range_loop",
    "clean_option_carries_index",
    "clean_copy_same_length",
    "clean_copy_two_arrays",
    "clean_last_of_guarded",
    "clean_pass_array_of_four",
    "clean_chunks_of_a_constant",
    "clean_step_by_a_constant",
    "clean_split_after_guard",
    "clean_convert_constant",
    "clean_chunk_count",
    "clean_copy_into_array",
    "clean_copy_after_guard",
    "clean_nonzero_of_set_bit",
    "clean_split_in_half",
    "clean_clamped_to_last",
    "clean_prefix_under_guard",
    "clean_guard_within_a_guard",
    "clean_countdown_loop",
    "clean_fill_bounded",
    "clean_middle_of_guarded",
    "clean_offset_below_the_end",
    "clean_prefix_of_both",
    "clean_split_at_a_byte",
    "clean_first_half",
    "clean_shift_along",
    "clean_inner_of_guarded",
    "clean_char_high_bits",
    "clean_bool_index",
    "clean_copy_prefix_under_guard",
    "clean_copy_the_shorter",
    "clean_modulo_above_guard",
    "clean_table_of_threes",
    "clean_drop_beside_a_guard",
    "clean_deque_walk",
    "clean_atomic_load",
    "clean_atomic_fence",
    "clean_atomic_compare_exchange",
    "clean_masked_switch",
    "clean_remainder_switch",
    "clean_vector_index_guard",
    "clean_deque_index_guard",
    "clean_string_index_guard",
    "clean_scan_until",
    "clean_scan_break",
    "clean_index_after_scan",
    "clean_constant_table",
    "clean_reach_within_guard",
    "clean_window_of_sixteen",
    "clean_guard_before_borrow",
    "clean_range_between_guards",
    "clean_index_under_chained_bound",
    "clean_index_of_paired_slice",
];

/// The functions that cannot panic and that the analysis cannot yet say so
/// about.
///
/// Each is a false positive kept in view rather than hidden: the code is
/// clean, and what is missing is a rule that proves it without leaning on
/// one that is unsound. The test insists they are still reported, so that
/// whoever supplies the missing rule is told to move the entry.
const NOT_SETTLED: &[&str] = &[];

/// The functions that must stay clean in a debug build as well.
///
/// A debug build folds the same guards without the optimizer's help: no
/// inlining has merged the comparison into the check, so every settled
/// verdict below is the analysis's own reasoning. The full clean list is
/// not used because a debug build genuinely adds checks inside the standard
/// library that some of those functions reach.
const MUST_BE_CLEAN_IN_DEBUG: &[&str] = &[
    "clean_divide_by_constant",
    "clean_guarded_divide",
    "clean_guarded_divide_ne",
    "clean_guarded_remainder",
    "clean_modulo_index",
    "clean_masked_index",
    "clean_guarded_index",
    "clean_guarded_index_flipped",
    "clean_while_index",
    "clean_nonzero_divide",
    "clean_divide_by_max",
    "clean_remainder_by_max",
    "clean_divide_by_clamp",
    "clean_divide_by_helper",
    "clean_divide_by_either_arm",
    "clean_precondition_met",
    "Cursor::clean_field_index",
    "Cursor::clean_field_divide",
    "clean_byte_index",
    "clean_unwrap_built",
    "clean_unwrap_matched",
    "clean_unwrap_ok",
    "clean_match_panic",
    "clean_generic_guard",
    "clean_shifted_index",
    "clean_shifted_left_index",
    "clean_or_divide",
    "clean_or_index",
    "clean_xor_index",
    "clean_divided_index",
    "clean_remainder_by_bounded",
    "clean_leading_zeros_index",
    "clean_trailing_zeros_index",
    "clean_count_ones_index",
];

/// The categories one function was reported with, if it was reported.
fn found(
    reported: &[(String, Vec<String>)],
    name: &str,
) -> Option<Vec<String>> {
    reported
        .iter()
        .find(|(function, _)| function == name)
        .map(|(_, categories)| categories.clone())
}

#[test]
fn a_known_crate_reports_exactly_its_panics() {
    let reported = analyse_fixture("release", &[]);
    for (function, category) in MUST_PANIC {
        let categories = found(&reported, function).unwrap_or_else(|| {
            panic!("{function} can panic with {category} and was not reported")
        });
        assert!(
            categories.iter().any(|c| c == category),
            "{function} can panic with {category}, but was reported with \
             {categories:?}"
        );
    }

    for (function, category) in MUST_NOT_PANIC {
        let categories = found(&reported, function).unwrap_or_default();
        assert!(
            !categories.iter().any(|c| c == category),
            "{function} cannot panic with {category}, but was reported with \
             {categories:?}"
        );
    }

    for function in MUST_BE_CLEAN_IN_DEBUG
        .iter()
        .chain(MUST_BE_CLEAN_IN_RELEASE)
    {
        assert!(
            found(&reported, function).is_none(),
            "{function} cannot panic, but was reported with {:?}",
            found(&reported, function)
        );
    }

    for function in NOT_SETTLED {
        assert!(
            found(&reported, function).is_some(),
            "{function} is recorded as one the analysis cannot settle, but \
             it is now clean; move it to one of the clean lists"
        );
    }
}

#[test]
fn a_debug_build_still_folds_the_guards() {
    let reported = analyse_fixture("debug", &[]);
    for function in MUST_BE_CLEAN_IN_DEBUG {
        assert!(
            found(&reported, function).is_none(),
            "{function} cannot panic in a debug build either, but was \
             reported with {:?}",
            found(&reported, function)
        );
    }

    for (function, category) in MUST_PANIC {
        let categories = found(&reported, function).unwrap_or_else(|| {
            panic!(
                "{function} can panic with {category} in a debug build and \
                 was not reported"
            )
        });
        assert!(
            categories.iter().any(|c| c == category),
            "{function} can panic with {category}, but a debug build \
             reported {categories:?}"
        );
    }
}

#[test]
fn a_panic_every_call_reaches_is_reported_as_always() {
    let doc = support::analyse_fixture_json("release", &[]);
    let findings = doc["findings"].as_array().cloned().unwrap_or_default();
    let always_of = |name: &str| -> Vec<String> {
        findings
            .iter()
            .find(|f| f["function"] == name)
            .and_then(|f| f["always"].as_array())
            .map(|list| {
                list.iter()
                    .filter_map(|v| v.as_str().map(str::to_owned))
                    .collect()
            })
            .unwrap_or_default()
    };
    assert_eq!(
        always_of("must_always_stub"),
        vec!["explicit".to_owned()],
        "a stub that panics on every call is always a panic"
    );
    assert!(
        always_of("must_divide").is_empty(),
        "a check that depends on the argument is not always a panic"
    );
    assert!(
        always_of("must_after_call_that_cannot_return").is_empty(),
        "a panic reached through a call is the callee's to call always"
    );
}

#[test]
fn candidates_expand_dyn_and_pointer_calls() {
    let reported = analyse_fixture("release", &["--candidates"]);

    let dyn_call = found(&reported, "must_dyn_speak").unwrap_or_default();
    assert!(
        dyn_call.iter().any(|c| c == "explicit"),
        "one implementation panics, so following candidates must surface \
         it, got {dyn_call:?}"
    );
    assert!(
        dyn_call.iter().any(|c| c == "dyn-call"),
        "candidates narrow the unknown, they do not close it, got \
         {dyn_call:?}"
    );

    let pointer = found(&reported, "must_fn_ptr").unwrap_or_default();
    assert!(
        pointer.iter().any(|c| c == "explicit"),
        "a reified function of this signature panics, got {pointer:?}"
    );
    assert!(
        pointer.iter().any(|c| c == "fn-pointer"),
        "the pointer could still hold something unseen, got {pointer:?}"
    );
}

#[test]
fn a_static_panic_message_is_quoted_in_the_reason() {
    let exe = std::path::PathBuf::from(env!("CARGO_BIN_EXE_panicgraph"));
    let output = std::process::Command::new(&exe)
        .arg("--manifest-dir")
        .arg(support::fixture_dir())
        .arg("--suppress")
        .arg("")
        .output()
        .expect("the front end should run");
    let text = String::from_utf8_lossy(&output.stdout);
    assert!(
        text.contains("panics with \"assertion failed: "),
        "the report should quote the message a panic carries:\n{text}"
    );
}

#[test]
fn closures_fold_into_their_parent_on_request() {
    let reported = analyse_fixture("release", &["--closures", "parent"]);
    assert!(
        reported.iter().all(|(f, _)| !f.contains("{closure")),
        "the parent view must not name closures on their own"
    );
    let folded = reported
        .iter()
        .find(|(f, _)| f == "must_not_catch_explicit")
        .map(|(_, c)| c.clone())
        .unwrap_or_default();
    assert!(
        folded.iter().any(|c| c == "explicit"),
        "the compact view attributes a closure's panics to the function it \
         is written in, even the contained ones; got {folded:?}"
    );
}