diff options
| author | Jake Goulding <jake.goulding@gmail.com> | 2025-03-28 16:12:07 -0400 |
|---|---|---|
| committer | Jake Goulding <jake.goulding@gmail.com> | 2025-06-04 10:40:04 -0400 |
| commit | 9a50cb4a0caa07b7b751afb70464066e4d6985d3 (patch) | |
| tree | 01a480f7f1053d317f9b207b6ba2516117083949 /compiler/rustc_lint | |
| parent | d2bf16ad6d80827800798a00ba584f95e9689573 (diff) | |
| download | rust-9a50cb4a0caa07b7b751afb70464066e4d6985d3.tar.gz rust-9a50cb4a0caa07b7b751afb70464066e4d6985d3.zip | |
Introduce the `mismatched_lifetime_syntaxes` lint
Diffstat (limited to 'compiler/rustc_lint')
| -rw-r--r-- | compiler/rustc_lint/messages.ftl | 22 | ||||
| -rw-r--r-- | compiler/rustc_lint/src/lib.rs | 3 | ||||
| -rw-r--r-- | compiler/rustc_lint/src/lifetime_syntax.rs | 503 | ||||
| -rw-r--r-- | compiler/rustc_lint/src/lints.rs | 125 |
4 files changed, 653 insertions, 0 deletions
diff --git a/compiler/rustc_lint/messages.ftl b/compiler/rustc_lint/messages.ftl index 17485a838f3..ac9c772c427 100644 --- a/compiler/rustc_lint/messages.ftl +++ b/compiler/rustc_lint/messages.ftl @@ -518,6 +518,28 @@ lint_metavariable_still_repeating = variable `{$name}` is still repeating at thi lint_metavariable_wrong_operator = meta-variable repeats with different Kleene operator +lint_mismatched_lifetime_syntaxes = + lifetime flowing from input to output with different syntax can be confusing + .label_mismatched_lifetime_syntaxes_inputs = + {$n_inputs -> + [one] this lifetime flows + *[other] these lifetimes flow + } to the output + .label_mismatched_lifetime_syntaxes_outputs = + the {$n_outputs -> + [one] lifetime gets + *[other] lifetimes get + } resolved as `{$lifetime_name}` + +lint_mismatched_lifetime_syntaxes_suggestion_explicit = + one option is to consistently use `{$lifetime_name}` + +lint_mismatched_lifetime_syntaxes_suggestion_implicit = + one option is to consistently remove the lifetime + +lint_mismatched_lifetime_syntaxes_suggestion_mixed = + one option is to remove the lifetime for references and use the anonymous lifetime for paths + lint_missing_fragment_specifier = missing fragment specifier lint_missing_unsafe_on_extern = extern blocks should be unsafe diff --git a/compiler/rustc_lint/src/lib.rs b/compiler/rustc_lint/src/lib.rs index 0a52e42e442..0439befc6ba 100644 --- a/compiler/rustc_lint/src/lib.rs +++ b/compiler/rustc_lint/src/lib.rs @@ -55,6 +55,7 @@ mod invalid_from_utf8; mod late; mod let_underscore; mod levels; +mod lifetime_syntax; mod lints; mod macro_expr_fragment_specifier_2024_migration; mod map_unit_fn; @@ -96,6 +97,7 @@ use impl_trait_overcaptures::ImplTraitOvercaptures; use internal::*; use invalid_from_utf8::*; use let_underscore::*; +use lifetime_syntax::*; use macro_expr_fragment_specifier_2024_migration::*; use map_unit_fn::*; use multiple_supertrait_upcastable::*; @@ -246,6 +248,7 @@ late_lint_methods!( StaticMutRefs: StaticMutRefs, UnqualifiedLocalImports: UnqualifiedLocalImports, CheckTransmutes: CheckTransmutes, + LifetimeSyntax: LifetimeSyntax, ] ] ); diff --git a/compiler/rustc_lint/src/lifetime_syntax.rs b/compiler/rustc_lint/src/lifetime_syntax.rs new file mode 100644 index 00000000000..f895cdb2a54 --- /dev/null +++ b/compiler/rustc_lint/src/lifetime_syntax.rs @@ -0,0 +1,503 @@ +use rustc_data_structures::fx::FxIndexMap; +use rustc_hir::intravisit::{self, Visitor}; +use rustc_hir::{self as hir, LifetimeSource}; +use rustc_session::{declare_lint, declare_lint_pass}; +use rustc_span::Span; +use tracing::instrument; + +use crate::{LateContext, LateLintPass, LintContext, lints}; + +declare_lint! { + /// The `mismatched_lifetime_syntaxes` lint detects when the same + /// lifetime is referred to by different syntaxes between function + /// arguments and return values. + /// + /// The three kinds of syntaxes are: + /// + /// 1. Named lifetimes. These are references (`&'a str`) or paths + /// (`Person<'a>`) that use a lifetime with a name, such as + /// `'static` or `'a`. + /// + /// 2. Elided lifetimes. These are references with no explicit + /// lifetime (`&str`), references using the anonymous lifetime + /// (`&'_ str`), and paths using the anonymous lifetime + /// (`Person<'_>`). + /// + /// 3. Hidden lifetimes. These are paths that do not contain any + /// visual indication that it contains a lifetime (`Person`). + /// + /// ### Example + /// + /// ```rust,compile_fail + /// #![deny(mismatched_lifetime_syntaxes)] + /// + /// pub fn mixing_named_with_elided(v: &'static u8) -> &u8 { + /// v + /// } + /// + /// struct Person<'a> { + /// name: &'a str, + /// } + /// + /// pub fn mixing_hidden_with_elided(v: Person) -> Person<'_> { + /// v + /// } + /// + /// struct Foo; + /// + /// impl Foo { + /// // Lifetime elision results in the output lifetime becoming + /// // `'static`, which is not what was intended. + /// pub fn get_mut(&'static self, x: &mut u8) -> &mut u8 { + /// unsafe { &mut *(x as *mut _) } + /// } + /// } + /// ``` + /// + /// {{produces}} + /// + /// ### Explanation + /// + /// Lifetime elision is useful because it frees you from having to + /// give each lifetime its own name and show the relation of input + /// and output lifetimes for common cases. However, a lifetime + /// that uses inconsistent syntax between related arguments and + /// return values is more confusing. + /// + /// In certain `unsafe` code, lifetime elision combined with + /// inconsistent lifetime syntax may result in unsound code. + pub MISMATCHED_LIFETIME_SYNTAXES, + Allow, + "detects when a lifetime uses different syntax between arguments and return values" +} + +declare_lint_pass!(LifetimeSyntax => [MISMATCHED_LIFETIME_SYNTAXES]); + +impl<'tcx> LateLintPass<'tcx> for LifetimeSyntax { + #[instrument(skip_all)] + fn check_fn( + &mut self, + cx: &LateContext<'tcx>, + _: hir::intravisit::FnKind<'tcx>, + fd: &'tcx hir::FnDecl<'tcx>, + _: &'tcx hir::Body<'tcx>, + _: rustc_span::Span, + _: rustc_span::def_id::LocalDefId, + ) { + let mut input_map = Default::default(); + let mut output_map = Default::default(); + + for input in fd.inputs { + LifetimeInfoCollector::collect(input, &mut input_map); + } + + if let hir::FnRetTy::Return(output) = fd.output { + LifetimeInfoCollector::collect(output, &mut output_map); + } + + report_mismatches(cx, &input_map, &output_map); + } +} + +#[instrument(skip_all)] +fn report_mismatches<'tcx>( + cx: &LateContext<'tcx>, + inputs: &LifetimeInfoMap<'tcx>, + outputs: &LifetimeInfoMap<'tcx>, +) { + for (resolved_lifetime, output_info) in outputs { + if let Some(input_info) = inputs.get(resolved_lifetime) { + if !lifetimes_use_matched_syntax(input_info, output_info) { + emit_mismatch_diagnostic(cx, input_info, output_info); + } + } + } +} + +fn lifetimes_use_matched_syntax(input_info: &[Info<'_>], output_info: &[Info<'_>]) -> bool { + // Categorize lifetimes into source/syntax buckets. + let mut n_hidden = 0; + let mut n_elided = 0; + let mut n_named = 0; + + for info in input_info.iter().chain(output_info) { + use LifetimeSource::*; + use hir::LifetimeSyntax::*; + + let syntax_source = (info.lifetime.syntax, info.lifetime.source); + + match syntax_source { + // Ignore any other kind of lifetime. + (_, Other) => continue, + + // E.g. `&T`. + (Implicit, Reference | OutlivesBound | PreciseCapturing) | + // E.g. `&'_ T`. + (ExplicitAnonymous, Reference | OutlivesBound | PreciseCapturing) | + // E.g. `ContainsLifetime<'_>`. + (ExplicitAnonymous, Path { .. }) => n_elided += 1, + + // E.g. `ContainsLifetime`. + (Implicit, Path { .. }) => n_hidden += 1, + + // E.g. `&'a T`. + (ExplicitBound, Reference | OutlivesBound | PreciseCapturing) | + // E.g. `ContainsLifetime<'a>`. + (ExplicitBound, Path { .. }) => n_named += 1, + }; + } + + let syntax_counts = (n_hidden, n_elided, n_named); + tracing::debug!(?syntax_counts); + + matches!(syntax_counts, (_, 0, 0) | (0, _, 0) | (0, 0, _)) +} + +fn emit_mismatch_diagnostic<'tcx>( + cx: &LateContext<'tcx>, + input_info: &[Info<'_>], + output_info: &[Info<'_>], +) { + // There can only ever be zero or one bound lifetime + // for a given lifetime resolution. + let mut bound_lifetime = None; + + // We offer the following kinds of suggestions (when appropriate + // such that the suggestion wouldn't violate the lint): + // + // 1. Every lifetime becomes named, when there is already a + // user-provided name. + // + // 2. A "mixed" signature, where references become implicit + // and paths become explicitly anonymous. + // + // 3. Every lifetime becomes implicit. + // + // 4. Every lifetime becomes explicitly anonymous. + // + // Number 2 is arguably the most common pattern and the one we + // should push strongest. Number 3 is likely the next most common, + // followed by number 1. Coming in at a distant last would be + // number 4. + // + // Beyond these, there are variants of acceptable signatures that + // we won't suggest because they are very low-value. For example, + // we will never suggest `fn(&T1, &'_ T2) -> &T3` even though that + // would pass the lint. + // + // The following collections are the lifetime instances that we + // suggest changing to a given alternate style. + + // 1. Convert all to named. + let mut suggest_change_to_explicit_bound = Vec::new(); + + // 2. Convert to mixed. We track each kind of change separately. + let mut suggest_change_to_mixed_implicit = Vec::new(); + let mut suggest_change_to_mixed_explicit_anonymous = Vec::new(); + + // 3. Convert all to implicit. + let mut suggest_change_to_implicit = Vec::new(); + + // 4. Convert all to explicit anonymous. + let mut suggest_change_to_explicit_anonymous = Vec::new(); + + // Some styles prevent using implicit syntax at all. + let mut allow_suggesting_implicit = true; + + // It only makes sense to suggest mixed if we have both sources. + let mut saw_a_reference = false; + let mut saw_a_path = false; + + for info in input_info.iter().chain(output_info) { + use LifetimeSource::*; + use hir::LifetimeSyntax::*; + + let syntax_source = (info.lifetime.syntax, info.lifetime.source); + + if let (_, Other) = syntax_source { + // Ignore any other kind of lifetime. + continue; + } + + if let (ExplicitBound, _) = syntax_source { + bound_lifetime = Some(info); + } + + match syntax_source { + // E.g. `&T`. + (Implicit, Reference) => { + suggest_change_to_explicit_anonymous.push(info); + suggest_change_to_explicit_bound.push(info); + } + + // E.g. `&'_ T`. + (ExplicitAnonymous, Reference) => { + suggest_change_to_implicit.push(info); + suggest_change_to_mixed_implicit.push(info); + suggest_change_to_explicit_bound.push(info); + } + + // E.g. `ContainsLifetime`. + (Implicit, Path { .. }) => { + suggest_change_to_mixed_explicit_anonymous.push(info); + suggest_change_to_explicit_anonymous.push(info); + suggest_change_to_explicit_bound.push(info); + } + + // E.g. `ContainsLifetime<'_>`. + (ExplicitAnonymous, Path { .. }) => { + suggest_change_to_explicit_bound.push(info); + } + + // E.g. `&'a T`. + (ExplicitBound, Reference) => { + suggest_change_to_implicit.push(info); + suggest_change_to_mixed_implicit.push(info); + suggest_change_to_explicit_anonymous.push(info); + } + + // E.g. `ContainsLifetime<'a>`. + (ExplicitBound, Path { .. }) => { + suggest_change_to_mixed_explicit_anonymous.push(info); + suggest_change_to_explicit_anonymous.push(info); + } + + (Implicit, OutlivesBound | PreciseCapturing) => { + panic!("This syntax / source combination is not possible"); + } + + // E.g. `+ '_`, `+ use<'_>`. + (ExplicitAnonymous, OutlivesBound | PreciseCapturing) => { + suggest_change_to_explicit_bound.push(info); + } + + // E.g. `+ 'a`, `+ use<'a>`. + (ExplicitBound, OutlivesBound | PreciseCapturing) => { + suggest_change_to_mixed_explicit_anonymous.push(info); + suggest_change_to_explicit_anonymous.push(info); + } + + (_, Other) => { + panic!("This syntax / source combination has already been skipped"); + } + } + + if matches!(syntax_source, (_, Path { .. } | OutlivesBound | PreciseCapturing)) { + allow_suggesting_implicit = false; + } + + match syntax_source { + (_, Reference) => saw_a_reference = true, + (_, Path { .. }) => saw_a_path = true, + _ => {} + } + } + + let make_implicit_suggestions = + |infos: &[&Info<'_>]| infos.iter().map(|i| i.removing_span()).collect::<Vec<_>>(); + + let inputs = input_info.iter().map(|info| info.reporting_span()).collect(); + let outputs = output_info.iter().map(|info| info.reporting_span()).collect(); + + let explicit_bound_suggestion = bound_lifetime.map(|info| { + build_mismatch_suggestion(info.lifetime_name(), &suggest_change_to_explicit_bound) + }); + + let is_bound_static = bound_lifetime.is_some_and(|info| info.is_static()); + + tracing::debug!(?bound_lifetime, ?explicit_bound_suggestion, ?is_bound_static); + + let should_suggest_mixed = + // Do we have a mixed case? + (saw_a_reference && saw_a_path) && + // Is there anything to change? + (!suggest_change_to_mixed_implicit.is_empty() || + !suggest_change_to_mixed_explicit_anonymous.is_empty()) && + // If we have `'static`, we don't want to remove it. + !is_bound_static; + + let mixed_suggestion = should_suggest_mixed.then(|| { + let implicit_suggestions = make_implicit_suggestions(&suggest_change_to_mixed_implicit); + + let explicit_anonymous_suggestions = suggest_change_to_mixed_explicit_anonymous + .iter() + .map(|info| info.suggestion("'_")) + .collect(); + + lints::MismatchedLifetimeSyntaxesSuggestion::Mixed { + implicit_suggestions, + explicit_anonymous_suggestions, + tool_only: false, + } + }); + + tracing::debug!( + ?suggest_change_to_mixed_implicit, + ?suggest_change_to_mixed_explicit_anonymous, + ?mixed_suggestion, + ); + + let should_suggest_implicit = + // Is there anything to change? + !suggest_change_to_implicit.is_empty() && + // We never want to hide the lifetime in a path (or similar). + allow_suggesting_implicit && + // If we have `'static`, we don't want to remove it. + !is_bound_static; + + let implicit_suggestion = should_suggest_implicit.then(|| { + let suggestions = make_implicit_suggestions(&suggest_change_to_implicit); + + lints::MismatchedLifetimeSyntaxesSuggestion::Implicit { suggestions, tool_only: false } + }); + + tracing::debug!( + ?should_suggest_implicit, + ?suggest_change_to_implicit, + allow_suggesting_implicit, + ?implicit_suggestion, + ); + + let should_suggest_explicit_anonymous = + // Is there anything to change? + !suggest_change_to_explicit_anonymous.is_empty() && + // If we have `'static`, we don't want to remove it. + !is_bound_static; + + let explicit_anonymous_suggestion = should_suggest_explicit_anonymous + .then(|| build_mismatch_suggestion("'_", &suggest_change_to_explicit_anonymous)); + + tracing::debug!( + ?should_suggest_explicit_anonymous, + ?suggest_change_to_explicit_anonymous, + ?explicit_anonymous_suggestion, + ); + + let lifetime_name = bound_lifetime.map(|info| info.lifetime_name()).unwrap_or("'_").to_owned(); + + // We can produce a number of suggestions which may overwhelm + // the user. Instead, we order the suggestions based on Rust + // idioms. The "best" choice is shown to the user and the + // remaining choices are shown to tools only. + let mut suggestions = Vec::new(); + suggestions.extend(explicit_bound_suggestion); + suggestions.extend(mixed_suggestion); + suggestions.extend(implicit_suggestion); + suggestions.extend(explicit_anonymous_suggestion); + + cx.emit_span_lint( + MISMATCHED_LIFETIME_SYNTAXES, + Vec::clone(&inputs), + lints::MismatchedLifetimeSyntaxes { lifetime_name, inputs, outputs, suggestions }, + ); +} + +fn build_mismatch_suggestion( + lifetime_name: &str, + infos: &[&Info<'_>], +) -> lints::MismatchedLifetimeSyntaxesSuggestion { + let lifetime_name = lifetime_name.to_owned(); + + let suggestions = infos.iter().map(|info| info.suggestion(&lifetime_name)).collect(); + + lints::MismatchedLifetimeSyntaxesSuggestion::Explicit { + lifetime_name, + suggestions, + tool_only: false, + } +} + +#[derive(Debug)] +struct Info<'tcx> { + type_span: Span, + referenced_type_span: Option<Span>, + lifetime: &'tcx hir::Lifetime, +} + +impl<'tcx> Info<'tcx> { + fn lifetime_name(&self) -> &str { + self.lifetime.ident.as_str() + } + + fn is_static(&self) -> bool { + self.lifetime.is_static() + } + + /// When reporting a lifetime that is implicit, we expand the span + /// to include the type. Otherwise we end up pointing at nothing, + /// which is a bit confusing. + fn reporting_span(&self) -> Span { + if self.lifetime.is_implicit() { self.type_span } else { self.lifetime.ident.span } + } + + /// When removing an explicit lifetime from a reference, + /// we want to remove the whitespace after the lifetime. + /// + /// ```rust + /// fn x(a: &'_ u8) {} + /// ``` + /// + /// Should become: + /// + /// ```rust + /// fn x(a: &u8) {} + /// ``` + // FIXME: Ideally, we'd also remove the lifetime declaration. + fn removing_span(&self) -> Span { + let mut span = self.suggestion("'dummy").0; + + if let Some(referenced_type_span) = self.referenced_type_span { + span = span.until(referenced_type_span); + } + + span + } + + fn suggestion(&self, lifetime_name: &str) -> (Span, String) { + self.lifetime.suggestion(lifetime_name) + } +} + +type LifetimeInfoMap<'tcx> = FxIndexMap<&'tcx hir::LifetimeKind, Vec<Info<'tcx>>>; + +struct LifetimeInfoCollector<'a, 'tcx> { + type_span: Span, + referenced_type_span: Option<Span>, + map: &'a mut LifetimeInfoMap<'tcx>, +} + +impl<'a, 'tcx> LifetimeInfoCollector<'a, 'tcx> { + fn collect(ty: &'tcx hir::Ty<'tcx>, map: &'a mut LifetimeInfoMap<'tcx>) { + let mut this = Self { type_span: ty.span, referenced_type_span: None, map }; + + intravisit::walk_unambig_ty(&mut this, ty); + } +} + +impl<'a, 'tcx> Visitor<'tcx> for LifetimeInfoCollector<'a, 'tcx> { + #[instrument(skip(self))] + fn visit_lifetime(&mut self, lifetime: &'tcx hir::Lifetime) { + let type_span = self.type_span; + let referenced_type_span = self.referenced_type_span; + + let info = Info { type_span, referenced_type_span, lifetime }; + + self.map.entry(&lifetime.kind).or_default().push(info); + } + + #[instrument(skip(self))] + fn visit_ty(&mut self, ty: &'tcx hir::Ty<'tcx, hir::AmbigArg>) -> Self::Result { + let old_type_span = self.type_span; + let old_referenced_type_span = self.referenced_type_span; + + self.type_span = ty.span; + if let hir::TyKind::Ref(_, ty) = ty.kind { + self.referenced_type_span = Some(ty.ty.span); + } + + intravisit::walk_ty(self, ty); + + self.type_span = old_type_span; + self.referenced_type_span = old_referenced_type_span; + } +} diff --git a/compiler/rustc_lint/src/lints.rs b/compiler/rustc_lint/src/lints.rs index 10d0e2c93a8..53fbe158885 100644 --- a/compiler/rustc_lint/src/lints.rs +++ b/compiler/rustc_lint/src/lints.rs @@ -3241,3 +3241,128 @@ pub(crate) struct ReservedMultihash { #[suggestion(code = " ", applicability = "machine-applicable")] pub suggestion: Span, } + +#[derive(Debug)] +pub(crate) struct MismatchedLifetimeSyntaxes { + pub lifetime_name: String, + pub inputs: Vec<Span>, + pub outputs: Vec<Span>, + + pub suggestions: Vec<MismatchedLifetimeSyntaxesSuggestion>, +} + +impl<'a, G: EmissionGuarantee> LintDiagnostic<'a, G> for MismatchedLifetimeSyntaxes { + fn decorate_lint<'b>(self, diag: &'b mut Diag<'a, G>) { + diag.primary_message(fluent::lint_mismatched_lifetime_syntaxes); + + diag.arg("lifetime_name", self.lifetime_name); + + diag.arg("n_inputs", self.inputs.len()); + for input in self.inputs { + let a = diag.eagerly_translate(fluent::lint_label_mismatched_lifetime_syntaxes_inputs); + diag.span_label(input, a); + } + + diag.arg("n_outputs", self.outputs.len()); + for output in self.outputs { + let a = diag.eagerly_translate(fluent::lint_label_mismatched_lifetime_syntaxes_outputs); + diag.span_label(output, a); + } + + let mut suggestions = self.suggestions.into_iter(); + if let Some(s) = suggestions.next() { + diag.subdiagnostic(s); + + for mut s in suggestions { + s.make_tool_only(); + diag.subdiagnostic(s); + } + } + } +} + +#[derive(Debug)] +pub(crate) enum MismatchedLifetimeSyntaxesSuggestion { + Implicit { + suggestions: Vec<Span>, + tool_only: bool, + }, + + Mixed { + implicit_suggestions: Vec<Span>, + explicit_anonymous_suggestions: Vec<(Span, String)>, + tool_only: bool, + }, + + Explicit { + lifetime_name: String, + suggestions: Vec<(Span, String)>, + tool_only: bool, + }, +} + +impl MismatchedLifetimeSyntaxesSuggestion { + fn make_tool_only(&mut self) { + use MismatchedLifetimeSyntaxesSuggestion::*; + + let tool_only = match self { + Implicit { tool_only, .. } | Mixed { tool_only, .. } | Explicit { tool_only, .. } => { + tool_only + } + }; + + *tool_only = true; + } +} + +impl Subdiagnostic for MismatchedLifetimeSyntaxesSuggestion { + fn add_to_diag<G: EmissionGuarantee>(self, diag: &mut Diag<'_, G>) { + use MismatchedLifetimeSyntaxesSuggestion::*; + + let style = |tool_only| { + if tool_only { SuggestionStyle::CompletelyHidden } else { SuggestionStyle::ShowAlways } + }; + + match self { + Implicit { suggestions, tool_only } => { + let suggestions = suggestions.into_iter().map(|s| (s, String::new())).collect(); + diag.multipart_suggestion_with_style( + fluent::lint_mismatched_lifetime_syntaxes_suggestion_implicit, + suggestions, + Applicability::MachineApplicable, + style(tool_only), + ); + } + + Mixed { implicit_suggestions, explicit_anonymous_suggestions, tool_only } => { + let implicit_suggestions = + implicit_suggestions.into_iter().map(|s| (s, String::new())); + + let suggestions = + implicit_suggestions.chain(explicit_anonymous_suggestions).collect(); + + diag.multipart_suggestion_with_style( + fluent::lint_mismatched_lifetime_syntaxes_suggestion_mixed, + suggestions, + Applicability::MachineApplicable, + style(tool_only), + ); + } + + Explicit { lifetime_name, suggestions, tool_only } => { + diag.arg("lifetime_name", lifetime_name); + + let msg = diag.eagerly_translate( + fluent::lint_mismatched_lifetime_syntaxes_suggestion_explicit, + ); + + diag.multipart_suggestion_with_style( + msg, + suggestions, + Applicability::MachineApplicable, + style(tool_only), + ); + } + } + } +} |
