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}