From d3621eeb02652038a8185f60d78fb4791a732dc6 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 13 Jun 2021 19:35:30 +0300 Subject: internal: refactor unimplemented builtin macro diagnostic --- .../src/diagnostics/unimplemented_builtin_macro.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 crates/ide/src/diagnostics/unimplemented_builtin_macro.rs (limited to 'crates/ide/src/diagnostics') diff --git a/crates/ide/src/diagnostics/unimplemented_builtin_macro.rs b/crates/ide/src/diagnostics/unimplemented_builtin_macro.rs new file mode 100644 index 000000000..09faa3bbc --- /dev/null +++ b/crates/ide/src/diagnostics/unimplemented_builtin_macro.rs @@ -0,0 +1,19 @@ +use crate::{ + diagnostics::{Diagnostic, DiagnosticsContext}, + Severity, +}; + +// Diagnostic: unimplemented-builtin-macro +// +// This diagnostic is shown for builtin macros which are not yet implemented by rust-analyzer +pub(super) fn unimplemented_builtin_macro( + ctx: &DiagnosticsContext<'_>, + d: &hir::UnimplementedBuiltinMacro, +) -> Diagnostic { + Diagnostic::new( + "unimplemented-builtin-macro", + "unimplemented built-in macro".to_string(), + ctx.sema.diagnostics_display_range(d.node.clone()).range, + ) + .severity(Severity::WeakWarning) +} -- cgit v1.2.3 From 7166e8549bad95b05f66acd508d07a6cd97d3dc0 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 13 Jun 2021 19:45:16 +0300 Subject: internal: refactor NoSuchField diagnostic --- crates/ide/src/diagnostics/fixes/create_field.rs | 155 ------------ crates/ide/src/diagnostics/no_such_field.rs | 286 +++++++++++++++++++++++ 2 files changed, 286 insertions(+), 155 deletions(-) create mode 100644 crates/ide/src/diagnostics/no_such_field.rs (limited to 'crates/ide/src/diagnostics') diff --git a/crates/ide/src/diagnostics/fixes/create_field.rs b/crates/ide/src/diagnostics/fixes/create_field.rs index f6e45967a..8b1378917 100644 --- a/crates/ide/src/diagnostics/fixes/create_field.rs +++ b/crates/ide/src/diagnostics/fixes/create_field.rs @@ -1,156 +1 @@ -use hir::{db::AstDatabase, diagnostics::NoSuchField, HasSource, HirDisplay, Semantics}; -use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase}; -use syntax::{ - ast::{self, edit::IndentLevel, make}, - AstNode, -}; -use text_edit::TextEdit; -use crate::{ - diagnostics::{fix, DiagnosticWithFixes}, - Assist, AssistResolveStrategy, -}; -impl DiagnosticWithFixes for NoSuchField { - fn fixes( - &self, - sema: &Semantics, - _resolve: &AssistResolveStrategy, - ) -> Option> { - let root = sema.db.parse_or_expand(self.file)?; - missing_record_expr_field_fixes( - sema, - self.file.original_file(sema.db), - &self.field.to_node(&root), - ) - } -} - -fn missing_record_expr_field_fixes( - sema: &Semantics, - usage_file_id: FileId, - record_expr_field: &ast::RecordExprField, -) -> Option> { - let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; - let def_id = sema.resolve_variant(record_lit)?; - let module; - let def_file_id; - let record_fields = match def_id { - hir::VariantDef::Struct(s) => { - module = s.module(sema.db); - let source = s.source(sema.db)?; - def_file_id = source.file_id; - let fields = source.value.field_list()?; - record_field_list(fields)? - } - hir::VariantDef::Union(u) => { - module = u.module(sema.db); - let source = u.source(sema.db)?; - def_file_id = source.file_id; - source.value.record_field_list()? - } - hir::VariantDef::Variant(e) => { - module = e.module(sema.db); - let source = e.source(sema.db)?; - def_file_id = source.file_id; - let fields = source.value.field_list()?; - record_field_list(fields)? - } - }; - let def_file_id = def_file_id.original_file(sema.db); - - let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; - if new_field_type.is_unknown() { - return None; - } - let new_field = make::record_field( - None, - make::name(&record_expr_field.field_name()?.text()), - make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), - ); - - let last_field = record_fields.fields().last()?; - let last_field_syntax = last_field.syntax(); - let indent = IndentLevel::from_node(last_field_syntax); - - let mut new_field = new_field.to_string(); - if usage_file_id != def_file_id { - new_field = format!("pub(crate) {}", new_field); - } - new_field = format!("\n{}{}", indent, new_field); - - let needs_comma = !last_field_syntax.to_string().ends_with(','); - if needs_comma { - new_field = format!(",{}", new_field); - } - - let source_change = SourceChange::from_text_edit( - def_file_id, - TextEdit::insert(last_field_syntax.text_range().end(), new_field), - ); - - return Some(vec![fix( - "create_field", - "Create field", - source_change, - record_expr_field.syntax().text_range(), - )]); - - fn record_field_list(field_def_list: ast::FieldList) -> Option { - match field_def_list { - ast::FieldList::RecordFieldList(it) => Some(it), - ast::FieldList::TupleFieldList(_) => None, - } - } -} - -#[cfg(test)] -mod tests { - use crate::diagnostics::tests::check_fix; - - #[test] - fn test_add_field_from_usage() { - check_fix( - r" -fn main() { - Foo { bar: 3, baz$0: false}; -} -struct Foo { - bar: i32 -} -", - r" -fn main() { - Foo { bar: 3, baz: false}; -} -struct Foo { - bar: i32, - baz: bool -} -", - ) - } - - #[test] - fn test_add_field_in_other_file_from_usage() { - check_fix( - r#" -//- /main.rs -mod foo; - -fn main() { - foo::Foo { bar: 3, $0baz: false}; -} -//- /foo.rs -struct Foo { - bar: i32 -} -"#, - r#" -struct Foo { - bar: i32, - pub(crate) baz: bool -} -"#, - ) - } -} diff --git a/crates/ide/src/diagnostics/no_such_field.rs b/crates/ide/src/diagnostics/no_such_field.rs new file mode 100644 index 000000000..61962de28 --- /dev/null +++ b/crates/ide/src/diagnostics/no_such_field.rs @@ -0,0 +1,286 @@ +use hir::{db::AstDatabase, HasSource, HirDisplay, Semantics}; +use ide_db::{base_db::FileId, source_change::SourceChange, RootDatabase}; +use syntax::{ + ast::{self, edit::IndentLevel, make}, + AstNode, +}; +use text_edit::TextEdit; + +use crate::{ + diagnostics::{fix, Diagnostic, DiagnosticsContext}, + Assist, +}; + +// Diagnostic: no-such-field +// +// This diagnostic is triggered if created structure does not have field provided in record. +pub(super) fn no_such_field(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Diagnostic { + Diagnostic::new( + "no-such-field", + "no such field".to_string(), + ctx.sema.diagnostics_display_range(d.field.clone().map(|it| it.into())).range, + ) + .with_fixes(fixes(ctx, d)) +} + +fn fixes(ctx: &DiagnosticsContext<'_>, d: &hir::NoSuchField) -> Option> { + let root = ctx.sema.db.parse_or_expand(d.field.file_id)?; + missing_record_expr_field_fixes( + &ctx.sema, + d.field.file_id.original_file(ctx.sema.db), + &d.field.value.to_node(&root), + ) +} + +fn missing_record_expr_field_fixes( + sema: &Semantics, + usage_file_id: FileId, + record_expr_field: &ast::RecordExprField, +) -> Option> { + let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?; + let def_id = sema.resolve_variant(record_lit)?; + let module; + let def_file_id; + let record_fields = match def_id { + hir::VariantDef::Struct(s) => { + module = s.module(sema.db); + let source = s.source(sema.db)?; + def_file_id = source.file_id; + let fields = source.value.field_list()?; + record_field_list(fields)? + } + hir::VariantDef::Union(u) => { + module = u.module(sema.db); + let source = u.source(sema.db)?; + def_file_id = source.file_id; + source.value.record_field_list()? + } + hir::VariantDef::Variant(e) => { + module = e.module(sema.db); + let source = e.source(sema.db)?; + def_file_id = source.file_id; + let fields = source.value.field_list()?; + record_field_list(fields)? + } + }; + let def_file_id = def_file_id.original_file(sema.db); + + let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?; + if new_field_type.is_unknown() { + return None; + } + let new_field = make::record_field( + None, + make::name(&record_expr_field.field_name()?.text()), + make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?), + ); + + let last_field = record_fields.fields().last()?; + let last_field_syntax = last_field.syntax(); + let indent = IndentLevel::from_node(last_field_syntax); + + let mut new_field = new_field.to_string(); + if usage_file_id != def_file_id { + new_field = format!("pub(crate) {}", new_field); + } + new_field = format!("\n{}{}", indent, new_field); + + let needs_comma = !last_field_syntax.to_string().ends_with(','); + if needs_comma { + new_field = format!(",{}", new_field); + } + + let source_change = SourceChange::from_text_edit( + def_file_id, + TextEdit::insert(last_field_syntax.text_range().end(), new_field), + ); + + return Some(vec![fix( + "create_field", + "Create field", + source_change, + record_expr_field.syntax().text_range(), + )]); + + fn record_field_list(field_def_list: ast::FieldList) -> Option { + match field_def_list { + ast::FieldList::RecordFieldList(it) => Some(it), + ast::FieldList::TupleFieldList(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::tests::{check_diagnostics, check_fix}; + + #[test] + fn no_such_field_diagnostics() { + check_diagnostics( + r#" +struct S { foo: i32, bar: () } +impl S { + fn new() -> S { + S { + //^ Missing structure fields: + //| - bar + foo: 92, + baz: 62, + //^^^^^^^ no such field + } + } +} +"#, + ); + } + #[test] + fn no_such_field_with_feature_flag_diagnostics() { + check_diagnostics( + r#" +//- /lib.rs crate:foo cfg:feature=foo +struct MyStruct { + my_val: usize, + #[cfg(feature = "foo")] + bar: bool, +} + +impl MyStruct { + #[cfg(feature = "foo")] + pub(crate) fn new(my_val: usize, bar: bool) -> Self { + Self { my_val, bar } + } + #[cfg(not(feature = "foo"))] + pub(crate) fn new(my_val: usize, _bar: bool) -> Self { + Self { my_val } + } +} +"#, + ); + } + + #[test] + fn no_such_field_enum_with_feature_flag_diagnostics() { + check_diagnostics( + r#" +//- /lib.rs crate:foo cfg:feature=foo +enum Foo { + #[cfg(not(feature = "foo"))] + Buz, + #[cfg(feature = "foo")] + Bar, + Baz +} + +fn test_fn(f: Foo) { + match f { + Foo::Bar => {}, + Foo::Baz => {}, + } +} +"#, + ); + } + + #[test] + fn no_such_field_with_feature_flag_diagnostics_on_struct_lit() { + check_diagnostics( + r#" +//- /lib.rs crate:foo cfg:feature=foo +struct S { + #[cfg(feature = "foo")] + foo: u32, + #[cfg(not(feature = "foo"))] + bar: u32, +} + +impl S { + #[cfg(feature = "foo")] + fn new(foo: u32) -> Self { + Self { foo } + } + #[cfg(not(feature = "foo"))] + fn new(bar: u32) -> Self { + Self { bar } + } + fn new2(bar: u32) -> Self { + #[cfg(feature = "foo")] + { Self { foo: bar } } + #[cfg(not(feature = "foo"))] + { Self { bar } } + } + fn new2(val: u32) -> Self { + Self { + #[cfg(feature = "foo")] + foo: val, + #[cfg(not(feature = "foo"))] + bar: val, + } + } +} +"#, + ); + } + + #[test] + fn no_such_field_with_type_macro() { + check_diagnostics( + r#" +macro_rules! Type { () => { u32 }; } +struct Foo { bar: Type![] } + +impl Foo { + fn new() -> Self { + Foo { bar: 0 } + } +} +"#, + ); + } + + #[test] + fn test_add_field_from_usage() { + check_fix( + r" +fn main() { + Foo { bar: 3, baz$0: false}; +} +struct Foo { + bar: i32 +} +", + r" +fn main() { + Foo { bar: 3, baz: false}; +} +struct Foo { + bar: i32, + baz: bool +} +", + ) + } + + #[test] + fn test_add_field_in_other_file_from_usage() { + check_fix( + r#" +//- /main.rs +mod foo; + +fn main() { + foo::Foo { bar: 3, $0baz: false}; +} +//- /foo.rs +struct Foo { + bar: i32 +} +"#, + r#" +struct Foo { + bar: i32, + pub(crate) baz: bool +} +"#, + ) + } +} -- cgit v1.2.3 From 886b66cd03cbe7cb13e248d7c7bbdeba66c7796a Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 13 Jun 2021 19:51:19 +0300 Subject: internal: refactor BreakOutsideOfLoop diagnostic --- .../ide/src/diagnostics/break_outside_of_loop.rs | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 crates/ide/src/diagnostics/break_outside_of_loop.rs (limited to 'crates/ide/src/diagnostics') diff --git a/crates/ide/src/diagnostics/break_outside_of_loop.rs b/crates/ide/src/diagnostics/break_outside_of_loop.rs new file mode 100644 index 000000000..80e68f3cc --- /dev/null +++ b/crates/ide/src/diagnostics/break_outside_of_loop.rs @@ -0,0 +1,30 @@ +use crate::diagnostics::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: break-outside-of-loop +// +// This diagnostic is triggered if the `break` keyword is used outside of a loop. +pub(super) fn break_outside_of_loop( + ctx: &DiagnosticsContext<'_>, + d: &hir::BreakOutsideOfLoop, +) -> Diagnostic { + Diagnostic::new( + "break-outside-of-loop", + "break outside of loop", + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::tests::check_diagnostics; + + #[test] + fn break_outside_of_loop() { + check_diagnostics( + r#" +fn foo() { break; } + //^^^^^ break outside of loop +"#, + ); + } +} -- cgit v1.2.3 From bccf77f26cd504de14f7d7d03f9f2a85d0fabb3d Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 13 Jun 2021 20:00:27 +0300 Subject: internal: refactor missing unsafe diagnostic --- crates/ide/src/diagnostics/missing_unsafe.rs | 101 +++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 crates/ide/src/diagnostics/missing_unsafe.rs (limited to 'crates/ide/src/diagnostics') diff --git a/crates/ide/src/diagnostics/missing_unsafe.rs b/crates/ide/src/diagnostics/missing_unsafe.rs new file mode 100644 index 000000000..5c47e8d0a --- /dev/null +++ b/crates/ide/src/diagnostics/missing_unsafe.rs @@ -0,0 +1,101 @@ +use crate::diagnostics::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: missing-unsafe +// +// This diagnostic is triggered if an operation marked as `unsafe` is used outside of an `unsafe` function or block. +pub(super) fn missing_unsafe(ctx: &DiagnosticsContext<'_>, d: &hir::MissingUnsafe) -> Diagnostic { + Diagnostic::new( + "missing-unsafe", + "this operation is unsafe and requires an unsafe function or block", + ctx.sema.diagnostics_display_range(d.expr.clone().map(|it| it.into())).range, + ) +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::tests::check_diagnostics; + + #[test] + fn missing_unsafe_diagnostic_with_raw_ptr() { + check_diagnostics( + r#" +fn main() { + let x = &5 as *const usize; + unsafe { let y = *x; } + let z = *x; +} //^^ this operation is unsafe and requires an unsafe function or block +"#, + ) + } + + #[test] + fn missing_unsafe_diagnostic_with_unsafe_call() { + check_diagnostics( + r#" +struct HasUnsafe; + +impl HasUnsafe { + unsafe fn unsafe_fn(&self) { + let x = &5 as *const usize; + let y = *x; + } +} + +unsafe fn unsafe_fn() { + let x = &5 as *const usize; + let y = *x; +} + +fn main() { + unsafe_fn(); + //^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block + HasUnsafe.unsafe_fn(); + //^^^^^^^^^^^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block + unsafe { + unsafe_fn(); + HasUnsafe.unsafe_fn(); + } +} +"#, + ); + } + + #[test] + fn missing_unsafe_diagnostic_with_static_mut() { + check_diagnostics( + r#" +struct Ty { + a: u8, +} + +static mut STATIC_MUT: Ty = Ty { a: 0 }; + +fn main() { + let x = STATIC_MUT.a; + //^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block + unsafe { + let x = STATIC_MUT.a; + } +} +"#, + ); + } + + #[test] + fn no_missing_unsafe_diagnostic_with_safe_intrinsic() { + check_diagnostics( + r#" +extern "rust-intrinsic" { + pub fn bitreverse(x: u32) -> u32; // Safe intrinsic + pub fn floorf32(x: f32) -> f32; // Unsafe intrinsic +} + +fn main() { + let _ = bitreverse(12); + let _ = floorf32(12.0); + //^^^^^^^^^^^^^^ this operation is unsafe and requires an unsafe function or block +} +"#, + ); + } +} -- cgit v1.2.3 From 8d391ec981562785ec92ce3afe950972c523f925 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Sun, 13 Jun 2021 20:06:25 +0300 Subject: internal: refactor mismatched args count diagnostic --- crates/ide/src/diagnostics/mismatched_arg_count.rs | 272 +++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 crates/ide/src/diagnostics/mismatched_arg_count.rs (limited to 'crates/ide/src/diagnostics') diff --git a/crates/ide/src/diagnostics/mismatched_arg_count.rs b/crates/ide/src/diagnostics/mismatched_arg_count.rs new file mode 100644 index 000000000..08e1cfa5f --- /dev/null +++ b/crates/ide/src/diagnostics/mismatched_arg_count.rs @@ -0,0 +1,272 @@ +use crate::diagnostics::{Diagnostic, DiagnosticsContext}; + +// Diagnostic: mismatched-arg-count +// +// This diagnostic is triggered if a function is invoked with an incorrect amount of arguments. +pub(super) fn mismatched_arg_count( + ctx: &DiagnosticsContext<'_>, + d: &hir::MismatchedArgCount, +) -> Diagnostic { + let s = if d.expected == 1 { "" } else { "s" }; + let message = format!("expected {} argument{}, found {}", d.expected, s, d.found); + Diagnostic::new( + "mismatched-arg-count", + message, + ctx.sema.diagnostics_display_range(d.call_expr.clone().map(|it| it.into())).range, + ) +} + +#[cfg(test)] +mod tests { + use crate::diagnostics::tests::check_diagnostics; + + #[test] + fn simple_free_fn_zero() { + check_diagnostics( + r#" +fn zero() {} +fn f() { zero(1); } + //^^^^^^^ expected 0 arguments, found 1 +"#, + ); + + check_diagnostics( + r#" +fn zero() {} +fn f() { zero(); } +"#, + ); + } + + #[test] + fn simple_free_fn_one() { + check_diagnostics( + r#" +fn one(arg: u8) {} +fn f() { one(); } + //^^^^^ expected 1 argument, found 0 +"#, + ); + + check_diagnostics( + r#" +fn one(arg: u8) {} +fn f() { one(1); } +"#, + ); + } + + #[test] + fn method_as_fn() { + check_diagnostics( + r#" +struct S; +impl S { fn method(&self) {} } + +fn f() { + S::method(); +} //^^^^^^^^^^^ expected 1 argument, found 0 +"#, + ); + + check_diagnostics( + r#" +struct S; +impl S { fn method(&self) {} } + +fn f() { + S::method(&S); + S.method(); +} +"#, + ); + } + + #[test] + fn method_with_arg() { + check_diagnostics( + r#" +struct S; +impl S { fn method(&self, arg: u8) {} } + + fn f() { + S.method(); + } //^^^^^^^^^^ expected 1 argument, found 0 + "#, + ); + + check_diagnostics( + r#" +struct S; +impl S { fn method(&self, arg: u8) {} } + +fn f() { + S::method(&S, 0); + S.method(1); +} +"#, + ); + } + + #[test] + fn method_unknown_receiver() { + // note: this is incorrect code, so there might be errors on this in the + // future, but we shouldn't emit an argument count diagnostic here + check_diagnostics( + r#" +trait Foo { fn method(&self, arg: usize) {} } + +fn f() { + let x; + x.method(); +} +"#, + ); + } + + #[test] + fn tuple_struct() { + check_diagnostics( + r#" +struct Tup(u8, u16); +fn f() { + Tup(0); +} //^^^^^^ expected 2 arguments, found 1 +"#, + ) + } + + #[test] + fn enum_variant() { + check_diagnostics( + r#" +enum En { Variant(u8, u16), } +fn f() { + En::Variant(0); +} //^^^^^^^^^^^^^^ expected 2 arguments, found 1 +"#, + ) + } + + #[test] + fn enum_variant_type_macro() { + check_diagnostics( + r#" +macro_rules! Type { + () => { u32 }; +} +enum Foo { + Bar(Type![]) +} +impl Foo { + fn new() { + Foo::Bar(0); + Foo::Bar(0, 1); + //^^^^^^^^^^^^^^ expected 1 argument, found 2 + Foo::Bar(); + //^^^^^^^^^^ expected 1 argument, found 0 + } +} + "#, + ); + } + + #[test] + fn varargs() { + check_diagnostics( + r#" +extern "C" { + fn fixed(fixed: u8); + fn varargs(fixed: u8, ...); + fn varargs2(...); +} + +fn f() { + unsafe { + fixed(0); + fixed(0, 1); + //^^^^^^^^^^^ expected 1 argument, found 2 + varargs(0); + varargs(0, 1); + varargs2(); + varargs2(0); + varargs2(0, 1); + } +} + "#, + ) + } + + #[test] + fn arg_count_lambda() { + check_diagnostics( + r#" +fn main() { + let f = |()| (); + f(); + //^^^ expected 1 argument, found 0 + f(()); + f((), ()); + //^^^^^^^^^ expected 1 argument, found 2 +} +"#, + ) + } + + #[test] + fn cfgd_out_call_arguments() { + check_diagnostics( + r#" +struct C(#[cfg(FALSE)] ()); +impl C { + fn new() -> Self { + Self( + #[cfg(FALSE)] + (), + ) + } + + fn method(&self) {} +} + +fn main() { + C::new().method(#[cfg(FALSE)] 0); +} + "#, + ); + } + + #[test] + fn cfgd_out_fn_params() { + check_diagnostics( + r#" +fn foo(#[cfg(NEVER)] x: ()) {} + +struct S; + +impl S { + fn method(#[cfg(NEVER)] self) {} + fn method2(#[cfg(NEVER)] self, arg: u8) {} + fn method3(self, #[cfg(NEVER)] arg: u8) {} +} + +extern "C" { + fn fixed(fixed: u8, #[cfg(NEVER)] ...); + fn varargs(#[cfg(not(NEVER))] ...); +} + +fn main() { + foo(); + S::method(); + S::method2(0); + S::method3(S); + S.method3(); + unsafe { + fixed(0); + varargs(1, 2, 3); + } +} + "#, + ) + } +} -- cgit v1.2.3