1use super::CallsLoop;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{Severity, SolLint},
5};
6use solar::{
7 ast::{DataLocation, ElementaryType, StateMutability, Visibility},
8 interface::{kw, sym},
9 sema::{
10 Gcx, Ty,
11 builtins::Builtin,
12 hir::{
13 self, Block, ContractId, Expr, ExprKind, Function, FunctionId, Hir, ItemId, Res, Stmt,
14 StmtKind, TypeKind,
15 },
16 ty::{TyFn, TyKind},
17 },
18};
19use std::collections::HashSet;
20
21declare_forge_lint!(CALLS_LOOP, Severity::Low, "calls-loop", "external call inside a loop");
22
23impl<'hir> LateLintPass<'hir> for CallsLoop {
24 fn check_function(
25 &mut self,
26 ctx: &LintContext,
27 gcx: Gcx<'hir>,
28 hir: &'hir Hir<'hir>,
29 func: &'hir Function<'hir>,
30 ) {
31 let Some(body) = func.body else { return };
32
33 let mut analyzer = Analyzer::new(ctx, gcx, hir);
34 analyzer.analyze_callable(func, body, 0);
35 }
36}
37
38type Placeholder<'hir> = Option<(&'hir [hir::Modifier<'hir>], usize, Block<'hir>)>;
39
40struct Analyzer<'ctx, 's, 'c, 'hir> {
41 ctx: &'ctx LintContext<'s, 'c>,
42 gcx: Gcx<'hir>,
43 hir: &'hir Hir<'hir>,
44 call_stack: Vec<FunctionId>,
45 analyzed_loop_calls: HashSet<FunctionId>,
46 emitted: HashSet<solar::interface::Span>,
47}
48
49impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> {
50 fn new(ctx: &'ctx LintContext<'s, 'c>, gcx: Gcx<'hir>, hir: &'hir Hir<'hir>) -> Self {
51 Self {
52 ctx,
53 gcx,
54 hir,
55 call_stack: Vec::new(),
56 analyzed_loop_calls: HashSet::new(),
57 emitted: HashSet::new(),
58 }
59 }
60
61 fn analyze_callable(&mut self, func: &'hir Function<'hir>, body: Block<'hir>, loop_depth: u32) {
62 self.analyze_modifier_chain(func.modifiers, 0, body, loop_depth);
63 }
64
65 fn analyze_modifier_chain(
66 &mut self,
67 modifiers: &'hir [hir::Modifier<'hir>],
68 index: usize,
69 body: Block<'hir>,
70 loop_depth: u32,
71 ) {
72 let Some(modifier) = modifiers.get(index) else {
73 return self.analyze_block(body, None, loop_depth);
74 };
75
76 for arg in modifier.args.exprs() {
77 self.analyze_expr(arg, loop_depth);
78 }
79
80 let Some(modifier_id) = modifier.id.as_function() else {
81 return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
82 };
83
84 if self.call_stack.contains(&modifier_id) {
85 return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
86 }
87
88 let modifier_func = self.hir.function(modifier_id);
89 let Some(modifier_body) = modifier_func.body else {
90 return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
91 };
92
93 self.call_stack.push(modifier_id);
94 self.analyze_block(modifier_body, Some((modifiers, index + 1, body)), loop_depth);
95 self.call_stack.pop();
96 }
97
98 fn analyze_block(
99 &mut self,
100 block: Block<'hir>,
101 placeholder: Placeholder<'hir>,
102 loop_depth: u32,
103 ) {
104 for stmt in block.stmts {
105 self.analyze_stmt(stmt, placeholder, loop_depth);
106 }
107 }
108
109 fn analyze_stmt(
110 &mut self,
111 stmt: &'hir Stmt<'hir>,
112 placeholder: Placeholder<'hir>,
113 loop_depth: u32,
114 ) {
115 match stmt.kind {
116 StmtKind::DeclSingle(var_id) => {
117 if let Some(init) = self.hir.variable(var_id).initializer {
118 self.analyze_expr(init, loop_depth);
119 }
120 }
121 StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => {
122 self.analyze_expr(expr, loop_depth);
123 }
124 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
125 self.analyze_block(block, placeholder, loop_depth);
126 }
127 StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
128 self.analyze_expr(expr, loop_depth);
129 }
130 StmtKind::Return(Some(expr)) => {
131 self.analyze_expr(expr, loop_depth);
132 }
133 StmtKind::Loop(block, _) => {
134 self.analyze_block(block, placeholder, loop_depth + 1);
135 }
136 StmtKind::If(cond, then_stmt, else_stmt) => {
137 self.analyze_expr(cond, loop_depth);
138 self.analyze_stmt(then_stmt, placeholder, loop_depth);
139 if let Some(else_stmt) = else_stmt {
140 self.analyze_stmt(else_stmt, placeholder, loop_depth);
141 }
142 }
143 StmtKind::Try(try_stmt) => {
144 self.analyze_expr(&try_stmt.expr, loop_depth);
145 for clause in try_stmt.clauses {
146 self.analyze_block(clause.block, placeholder, loop_depth);
147 }
148 }
149 StmtKind::Placeholder => {
150 if let Some((modifiers, index, body)) = placeholder {
151 self.analyze_modifier_chain(modifiers, index, body, loop_depth);
152 }
153 }
154 StmtKind::Return(None)
155 | StmtKind::Break
156 | StmtKind::Continue
157 | StmtKind::AssemblyBlock(_)
158 | StmtKind::Switch(_)
159 | StmtKind::Err(_) => {}
160 }
161 }
162
163 fn analyze_expr(&mut self, expr: &'hir Expr<'hir>, loop_depth: u32) {
164 match &expr.kind {
165 ExprKind::Call(callee, args, opts) => {
166 self.analyze_expr(callee, loop_depth);
167 if let Some(opts) = opts {
168 for opt in opts.args {
169 self.analyze_expr(&opt.value, loop_depth);
170 }
171 }
172 for arg in args.exprs() {
173 self.analyze_expr(arg, loop_depth);
174 }
175
176 if loop_depth > 0 {
177 if is_external_call(self.gcx, self.hir, callee, args.len()) {
178 self.emit(expr);
179 }
180 for func_id in resolved_internal_function_ids(self.hir, callee) {
181 self.analyze_internal_call(func_id, loop_depth);
182 }
183 }
184 }
185 ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
186 self.analyze_expr(lhs, loop_depth);
187 self.analyze_expr(rhs, loop_depth);
188 }
189 ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
190 self.analyze_expr(inner, loop_depth);
191 }
192 ExprKind::Index(base, index) => {
193 self.analyze_expr(base, loop_depth);
194 if let Some(index) = index {
195 self.analyze_expr(index, loop_depth);
196 }
197 }
198 ExprKind::Slice(base, start, end) => {
199 self.analyze_expr(base, loop_depth);
200 if let Some(start) = start {
201 self.analyze_expr(start, loop_depth);
202 }
203 if let Some(end) = end {
204 self.analyze_expr(end, loop_depth);
205 }
206 }
207 ExprKind::Ternary(cond, then_expr, else_expr) => {
208 self.analyze_expr(cond, loop_depth);
209 self.analyze_expr(then_expr, loop_depth);
210 self.analyze_expr(else_expr, loop_depth);
211 }
212 ExprKind::Array(exprs) => {
213 for expr in *exprs {
214 self.analyze_expr(expr, loop_depth);
215 }
216 }
217 ExprKind::Tuple(exprs) => {
218 for expr in exprs.iter().copied().flatten() {
219 self.analyze_expr(expr, loop_depth);
220 }
221 }
222 ExprKind::Member(base, _) => self.analyze_expr(base, loop_depth),
223 ExprKind::Ident(_)
224 | ExprKind::Lit(_)
225 | ExprKind::New(_)
226 | ExprKind::TypeCall(_)
227 | ExprKind::Type(_)
228 | ExprKind::YulMember(..)
229 | ExprKind::Err(_) => {}
230 }
231 }
232
233 fn analyze_internal_call(&mut self, func_id: FunctionId, loop_depth: u32) {
234 if self.call_stack.contains(&func_id) {
235 return;
236 }
237 if !self.analyzed_loop_calls.insert(func_id) {
238 return;
239 }
240
241 let func = self.hir.function(func_id);
242 let Some(body) = func.body else { return };
243
244 self.call_stack.push(func_id);
245 self.analyze_callable(func, body, loop_depth);
246 self.call_stack.pop();
247 }
248
249 fn emit(&mut self, expr: &Expr<'_>) {
250 if self.emitted.insert(expr.span) {
251 self.ctx.emit(&CALLS_LOOP, expr.span);
252 }
253 }
254}
255
256pub(super) fn is_external_call<'gcx>(
257 gcx: Gcx<'gcx>,
258 hir: &'gcx Hir<'gcx>,
259 callee: &Expr<'gcx>,
260 explicit_arg_count: usize,
261) -> bool {
262 if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
264 return true;
265 }
266 let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
267
268 if matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall)
269 && is_address_like(gcx, base)
270 {
271 return true;
272 }
273
274 if matches!(member.name, sym::send | sym::transfer) && is_address_like(gcx, base) {
275 return true;
276 }
277
278 if is_this(base) {
279 return true;
280 }
281
282 if is_super(base) {
285 return false;
286 }
287
288 if resolves_to_internal_library_extension(gcx, hir, base, *member, explicit_arg_count) {
289 return false;
290 }
291
292 external_member_signatures(gcx, hir, base, member.name, explicit_arg_count)
294 .into_iter()
295 .any(|(vis, _)| vis >= Visibility::Public)
296}
297
298pub(super) fn is_state_mutating_external_call<'gcx>(
301 gcx: Gcx<'gcx>,
302 hir: &'gcx Hir<'gcx>,
303 callee: &Expr<'gcx>,
304 explicit_arg_count: usize,
305 enclosing_contract: Option<ContractId>,
306) -> bool {
307 if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
309 return true;
310 }
311 let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
312
313 if matches!(member.name, kw::Call | kw::Delegatecall) && is_address_like(gcx, base) {
315 return true;
316 }
317
318 if member.name == kw::Staticcall && is_address_like(gcx, base) {
319 return false;
320 }
321
322 if matches!(member.name, sym::send | sym::transfer) && is_address_like(gcx, base) {
323 return true;
324 }
325
326 if is_this(base) {
327 return self_call_is_state_mutating(
330 hir,
331 enclosing_contract,
332 member.name,
333 explicit_arg_count,
334 );
335 }
336
337 if is_super(base) {
339 return false;
340 }
341
342 if resolves_to_internal_library_extension(gcx, hir, base, *member, explicit_arg_count) {
343 return false;
344 }
345
346 external_member_signatures(gcx, hir, base, member.name, explicit_arg_count).into_iter().any(
349 |(vis, mut_)| {
350 vis >= Visibility::Public
351 && !matches!(mut_, StateMutability::View | StateMutability::Pure)
352 },
353 )
354}
355
356fn self_call_is_state_mutating(
360 hir: &Hir<'_>,
361 enclosing_contract: Option<ContractId>,
362 member_name: solar::interface::Symbol,
363 explicit_arg_count: usize,
364) -> bool {
365 let Some(contract_id) = enclosing_contract else { return true };
366
367 let mut matched = false;
368 for item_id in hir.contract_item_ids(contract_id) {
369 let Some(func_id) = item_id.as_function() else { continue };
370 let func = hir.function(func_id);
371 if func.name.is_none_or(|name| name.name != member_name) {
372 continue;
373 }
374 if func.parameters.len() != explicit_arg_count {
375 continue;
376 }
377 if func.visibility < Visibility::Public {
379 continue;
380 }
381 matched = true;
382 if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) {
383 return true;
384 }
385 }
386 !matched
388}
389
390fn resolves_to_internal_library_extension<'gcx>(
391 gcx: Gcx<'gcx>,
392 hir: &Hir<'gcx>,
393 base: &Expr<'gcx>,
394 member: solar::interface::Ident,
395 explicit_arg_count: usize,
396) -> bool {
397 member_function_ids(gcx, hir, base, member.name).into_iter().any(|func_id| {
398 let func = hir.function(func_id);
399 func.parameters.len() == explicit_arg_count + 1
400 && matches!(func.visibility, Visibility::Internal | Visibility::Private)
401 && func.contract.is_some_and(|contract_id| hir.contract(contract_id).kind.is_library())
402 })
403}
404
405fn member_function_ids<'gcx>(
406 gcx: Gcx<'gcx>,
407 hir: &Hir<'gcx>,
408 base: &Expr<'gcx>,
409 member_name: solar::interface::Symbol,
410) -> Vec<FunctionId> {
411 let Some(base_ty) = semantic_expr_ty(gcx, hir, base) else { return Vec::new() };
412
413 gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
414 .filter(|member| member.name == member_name)
415 .filter_map(|member| match (member.res, member.ty.kind) {
416 (Some(Res::Item(ItemId::Function(func_id))), _) => Some(func_id),
417 (_, TyKind::Fn(func)) => func.function_id,
418 _ => None,
419 })
420 .collect()
421}
422
423fn external_member_signatures<'gcx>(
427 gcx: Gcx<'gcx>,
428 hir: &Hir<'gcx>,
429 base: &Expr<'gcx>,
430 member_name: solar::interface::Symbol,
431 explicit_arg_count: usize,
432) -> Vec<(Visibility, StateMutability)> {
433 let Some(base_ty) = semantic_expr_ty(gcx, hir, base) else { return Vec::new() };
434
435 let all: Vec<(Visibility, StateMutability, usize)> = gcx
437 .members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
438 .filter(|member| member.name == member_name)
439 .filter_map(|member| match (member.res, member.ty.kind) {
440 (Some(Res::Item(ItemId::Function(func_id))), _) => {
441 let f = hir.function(func_id);
442 Some((f.visibility, f.state_mutability, f.parameters.len()))
443 }
444 (_, TyKind::Fn(func)) => Some(function_signature_from_ty_fn(hir, func)),
445 _ => None,
446 })
447 .collect();
448
449 let arity_matched: Vec<_> =
451 all.iter().filter(|(_, _, n)| *n == explicit_arg_count).copied().collect();
452 let chosen = if arity_matched.is_empty() { all } else { arity_matched };
453 chosen.into_iter().map(|(v, m, _)| (v, m)).collect()
454}
455
456fn function_signature_from_ty_fn(
457 hir: &Hir<'_>,
458 func: &TyFn<'_>,
459) -> (Visibility, StateMutability, usize) {
460 if let Some(func_id) = func.function_id {
461 let f = hir.function(func_id);
462 (f.visibility, f.state_mutability, f.parameters.len())
463 } else if func.is_internal() {
464 (Visibility::Internal, func.state_mutability, func.parameters.len())
465 } else {
466 (Visibility::External, func.state_mutability, func.parameters.len())
467 }
468}
469
470pub(super) fn resolved_internal_function_ids<'hir>(
471 hir: &'hir Hir<'hir>,
472 callee: &'hir Expr<'hir>,
473) -> impl Iterator<Item = FunctionId> + 'hir {
474 let reses = match &callee.peel_parens().kind {
475 ExprKind::Ident(reses) => *reses,
476 _ => &[],
477 };
478
479 reses.iter().filter_map(|res| match res {
480 Res::Item(ItemId::Function(func_id)) if is_internal_callable(hir.function(*func_id)) => {
481 Some(*func_id)
482 }
483 _ => None,
484 })
485}
486
487pub(super) fn resolved_super_function_ids<'hir>(
490 hir: &'hir Hir<'hir>,
491 enclosing_contract: Option<ContractId>,
492 callee: &'hir Expr<'hir>,
493 explicit_arg_count: usize,
494) -> Vec<FunctionId> {
495 let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return Vec::new() };
496 if !is_super(base) {
497 return Vec::new();
498 }
499 let Some(contract_id) = enclosing_contract else { return Vec::new() };
500
501 let mut out = Vec::new();
502 for base_id in hir.contract(contract_id).linearized_bases.iter().skip(1).copied() {
504 for item_id in hir.contract(base_id).items {
505 let Some(func_id) = item_id.as_function() else { continue };
506 let func = hir.function(func_id);
507 if func.name.is_some_and(|name| name.name == member.name)
508 && func.parameters.len() == explicit_arg_count
509 && matches!(func.visibility, Visibility::Internal | Visibility::Public)
510 {
511 out.push(func_id);
512 return out;
513 }
514 }
515 }
516 out
517}
518
519const fn is_internal_callable(func: &Function<'_>) -> bool {
520 func.kind.is_function()
521 && matches!(
522 func.visibility,
523 Visibility::Public | Visibility::Internal | Visibility::Private
524 )
525}
526
527fn is_this(expr: &Expr<'_>) -> bool {
528 matches!(
529 &expr.peel_parens().kind,
530 ExprKind::Ident(reses)
531 if reses.iter().any(|res| {
532 matches!(res, Res::Builtin(builtin) if builtin.name() == sym::this)
533 })
534 )
535}
536
537fn is_super(expr: &Expr<'_>) -> bool {
539 matches!(
540 &expr.peel_parens().kind,
541 ExprKind::Ident(reses)
542 if reses.iter().any(|res| {
543 matches!(res, Res::Builtin(builtin) if builtin.name() == sym::super_)
544 })
545 )
546}
547
548fn is_address_like<'hir>(gcx: Gcx<'hir>, expr: &'hir Expr<'hir>) -> bool {
549 match &expr.peel_parens().kind {
550 ExprKind::Payable(_) => true,
551 ExprKind::Call(callee, _, _) if is_address_type_expr(callee) => true,
552 _ => semantic_expr_ty(gcx, &gcx.hir, expr).is_some_and(type_is_address_like),
553 }
554}
555
556fn is_address_type_expr(expr: &Expr<'_>) -> bool {
557 matches!(
558 &expr.peel_parens().kind,
559 ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
560 )
561}
562
563fn type_is_address_like(ty: Ty<'_>) -> bool {
564 matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
565}
566
567fn semantic_expr_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, expr: &Expr<'gcx>) -> Option<Ty<'gcx>> {
568 if !is_typeless_builtin_expr(expr)
569 && let Some(ty) = gcx.type_of_expr(expr.peel_parens().id)
570 {
571 return Some(ty);
572 }
573
574 match &expr.peel_parens().kind {
575 ExprKind::Ident(reses) => {
576 let res = unique(reses.iter().filter(|res| !matches!(res, Res::Err(_))).copied())
577 .or_else(|| {
578 unique(reses.iter().filter_map(|res| {
579 res.as_variable().map(|var_id| Res::Item(ItemId::Variable(var_id)))
580 }))
581 })?;
582 if matches!(res, Res::Builtin(builtin) if is_typeless_builtin(builtin)) {
583 return None;
584 }
585 let ty = gcx.type_of_res(res);
586 Some(match res {
587 Res::Item(ItemId::Variable(var_id)) => {
588 ty.with_loc_if_ref_opt(gcx, variable_data_location(hir, var_id))
589 }
590 _ => ty,
591 })
592 }
593 ExprKind::Index(base, _) => semantic_index_ty(gcx, hir, base),
594 ExprKind::Member(base, member) => semantic_member_ty(gcx, hir, base, member.name),
595 ExprKind::Call(callee, _, _) => {
596 let callee_ty = semantic_expr_ty(gcx, hir, callee)?;
597 match callee_ty.kind {
598 TyKind::Fn(func) => semantic_fn_call_return_ty(gcx, func.returns),
599 TyKind::Type(to) => Some(to),
600 _ => None,
601 }
602 }
603 ExprKind::New(ty) | ExprKind::Type(ty) | ExprKind::TypeCall(ty) => {
604 Some(gcx.mk_ty(TyKind::Type(gcx.type_of_hir_ty(ty))))
605 }
606 ExprKind::Payable(_) => Some(gcx.types.address_payable),
607 _ => None,
608 }
609}
610
611fn is_typeless_builtin_expr(expr: &Expr<'_>) -> bool {
612 matches!(
613 &expr.peel_parens().kind,
614 ExprKind::Ident(reses)
615 if reses.iter().any(|res| {
616 matches!(res, Res::Builtin(builtin) if is_typeless_builtin(*builtin))
617 })
618 )
619}
620
621const fn is_typeless_builtin(builtin: Builtin) -> bool {
622 matches!(
623 builtin,
624 Builtin::This
625 | Builtin::Super
626 | Builtin::ArrayPush0
627 | Builtin::ArrayPush
628 | Builtin::ArrayPop
629 | Builtin::TypeMin
630 | Builtin::TypeMax
631 | Builtin::UdvtWrap
632 | Builtin::UdvtUnwrap
633 )
634}
635
636fn semantic_index_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, base: &Expr<'gcx>) -> Option<Ty<'gcx>> {
637 let base_ty = semantic_expr_ty(gcx, hir, base)?;
638 let loc = indexed_base_data_location(base_ty);
639 match base_ty.peel_refs().kind {
640 TyKind::Mapping(_, value) => Some(value.with_loc_if_ref_opt(gcx, loc)),
641 _ => base_ty.base_type(gcx),
642 }
643}
644
645fn indexed_base_data_location(ty: Ty<'_>) -> Option<DataLocation> {
646 ty.loc().or_else(|| {
647 matches!(ty.kind, TyKind::Mapping(..)).then_some(DataLocation::Storage)
650 })
651}
652
653fn semantic_member_ty<'gcx>(
654 gcx: Gcx<'gcx>,
655 hir: &Hir<'gcx>,
656 base: &Expr<'gcx>,
657 member_name: solar::interface::Symbol,
658) -> Option<Ty<'gcx>> {
659 let base_ty = semantic_expr_ty(gcx, hir, base)?;
660 unique(
661 gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
662 .filter(|member| member.name == member_name)
663 .map(|member| member.ty),
664 )
665}
666
667fn semantic_fn_call_return_ty<'gcx>(gcx: Gcx<'gcx>, returns: &'gcx [Ty<'gcx>]) -> Option<Ty<'gcx>> {
668 Some(match returns {
669 [] => gcx.types.unit,
670 [ret] => *ret,
671 _ => gcx.mk_ty_tuple(returns),
672 })
673}
674
675fn base_item_source(hir: &Hir<'_>, expr: &Expr<'_>) -> solar::sema::hir::SourceId {
676 referenced_item(expr)
677 .map(|id| hir.item(id).source())
678 .unwrap_or_else(|| hir.sources_enumerated().next().expect("HIR has a source").0)
679}
680
681fn base_contract(hir: &Hir<'_>, expr: &Expr<'_>) -> Option<ContractId> {
682 referenced_item(expr).and_then(|id| hir.item(id).contract())
683}
684
685fn referenced_item(expr: &Expr<'_>) -> Option<ItemId> {
686 match &expr.peel_parens().kind {
687 ExprKind::Ident([Res::Item(id), ..]) => Some(*id),
688 _ => None,
689 }
690}
691
692fn variable_data_location(hir: &Hir<'_>, var_id: hir::VariableId) -> Option<DataLocation> {
693 let var = hir.variable(var_id);
694 var.data_location.or_else(|| {
695 (var.parent.is_none() && var.contract.is_some()).then_some(DataLocation::Storage)
696 })
697}
698
699fn unique<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
700 let first = iter.next()?;
701 iter.next().is_none().then_some(first)
702}