panicgraph 0.2.1

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
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
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
//! 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_insert_into_map", "capacity-overflow"),
    ("must_index_through_oom", "index"),
    ("must_index_through_precondition_check", "index"),
    ("must_index_under_raw_length", "index"),
    ("must_index_after_write_under_pointer", "index"),
    ("must_rethrow", "explicit"),
    ("must_lock", "poison"),
    ("must_write", "fmt"),
    ("must_unwrap_poisoned", "poison"),
    ("must_unwrap_result_of_format", "unwrap"),
    ("must_unwrap_option_of_poison", "unwrap"),
    ("must_rc_clone", "refcount-overflow"),
    ("must_rc_clone_through_helper", "refcount-overflow"),
    ("must_slice_str", "str-boundary"),
    ("must_borrow", "borrow"),
    ("must_dyn", "dyn-call"),
    ("must_foreign", "foreign"),
    ("must_catch_abort", "alloc-failure"),
    ("must_catch_across_ffi", "explicit"),
    ("must_index_off_by_one", "index"),
    ("must_index_wrong_slice", "index"),
    ("must_modulo_signed", "index"),
    ("must_fn_ptr", "fn-pointer"),
    ("must_call_closure_pointer", "fn-pointer"),
    ("must_generic", "generic-bound"),
    ("must_pick_through_generic", "explicit"),
    ("must_pick_any", "generic-bound"),
    ("must_dyn_speak", "dyn-call"),
    ("must_drop_object", "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_nonnull_of_raw_field", "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"),
    ("must_panic_unless_quitting", "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"),
    ("must_insert_into_map", "explicit"),
    ("must_index_through_oom", "alloc-failure"),
    ("must_index_through_precondition_check", "ub-check"),
    ("must_unwrap_poisoned", "unwrap"),
    ("must_unwrap_result_of_format", "fmt"),
    ("must_unwrap_option_of_poison", "poison"),
];

/// 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",
    "clean_index_under_referenced_length",
    "clean_nonnull_of_referenced_field",
];

/// 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_build_whose_panics_abort_catches_nothing() {
    // Under `panic = "abort"` a catch contains nothing.
    let reported = analyse_fixture("abort", &[]);
    for (function, category) in [
        ("must_not_catch_explicit", "explicit"),
        ("must_catch_across_ffi", "explicit"),
        ("must_catch_abort", "alloc-failure"),
    ] {
        let categories = found(&reported, function).unwrap_or_default();
        assert!(
            categories.iter().any(|c| c == category),
            "{function} reaches {category} when panics abort, but was \
             reported with {categories:?}"
        );
    }
}

#[test]
fn an_unwrap_is_named_by_the_error_it_discards_inlined_or_not() {
    // A release build inlines these unwraps and a debug build calls them;
    // either way the discarded error names the panic.
    for profile in ["release", "debug"] {
        let reported = analyse_fixture(profile, &[]);
        for (function, category, not) in [
            ("must_lock", "poison", "unwrap"),
            ("must_unwrap_poisoned", "poison", "unwrap"),
            ("must_unwrap_result_of_format", "unwrap", "fmt"),
            ("must_unwrap_option_of_poison", "unwrap", "poison"),
        ] {
            let categories = found(&reported, function).unwrap_or_default();
            assert!(
                categories.iter().any(|c| c == category)
                    && !categories.iter().any(|c| c == not),
                "{function} discards what makes it {category} rather than \
                 {not}, but a {profile} build reported {categories:?}"
            );
        }
    }
}

#[test]
fn reading_the_tests_keeps_the_generic_functions() {
    // The test build compiles the generic functions again, and its copies
    // must not replace the crate's own, which the report names.
    let reported = analyse_fixture("release", &["--with-tests"]);
    for (function, category) in [
        ("must_assert_generic", "explicit"),
        ("must_generic_size_divide", "divide-by-zero"),
        ("must_generic", "generic-bound"),
        ("must_pick_any", "generic-bound"),
    ] {
        let categories = found(&reported, function).unwrap_or_else(|| {
            panic!(
                "{function} can panic with {category} and was not reported \
                 once the tests were read"
            )
        });
        assert!(
            categories.iter().any(|c| c == category),
            "{function} can panic with {category}, but reading the tests \
             reported {categories:?}"
        );
    }
    assert!(
        reported
            .iter()
            .all(|(function, _)| !function.contains("tests::")),
        "the tests themselves are not reported, got {reported:?}"
    );
}

#[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"
    );
    assert!(
        always_of("must_panic_unless_quitting").is_empty(),
        "an argument that ends the process first avoids the panic"
    );
}

#[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 dropped = found(&reported, "must_drop_object").unwrap_or_default();
    assert!(
        dropped.iter().any(|c| c == "explicit"),
        "one type made into an object panics in its drop, so following \
         candidates must surface it, got {dropped:?}"
    );

    let closure =
        found(&reported, "must_call_closure_pointer").unwrap_or_default();
    assert!(
        closure.iter().any(|c| c == "explicit"),
        "a closure made into a pointer of this signature panics, got \
         {closure:?}"
    );

    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 output = support::run_on_fixture(&["--suppress", ""]);
    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:?}"
    );
}