]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/lifetimes.rs
Rollup merge of #104901 - krtab:filetype_compare, r=the8472
[rust.git] / src / tools / clippy / clippy_lints / src / lifetimes.rs
1 use clippy_utils::diagnostics::{span_lint, span_lint_and_then};
2 use clippy_utils::trait_ref_of_method;
3 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
4 use rustc_hir::intravisit::nested_filter::{self as hir_nested_filter, NestedFilter};
5 use rustc_hir::intravisit::{
6     walk_fn_decl, walk_generic_arg, walk_generic_param, walk_generics, walk_impl_item_ref, walk_item, walk_param_bound,
7     walk_poly_trait_ref, walk_trait_ref, walk_ty, Visitor,
8 };
9 use rustc_hir::lang_items;
10 use rustc_hir::FnRetTy::Return;
11 use rustc_hir::{
12     BareFnTy, BodyId, FnDecl, GenericArg, GenericBound, GenericParam, GenericParamKind, Generics, Impl, ImplItem,
13     ImplItemKind, Item, ItemKind, Lifetime, LifetimeName, LifetimeParamKind, PolyTraitRef, PredicateOrigin, TraitFn,
14     TraitItem, TraitItemKind, Ty, TyKind, WherePredicate,
15 };
16 use rustc_lint::{LateContext, LateLintPass};
17 use rustc_middle::hir::nested_filter as middle_nested_filter;
18 use rustc_middle::ty::TyCtxt;
19 use rustc_session::{declare_lint_pass, declare_tool_lint};
20 use rustc_span::def_id::LocalDefId;
21 use rustc_span::source_map::Span;
22 use rustc_span::symbol::{kw, Ident, Symbol};
23
24 declare_clippy_lint! {
25     /// ### What it does
26     /// Checks for lifetime annotations which can be removed by
27     /// relying on lifetime elision.
28     ///
29     /// ### Why is this bad?
30     /// The additional lifetimes make the code look more
31     /// complicated, while there is nothing out of the ordinary going on. Removing
32     /// them leads to more readable code.
33     ///
34     /// ### Known problems
35     /// - We bail out if the function has a `where` clause where lifetimes
36     /// are mentioned due to potential false positives.
37     /// - Lifetime bounds such as `impl Foo + 'a` and `T: 'a` must be elided with the
38     /// placeholder notation `'_` because the fully elided notation leaves the type bound to `'static`.
39     ///
40     /// ### Example
41     /// ```rust
42     /// // Unnecessary lifetime annotations
43     /// fn in_and_out<'a>(x: &'a u8, y: u8) -> &'a u8 {
44     ///     x
45     /// }
46     /// ```
47     ///
48     /// Use instead:
49     /// ```rust
50     /// fn elided(x: &u8, y: u8) -> &u8 {
51     ///     x
52     /// }
53     /// ```
54     #[clippy::version = "pre 1.29.0"]
55     pub NEEDLESS_LIFETIMES,
56     complexity,
57     "using explicit lifetimes for references in function arguments when elision rules \
58      would allow omitting them"
59 }
60
61 declare_clippy_lint! {
62     /// ### What it does
63     /// Checks for lifetimes in generics that are never used
64     /// anywhere else.
65     ///
66     /// ### Why is this bad?
67     /// The additional lifetimes make the code look more
68     /// complicated, while there is nothing out of the ordinary going on. Removing
69     /// them leads to more readable code.
70     ///
71     /// ### Example
72     /// ```rust
73     /// // unnecessary lifetimes
74     /// fn unused_lifetime<'a>(x: u8) {
75     ///     // ..
76     /// }
77     /// ```
78     ///
79     /// Use instead:
80     /// ```rust
81     /// fn no_lifetime(x: u8) {
82     ///     // ...
83     /// }
84     /// ```
85     #[clippy::version = "pre 1.29.0"]
86     pub EXTRA_UNUSED_LIFETIMES,
87     complexity,
88     "unused lifetimes in function definitions"
89 }
90
91 declare_lint_pass!(Lifetimes => [NEEDLESS_LIFETIMES, EXTRA_UNUSED_LIFETIMES]);
92
93 impl<'tcx> LateLintPass<'tcx> for Lifetimes {
94     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx Item<'_>) {
95         if let ItemKind::Fn(ref sig, generics, id) = item.kind {
96             check_fn_inner(cx, sig.decl, Some(id), None, generics, item.span, true);
97         } else if let ItemKind::Impl(impl_) = item.kind {
98             if !item.span.from_expansion() {
99                 report_extra_impl_lifetimes(cx, impl_);
100             }
101         }
102     }
103
104     fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx ImplItem<'_>) {
105         if let ImplItemKind::Fn(ref sig, id) = item.kind {
106             let report_extra_lifetimes = trait_ref_of_method(cx, item.owner_id.def_id).is_none();
107             check_fn_inner(
108                 cx,
109                 sig.decl,
110                 Some(id),
111                 None,
112                 item.generics,
113                 item.span,
114                 report_extra_lifetimes,
115             );
116         }
117     }
118
119     fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx TraitItem<'_>) {
120         if let TraitItemKind::Fn(ref sig, ref body) = item.kind {
121             let (body, trait_sig) = match *body {
122                 TraitFn::Required(sig) => (None, Some(sig)),
123                 TraitFn::Provided(id) => (Some(id), None),
124             };
125             check_fn_inner(cx, sig.decl, body, trait_sig, item.generics, item.span, true);
126         }
127     }
128 }
129
130 /// The lifetime of a &-reference.
131 #[derive(PartialEq, Eq, Hash, Debug, Clone)]
132 enum RefLt {
133     Unnamed,
134     Static,
135     Named(LocalDefId),
136 }
137
138 fn check_fn_inner<'tcx>(
139     cx: &LateContext<'tcx>,
140     decl: &'tcx FnDecl<'_>,
141     body: Option<BodyId>,
142     trait_sig: Option<&[Ident]>,
143     generics: &'tcx Generics<'_>,
144     span: Span,
145     report_extra_lifetimes: bool,
146 ) {
147     if span.from_expansion() || has_where_lifetimes(cx, generics) {
148         return;
149     }
150
151     let types = generics
152         .params
153         .iter()
154         .filter(|param| matches!(param.kind, GenericParamKind::Type { .. }));
155
156     for typ in types {
157         for pred in generics.bounds_for_param(cx.tcx.hir().local_def_id(typ.hir_id)) {
158             if pred.origin == PredicateOrigin::WhereClause {
159                 // has_where_lifetimes checked that this predicate contains no lifetime.
160                 continue;
161             }
162
163             for bound in pred.bounds {
164                 let mut visitor = RefVisitor::new(cx);
165                 walk_param_bound(&mut visitor, bound);
166                 if visitor.lts.iter().any(|lt| matches!(lt, RefLt::Named(_))) {
167                     return;
168                 }
169                 if let GenericBound::Trait(ref trait_ref, _) = *bound {
170                     let params = &trait_ref
171                         .trait_ref
172                         .path
173                         .segments
174                         .last()
175                         .expect("a path must have at least one segment")
176                         .args;
177                     if let Some(params) = *params {
178                         let lifetimes = params.args.iter().filter_map(|arg| match arg {
179                             GenericArg::Lifetime(lt) => Some(lt),
180                             _ => None,
181                         });
182                         for bound in lifetimes {
183                             if !bound.is_static() && !bound.is_elided() {
184                                 return;
185                             }
186                         }
187                     }
188                 }
189             }
190         }
191     }
192
193     if let Some(elidable_lts) = could_use_elision(cx, decl, body, trait_sig, generics.params) {
194         let lts = elidable_lts
195             .iter()
196             // In principle, the result of the call to `Node::ident` could be `unwrap`ped, as `DefId` should refer to a
197             // `Node::GenericParam`.
198             .filter_map(|&(def_id, _)| cx.tcx.hir().get_by_def_id(def_id).ident())
199             .map(|ident| ident.to_string())
200             .collect::<Vec<_>>()
201             .join(", ");
202
203         span_lint_and_then(
204             cx,
205             NEEDLESS_LIFETIMES,
206             span.with_hi(decl.output.span().hi()),
207             &format!("the following explicit lifetimes could be elided: {lts}"),
208             |diag| {
209                 if let Some(span) = elidable_lts.iter().find_map(|&(_, span)| span) {
210                     diag.span_help(span, "replace with `'_` in generic arguments such as here");
211                 }
212             },
213         );
214     }
215
216     if report_extra_lifetimes {
217         self::report_extra_lifetimes(cx, decl, generics);
218     }
219 }
220
221 // elision doesn't work for explicit self types, see rust-lang/rust#69064
222 fn explicit_self_type<'tcx>(cx: &LateContext<'tcx>, func: &FnDecl<'tcx>, ident: Option<Ident>) -> bool {
223     if_chain! {
224         if let Some(ident) = ident;
225         if ident.name == kw::SelfLower;
226         if !func.implicit_self.has_implicit_self();
227
228         if let Some(self_ty) = func.inputs.first();
229         then {
230             let mut visitor = RefVisitor::new(cx);
231             visitor.visit_ty(self_ty);
232
233             !visitor.all_lts().is_empty()
234         } else {
235             false
236         }
237     }
238 }
239
240 fn could_use_elision<'tcx>(
241     cx: &LateContext<'tcx>,
242     func: &'tcx FnDecl<'_>,
243     body: Option<BodyId>,
244     trait_sig: Option<&[Ident]>,
245     named_generics: &'tcx [GenericParam<'_>],
246 ) -> Option<Vec<(LocalDefId, Option<Span>)>> {
247     // There are two scenarios where elision works:
248     // * no output references, all input references have different LT
249     // * output references, exactly one input reference with same LT
250     // All lifetimes must be unnamed, 'static or defined without bounds on the
251     // level of the current item.
252
253     // check named LTs
254     let allowed_lts = allowed_lts_from(cx.tcx, named_generics);
255
256     // these will collect all the lifetimes for references in arg/return types
257     let mut input_visitor = RefVisitor::new(cx);
258     let mut output_visitor = RefVisitor::new(cx);
259
260     // extract lifetimes in input argument types
261     for arg in func.inputs {
262         input_visitor.visit_ty(arg);
263     }
264     // extract lifetimes in output type
265     if let Return(ty) = func.output {
266         output_visitor.visit_ty(ty);
267     }
268     for lt in named_generics {
269         input_visitor.visit_generic_param(lt);
270     }
271
272     if input_visitor.abort() || output_visitor.abort() {
273         return None;
274     }
275
276     let input_lts = input_visitor.lts;
277     let output_lts = output_visitor.lts;
278
279     if let Some(trait_sig) = trait_sig {
280         if explicit_self_type(cx, func, trait_sig.first().copied()) {
281             return None;
282         }
283     }
284
285     if let Some(body_id) = body {
286         let body = cx.tcx.hir().body(body_id);
287
288         let first_ident = body.params.first().and_then(|param| param.pat.simple_ident());
289         if explicit_self_type(cx, func, first_ident) {
290             return None;
291         }
292
293         let mut checker = BodyLifetimeChecker {
294             lifetimes_used_in_body: false,
295         };
296         checker.visit_expr(body.value);
297         if checker.lifetimes_used_in_body {
298             return None;
299         }
300     }
301
302     // check for lifetimes from higher scopes
303     for lt in input_lts.iter().chain(output_lts.iter()) {
304         if !allowed_lts.contains(lt) {
305             return None;
306         }
307     }
308
309     // check for higher-ranked trait bounds
310     if !input_visitor.nested_elision_site_lts.is_empty() || !output_visitor.nested_elision_site_lts.is_empty() {
311         let allowed_lts: FxHashSet<_> = allowed_lts
312             .iter()
313             .filter_map(|lt| match lt {
314                 RefLt::Named(def_id) => Some(cx.tcx.item_name(def_id.to_def_id())),
315                 _ => None,
316             })
317             .collect();
318         for lt in input_visitor.nested_elision_site_lts {
319             if let RefLt::Named(def_id) = lt {
320                 if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) {
321                     return None;
322                 }
323             }
324         }
325         for lt in output_visitor.nested_elision_site_lts {
326             if let RefLt::Named(def_id) = lt {
327                 if allowed_lts.contains(&cx.tcx.item_name(def_id.to_def_id())) {
328                     return None;
329                 }
330             }
331         }
332     }
333
334     // A lifetime can be newly elided if:
335     // - It occurs only once among the inputs.
336     // - If there are multiple input lifetimes, then the newly elided lifetime does not occur among the
337     //   outputs (because eliding such an lifetime would create an ambiguity).
338     let elidable_lts = named_lifetime_occurrences(&input_lts)
339         .into_iter()
340         .filter_map(|(def_id, occurrences)| {
341             if occurrences == 1 && (input_lts.len() == 1 || !output_lts.contains(&RefLt::Named(def_id))) {
342                 Some((
343                     def_id,
344                     input_visitor
345                         .lifetime_generic_arg_spans
346                         .get(&def_id)
347                         .or_else(|| output_visitor.lifetime_generic_arg_spans.get(&def_id))
348                         .copied(),
349                 ))
350             } else {
351                 None
352             }
353         })
354         .collect::<Vec<_>>();
355
356     if elidable_lts.is_empty() {
357         None
358     } else {
359         Some(elidable_lts)
360     }
361 }
362
363 fn allowed_lts_from(tcx: TyCtxt<'_>, named_generics: &[GenericParam<'_>]) -> FxHashSet<RefLt> {
364     let mut allowed_lts = FxHashSet::default();
365     for par in named_generics.iter() {
366         if let GenericParamKind::Lifetime { .. } = par.kind {
367             allowed_lts.insert(RefLt::Named(tcx.hir().local_def_id(par.hir_id)));
368         }
369     }
370     allowed_lts.insert(RefLt::Unnamed);
371     allowed_lts.insert(RefLt::Static);
372     allowed_lts
373 }
374
375 /// Number of times each named lifetime occurs in the given slice. Returns a vector to preserve
376 /// relative order.
377 #[must_use]
378 fn named_lifetime_occurrences(lts: &[RefLt]) -> Vec<(LocalDefId, usize)> {
379     let mut occurrences = Vec::new();
380     for lt in lts {
381         if let &RefLt::Named(curr_def_id) = lt {
382             if let Some(pair) = occurrences
383                 .iter_mut()
384                 .find(|(prev_def_id, _)| *prev_def_id == curr_def_id)
385             {
386                 pair.1 += 1;
387             } else {
388                 occurrences.push((curr_def_id, 1));
389             }
390         }
391     }
392     occurrences
393 }
394
395 /// A visitor usable for `rustc_front::visit::walk_ty()`.
396 struct RefVisitor<'a, 'tcx> {
397     cx: &'a LateContext<'tcx>,
398     lts: Vec<RefLt>,
399     lifetime_generic_arg_spans: FxHashMap<LocalDefId, Span>,
400     nested_elision_site_lts: Vec<RefLt>,
401     unelided_trait_object_lifetime: bool,
402 }
403
404 impl<'a, 'tcx> RefVisitor<'a, 'tcx> {
405     fn new(cx: &'a LateContext<'tcx>) -> Self {
406         Self {
407             cx,
408             lts: Vec::new(),
409             lifetime_generic_arg_spans: FxHashMap::default(),
410             nested_elision_site_lts: Vec::new(),
411             unelided_trait_object_lifetime: false,
412         }
413     }
414
415     fn record(&mut self, lifetime: &Option<Lifetime>) {
416         if let Some(ref lt) = *lifetime {
417             if lt.is_static() {
418                 self.lts.push(RefLt::Static);
419             } else if lt.is_anonymous() {
420                 // Fresh lifetimes generated should be ignored.
421                 self.lts.push(RefLt::Unnamed);
422             } else if let LifetimeName::Param(def_id) = lt.res {
423                 self.lts.push(RefLt::Named(def_id));
424             }
425         } else {
426             self.lts.push(RefLt::Unnamed);
427         }
428     }
429
430     fn all_lts(&self) -> Vec<RefLt> {
431         self.lts
432             .iter()
433             .chain(self.nested_elision_site_lts.iter())
434             .cloned()
435             .collect::<Vec<_>>()
436     }
437
438     fn abort(&self) -> bool {
439         self.unelided_trait_object_lifetime
440     }
441 }
442
443 impl<'a, 'tcx> Visitor<'tcx> for RefVisitor<'a, 'tcx> {
444     // for lifetimes as parameters of generics
445     fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) {
446         self.record(&Some(*lifetime));
447     }
448
449     fn visit_poly_trait_ref(&mut self, poly_tref: &'tcx PolyTraitRef<'tcx>) {
450         let trait_ref = &poly_tref.trait_ref;
451         if let Some(id) = trait_ref.trait_def_id() && lang_items::FN_TRAITS.iter().any(|&item| {
452             self.cx.tcx.lang_items().get(item) == Some(id)
453         }) {
454             let mut sub_visitor = RefVisitor::new(self.cx);
455             sub_visitor.visit_trait_ref(trait_ref);
456             self.nested_elision_site_lts.append(&mut sub_visitor.all_lts());
457         } else {
458             walk_poly_trait_ref(self, poly_tref);
459         }
460     }
461
462     fn visit_ty(&mut self, ty: &'tcx Ty<'_>) {
463         match ty.kind {
464             TyKind::OpaqueDef(item, bounds, _) => {
465                 let map = self.cx.tcx.hir();
466                 let item = map.item(item);
467                 let len = self.lts.len();
468                 walk_item(self, item);
469                 self.lts.truncate(len);
470                 self.lts.extend(bounds.iter().filter_map(|bound| match bound {
471                     GenericArg::Lifetime(l) => Some(if let LifetimeName::Param(def_id) = l.res {
472                         RefLt::Named(def_id)
473                     } else {
474                         RefLt::Unnamed
475                     }),
476                     _ => None,
477                 }));
478             },
479             TyKind::BareFn(&BareFnTy { decl, .. }) => {
480                 let mut sub_visitor = RefVisitor::new(self.cx);
481                 sub_visitor.visit_fn_decl(decl);
482                 self.nested_elision_site_lts.append(&mut sub_visitor.all_lts());
483             },
484             TyKind::TraitObject(bounds, lt, _) => {
485                 if !lt.is_elided() {
486                     self.unelided_trait_object_lifetime = true;
487                 }
488                 for bound in bounds {
489                     self.visit_poly_trait_ref(bound);
490                 }
491             },
492             _ => walk_ty(self, ty),
493         }
494     }
495
496     fn visit_generic_arg(&mut self, generic_arg: &'tcx GenericArg<'tcx>) {
497         if let GenericArg::Lifetime(l) = generic_arg && let LifetimeName::Param(def_id) = l.res {
498             self.lifetime_generic_arg_spans.entry(def_id).or_insert(l.ident.span);
499         }
500         walk_generic_arg(self, generic_arg);
501     }
502 }
503
504 /// Are any lifetimes mentioned in the `where` clause? If so, we don't try to
505 /// reason about elision.
506 fn has_where_lifetimes<'tcx>(cx: &LateContext<'tcx>, generics: &'tcx Generics<'_>) -> bool {
507     for predicate in generics.predicates {
508         match *predicate {
509             WherePredicate::RegionPredicate(..) => return true,
510             WherePredicate::BoundPredicate(ref pred) => {
511                 // a predicate like F: Trait or F: for<'a> Trait<'a>
512                 let mut visitor = RefVisitor::new(cx);
513                 // walk the type F, it may not contain LT refs
514                 walk_ty(&mut visitor, pred.bounded_ty);
515                 if !visitor.all_lts().is_empty() {
516                     return true;
517                 }
518                 // if the bounds define new lifetimes, they are fine to occur
519                 let allowed_lts = allowed_lts_from(cx.tcx, pred.bound_generic_params);
520                 // now walk the bounds
521                 for bound in pred.bounds.iter() {
522                     walk_param_bound(&mut visitor, bound);
523                 }
524                 // and check that all lifetimes are allowed
525                 if visitor.all_lts().iter().any(|it| !allowed_lts.contains(it)) {
526                     return true;
527                 }
528             },
529             WherePredicate::EqPredicate(ref pred) => {
530                 let mut visitor = RefVisitor::new(cx);
531                 walk_ty(&mut visitor, pred.lhs_ty);
532                 walk_ty(&mut visitor, pred.rhs_ty);
533                 if !visitor.lts.is_empty() {
534                     return true;
535                 }
536             },
537         }
538     }
539     false
540 }
541
542 struct LifetimeChecker<'cx, 'tcx, F> {
543     cx: &'cx LateContext<'tcx>,
544     map: FxHashMap<Symbol, Span>,
545     phantom: std::marker::PhantomData<F>,
546 }
547
548 impl<'cx, 'tcx, F> LifetimeChecker<'cx, 'tcx, F> {
549     fn new(cx: &'cx LateContext<'tcx>, map: FxHashMap<Symbol, Span>) -> LifetimeChecker<'cx, 'tcx, F> {
550         Self {
551             cx,
552             map,
553             phantom: std::marker::PhantomData,
554         }
555     }
556 }
557
558 impl<'cx, 'tcx, F> Visitor<'tcx> for LifetimeChecker<'cx, 'tcx, F>
559 where
560     F: NestedFilter<'tcx>,
561 {
562     type Map = rustc_middle::hir::map::Map<'tcx>;
563     type NestedFilter = F;
564
565     // for lifetimes as parameters of generics
566     fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) {
567         self.map.remove(&lifetime.ident.name);
568     }
569
570     fn visit_generic_param(&mut self, param: &'tcx GenericParam<'_>) {
571         // don't actually visit `<'a>` or `<'a: 'b>`
572         // we've already visited the `'a` declarations and
573         // don't want to spuriously remove them
574         // `'b` in `'a: 'b` is useless unless used elsewhere in
575         // a non-lifetime bound
576         if let GenericParamKind::Type { .. } = param.kind {
577             walk_generic_param(self, param);
578         }
579     }
580
581     fn nested_visit_map(&mut self) -> Self::Map {
582         self.cx.tcx.hir()
583     }
584 }
585
586 fn report_extra_lifetimes<'tcx>(cx: &LateContext<'tcx>, func: &'tcx FnDecl<'_>, generics: &'tcx Generics<'_>) {
587     let hs = generics
588         .params
589         .iter()
590         .filter_map(|par| match par.kind {
591             GenericParamKind::Lifetime {
592                 kind: LifetimeParamKind::Explicit,
593             } => Some((par.name.ident().name, par.span)),
594             _ => None,
595         })
596         .collect();
597     let mut checker = LifetimeChecker::<hir_nested_filter::None>::new(cx, hs);
598
599     walk_generics(&mut checker, generics);
600     walk_fn_decl(&mut checker, func);
601
602     for &v in checker.map.values() {
603         span_lint(
604             cx,
605             EXTRA_UNUSED_LIFETIMES,
606             v,
607             "this lifetime isn't used in the function definition",
608         );
609     }
610 }
611
612 fn report_extra_impl_lifetimes<'tcx>(cx: &LateContext<'tcx>, impl_: &'tcx Impl<'_>) {
613     let hs = impl_
614         .generics
615         .params
616         .iter()
617         .filter_map(|par| match par.kind {
618             GenericParamKind::Lifetime {
619                 kind: LifetimeParamKind::Explicit,
620             } => Some((par.name.ident().name, par.span)),
621             _ => None,
622         })
623         .collect();
624     let mut checker = LifetimeChecker::<middle_nested_filter::All>::new(cx, hs);
625
626     walk_generics(&mut checker, impl_.generics);
627     if let Some(ref trait_ref) = impl_.of_trait {
628         walk_trait_ref(&mut checker, trait_ref);
629     }
630     walk_ty(&mut checker, impl_.self_ty);
631     for item in impl_.items {
632         walk_impl_item_ref(&mut checker, item);
633     }
634
635     for &v in checker.map.values() {
636         span_lint(cx, EXTRA_UNUSED_LIFETIMES, v, "this lifetime isn't used in the impl");
637     }
638 }
639
640 struct BodyLifetimeChecker {
641     lifetimes_used_in_body: bool,
642 }
643
644 impl<'tcx> Visitor<'tcx> for BodyLifetimeChecker {
645     // for lifetimes as parameters of generics
646     fn visit_lifetime(&mut self, lifetime: &'tcx Lifetime) {
647         if !lifetime.is_anonymous() && lifetime.ident.name != kw::StaticLifetime {
648             self.lifetimes_used_in_body = true;
649         }
650     }
651 }