]> git.lizzy.rs Git - rust.git/blob - crates/ra_ide/src/diagnostics.rs
Add a builder for DiagnosticSink
[rust.git] / crates / ra_ide / src / diagnostics.rs
1 //! Collects diagnostics & fixits  for a single file.
2 //!
3 //! The tricky bit here is that diagnostics are produced by hir in terms of
4 //! macro-expanded files, but we need to present them to the users in terms of
5 //! original files. So we need to map the ranges.
6
7 use std::cell::RefCell;
8
9 use hir::{
10     diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder},
11     HasSource, HirDisplay, Semantics, VariantDef,
12 };
13 use itertools::Itertools;
14 use ra_db::SourceDatabase;
15 use ra_ide_db::RootDatabase;
16 use ra_prof::profile;
17 use ra_syntax::{
18     algo,
19     ast::{self, edit::IndentLevel, make, AstNode},
20     SyntaxNode, TextRange, T,
21 };
22 use ra_text_edit::{TextEdit, TextEditBuilder};
23
24 use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit};
25
26 #[derive(Debug, Copy, Clone)]
27 pub enum Severity {
28     Error,
29     WeakWarning,
30 }
31
32 pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> {
33     let _p = profile("diagnostics");
34     let sema = Semantics::new(db);
35     let parse = db.parse(file_id);
36     let mut res = Vec::new();
37
38     // [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
39     res.extend(parse.errors().iter().take(128).map(|err| Diagnostic {
40         range: err.range(),
41         message: format!("Syntax Error: {}", err),
42         severity: Severity::Error,
43         fix: None,
44     }));
45
46     for node in parse.tree().syntax().descendants() {
47         check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
48         check_struct_shorthand_initialization(&mut res, file_id, &node);
49     }
50     let res = RefCell::new(res);
51     let mut sink = DiagnosticSinkBuilder::new()
52         .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
53             let original_file = d.source().file_id.original_file(db);
54             let fix = Fix::new(
55                 "Create module",
56                 FileSystemEdit::CreateFile { anchor: original_file, dst: d.candidate.clone() }
57                     .into(),
58             );
59             res.borrow_mut().push(Diagnostic {
60                 range: sema.diagnostics_range(d).range,
61                 message: d.message(),
62                 severity: Severity::Error,
63                 fix: Some(fix),
64             })
65         })
66         .on::<hir::diagnostics::MissingFields, _>(|d| {
67             // Note that although we could add a diagnostics to
68             // fill the missing tuple field, e.g :
69             // `struct A(usize);`
70             // `let a = A { 0: () }`
71             // but it is uncommon usage and it should not be encouraged.
72             let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
73                 None
74             } else {
75                 let mut field_list = d.ast(db);
76                 for f in d.missed_fields.iter() {
77                     let field =
78                         make::record_field(make::name_ref(&f.to_string()), Some(make::expr_unit()));
79                     field_list = field_list.append_field(&field);
80                 }
81
82                 let edit = {
83                     let mut builder = TextEditBuilder::default();
84                     algo::diff(&d.ast(db).syntax(), &field_list.syntax())
85                         .into_text_edit(&mut builder);
86                     builder.finish()
87                 };
88                 Some(Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into()))
89             };
90
91             res.borrow_mut().push(Diagnostic {
92                 range: sema.diagnostics_range(d).range,
93                 message: d.message(),
94                 severity: Severity::Error,
95                 fix,
96             })
97         })
98         .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
99             let node = d.ast(db);
100             let replacement = format!("Ok({})", node.syntax());
101             let edit = TextEdit::replace(node.syntax().text_range(), replacement);
102             let source_change = SourceFileEdit { file_id, edit }.into();
103             let fix = Fix::new("Wrap with ok", source_change);
104             res.borrow_mut().push(Diagnostic {
105                 range: sema.diagnostics_range(d).range,
106                 message: d.message(),
107                 severity: Severity::Error,
108                 fix: Some(fix),
109             })
110         })
111         .on::<hir::diagnostics::NoSuchField, _>(|d| {
112             res.borrow_mut().push(Diagnostic {
113                 range: sema.diagnostics_range(d).range,
114                 message: d.message(),
115                 severity: Severity::Error,
116                 fix: missing_struct_field_fix(&sema, file_id, d),
117             })
118         })
119         .build(|d| {
120             res.borrow_mut().push(Diagnostic {
121                 message: d.message(),
122                 range: sema.diagnostics_range(d).range,
123                 severity: Severity::Error,
124                 fix: None,
125             })
126         });
127
128     if let Some(m) = sema.to_module_def(file_id) {
129         m.diagnostics(db, &mut sink);
130     };
131     drop(sink);
132     res.into_inner()
133 }
134
135 fn missing_struct_field_fix(
136     sema: &Semantics<RootDatabase>,
137     usage_file_id: FileId,
138     d: &hir::diagnostics::NoSuchField,
139 ) -> Option<Fix> {
140     let record_expr = sema.ast(d);
141
142     let record_lit = ast::RecordLit::cast(record_expr.syntax().parent()?.parent()?)?;
143     let def_id = sema.resolve_variant(record_lit)?;
144     let module;
145     let def_file_id;
146     let record_fields = match VariantDef::from(def_id) {
147         VariantDef::Struct(s) => {
148             module = s.module(sema.db);
149             let source = s.source(sema.db);
150             def_file_id = source.file_id;
151             let fields = source.value.field_def_list()?;
152             record_field_def_list(fields)?
153         }
154         VariantDef::Union(u) => {
155             module = u.module(sema.db);
156             let source = u.source(sema.db);
157             def_file_id = source.file_id;
158             source.value.record_field_def_list()?
159         }
160         VariantDef::EnumVariant(e) => {
161             module = e.module(sema.db);
162             let source = e.source(sema.db);
163             def_file_id = source.file_id;
164             let fields = source.value.field_def_list()?;
165             record_field_def_list(fields)?
166         }
167     };
168     let def_file_id = def_file_id.original_file(sema.db);
169
170     let new_field_type = sema.type_of_expr(&record_expr.expr()?)?;
171     if new_field_type.is_unknown() {
172         return None;
173     }
174     let new_field = make::record_field_def(
175         record_expr.field_name()?,
176         make::type_ref(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
177     );
178
179     let last_field = record_fields.fields().last()?;
180     let last_field_syntax = last_field.syntax();
181     let indent = IndentLevel::from_node(last_field_syntax);
182
183     let mut new_field = new_field.to_string();
184     if usage_file_id != def_file_id {
185         new_field = format!("pub(crate) {}", new_field);
186     }
187     new_field = format!("\n{}{}", indent, new_field);
188
189     let needs_comma = !last_field_syntax.to_string().ends_with(',');
190     if needs_comma {
191         new_field = format!(",{}", new_field);
192     }
193
194     let source_change = SourceFileEdit {
195         file_id: def_file_id,
196         edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
197     };
198     let fix = Fix::new("Create field", source_change.into());
199     return Some(fix);
200
201     fn record_field_def_list(field_def_list: ast::FieldDefList) -> Option<ast::RecordFieldDefList> {
202         match field_def_list {
203             ast::FieldDefList::RecordFieldDefList(it) => Some(it),
204             ast::FieldDefList::TupleFieldDefList(_) => None,
205         }
206     }
207 }
208
209 fn check_unnecessary_braces_in_use_statement(
210     acc: &mut Vec<Diagnostic>,
211     file_id: FileId,
212     node: &SyntaxNode,
213 ) -> Option<()> {
214     let use_tree_list = ast::UseTreeList::cast(node.clone())?;
215     if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
216         let range = use_tree_list.syntax().text_range();
217         let edit =
218             text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
219                 .unwrap_or_else(|| {
220                     let to_replace = single_use_tree.syntax().text().to_string();
221                     let mut edit_builder = TextEditBuilder::default();
222                     edit_builder.delete(range);
223                     edit_builder.insert(range.start(), to_replace);
224                     edit_builder.finish()
225                 });
226
227         acc.push(Diagnostic {
228             range,
229             message: "Unnecessary braces in use statement".to_string(),
230             severity: Severity::WeakWarning,
231             fix: Some(Fix::new(
232                 "Remove unnecessary braces",
233                 SourceFileEdit { file_id, edit }.into(),
234             )),
235         });
236     }
237
238     Some(())
239 }
240
241 fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
242     single_use_tree: &ast::UseTree,
243 ) -> Option<TextEdit> {
244     let use_tree_list_node = single_use_tree.syntax().parent()?;
245     if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
246         let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
247         let end = use_tree_list_node.text_range().end();
248         let range = TextRange::new(start, end);
249         return Some(TextEdit::delete(range));
250     }
251     None
252 }
253
254 fn check_struct_shorthand_initialization(
255     acc: &mut Vec<Diagnostic>,
256     file_id: FileId,
257     node: &SyntaxNode,
258 ) -> Option<()> {
259     let record_lit = ast::RecordLit::cast(node.clone())?;
260     let record_field_list = record_lit.record_field_list()?;
261     for record_field in record_field_list.fields() {
262         if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) {
263             let field_name = name_ref.syntax().text().to_string();
264             let field_expr = expr.syntax().text().to_string();
265             let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
266             if field_name == field_expr && !field_name_is_tup_index {
267                 let mut edit_builder = TextEditBuilder::default();
268                 edit_builder.delete(record_field.syntax().text_range());
269                 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
270                 let edit = edit_builder.finish();
271
272                 acc.push(Diagnostic {
273                     range: record_field.syntax().text_range(),
274                     message: "Shorthand struct initialization".to_string(),
275                     severity: Severity::WeakWarning,
276                     fix: Some(Fix::new(
277                         "Use struct shorthand initialization",
278                         SourceFileEdit { file_id, edit }.into(),
279                     )),
280                 });
281             }
282         }
283     }
284     Some(())
285 }
286
287 #[cfg(test)]
288 mod tests {
289     use stdx::trim_indent;
290     use test_utils::assert_eq_text;
291
292     use crate::mock_analysis::{analysis_and_position, single_file, MockAnalysis};
293     use expect::{expect, Expect};
294
295     /// Takes a multi-file input fixture with annotated cursor positions,
296     /// and checks that:
297     ///  * a diagnostic is produced
298     ///  * this diagnostic touches the input cursor position
299     ///  * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
300     fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
301         let after = trim_indent(ra_fixture_after);
302
303         let (analysis, file_position) = analysis_and_position(ra_fixture_before);
304         let diagnostic = analysis.diagnostics(file_position.file_id).unwrap().pop().unwrap();
305         let mut fix = diagnostic.fix.unwrap();
306         let edit = fix.source_change.source_file_edits.pop().unwrap().edit;
307         let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
308         let actual = {
309             let mut actual = target_file_contents.to_string();
310             edit.apply(&mut actual);
311             actual
312         };
313
314         assert_eq_text!(&after, &actual);
315         assert!(
316             diagnostic.range.start() <= file_position.offset
317                 && diagnostic.range.end() >= file_position.offset,
318             "diagnostic range {:?} does not touch cursor position {:?}",
319             diagnostic.range,
320             file_position.offset
321         );
322     }
323
324     /// Checks that a diagnostic applies to the file containing the `<|>` cursor marker
325     /// which has a fix that can apply to other files.
326     fn check_apply_diagnostic_fix_in_other_file(ra_fixture_before: &str, ra_fixture_after: &str) {
327         let ra_fixture_after = &trim_indent(ra_fixture_after);
328         let (analysis, file_pos) = analysis_and_position(ra_fixture_before);
329         let current_file_id = file_pos.file_id;
330         let diagnostic = analysis.diagnostics(current_file_id).unwrap().pop().unwrap();
331         let mut fix = diagnostic.fix.unwrap();
332         let edit = fix.source_change.source_file_edits.pop().unwrap();
333         let changed_file_id = edit.file_id;
334         let before = analysis.file_text(changed_file_id).unwrap();
335         let actual = {
336             let mut actual = before.to_string();
337             edit.edit.apply(&mut actual);
338             actual
339         };
340         assert_eq_text!(ra_fixture_after, &actual);
341     }
342
343     /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
344     /// apply to the file containing the cursor.
345     fn check_no_diagnostics(ra_fixture: &str) {
346         let mock = MockAnalysis::with_files(ra_fixture);
347         let files = mock.files().map(|(it, _)| it).collect::<Vec<_>>();
348         let analysis = mock.analysis();
349         let diagnostics = files
350             .into_iter()
351             .flat_map(|file_id| analysis.diagnostics(file_id).unwrap())
352             .collect::<Vec<_>>();
353         assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
354     }
355
356     fn check_expect(ra_fixture: &str, expect: Expect) {
357         let (analysis, file_id) = single_file(ra_fixture);
358         let diagnostics = analysis.diagnostics(file_id).unwrap();
359         expect.assert_debug_eq(&diagnostics)
360     }
361
362     #[test]
363     fn test_wrap_return_type() {
364         check_fix(
365             r#"
366 //- /main.rs
367 use core::result::Result::{self, Ok, Err};
368
369 fn div(x: i32, y: i32) -> Result<i32, ()> {
370     if y == 0 {
371         return Err(());
372     }
373     x / y<|>
374 }
375 //- /core/lib.rs
376 pub mod result {
377     pub enum Result<T, E> { Ok(T), Err(E) }
378 }
379 "#,
380             r#"
381 use core::result::Result::{self, Ok, Err};
382
383 fn div(x: i32, y: i32) -> Result<i32, ()> {
384     if y == 0 {
385         return Err(());
386     }
387     Ok(x / y)
388 }
389 "#,
390         );
391     }
392
393     #[test]
394     fn test_wrap_return_type_handles_generic_functions() {
395         check_fix(
396             r#"
397 //- /main.rs
398 use core::result::Result::{self, Ok, Err};
399
400 fn div<T>(x: T) -> Result<T, i32> {
401     if x == 0 {
402         return Err(7);
403     }
404     <|>x
405 }
406 //- /core/lib.rs
407 pub mod result {
408     pub enum Result<T, E> { Ok(T), Err(E) }
409 }
410 "#,
411             r#"
412 use core::result::Result::{self, Ok, Err};
413
414 fn div<T>(x: T) -> Result<T, i32> {
415     if x == 0 {
416         return Err(7);
417     }
418     Ok(x)
419 }
420 "#,
421         );
422     }
423
424     #[test]
425     fn test_wrap_return_type_handles_type_aliases() {
426         check_fix(
427             r#"
428 //- /main.rs
429 use core::result::Result::{self, Ok, Err};
430
431 type MyResult<T> = Result<T, ()>;
432
433 fn div(x: i32, y: i32) -> MyResult<i32> {
434     if y == 0 {
435         return Err(());
436     }
437     x <|>/ y
438 }
439 //- /core/lib.rs
440 pub mod result {
441     pub enum Result<T, E> { Ok(T), Err(E) }
442 }
443 "#,
444             r#"
445 use core::result::Result::{self, Ok, Err};
446
447 type MyResult<T> = Result<T, ()>;
448
449 fn div(x: i32, y: i32) -> MyResult<i32> {
450     if y == 0 {
451         return Err(());
452     }
453     Ok(x / y)
454 }
455 "#,
456         );
457     }
458
459     #[test]
460     fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() {
461         check_no_diagnostics(
462             r#"
463 //- /main.rs
464 use core::result::Result::{self, Ok, Err};
465
466 fn foo() -> Result<(), i32> { 0 }
467
468 //- /core/lib.rs
469 pub mod result {
470     pub enum Result<T, E> { Ok(T), Err(E) }
471 }
472 "#,
473         );
474     }
475
476     #[test]
477     fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() {
478         check_no_diagnostics(
479             r#"
480 //- /main.rs
481 use core::result::Result::{self, Ok, Err};
482
483 enum SomeOtherEnum { Ok(i32), Err(String) }
484
485 fn foo() -> SomeOtherEnum { 0 }
486
487 //- /core/lib.rs
488 pub mod result {
489     pub enum Result<T, E> { Ok(T), Err(E) }
490 }
491 "#,
492         );
493     }
494
495     #[test]
496     fn test_fill_struct_fields_empty() {
497         check_fix(
498             r#"
499 struct TestStruct { one: i32, two: i64 }
500
501 fn test_fn() {
502     let s = TestStruct {<|>};
503 }
504 "#,
505             r#"
506 struct TestStruct { one: i32, two: i64 }
507
508 fn test_fn() {
509     let s = TestStruct { one: (), two: ()};
510 }
511 "#,
512         );
513     }
514
515     #[test]
516     fn test_fill_struct_fields_self() {
517         check_fix(
518             r#"
519 struct TestStruct { one: i32 }
520
521 impl TestStruct {
522     fn test_fn() { let s = Self {<|>}; }
523 }
524 "#,
525             r#"
526 struct TestStruct { one: i32 }
527
528 impl TestStruct {
529     fn test_fn() { let s = Self { one: ()}; }
530 }
531 "#,
532         );
533     }
534
535     #[test]
536     fn test_fill_struct_fields_enum() {
537         check_fix(
538             r#"
539 enum Expr {
540     Bin { lhs: Box<Expr>, rhs: Box<Expr> }
541 }
542
543 impl Expr {
544     fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
545         Expr::Bin {<|> }
546     }
547 }
548 "#,
549             r#"
550 enum Expr {
551     Bin { lhs: Box<Expr>, rhs: Box<Expr> }
552 }
553
554 impl Expr {
555     fn new_bin(lhs: Box<Expr>, rhs: Box<Expr>) -> Expr {
556         Expr::Bin { lhs: (), rhs: () }
557     }
558 }
559 "#,
560         );
561     }
562
563     #[test]
564     fn test_fill_struct_fields_partial() {
565         check_fix(
566             r#"
567 struct TestStruct { one: i32, two: i64 }
568
569 fn test_fn() {
570     let s = TestStruct{ two: 2<|> };
571 }
572 "#,
573             r"
574 struct TestStruct { one: i32, two: i64 }
575
576 fn test_fn() {
577     let s = TestStruct{ two: 2, one: () };
578 }
579 ",
580         );
581     }
582
583     #[test]
584     fn test_fill_struct_fields_no_diagnostic() {
585         check_no_diagnostics(
586             r"
587             struct TestStruct { one: i32, two: i64 }
588
589             fn test_fn() {
590                 let one = 1;
591                 let s = TestStruct{ one, two: 2 };
592             }
593         ",
594         );
595     }
596
597     #[test]
598     fn test_fill_struct_fields_no_diagnostic_on_spread() {
599         check_no_diagnostics(
600             r"
601             struct TestStruct { one: i32, two: i64 }
602
603             fn test_fn() {
604                 let one = 1;
605                 let s = TestStruct{ ..a };
606             }
607         ",
608         );
609     }
610
611     #[test]
612     fn test_unresolved_module_diagnostic() {
613         check_expect(
614             r#"mod foo;"#,
615             expect![[r#"
616                 [
617                     Diagnostic {
618                         message: "unresolved module",
619                         range: 0..8,
620                         severity: Error,
621                         fix: Some(
622                             Fix {
623                                 label: "Create module",
624                                 source_change: SourceChange {
625                                     source_file_edits: [],
626                                     file_system_edits: [
627                                         CreateFile {
628                                             anchor: FileId(
629                                                 1,
630                                             ),
631                                             dst: "foo.rs",
632                                         },
633                                     ],
634                                     is_snippet: false,
635                                 },
636                             },
637                         ),
638                     },
639                 ]
640             "#]],
641         );
642     }
643
644     #[test]
645     fn range_mapping_out_of_macros() {
646         // FIXME: this is very wrong, but somewhat tricky to fix.
647         check_fix(
648             r#"
649 fn some() {}
650 fn items() {}
651 fn here() {}
652
653 macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
654
655 fn main() {
656     let _x = id![Foo { a: <|>42 }];
657 }
658
659 pub struct Foo { pub a: i32, pub b: i32 }
660 "#,
661             r#"
662 fn {a:42, b: ()} {}
663 fn items() {}
664 fn here() {}
665
666 macro_rules! id { ($($tt:tt)*) => { $($tt)*}; }
667
668 fn main() {
669     let _x = id![Foo { a: 42 }];
670 }
671
672 pub struct Foo { pub a: i32, pub b: i32 }
673 "#,
674         );
675     }
676
677     #[test]
678     fn test_check_unnecessary_braces_in_use_statement() {
679         check_no_diagnostics(
680             r#"
681 use a;
682 use a::{c, d::e};
683 "#,
684         );
685         check_fix(r#"use {<|>b};"#, r#"use b;"#);
686         check_fix(r#"use {b<|>};"#, r#"use b;"#);
687         check_fix(r#"use a::{c<|>};"#, r#"use a::c;"#);
688         check_fix(r#"use a::{self<|>};"#, r#"use a;"#);
689         check_fix(r#"use a::{c, d::{e<|>}};"#, r#"use a::{c, d::e};"#);
690     }
691
692     #[test]
693     fn test_check_struct_shorthand_initialization() {
694         check_no_diagnostics(
695             r#"
696 struct A { a: &'static str }
697 fn main() { A { a: "hello" } }
698 "#,
699         );
700         check_no_diagnostics(
701             r#"
702 struct A(usize);
703 fn main() { A { 0: 0 } }
704 "#,
705         );
706
707         check_fix(
708             r#"
709 struct A { a: &'static str }
710 fn main() {
711     let a = "haha";
712     A { a<|>: a }
713 }
714 "#,
715             r#"
716 struct A { a: &'static str }
717 fn main() {
718     let a = "haha";
719     A { a }
720 }
721 "#,
722         );
723
724         check_fix(
725             r#"
726 struct A { a: &'static str, b: &'static str }
727 fn main() {
728     let a = "haha";
729     let b = "bb";
730     A { a<|>: a, b }
731 }
732 "#,
733             r#"
734 struct A { a: &'static str, b: &'static str }
735 fn main() {
736     let a = "haha";
737     let b = "bb";
738     A { a, b }
739 }
740 "#,
741         );
742     }
743
744     #[test]
745     fn test_add_field_from_usage() {
746         check_fix(
747             r"
748 fn main() {
749     Foo { bar: 3, baz<|>: false};
750 }
751 struct Foo {
752     bar: i32
753 }
754 ",
755             r"
756 fn main() {
757     Foo { bar: 3, baz: false};
758 }
759 struct Foo {
760     bar: i32,
761     baz: bool
762 }
763 ",
764         )
765     }
766
767     #[test]
768     fn test_add_field_in_other_file_from_usage() {
769         check_apply_diagnostic_fix_in_other_file(
770             r"
771             //- /main.rs
772             mod foo;
773
774             fn main() {
775                 <|>foo::Foo { bar: 3, baz: false};
776             }
777             //- /foo.rs
778             struct Foo {
779                 bar: i32
780             }
781             ",
782             r"
783             struct Foo {
784                 bar: i32,
785                 pub(crate) baz: bool
786             }
787             ",
788         )
789     }
790 }