diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2019-11-13 08:59:48 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2019-11-13 08:59:48 +0000 |
commit | d523366299c8d4813e9845c9402b8dd7b779856a (patch) | |
tree | 1e42b170b230271a5a906f0e9c19cbec27429929 | |
parent | 6ca0d79cff8746ea4f0c8fb8645d14b8b81bc7fc (diff) | |
parent | 4cea6bb6f11e28a2d1d2e023d46caa82b0f38796 (diff) |
Merge #2226
2226: Use strongly-typed ast building for early-return assist r=matklad a=matklad
Co-authored-by: Aleksey Kladov <[email protected]>
-rw-r--r-- | crates/ra_assists/src/assists/early_return.rs | 128 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/edit.rs | 2 | ||||
-rw-r--r-- | crates/ra_syntax/src/ast/make.rs | 71 | ||||
-rw-r--r-- | xtask/src/main.rs | 2 |
4 files changed, 132 insertions, 71 deletions
diff --git a/crates/ra_assists/src/assists/early_return.rs b/crates/ra_assists/src/assists/early_return.rs index 570a07a20..264412526 100644 --- a/crates/ra_assists/src/assists/early_return.rs +++ b/crates/ra_assists/src/assists/early_return.rs | |||
@@ -1,4 +1,4 @@ | |||
1 | use std::ops::RangeInclusive; | 1 | use std::{iter::once, ops::RangeInclusive}; |
2 | 2 | ||
3 | use hir::db::HirDatabase; | 3 | use hir::db::HirDatabase; |
4 | use ra_syntax::{ | 4 | use ra_syntax::{ |
@@ -38,27 +38,30 @@ use crate::{ | |||
38 | // ``` | 38 | // ``` |
39 | pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { | 39 | pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Option<Assist> { |
40 | let if_expr: ast::IfExpr = ctx.find_node_at_offset()?; | 40 | let if_expr: ast::IfExpr = ctx.find_node_at_offset()?; |
41 | if if_expr.else_branch().is_some() { | ||
42 | return None; | ||
43 | } | ||
44 | |||
41 | let cond = if_expr.condition()?; | 45 | let cond = if_expr.condition()?; |
42 | let mut if_let_ident: Option<String> = None; | ||
43 | 46 | ||
44 | // Check if there is an IfLet that we can handle. | 47 | // Check if there is an IfLet that we can handle. |
45 | match cond.pat() { | 48 | let if_let_pat = match cond.pat() { |
46 | None => {} // No IfLet, supported. | 49 | None => None, // No IfLet, supported. |
47 | Some(TupleStructPat(ref pat)) if pat.args().count() == 1usize => match &pat.path() { | 50 | Some(TupleStructPat(pat)) if pat.args().count() == 1 => { |
48 | Some(p) => match p.qualifier() { | 51 | let path = pat.path()?; |
49 | None => if_let_ident = Some(p.syntax().text().to_string()), | 52 | match path.qualifier() { |
50 | _ => return None, | 53 | None => { |
51 | }, | 54 | let bound_ident = pat.args().next().unwrap(); |
52 | _ => return None, | 55 | Some((path, bound_ident)) |
53 | }, | 56 | } |
54 | _ => return None, // Unsupported IfLet. | 57 | Some(_) => return None, |
58 | } | ||
59 | } | ||
60 | Some(_) => return None, // Unsupported IfLet. | ||
55 | }; | 61 | }; |
56 | 62 | ||
57 | let expr = cond.expr()?; | 63 | let cond_expr = cond.expr()?; |
58 | let then_block = if_expr.then_branch()?.block()?; | 64 | let then_block = if_expr.then_branch()?.block()?; |
59 | if if_expr.else_branch().is_some() { | ||
60 | return None; | ||
61 | } | ||
62 | 65 | ||
63 | let parent_block = if_expr.syntax().parent()?.ancestors().find_map(ast::Block::cast)?; | 66 | let parent_block = if_expr.syntax().parent()?.ancestors().find_map(ast::Block::cast)?; |
64 | 67 | ||
@@ -79,11 +82,11 @@ pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Opt | |||
79 | 82 | ||
80 | let parent_container = parent_block.syntax().parent()?.parent()?; | 83 | let parent_container = parent_block.syntax().parent()?.parent()?; |
81 | 84 | ||
82 | let early_expression = match parent_container.kind() { | 85 | let early_expression: ast::Expr = match parent_container.kind() { |
83 | WHILE_EXPR | LOOP_EXPR => Some("continue"), | 86 | WHILE_EXPR | LOOP_EXPR => make::expr_continue().into(), |
84 | FN_DEF => Some("return"), | 87 | FN_DEF => make::expr_return().into(), |
85 | _ => None, | 88 | _ => return None, |
86 | }?; | 89 | }; |
87 | 90 | ||
88 | if then_block.syntax().first_child_or_token().map(|t| t.kind() == L_CURLY).is_none() { | 91 | if then_block.syntax().first_child_or_token().map(|t| t.kind() == L_CURLY).is_none() { |
89 | return None; | 92 | return None; |
@@ -94,22 +97,43 @@ pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Opt | |||
94 | 97 | ||
95 | ctx.add_assist(AssistId("convert_to_guarded_return"), "convert to guarded return", |edit| { | 98 | ctx.add_assist(AssistId("convert_to_guarded_return"), "convert to guarded return", |edit| { |
96 | let if_indent_level = IndentLevel::from_node(&if_expr.syntax()); | 99 | let if_indent_level = IndentLevel::from_node(&if_expr.syntax()); |
97 | let new_block = match if_let_ident { | 100 | let new_block = match if_let_pat { |
98 | None => { | 101 | None => { |
99 | // If. | 102 | // If. |
100 | let early_expression = &(early_expression.to_owned() + ";"); | 103 | let early_expression = &(early_expression.syntax().to_string() + ";"); |
101 | let new_expr = | 104 | let new_expr = if_indent_level |
102 | if_indent_level.increase_indent(make::if_expression(&expr, early_expression)); | 105 | .increase_indent(make::if_expression(&cond_expr, early_expression)); |
103 | replace(new_expr, &then_block, &parent_block, &if_expr) | 106 | replace(new_expr.syntax(), &then_block, &parent_block, &if_expr) |
104 | } | 107 | } |
105 | Some(if_let_ident) => { | 108 | Some((path, bound_ident)) => { |
106 | // If-let. | 109 | // If-let. |
107 | let new_expr = if_indent_level.increase_indent(make::let_match_early( | 110 | let match_expr = { |
108 | expr, | 111 | let happy_arm = make::match_arm( |
109 | &if_let_ident, | 112 | once( |
110 | early_expression, | 113 | make::tuple_struct_pat( |
111 | )); | 114 | path, |
112 | replace(new_expr, &then_block, &parent_block, &if_expr) | 115 | once(make::bind_pat(make::name("it")).into()), |
116 | ) | ||
117 | .into(), | ||
118 | ), | ||
119 | make::expr_path(make::path_from_name_ref(make::name_ref("it"))).into(), | ||
120 | ); | ||
121 | |||
122 | let sad_arm = make::match_arm( | ||
123 | // FIXME: would be cool to use `None` or `Err(_)` if appropriate | ||
124 | once(make::placeholder_pat().into()), | ||
125 | early_expression.into(), | ||
126 | ); | ||
127 | |||
128 | make::expr_match(cond_expr, make::match_arm_list(vec![happy_arm, sad_arm])) | ||
129 | }; | ||
130 | |||
131 | let let_stmt = make::let_stmt( | ||
132 | make::bind_pat(make::name(&bound_ident.syntax().to_string())).into(), | ||
133 | Some(match_expr.into()), | ||
134 | ); | ||
135 | let let_stmt = if_indent_level.increase_indent(let_stmt); | ||
136 | replace(let_stmt.syntax(), &then_block, &parent_block, &if_expr) | ||
113 | } | 137 | } |
114 | }; | 138 | }; |
115 | edit.target(if_expr.syntax().text_range()); | 139 | edit.target(if_expr.syntax().text_range()); |
@@ -117,7 +141,7 @@ pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Opt | |||
117 | edit.set_cursor(cursor_position); | 141 | edit.set_cursor(cursor_position); |
118 | 142 | ||
119 | fn replace( | 143 | fn replace( |
120 | new_expr: impl AstNode, | 144 | new_expr: &SyntaxNode, |
121 | then_block: &Block, | 145 | then_block: &Block, |
122 | parent_block: &Block, | 146 | parent_block: &Block, |
123 | if_expr: &ast::IfExpr, | 147 | if_expr: &ast::IfExpr, |
@@ -130,7 +154,7 @@ pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Opt | |||
130 | } else { | 154 | } else { |
131 | end_of_then | 155 | end_of_then |
132 | }; | 156 | }; |
133 | let mut then_statements = new_expr.syntax().children_with_tokens().chain( | 157 | let mut then_statements = new_expr.children_with_tokens().chain( |
134 | then_block_items | 158 | then_block_items |
135 | .syntax() | 159 | .syntax() |
136 | .children_with_tokens() | 160 | .children_with_tokens() |
@@ -151,9 +175,10 @@ pub(crate) fn convert_to_guarded_return(ctx: AssistCtx<impl HirDatabase>) -> Opt | |||
151 | 175 | ||
152 | #[cfg(test)] | 176 | #[cfg(test)] |
153 | mod tests { | 177 | mod tests { |
154 | use super::*; | ||
155 | use crate::helpers::{check_assist, check_assist_not_applicable}; | 178 | use crate::helpers::{check_assist, check_assist_not_applicable}; |
156 | 179 | ||
180 | use super::*; | ||
181 | |||
157 | #[test] | 182 | #[test] |
158 | fn convert_inside_fn() { | 183 | fn convert_inside_fn() { |
159 | check_assist( | 184 | check_assist( |
@@ -204,7 +229,7 @@ mod tests { | |||
204 | bar(); | 229 | bar(); |
205 | le<|>t n = match n { | 230 | le<|>t n = match n { |
206 | Some(it) => it, | 231 | Some(it) => it, |
207 | None => return, | 232 | _ => return, |
208 | }; | 233 | }; |
209 | foo(n); | 234 | foo(n); |
210 | 235 | ||
@@ -216,6 +241,29 @@ mod tests { | |||
216 | } | 241 | } |
217 | 242 | ||
218 | #[test] | 243 | #[test] |
244 | fn convert_if_let_result() { | ||
245 | check_assist( | ||
246 | convert_to_guarded_return, | ||
247 | r#" | ||
248 | fn main() { | ||
249 | if<|> let Ok(x) = Err(92) { | ||
250 | foo(x); | ||
251 | } | ||
252 | } | ||
253 | "#, | ||
254 | r#" | ||
255 | fn main() { | ||
256 | le<|>t x = match Err(92) { | ||
257 | Ok(it) => it, | ||
258 | _ => return, | ||
259 | }; | ||
260 | foo(x); | ||
261 | } | ||
262 | "#, | ||
263 | ); | ||
264 | } | ||
265 | |||
266 | #[test] | ||
219 | fn convert_let_ok_inside_fn() { | 267 | fn convert_let_ok_inside_fn() { |
220 | check_assist( | 268 | check_assist( |
221 | convert_to_guarded_return, | 269 | convert_to_guarded_return, |
@@ -235,7 +283,7 @@ mod tests { | |||
235 | bar(); | 283 | bar(); |
236 | le<|>t n = match n { | 284 | le<|>t n = match n { |
237 | Ok(it) => it, | 285 | Ok(it) => it, |
238 | None => return, | 286 | _ => return, |
239 | }; | 287 | }; |
240 | foo(n); | 288 | foo(n); |
241 | 289 | ||
@@ -293,7 +341,7 @@ mod tests { | |||
293 | while true { | 341 | while true { |
294 | le<|>t n = match n { | 342 | le<|>t n = match n { |
295 | Some(it) => it, | 343 | Some(it) => it, |
296 | None => continue, | 344 | _ => continue, |
297 | }; | 345 | }; |
298 | foo(n); | 346 | foo(n); |
299 | bar(); | 347 | bar(); |
@@ -350,7 +398,7 @@ mod tests { | |||
350 | loop { | 398 | loop { |
351 | le<|>t n = match n { | 399 | le<|>t n = match n { |
352 | Some(it) => it, | 400 | Some(it) => it, |
353 | None => continue, | 401 | _ => continue, |
354 | }; | 402 | }; |
355 | foo(n); | 403 | foo(n); |
356 | bar(); | 404 | bar(); |
diff --git a/crates/ra_syntax/src/ast/edit.rs b/crates/ra_syntax/src/ast/edit.rs index 47bdbb81a..6f005a2d8 100644 --- a/crates/ra_syntax/src/ast/edit.rs +++ b/crates/ra_syntax/src/ast/edit.rs | |||
@@ -358,7 +358,7 @@ fn replace_children<N: AstNode>( | |||
358 | fn test_increase_indent() { | 358 | fn test_increase_indent() { |
359 | let arm_list = { | 359 | let arm_list = { |
360 | let arm = make::match_arm(iter::once(make::placeholder_pat().into()), make::expr_unit()); | 360 | let arm = make::match_arm(iter::once(make::placeholder_pat().into()), make::expr_unit()); |
361 | make::match_arm_list(vec![arm.clone(), arm].into_iter()) | 361 | make::match_arm_list(vec![arm.clone(), arm]) |
362 | }; | 362 | }; |
363 | assert_eq!( | 363 | assert_eq!( |
364 | arm_list.syntax().to_string(), | 364 | arm_list.syntax().to_string(), |
diff --git a/crates/ra_syntax/src/ast/make.rs b/crates/ra_syntax/src/ast/make.rs index 95062ef6c..9749327fa 100644 --- a/crates/ra_syntax/src/ast/make.rs +++ b/crates/ra_syntax/src/ast/make.rs | |||
@@ -4,6 +4,10 @@ use itertools::Itertools; | |||
4 | 4 | ||
5 | use crate::{ast, AstNode, SourceFile}; | 5 | use crate::{ast, AstNode, SourceFile}; |
6 | 6 | ||
7 | pub fn name(text: &str) -> ast::Name { | ||
8 | ast_from_text(&format!("mod {};", text)) | ||
9 | } | ||
10 | |||
7 | pub fn name_ref(text: &str) -> ast::NameRef { | 11 | pub fn name_ref(text: &str) -> ast::NameRef { |
8 | ast_from_text(&format!("fn f() {{ {}; }}", text)) | 12 | ast_from_text(&format!("fn f() {{ {}; }}", text)) |
9 | } | 13 | } |
@@ -43,6 +47,21 @@ pub fn expr_unit() -> ast::Expr { | |||
43 | pub fn expr_unimplemented() -> ast::Expr { | 47 | pub fn expr_unimplemented() -> ast::Expr { |
44 | expr_from_text("unimplemented!()") | 48 | expr_from_text("unimplemented!()") |
45 | } | 49 | } |
50 | pub fn expr_path(path: ast::Path) -> ast::Expr { | ||
51 | expr_from_text(&path.syntax().to_string()) | ||
52 | } | ||
53 | pub fn expr_continue() -> ast::Expr { | ||
54 | expr_from_text("continue") | ||
55 | } | ||
56 | pub fn expr_break() -> ast::Expr { | ||
57 | expr_from_text("break") | ||
58 | } | ||
59 | pub fn expr_return() -> ast::Expr { | ||
60 | expr_from_text("return") | ||
61 | } | ||
62 | pub fn expr_match(expr: ast::Expr, match_arm_list: ast::MatchArmList) -> ast::Expr { | ||
63 | expr_from_text(&format!("match {} {}", expr.syntax(), match_arm_list.syntax())) | ||
64 | } | ||
46 | fn expr_from_text(text: &str) -> ast::Expr { | 65 | fn expr_from_text(text: &str) -> ast::Expr { |
47 | ast_from_text(&format!("const C: () = {};", text)) | 66 | ast_from_text(&format!("const C: () = {};", text)) |
48 | } | 67 | } |
@@ -65,9 +84,9 @@ pub fn placeholder_pat() -> ast::PlaceholderPat { | |||
65 | 84 | ||
66 | pub fn tuple_struct_pat( | 85 | pub fn tuple_struct_pat( |
67 | path: ast::Path, | 86 | path: ast::Path, |
68 | pats: impl Iterator<Item = ast::Pat>, | 87 | pats: impl IntoIterator<Item = ast::Pat>, |
69 | ) -> ast::TupleStructPat { | 88 | ) -> ast::TupleStructPat { |
70 | let pats_str = pats.map(|p| p.syntax().to_string()).join(", "); | 89 | let pats_str = pats.into_iter().map(|p| p.syntax().to_string()).join(", "); |
71 | return from_text(&format!("{}({})", path.syntax(), pats_str)); | 90 | return from_text(&format!("{}({})", path.syntax(), pats_str)); |
72 | 91 | ||
73 | fn from_text(text: &str) -> ast::TupleStructPat { | 92 | fn from_text(text: &str) -> ast::TupleStructPat { |
@@ -75,8 +94,8 @@ pub fn tuple_struct_pat( | |||
75 | } | 94 | } |
76 | } | 95 | } |
77 | 96 | ||
78 | pub fn record_pat(path: ast::Path, pats: impl Iterator<Item = ast::Pat>) -> ast::RecordPat { | 97 | pub fn record_pat(path: ast::Path, pats: impl IntoIterator<Item = ast::Pat>) -> ast::RecordPat { |
79 | let pats_str = pats.map(|p| p.syntax().to_string()).join(", "); | 98 | let pats_str = pats.into_iter().map(|p| p.syntax().to_string()).join(", "); |
80 | return from_text(&format!("{} {{ {} }}", path.syntax(), pats_str)); | 99 | return from_text(&format!("{} {{ {} }}", path.syntax(), pats_str)); |
81 | 100 | ||
82 | fn from_text(text: &str) -> ast::RecordPat { | 101 | fn from_text(text: &str) -> ast::RecordPat { |
@@ -92,8 +111,8 @@ pub fn path_pat(path: ast::Path) -> ast::PathPat { | |||
92 | } | 111 | } |
93 | } | 112 | } |
94 | 113 | ||
95 | pub fn match_arm(pats: impl Iterator<Item = ast::Pat>, expr: ast::Expr) -> ast::MatchArm { | 114 | pub fn match_arm(pats: impl IntoIterator<Item = ast::Pat>, expr: ast::Expr) -> ast::MatchArm { |
96 | let pats_str = pats.map(|p| p.syntax().to_string()).join(" | "); | 115 | let pats_str = pats.into_iter().map(|p| p.syntax().to_string()).join(" | "); |
97 | return from_text(&format!("{} => {}", pats_str, expr.syntax())); | 116 | return from_text(&format!("{} => {}", pats_str, expr.syntax())); |
98 | 117 | ||
99 | fn from_text(text: &str) -> ast::MatchArm { | 118 | fn from_text(text: &str) -> ast::MatchArm { |
@@ -101,8 +120,8 @@ pub fn match_arm(pats: impl Iterator<Item = ast::Pat>, expr: ast::Expr) -> ast:: | |||
101 | } | 120 | } |
102 | } | 121 | } |
103 | 122 | ||
104 | pub fn match_arm_list(arms: impl Iterator<Item = ast::MatchArm>) -> ast::MatchArmList { | 123 | pub fn match_arm_list(arms: impl IntoIterator<Item = ast::MatchArm>) -> ast::MatchArmList { |
105 | let arms_str = arms.map(|arm| format!("\n {}", arm.syntax())).join(","); | 124 | let arms_str = arms.into_iter().map(|arm| format!("\n {}", arm.syntax())).join(","); |
106 | return from_text(&format!("{},\n", arms_str)); | 125 | return from_text(&format!("{},\n", arms_str)); |
107 | 126 | ||
108 | fn from_text(text: &str) -> ast::MatchArmList { | 127 | fn from_text(text: &str) -> ast::MatchArmList { |
@@ -110,25 +129,11 @@ pub fn match_arm_list(arms: impl Iterator<Item = ast::MatchArm>) -> ast::MatchAr | |||
110 | } | 129 | } |
111 | } | 130 | } |
112 | 131 | ||
113 | pub fn let_match_early(expr: ast::Expr, path: &str, early_expression: &str) -> ast::LetStmt { | 132 | pub fn where_pred( |
114 | return from_text(&format!( | 133 | path: ast::Path, |
115 | r#"let {} = match {} {{ | 134 | bounds: impl IntoIterator<Item = ast::TypeBound>, |
116 | {}(it) => it, | 135 | ) -> ast::WherePred { |
117 | None => {}, | 136 | let bounds = bounds.into_iter().map(|b| b.syntax().to_string()).join(" + "); |
118 | }};"#, | ||
119 | expr.syntax().text(), | ||
120 | expr.syntax().text(), | ||
121 | path, | ||
122 | early_expression | ||
123 | )); | ||
124 | |||
125 | fn from_text(text: &str) -> ast::LetStmt { | ||
126 | ast_from_text(&format!("fn f() {{ {} }}", text)) | ||
127 | } | ||
128 | } | ||
129 | |||
130 | pub fn where_pred(path: ast::Path, bounds: impl Iterator<Item = ast::TypeBound>) -> ast::WherePred { | ||
131 | let bounds = bounds.map(|b| b.syntax().to_string()).join(" + "); | ||
132 | return from_text(&format!("{}: {}", path.syntax(), bounds)); | 137 | return from_text(&format!("{}: {}", path.syntax(), bounds)); |
133 | 138 | ||
134 | fn from_text(text: &str) -> ast::WherePred { | 139 | fn from_text(text: &str) -> ast::WherePred { |
@@ -136,8 +141,8 @@ pub fn where_pred(path: ast::Path, bounds: impl Iterator<Item = ast::TypeBound>) | |||
136 | } | 141 | } |
137 | } | 142 | } |
138 | 143 | ||
139 | pub fn where_clause(preds: impl Iterator<Item = ast::WherePred>) -> ast::WhereClause { | 144 | pub fn where_clause(preds: impl IntoIterator<Item = ast::WherePred>) -> ast::WhereClause { |
140 | let preds = preds.map(|p| p.syntax().to_string()).join(", "); | 145 | let preds = preds.into_iter().map(|p| p.syntax().to_string()).join(", "); |
141 | return from_text(preds.as_str()); | 146 | return from_text(preds.as_str()); |
142 | 147 | ||
143 | fn from_text(text: &str) -> ast::WhereClause { | 148 | fn from_text(text: &str) -> ast::WhereClause { |
@@ -153,6 +158,14 @@ pub fn if_expression(condition: &ast::Expr, statement: &str) -> ast::IfExpr { | |||
153 | )) | 158 | )) |
154 | } | 159 | } |
155 | 160 | ||
161 | pub fn let_stmt(pattern: ast::Pat, initializer: Option<ast::Expr>) -> ast::LetStmt { | ||
162 | let text = match initializer { | ||
163 | Some(it) => format!("let {} = {};", pattern.syntax(), it.syntax()), | ||
164 | None => format!("let {};", pattern.syntax()), | ||
165 | }; | ||
166 | ast_from_text(&format!("fn f() {{ {} }}", text)) | ||
167 | } | ||
168 | |||
156 | fn ast_from_text<N: AstNode>(text: &str) -> N { | 169 | fn ast_from_text<N: AstNode>(text: &str) -> N { |
157 | let parse = SourceFile::parse(text); | 170 | let parse = SourceFile::parse(text); |
158 | let res = parse.tree().syntax().descendants().find_map(N::cast).unwrap(); | 171 | let res = parse.tree().syntax().descendants().find_map(N::cast).unwrap(); |
diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 16bddfb28..c46eaa407 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs | |||
@@ -19,7 +19,7 @@ use xtask::{ | |||
19 | }; | 19 | }; |
20 | 20 | ||
21 | // Latest stable, feel free to send a PR if this lags behind. | 21 | // Latest stable, feel free to send a PR if this lags behind. |
22 | const REQUIRED_RUST_VERSION: u32 = 38; | 22 | const REQUIRED_RUST_VERSION: u32 = 39; |
23 | 23 | ||
24 | struct InstallOpt { | 24 | struct InstallOpt { |
25 | client: Option<ClientOpt>, | 25 | client: Option<ClientOpt>, |