Skip to main content

Maintain/Eliminate/Transform/
Collect.rs

1//=============================================================================//
2// File Path: Element/Maintain/Source/Eliminate/Transform/Collect.rs
3//=============================================================================//
4// Module: Collect - Candidate let-binding discovery
5//
6// Scans a block's statement list for `let` bindings that are structurally
7// eligible for inlining: simple (non-destructured) identifier pattern, no
8// `mut` / `ref` qualifiers, has an initialiser, and no `else` branch.
9//=============================================================================//
10
11use syn::{Block, Pat, Stmt};
12
13/// A `let` binding that may be inlinable.
14#[derive(Debug, Clone)]
15pub struct Candidate {
16	/// The identifier name (e.g. `"URI"`).
17	pub Ident:String,
18
19	/// Index of this `Stmt::Local` within `block.stmts`.
20	pub StmtIndex:usize,
21
22	/// Cloned copy of the initialiser expression.
23	/// Stored here so that we can borrow the expression independently while
24	/// mutating `block.stmts`.
25	pub Init:syn::Expr,
26}
27
28/// Collect all structurally eligible `let` bindings in `Block`.
29///
30/// Bindings are excluded when:
31/// - The pattern is not a plain identifier (destructuring, tuple, …).
32/// - The binding is `let mut` or `let ref`.
33/// - The binding has a `@ subpat`.
34/// - There is no initialiser (`let x;`).
35/// - There is a diverging `else` branch (`let … = … else { … }`).
36/// - The let statement carries attributes AND `InlineComments` is `false`.
37/// - The binding is the last statement in the block (nothing to substitute
38///   into).
39pub fn Collect(Block:&Block, InlineComments:bool) -> Vec<Candidate> {
40	let Len = Block.stmts.len();
41
42	Block
43		.stmts
44		.iter()
45		.enumerate()
46
47		// Must have at least one subsequent statement to substitute into.
48		.filter(|(Index, _)| *Index + 1 < Len)
49		.filter_map(|(Index, Stmt)| {
50			let Stmt::Local(Local) = Stmt else {
51				return None;
52			};
53
54			// Skip attributed lets (doc-comments / derive-like attrs) unless
55			// the caller explicitly opts in.
56			if !InlineComments && !Local.attrs.is_empty() {
57				return None;
58			}
59
60			// Must have an initialiser.
61			let Some(Init) = &Local.init else {
62				return None;
63			};
64
65			// No `let … = … else { … }` (diverging pattern).
66			if Init.diverge.is_some() {
67				return None;
68			}
69
70			// Pattern must be a plain identifier (optionally with a type
71			// annotation).  `let X: i32 = 5` has Pat::Type(Pat::Ident).
72			// No destructuring, no `mut`, no `ref`, no `@ subpat`.
73			let PatIdent = match &Local.pat {
74				Pat::Ident(P) => P,
75
76				Pat::Type(syn::PatType { pat, .. }) => {
77					if let Pat::Ident(P) = pat.as_ref() {
78						P
79					} else {
80						return None;
81					}
82				},
83
84				_ => return None,
85			};
86
87			if PatIdent.by_ref.is_some()
88
89				|| PatIdent.mutability.is_some()
90
91				|| PatIdent.subpat.is_some()
92
93			{
94				return None;
95			}
96
97			Some(Candidate {
98				Ident: PatIdent.ident.to_string(),
99				StmtIndex: Index,
100				Init: *Init.expr.clone(),
101			})
102		})
103		.collect()
104}
105
106// ---------------------------------------------------------------------------
107// Unit tests
108// ---------------------------------------------------------------------------
109
110#[cfg(test)]
111mod Tests {
112
113	use super::*;
114
115	fn CollectFrom(Src:&str) -> Vec<Candidate> {
116		let File:syn::File = syn::parse_str(Src).expect("parse");
117
118		// Grab the first function body.
119		for Item in &File.items {
120			if let syn::Item::Fn(F) = Item {
121				return Collect(&F.block, false);
122			}
123		}
124
125		vec![]
126	}
127
128	#[test]
129	fn SimpleLetIsCandidate() {
130		let Candidates = CollectFrom("fn f() { let X = 5; f(X); }");
131
132		assert_eq!(Candidates.len(), 1);
133
134		assert_eq!(Candidates[0].Ident, "X");
135	}
136
137	#[test]
138	fn MutLetExcluded() {
139		let Candidates = CollectFrom("fn f() { let mut X = 5; f(X); }");
140
141		assert!(Candidates.is_empty());
142	}
143
144	#[test]
145	fn DestructuringExcluded() {
146		let Candidates = CollectFrom("fn f() { let (A, B) = pair; f(A); }");
147
148		assert!(Candidates.is_empty());
149	}
150
151	#[test]
152	fn NoInitExcluded() {
153		let Candidates = CollectFrom("fn f() { let X: i32; X = 5; f(X); }");
154
155		assert!(Candidates.is_empty());
156	}
157
158	#[test]
159	fn LastStmtExcluded() {
160		// X is the last statement - nothing to substitute into.
161		let Candidates = CollectFrom("fn f() { f(1); let X = 5; }");
162
163		assert!(Candidates.is_empty());
164	}
165
166	#[test]
167	fn LetElseExcluded() {
168		let Candidates = CollectFrom(
169			r#"fn f() {
170                let Ok(X) = foo() else { return; };
171                f(X);
172            }"#,
173		);
174
175		assert!(Candidates.is_empty());
176	}
177
178	#[test]
179	fn TwoConsecutiveCandidates() {
180		let Candidates = CollectFrom(
181			r#"fn f() {
182                let A = 1;
183                let B = A + 1;
184                g(B);
185            }"#,
186		);
187
188		assert_eq!(Candidates.len(), 2);
189
190		assert_eq!(Candidates[0].Ident, "A");
191
192		assert_eq!(Candidates[1].Ident, "B");
193	}
194
195	#[test]
196	fn AttributedLetExcludedByDefault() {
197		let Candidates = CollectFrom(
198			r#"fn f() {
199                #[allow(unused)]
200                let X = 5;
201                g(X);
202            }"#,
203		);
204
205		assert!(Candidates.is_empty());
206	}
207
208	#[test]
209	fn AttributedLetIncludedWhenOptIn() {
210		let File:syn::File = syn::parse_str(
211			r#"fn f() {
212                #[allow(unused)]
213                let X = 5;
214                g(X);
215            }"#,
216		)
217		.unwrap();
218
219		let Block = match &File.items[0] {
220			syn::Item::Fn(F) => &F.block,
221
222			_ => panic!(),
223		};
224
225		let Candidates = Collect(Block, true);
226
227		assert_eq!(Candidates.len(), 1);
228	}
229}