Skip to main content

codehelion_core/
boilerplate.rs

1//! Boilerplate classification: recognising units whose duplication is
2//! expected rather than a finding.
3//!
4//! Some code is duplicated because the language leaves no other way to write
5//! it. A getter, a delegating wrapper and a run of macro invocations are all
6//! genuine clones by every similarity measure, and reporting them crowds out
7//! the duplication a reader can act on. This module names those shapes so
8//! presentation can decide what to do with them.
9//!
10//! # What this is not
11//!
12//! Classification is *syntactic and conservative*. It reads the unit's IR
13//! subtree and nothing else: no name heuristics, no path guesses, no attempt
14//! to infer intent. Branching is where behaviour — and therefore the
15//! interesting kind of duplication — lives, so a unit that branches is
16//! classified only when the branch is a single guard and the body holds
17//! nothing else: that unit chooses an answer rather than working one out.
18//! Handing an error upwards is not branching at all: it leaves the unit with
19//! one path, and the caller with the same one it would have had.
20//!
21//! The classification is recorded, never acted on here: a classified unit is
22//! still analysed, still verified and still grouped. Whether a category is
23//! excluded from reports, ranked down or shown as-is is a presentation
24//! decision, so a user can always see what was set aside and why.
25//!
26//! # Where the counting stops
27//!
28//! Every rule below is a function of what the body counts, and that is a
29//! coarse reading: how many calls, not which; how many locals, not what they
30//! hold. The labelled corpora have reached the end of it. Bodies ruled
31//! opposite ways come out counted identically, and not in far-apart projects —
32//! one logging library declares a helper that takes a time point, converts it
33//! and hands the result to a call, and declares an overload that takes the
34//! current time, and hands that to a call. The first is copied into two sinks
35//! and is duplication worth removing; the second wraps its own sibling and is
36//! not. Two statements each, one delegation each, the same counts.
37//!
38//! Nor is it only the far apart that collide. An XML parser declares four
39//! integer parsers and two floating-point ones next to each other, each a
40//! guard on one call's result; the integer ones repeat a policy about how a
41//! leading `0x` is spelled and the floating-point ones repeat nothing, and the
42//! policy lives inside an argument expression. Counted, all six are the same
43//! body.
44//!
45//! So a rule reaching further than these does not buy coverage at the cost of
46//! accuracy — it trades a lookalike for a real finding, one for one. The
47//! categories here are the shapes no confirmed duplication was found in, and
48//! reaching past them needs something these counts do not carry: which callee
49//! is called, where a local's value goes, or what an argument holds. Another
50//! bound on the same numbers will not do it. The `boilerplate-screen` example
51//! prints the counts for every labelled unit, which is how a proposed rule is
52//! weighed against what it would cost before it is written.
53
54use crate::ir::{IrNode, Shape};
55
56/// Version of the classification rules.
57///
58/// Recorded alongside the other detector versions: a change in what counts as
59/// boilerplate changes which findings a report shows, so results from two
60/// versions are not comparable without saying so.
61pub const BOILERPLATE_VERSION: &str = "boilerplate-v1";
62
63/// A recognised boilerplate shape.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
65pub enum Boilerplate {
66    /// No call and no control flow, with at most one statement: getters,
67    /// setters and stubs. The body moves a value and does nothing else.
68    TrivialBody,
69    /// One call and no work around it: a wrapper that delegates. The locals
70    /// it declares and the value it hands back are part of spelling the call,
71    /// not of doing something with it.
72    Forwarding,
73    /// A body that is nothing but macro invocations, at least two of them.
74    /// What the macros expand to is unknown here — the repetition itself is
75    /// the observation.
76    MacroRepetition,
77    /// One guard and then an answer on each side of it, with nothing else:
78    /// the unit chooses between two results rather than producing one.
79    GuardedDispatch,
80    /// Several answers with nothing in the code choosing between them: the
81    /// build configuration picks one and the rest are never compiled.
82    ConfiguredAnswer,
83}
84
85impl Boilerplate {
86    /// Stable lowercase identifier used in reports and configuration.
87    #[must_use]
88    pub const fn name(self) -> &'static str {
89        match self {
90            Self::TrivialBody => "trivial-body",
91            Self::Forwarding => "forwarding",
92            Self::MacroRepetition => "macro-repetition",
93            Self::GuardedDispatch => "guarded-dispatch",
94            Self::ConfiguredAnswer => "configured-answer",
95        }
96    }
97
98    /// Every category, in the order reports and configuration list them.
99    #[must_use]
100    pub const fn all() -> [Self; 5] {
101        [
102            Self::TrivialBody,
103            Self::Forwarding,
104            Self::MacroRepetition,
105            Self::GuardedDispatch,
106            Self::ConfiguredAnswer,
107        ]
108    }
109
110    /// The category with this identifier, if any.
111    #[must_use]
112    pub fn from_name(name: &str) -> Option<Self> {
113        Self::all().into_iter().find(|kind| kind.name() == name)
114    }
115}
116
117/// What a unit's body contains, counted over its whole subtree.
118///
119/// Calls are counted apart from statements because the IR models a call as an
120/// expression: `f();` is one call node, not a statement wrapping one.
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
122pub struct BoilerplateCounts {
123    /// Branches, loops, multi-way conditionals and error handling.
124    pub control: usize,
125    /// Call expressions that are not themselves an argument to a call.
126    ///
127    /// Nesting is what separates delegation from work. `f(g(x), h(y))` is one
128    /// delegation whose arguments happen to be computed by call: counting
129    /// three calls there would say a wrapper does three things, when what it
130    /// does is call `f`. Sibling calls are counted apart, because two calls in
131    /// a row are two things done.
132    pub calls: usize,
133    /// Macro invocations that are not themselves an argument to a call.
134    ///
135    /// Counted the way calls are, and for the same reason. `f(tri!(g(x)))` is
136    /// one delegation whose argument happens to be spelled with a macro;
137    /// counting the macro would say the body does something besides call `f`,
138    /// when handing the argument over is all it does. A macro standing on its
139    /// own is still counted: nothing here can see what it expands to, so a
140    /// statement that is one is a statement whose contents are unknown.
141    pub macros: usize,
142    /// Statements other than macro invocations and control flow.
143    pub statements: usize,
144    /// Statements that do more than name a value or hand one back.
145    ///
146    /// A `return` around a delegation is punctuation. So is a local declared
147    /// to receive what the delegation writes: a callee that answers through a
148    /// pointer leaves its caller no other way to spell the call. An assignment
149    /// or a bare expression statement is work.
150    ///
151    /// Nothing here can see what an initialiser computes — the IR models a
152    /// declaration as one node whether it names a place or fills it with
153    /// arithmetic. That is why declarations are punctuation only beside a
154    /// delegation, never on their own.
155    pub work: usize,
156    /// Two-way conditionals, counted apart from the rest of the control flow.
157    ///
158    /// One of them is a guard. Several are a decision table, which is
159    /// something a reader can act on: two copies of one table differing in
160    /// their constants is exactly the duplication worth reporting.
161    pub branches: usize,
162    /// Local declarations, counted apart so a body can be required to have
163    /// none. See `work` for why an initialiser cannot be inspected.
164    pub declarations: usize,
165    /// `return` statements.
166    pub returns: usize,
167}
168
169/// Classify a unit by the shape of its body, or return `None` when it does
170/// something a reader would want to see duplicated.
171///
172/// The unit node itself is not counted; only what it contains.
173#[must_use]
174pub fn classify(unit: &IrNode) -> Option<Boilerplate> {
175    let body = counts(unit);
176    if body.control > 0 {
177        return dispatch(&body);
178    }
179    if body.macros >= 2 && body.calls == 0 && body.statements == 0 {
180        return Some(Boilerplate::MacroRepetition);
181    }
182    if body.macros > 0 {
183        return None;
184    }
185    if configured(&body) {
186        return Some(Boilerplate::ConfiguredAnswer);
187    }
188    match body.calls {
189        // Nothing is delegated, so the one statement is the whole body.
190        0 if body.statements <= 1 => Some(Boilerplate::TrivialBody),
191        // One delegation, with nothing around it but the names it needs.
192        1 if body.work == 0 => Some(Boilerplate::Forwarding),
193        _ => None,
194    }
195}
196
197/// Whether the body hands back more than one answer with nothing choosing
198/// between them.
199///
200/// A unit cannot return twice. Where the grammar shows two returns and no
201/// branch, loop or guard, something outside the grammar removed the choice:
202/// `#if` and `#ifdef` in C and C++, `#[cfg]` in Rust. Only one answer is
203/// compiled, and which one is a property of the build rather than of the code.
204///
205/// That makes two such units alike for a reason no reader can act on. They
206/// carry the same platform split, or the same feature flag, and the answer
207/// each spells is the one the other could not use. Consolidating them would
208/// mean deleting a configuration, not a duplicate.
209///
210/// The shape is the one [`dispatch`] asks for with the guard taken away, and
211/// bounded for the same reason: no local, no assignment, no bare expression,
212/// and no more calls than there are answers, so nothing happens here besides
213/// producing each answer. Anything more and the arms are doing work, which is
214/// work written once per configuration and worth reading.
215const fn configured(body: &BoilerplateCounts) -> bool {
216    body.returns >= 2 && body.work == 0 && body.declarations == 0 && body.calls <= body.returns
217}
218
219/// Classify a body that branches, which is only boilerplate in one shape.
220///
221/// A single guard, with an answer or one delegation on each side of it, is the
222/// unit picking between two results rather than producing one: a null check
223/// and a field, a capability check and one of two calls, a free guarded
224/// against a null pointer. Written once per type or per constant it is the
225/// language standing in for a parameter, and every copy says the same thing.
226///
227/// The shape is bounded because nothing here can read an expression: with no
228/// assignment, no bare expression statement, no local, nothing but `return`s
229/// and no more calls than there are answers, the body is one condition and
230/// what it chooses between. Two branches would be a decision table instead,
231/// and two copies of a table differing in their constants is duplication a
232/// reader can act on.
233///
234/// What this cannot separate is a guard whose answer is computed — `if (v >
235/// hi) return hi; return v;` reaches here as the same shape as a null check
236/// and a field read, because the IR carries no expression to tell them apart.
237fn dispatch(body: &BoilerplateCounts) -> Option<Boilerplate> {
238    let shaped = body.branches == 1
239        && body.control == body.branches
240        && body.macros == 0
241        && body.work == 0
242        && body.declarations == 0
243        && body.returns >= 2;
244    // Each answer is one thing, and the condition may be one more. Beyond that
245    // the body is computing something the IR cannot show.
246    (shaped && body.calls <= body.returns).then_some(Boilerplate::GuardedDispatch)
247}
248
249/// Count what a unit's subtree contains, excluding the unit node itself.
250#[must_use]
251pub fn counts(unit: &IrNode) -> BoilerplateCounts {
252    let mut body = BoilerplateCounts::default();
253    for child in &unit.children {
254        descend(child, false, &mut body);
255    }
256    body
257}
258
259/// Tally `node` and its subtree, remembering whether it sits inside a call.
260fn descend(node: &IrNode, in_call: bool, body: &mut BoilerplateCounts) {
261    tally(node, in_call, body);
262    let nested = in_call || node.shape == Shape::Call;
263    for child in &node.children {
264        descend(child, nested, body);
265    }
266}
267
268fn tally(node: &IrNode, in_call: bool, body: &mut BoilerplateCounts) {
269    match node.shape {
270        Shape::Branch => {
271            body.control += 1;
272            body.branches += 1;
273        }
274        Shape::Loop | Shape::Match | Shape::MatchArm | Shape::Break | Shape::Continue => {
275            body.control += 1;
276        }
277        Shape::Try => {
278            if handles(node) {
279                body.control += 1;
280            }
281        }
282        Shape::Call => {
283            if !in_call {
284                body.calls += 1;
285            }
286        }
287        Shape::MacroCall => {
288            if !in_call {
289                body.macros += 1;
290            }
291        }
292        Shape::Return => {
293            body.statements += 1;
294            body.returns += 1;
295        }
296        Shape::VarDecl => {
297            body.statements += 1;
298            body.declarations += 1;
299        }
300        Shape::Assign | Shape::ExprStmt => {
301            body.statements += 1;
302            body.work += 1;
303        }
304        _ => {}
305    }
306}
307
308/// Whether an error-handling node handles the error or only passes it on.
309///
310/// The two arrive as one shape because they are one concept, but they are not
311/// one amount of behaviour. `try`/`catch` writes a second path through the
312/// unit and carries that path in a block. Propagation — Rust's `?` — carries
313/// only the expression whose error it hands upwards, and leaves the unit with
314/// a single path. A block child is what tells them apart, in any language that
315/// has both.
316fn handles(node: &IrNode) -> bool {
317    node.children
318        .iter()
319        .any(|child| child.shape == Shape::Block)
320}
321
322#[cfg(test)]
323#[allow(clippy::unwrap_used, clippy::expect_used)]
324mod tests {
325    use super::*;
326    use crate::ir::ByteRange;
327
328    /// Build a unit node whose body holds the given shapes as one flat block.
329    fn unit(shapes: &[Shape]) -> IrNode {
330        let body = IrNode {
331            shape: Shape::Block,
332            name: None,
333            token_start: 0,
334            token_end: 0,
335            range: ByteRange { start: 0, end: 0 },
336            children: shapes
337                .iter()
338                .cloned()
339                .map(|shape| IrNode {
340                    shape,
341                    name: None,
342                    token_start: 0,
343                    token_end: 0,
344                    range: ByteRange { start: 0, end: 0 },
345                    children: Vec::new(),
346                })
347                .collect(),
348        };
349        IrNode {
350            shape: Shape::Function,
351            name: None,
352            token_start: 0,
353            token_end: 0,
354            range: ByteRange { start: 0, end: 0 },
355            children: vec![body],
356        }
357    }
358
359    #[test]
360    fn a_body_that_moves_one_value_is_trivial() {
361        // A getter: the tail expression is not even a statement.
362        assert_eq!(classify(&unit(&[])), Some(Boilerplate::TrivialBody));
363        // A setter.
364        assert_eq!(
365            classify(&unit(&[Shape::Assign])),
366            Some(Boilerplate::TrivialBody)
367        );
368        assert_eq!(
369            classify(&unit(&[Shape::Return])),
370            Some(Boilerplate::TrivialBody)
371        );
372        // Two statements are already more than moving one value.
373        assert_eq!(classify(&unit(&[Shape::Assign, Shape::Return])), None);
374    }
375
376    #[test]
377    fn exported_counts_are_the_classifier_input() {
378        let measured = counts(&unit(&[Shape::Assign, Shape::Return]));
379        assert_eq!(measured.control, 0);
380        assert_eq!(measured.calls, 0);
381        assert_eq!(measured.macros, 0);
382        assert_eq!(measured.statements, 2);
383        assert_eq!(measured.work, 1);
384        assert_eq!(measured.branches, 0);
385        assert_eq!(measured.declarations, 0);
386        assert_eq!(measured.returns, 1);
387    }
388
389    #[test]
390    fn a_single_call_and_nothing_else_is_forwarding() {
391        assert_eq!(
392            classify(&unit(&[Shape::Call])),
393            Some(Boilerplate::Forwarding)
394        );
395        // A call plus real work is not a wrapper.
396        assert_eq!(classify(&unit(&[Shape::Call, Shape::Assign])), None);
397        assert_eq!(classify(&unit(&[Shape::Call, Shape::Call])), None);
398    }
399
400    #[test]
401    fn a_local_the_delegation_answers_through_is_part_of_the_call() {
402        // `U32 val; read(&val, p); return val;` — the local exists because
403        // the callee answers through a pointer, and the C caller has no other
404        // way to write the call. All three statements are one delegation.
405        assert_eq!(
406            classify(&unit(&[Shape::VarDecl, Shape::Call, Shape::Return])),
407            Some(Boilerplate::Forwarding)
408        );
409        // Several out-parameters are still one call.
410        assert_eq!(
411            classify(&unit(&[
412                Shape::VarDecl,
413                Shape::VarDecl,
414                Shape::Call,
415                Shape::Return
416            ])),
417            Some(Boilerplate::Forwarding)
418        );
419        // Without a delegation there is nothing for the local to be part of,
420        // and an initialiser is invisible here: `U32 v = h * 31 + 7; return v;`
421        // has the same shape as `U32 v; return v;`, so neither is classified.
422        assert_eq!(classify(&unit(&[Shape::VarDecl, Shape::Return])), None);
423    }
424
425    #[test]
426    fn handing_an_error_upwards_is_not_a_second_path() {
427        // Rust's `?`: the node carries the expression whose error it passes
428        // on, and the unit still has one path. `Ok(open(p)?)`.
429        let propagate = nest(Shape::Call, vec![nest(Shape::Try, vec![leaf(Shape::Call)])]);
430        assert_eq!(
431            classify(&unit_of(vec![propagate])),
432            Some(Boilerplate::Forwarding)
433        );
434
435        // `try`/`catch`: the handler is a second path, and it arrives as a
436        // block. That is behaviour, whatever the delegation inside it looks
437        // like.
438        let handle = nest(
439            Shape::Try,
440            vec![
441                nest(Shape::Block, vec![leaf(Shape::Call)]),
442                nest(Shape::Block, vec![leaf(Shape::Call)]),
443            ],
444        );
445        assert_eq!(classify(&unit_of(vec![handle])), None);
446    }
447
448    /// A node of `shape` wrapping `children`, for the nesting cases.
449    fn nest(shape: Shape, children: Vec<IrNode>) -> IrNode {
450        IrNode {
451            shape,
452            name: None,
453            token_start: 0,
454            token_end: 0,
455            range: ByteRange { start: 0, end: 0 },
456            children,
457        }
458    }
459
460    fn leaf(shape: Shape) -> IrNode {
461        nest(shape, Vec::new())
462    }
463
464    /// A unit whose body is the given statements, given as whole subtrees.
465    fn unit_of(statements: Vec<IrNode>) -> IrNode {
466        nest(Shape::Function, vec![nest(Shape::Block, statements)])
467    }
468
469    #[test]
470    fn the_arguments_of_a_delegation_are_part_of_it() {
471        // `f(g(x))`: one thing done, by way of another. Counting the inner
472        // call as a second thing said this was not a wrapper, which is how
473        // the commonest wrapper in either language went unrecognised.
474        let delegation = nest(Shape::Call, vec![leaf(Shape::Call)]);
475        assert_eq!(
476            classify(&unit_of(vec![delegation])),
477            Some(Boilerplate::Forwarding)
478        );
479
480        // `return f(g(x), h(y));` — the `return` is punctuation around the
481        // same single delegation.
482        let wrapped = nest(
483            Shape::Return,
484            vec![nest(
485                Shape::Call,
486                vec![leaf(Shape::Call), leaf(Shape::Call)],
487            )],
488        );
489        assert_eq!(
490            classify(&unit_of(vec![wrapped])),
491            Some(Boilerplate::Forwarding)
492        );
493    }
494
495    #[test]
496    fn a_macro_inside_a_delegation_is_part_of_it() {
497        // `Ok(tri!(self.peek()).unwrap_or(b'\0'))` — a wrapper whose argument
498        // is spelled with a macro. Counting the macro as something the body
499        // does besides delegate is the same mistake counting the inner call
500        // would be, and it was made only for macros.
501        let delegation = nest(
502            Shape::Call,
503            vec![nest(Shape::Call, vec![leaf(Shape::MacroCall)])],
504        );
505        assert_eq!(
506            classify(&unit_of(vec![delegation])),
507            Some(Boilerplate::Forwarding)
508        );
509
510        // Standing on its own it is still counted: nothing here can see what a
511        // macro expands to, so a statement that is one is a statement whose
512        // contents are unknown.
513        let body = vec![leaf(Shape::Call), leaf(Shape::MacroCall)];
514        assert_eq!(classify(&unit_of(body)), None);
515    }
516
517    #[test]
518    fn a_repetition_of_macros_cannot_hide_inside_a_call() {
519        // The repetition rule asks for no calls at all, and a macro counts as
520        // nested only under one. So the two rules cannot reach the same body,
521        // and relaxing the macro count leaves the repetition rule where it was.
522        let body = vec![
523            nest(Shape::Call, vec![leaf(Shape::MacroCall)]),
524            nest(Shape::Call, vec![leaf(Shape::MacroCall)]),
525        ];
526        assert_eq!(classify(&unit_of(body)), None);
527    }
528
529    #[test]
530    fn two_calls_side_by_side_are_two_things_done() {
531        // Nesting is what makes a call part of a delegation. Siblings are not
532        // nested, however deep either of them runs.
533        let body = vec![
534            nest(Shape::Call, vec![leaf(Shape::Call)]),
535            leaf(Shape::Call),
536        ];
537        assert_eq!(classify(&unit_of(body)), None);
538    }
539
540    #[test]
541    fn work_beside_a_delegation_still_disqualifies_it() {
542        // A `return` is punctuation; an assignment is not.
543        let body = vec![
544            nest(Shape::Call, vec![leaf(Shape::Call)]),
545            leaf(Shape::Assign),
546        ];
547        assert_eq!(classify(&unit_of(body)), None);
548    }
549
550    #[test]
551    fn a_run_of_macro_invocations_is_recognised_as_repetition() {
552        assert_eq!(
553            classify(&unit(&[
554                Shape::MacroCall,
555                Shape::MacroCall,
556                Shape::MacroCall
557            ])),
558            Some(Boilerplate::MacroRepetition)
559        );
560        // One macro invocation is not a run, and says nothing about the body.
561        assert_eq!(classify(&unit(&[Shape::MacroCall])), None);
562        // Macros mixed with other work are not classified: what the macros
563        // expand to is unknown, so the body cannot be called trivial.
564        assert_eq!(
565            classify(&unit(&[Shape::MacroCall, Shape::MacroCall, Shape::Return])),
566            None
567        );
568    }
569
570    #[test]
571    fn a_guard_and_an_answer_on_each_side_is_a_dispatch() {
572        // `if (item == NULL) { return false; } return item->kind == KIND;`
573        let guarded = vec![
574            nest(Shape::Branch, vec![leaf(Shape::Return)]),
575            leaf(Shape::Return),
576        ];
577        assert_eq!(
578            classify(&unit_of(guarded)),
579            Some(Boilerplate::GuardedDispatch)
580        );
581
582        // A delegation on each side is the same choice: `if (c) return f(x);
583        // return g(x);`
584        let dispatched = vec![
585            nest(
586                Shape::Branch,
587                vec![nest(Shape::Return, vec![leaf(Shape::Call)])],
588            ),
589            nest(Shape::Return, vec![leaf(Shape::Call)]),
590        ];
591        assert_eq!(
592            classify(&unit_of(dispatched)),
593            Some(Boilerplate::GuardedDispatch)
594        );
595    }
596
597    #[test]
598    fn two_answers_and_no_guard_are_the_build_configuration_choosing() {
599        // `#ifdef _WIN32 return f(x); #else return g(x); #endif` — the
600        // directive leaves no node, so what reaches here is two returns and
601        // nothing between them.
602        let configured = vec![
603            nest(Shape::Return, vec![leaf(Shape::Call)]),
604            nest(Shape::Return, vec![leaf(Shape::Call)]),
605        ];
606        assert_eq!(
607            classify(&unit_of(configured)),
608            Some(Boilerplate::ConfiguredAnswer)
609        );
610
611        // A third arm, and an answer that calls nothing, are the same shape.
612        let three = vec![
613            leaf(Shape::Return),
614            leaf(Shape::Return),
615            leaf(Shape::Return),
616        ];
617        assert_eq!(
618            classify(&unit_of(three)),
619            Some(Boilerplate::ConfiguredAnswer)
620        );
621    }
622
623    #[test]
624    fn arms_that_do_something_are_written_once_per_configuration() {
625        // A local in one arm is work the other arm does differently, which is
626        // what a reader would want to see duplicated.
627        let declaring = vec![
628            leaf(Shape::VarDecl),
629            nest(Shape::Return, vec![leaf(Shape::Call)]),
630            nest(Shape::Return, vec![leaf(Shape::Call)]),
631        ];
632        assert_eq!(classify(&unit_of(declaring)), None);
633
634        let assigning = vec![
635            leaf(Shape::Assign),
636            leaf(Shape::Return),
637            leaf(Shape::Return),
638        ];
639        assert_eq!(classify(&unit_of(assigning)), None);
640
641        // More calls than answers means an arm is computing one.
642        let computing = vec![
643            nest(Shape::Return, vec![leaf(Shape::Call)]),
644            nest(
645                Shape::Return,
646                vec![leaf(Shape::Call), leaf(Shape::Call), leaf(Shape::Call)],
647            ),
648        ];
649        assert_eq!(classify(&unit_of(computing)), None);
650    }
651
652    #[test]
653    fn one_answer_is_not_a_configuration() {
654        // A body with a single return is whatever else it is; nothing chose
655        // it. `return f(x);` stays a wrapper.
656        let single = vec![nest(Shape::Return, vec![leaf(Shape::Call)])];
657        assert_eq!(classify(&unit_of(single)), Some(Boilerplate::Forwarding));
658    }
659
660    #[test]
661    fn more_than_one_guard_is_a_decision_table() {
662        // Two copies of a table differing in their constants is duplication
663        // worth reporting, so a body that decides is never set aside.
664        let table = vec![
665            nest(Shape::Branch, vec![leaf(Shape::Return)]),
666            nest(Shape::Branch, vec![leaf(Shape::Return)]),
667            leaf(Shape::Return),
668        ];
669        assert_eq!(classify(&unit_of(table)), None);
670    }
671
672    #[test]
673    fn work_beside_a_guard_is_not_a_choice_between_answers() {
674        let assigning = vec![
675            nest(Shape::Branch, vec![leaf(Shape::Assign)]),
676            leaf(Shape::Return),
677        ];
678        assert_eq!(classify(&unit_of(assigning)), None);
679        // A local is opaque here, so a guard beside one says nothing.
680        let declaring = vec![
681            leaf(Shape::VarDecl),
682            nest(Shape::Branch, vec![leaf(Shape::Return)]),
683            leaf(Shape::Return),
684        ];
685        assert_eq!(classify(&unit_of(declaring)), None);
686        // More calls than answers means the body is computing one.
687        let computing = vec![
688            nest(Shape::Branch, vec![leaf(Shape::Return)]),
689            nest(
690                Shape::Return,
691                vec![leaf(Shape::Call), leaf(Shape::Call), leaf(Shape::Call)],
692            ),
693        ];
694        assert_eq!(classify(&unit_of(computing)), None);
695    }
696
697    #[test]
698    fn control_flow_other_than_one_guard_is_never_boilerplate() {
699        for shape in [Shape::Branch, Shape::Loop, Shape::Match] {
700            assert_eq!(
701                classify(&unit(std::slice::from_ref(&shape))),
702                None,
703                "{shape:?}"
704            );
705            // Even alongside a shape that would otherwise classify.
706            assert_eq!(classify(&unit(&[Shape::Call, shape])), None);
707        }
708    }
709
710    #[test]
711    fn nested_bodies_count_towards_the_unit() {
712        // A closure that branches makes its enclosing unit non-trivial.
713        let mut node = unit(&[]);
714        node.children[0].children.push(IrNode {
715            shape: Shape::Closure,
716            name: None,
717            token_start: 0,
718            token_end: 0,
719            range: ByteRange { start: 0, end: 0 },
720            children: vec![IrNode {
721                shape: Shape::Branch,
722                name: None,
723                token_start: 0,
724                token_end: 0,
725                range: ByteRange { start: 0, end: 0 },
726                children: Vec::new(),
727            }],
728        });
729        assert_eq!(classify(&node), None);
730    }
731
732    #[test]
733    fn category_names_round_trip() {
734        for category in Boilerplate::all() {
735            assert_eq!(Boilerplate::from_name(category.name()), Some(category));
736        }
737        assert_eq!(Boilerplate::from_name("getter"), None);
738    }
739}