diff options
-rw-r--r-- | crates/ra_hir/src/diagnostics.rs | 28 | ||||
-rw-r--r-- | crates/ra_hir/src/expr/validation.rs | 53 | ||||
-rw-r--r-- | crates/ra_hir/src/name.rs | 2 | ||||
-rw-r--r-- | crates/ra_hir/src/ty/infer.rs | 19 | ||||
-rw-r--r-- | crates/ra_ide_api/src/diagnostics.rs | 215 | ||||
-rw-r--r-- | crates/ra_syntax/src/ptr.rs | 7 |
6 files changed, 319 insertions, 5 deletions
diff --git a/crates/ra_hir/src/diagnostics.rs b/crates/ra_hir/src/diagnostics.rs index 301109cb8..475dd5766 100644 --- a/crates/ra_hir/src/diagnostics.rs +++ b/crates/ra_hir/src/diagnostics.rs | |||
@@ -143,3 +143,31 @@ impl AstDiagnostic for MissingFields { | |||
143 | ast::RecordFieldList::cast(node).unwrap() | 143 | ast::RecordFieldList::cast(node).unwrap() |
144 | } | 144 | } |
145 | } | 145 | } |
146 | |||
147 | #[derive(Debug)] | ||
148 | pub struct MissingOkInTailExpr { | ||
149 | pub file: HirFileId, | ||
150 | pub expr: AstPtr<ast::Expr>, | ||
151 | } | ||
152 | |||
153 | impl Diagnostic for MissingOkInTailExpr { | ||
154 | fn message(&self) -> String { | ||
155 | "wrap return expression in Ok".to_string() | ||
156 | } | ||
157 | fn source(&self) -> Source<SyntaxNodePtr> { | ||
158 | Source { file_id: self.file, ast: self.expr.into() } | ||
159 | } | ||
160 | fn as_any(&self) -> &(dyn Any + Send + 'static) { | ||
161 | self | ||
162 | } | ||
163 | } | ||
164 | |||
165 | impl AstDiagnostic for MissingOkInTailExpr { | ||
166 | type AST = ast::Expr; | ||
167 | |||
168 | fn ast(&self, db: &impl HirDatabase) -> Self::AST { | ||
169 | let root = db.parse_or_expand(self.file).unwrap(); | ||
170 | let node = self.source().ast.to_node(&root); | ||
171 | ast::Expr::cast(node).unwrap() | ||
172 | } | ||
173 | } | ||
diff --git a/crates/ra_hir/src/expr/validation.rs b/crates/ra_hir/src/expr/validation.rs index 62f7d41f5..5d9d59ff8 100644 --- a/crates/ra_hir/src/expr/validation.rs +++ b/crates/ra_hir/src/expr/validation.rs | |||
@@ -6,11 +6,14 @@ use ra_syntax::ast::{AstNode, RecordLit}; | |||
6 | use super::{Expr, ExprId, RecordLitField}; | 6 | use super::{Expr, ExprId, RecordLitField}; |
7 | use crate::{ | 7 | use crate::{ |
8 | adt::AdtDef, | 8 | adt::AdtDef, |
9 | diagnostics::{DiagnosticSink, MissingFields}, | 9 | diagnostics::{DiagnosticSink, MissingFields, MissingOkInTailExpr}, |
10 | expr::AstPtr, | 10 | expr::AstPtr, |
11 | ty::InferenceResult, | 11 | name, |
12 | Function, HasSource, HirDatabase, Name, Path, | 12 | path::{PathKind, PathSegment}, |
13 | ty::{ApplicationTy, InferenceResult, Ty, TypeCtor}, | ||
14 | Function, HasSource, HirDatabase, ModuleDef, Name, Path, PerNs, Resolution, | ||
13 | }; | 15 | }; |
16 | use ra_syntax::ast; | ||
14 | 17 | ||
15 | pub(crate) struct ExprValidator<'a, 'b: 'a> { | 18 | pub(crate) struct ExprValidator<'a, 'b: 'a> { |
16 | func: Function, | 19 | func: Function, |
@@ -29,11 +32,17 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
29 | 32 | ||
30 | pub(crate) fn validate_body(&mut self, db: &impl HirDatabase) { | 33 | pub(crate) fn validate_body(&mut self, db: &impl HirDatabase) { |
31 | let body = self.func.body(db); | 34 | let body = self.func.body(db); |
35 | |||
32 | for e in body.exprs() { | 36 | for e in body.exprs() { |
33 | if let (id, Expr::RecordLit { path, fields, spread }) = e { | 37 | if let (id, Expr::RecordLit { path, fields, spread }) = e { |
34 | self.validate_record_literal(id, path, fields, *spread, db); | 38 | self.validate_record_literal(id, path, fields, *spread, db); |
35 | } | 39 | } |
36 | } | 40 | } |
41 | |||
42 | let body_expr = &body[body.body_expr()]; | ||
43 | if let Expr::Block { statements: _, tail: Some(t) } = body_expr { | ||
44 | self.validate_results_in_tail_expr(*t, db); | ||
45 | } | ||
37 | } | 46 | } |
38 | 47 | ||
39 | fn validate_record_literal( | 48 | fn validate_record_literal( |
@@ -87,4 +96,42 @@ impl<'a, 'b> ExprValidator<'a, 'b> { | |||
87 | }) | 96 | }) |
88 | } | 97 | } |
89 | } | 98 | } |
99 | |||
100 | fn validate_results_in_tail_expr(&mut self, id: ExprId, db: &impl HirDatabase) { | ||
101 | let mismatch = match self.infer.type_mismatch_for_expr(id) { | ||
102 | Some(m) => m, | ||
103 | None => return, | ||
104 | }; | ||
105 | |||
106 | let std_result_path = Path { | ||
107 | kind: PathKind::Abs, | ||
108 | segments: vec![ | ||
109 | PathSegment { name: name::STD, args_and_bindings: None }, | ||
110 | PathSegment { name: name::RESULT_MOD, args_and_bindings: None }, | ||
111 | PathSegment { name: name::RESULT_TYPE, args_and_bindings: None }, | ||
112 | ], | ||
113 | }; | ||
114 | |||
115 | let resolver = self.func.resolver(db); | ||
116 | let std_result_enum = | ||
117 | match resolver.resolve_path_segments(db, &std_result_path).into_fully_resolved() { | ||
118 | PerNs { types: Some(Resolution::Def(ModuleDef::Enum(e))), .. } => e, | ||
119 | _ => return, | ||
120 | }; | ||
121 | |||
122 | let std_result_ctor = TypeCtor::Adt(AdtDef::Enum(std_result_enum)); | ||
123 | let params = match &mismatch.expected { | ||
124 | Ty::Apply(ApplicationTy { ctor, parameters }) if ctor == &std_result_ctor => parameters, | ||
125 | _ => return, | ||
126 | }; | ||
127 | |||
128 | if params.len() == 2 && ¶ms[0] == &mismatch.actual { | ||
129 | let source_map = self.func.body_source_map(db); | ||
130 | let file_id = self.func.source(db).file_id; | ||
131 | |||
132 | if let Some(expr) = source_map.expr_syntax(id).and_then(|n| n.cast::<ast::Expr>()) { | ||
133 | self.sink.push(MissingOkInTailExpr { file: file_id, expr }); | ||
134 | } | ||
135 | } | ||
136 | } | ||
90 | } | 137 | } |
diff --git a/crates/ra_hir/src/name.rs b/crates/ra_hir/src/name.rs index 6d14eea8e..9c4822d91 100644 --- a/crates/ra_hir/src/name.rs +++ b/crates/ra_hir/src/name.rs | |||
@@ -120,6 +120,8 @@ pub(crate) const TRY: Name = Name::new(SmolStr::new_inline_from_ascii(3, b"Try") | |||
120 | pub(crate) const OK: Name = Name::new(SmolStr::new_inline_from_ascii(2, b"Ok")); | 120 | pub(crate) const OK: Name = Name::new(SmolStr::new_inline_from_ascii(2, b"Ok")); |
121 | pub(crate) const FUTURE_MOD: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"future")); | 121 | pub(crate) const FUTURE_MOD: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"future")); |
122 | pub(crate) const FUTURE_TYPE: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"Future")); | 122 | pub(crate) const FUTURE_TYPE: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"Future")); |
123 | pub(crate) const RESULT_MOD: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"result")); | ||
124 | pub(crate) const RESULT_TYPE: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"Result")); | ||
123 | pub(crate) const OUTPUT: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"Output")); | 125 | pub(crate) const OUTPUT: Name = Name::new(SmolStr::new_inline_from_ascii(6, b"Output")); |
124 | 126 | ||
125 | fn resolve_name(text: &SmolStr) -> SmolStr { | 127 | fn resolve_name(text: &SmolStr) -> SmolStr { |
diff --git a/crates/ra_hir/src/ty/infer.rs b/crates/ra_hir/src/ty/infer.rs index b33de5687..d94e8154b 100644 --- a/crates/ra_hir/src/ty/infer.rs +++ b/crates/ra_hir/src/ty/infer.rs | |||
@@ -106,6 +106,13 @@ impl Default for BindingMode { | |||
106 | } | 106 | } |
107 | } | 107 | } |
108 | 108 | ||
109 | /// A mismatch between an expected and an inferred type. | ||
110 | #[derive(Clone, PartialEq, Eq, Debug, Hash)] | ||
111 | pub struct TypeMismatch { | ||
112 | pub expected: Ty, | ||
113 | pub actual: Ty, | ||
114 | } | ||
115 | |||
109 | /// The result of type inference: A mapping from expressions and patterns to types. | 116 | /// The result of type inference: A mapping from expressions and patterns to types. |
110 | #[derive(Clone, PartialEq, Eq, Debug, Default)] | 117 | #[derive(Clone, PartialEq, Eq, Debug, Default)] |
111 | pub struct InferenceResult { | 118 | pub struct InferenceResult { |
@@ -120,6 +127,7 @@ pub struct InferenceResult { | |||
120 | diagnostics: Vec<InferenceDiagnostic>, | 127 | diagnostics: Vec<InferenceDiagnostic>, |
121 | pub(super) type_of_expr: ArenaMap<ExprId, Ty>, | 128 | pub(super) type_of_expr: ArenaMap<ExprId, Ty>, |
122 | pub(super) type_of_pat: ArenaMap<PatId, Ty>, | 129 | pub(super) type_of_pat: ArenaMap<PatId, Ty>, |
130 | pub(super) type_mismatches: ArenaMap<ExprId, TypeMismatch>, | ||
123 | } | 131 | } |
124 | 132 | ||
125 | impl InferenceResult { | 133 | impl InferenceResult { |
@@ -141,6 +149,9 @@ impl InferenceResult { | |||
141 | pub fn assoc_resolutions_for_pat(&self, id: PatId) -> Option<ImplItem> { | 149 | pub fn assoc_resolutions_for_pat(&self, id: PatId) -> Option<ImplItem> { |
142 | self.assoc_resolutions.get(&id.into()).copied() | 150 | self.assoc_resolutions.get(&id.into()).copied() |
143 | } | 151 | } |
152 | pub fn type_mismatch_for_expr(&self, expr: ExprId) -> Option<&TypeMismatch> { | ||
153 | self.type_mismatches.get(expr) | ||
154 | } | ||
144 | pub(crate) fn add_diagnostics( | 155 | pub(crate) fn add_diagnostics( |
145 | &self, | 156 | &self, |
146 | db: &impl HirDatabase, | 157 | db: &impl HirDatabase, |
@@ -1345,9 +1356,15 @@ impl<'a, D: HirDatabase> InferenceContext<'a, D> { | |||
1345 | }; | 1356 | }; |
1346 | // use a new type variable if we got Ty::Unknown here | 1357 | // use a new type variable if we got Ty::Unknown here |
1347 | let ty = self.insert_type_vars_shallow(ty); | 1358 | let ty = self.insert_type_vars_shallow(ty); |
1348 | self.unify(&ty, &expected.ty); | 1359 | let could_unify = self.unify(&ty, &expected.ty); |
1349 | let ty = self.resolve_ty_as_possible(&mut vec![], ty); | 1360 | let ty = self.resolve_ty_as_possible(&mut vec![], ty); |
1350 | self.write_expr_ty(tgt_expr, ty.clone()); | 1361 | self.write_expr_ty(tgt_expr, ty.clone()); |
1362 | if !could_unify { | ||
1363 | self.result.type_mismatches.insert( | ||
1364 | tgt_expr, | ||
1365 | TypeMismatch { expected: expected.ty.clone(), actual: ty.clone() }, | ||
1366 | ); | ||
1367 | } | ||
1351 | ty | 1368 | ty |
1352 | } | 1369 | } |
1353 | 1370 | ||
diff --git a/crates/ra_ide_api/src/diagnostics.rs b/crates/ra_ide_api/src/diagnostics.rs index c2b959cb3..1a4882824 100644 --- a/crates/ra_ide_api/src/diagnostics.rs +++ b/crates/ra_ide_api/src/diagnostics.rs | |||
@@ -75,6 +75,19 @@ pub(crate) fn diagnostics(db: &RootDatabase, file_id: FileId) -> Vec<Diagnostic> | |||
75 | severity: Severity::Error, | 75 | severity: Severity::Error, |
76 | fix: Some(fix), | 76 | fix: Some(fix), |
77 | }) | 77 | }) |
78 | }) | ||
79 | .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { | ||
80 | let node = d.ast(db); | ||
81 | let mut builder = TextEditBuilder::default(); | ||
82 | let replacement = format!("Ok({})", node.syntax()); | ||
83 | builder.replace(node.syntax().text_range(), replacement); | ||
84 | let fix = SourceChange::source_file_edit_from("wrap with ok", file_id, builder.finish()); | ||
85 | res.borrow_mut().push(Diagnostic { | ||
86 | range: d.highlight_range(), | ||
87 | message: d.message(), | ||
88 | severity: Severity::Error, | ||
89 | fix: Some(fix), | ||
90 | }) | ||
78 | }); | 91 | }); |
79 | if let Some(m) = source_binder::module_from_file_id(db, file_id) { | 92 | if let Some(m) = source_binder::module_from_file_id(db, file_id) { |
80 | m.diagnostics(db, &mut sink); | 93 | m.diagnostics(db, &mut sink); |
@@ -171,10 +184,11 @@ fn check_struct_shorthand_initialization( | |||
171 | #[cfg(test)] | 184 | #[cfg(test)] |
172 | mod tests { | 185 | mod tests { |
173 | use insta::assert_debug_snapshot_matches; | 186 | use insta::assert_debug_snapshot_matches; |
187 | use join_to_string::join; | ||
174 | use ra_syntax::SourceFile; | 188 | use ra_syntax::SourceFile; |
175 | use test_utils::assert_eq_text; | 189 | use test_utils::assert_eq_text; |
176 | 190 | ||
177 | use crate::mock_analysis::single_file; | 191 | use crate::mock_analysis::{analysis_and_position, single_file}; |
178 | 192 | ||
179 | use super::*; | 193 | use super::*; |
180 | 194 | ||
@@ -203,6 +217,48 @@ mod tests { | |||
203 | assert_eq_text!(after, &actual); | 217 | assert_eq_text!(after, &actual); |
204 | } | 218 | } |
205 | 219 | ||
220 | /// Takes a multi-file input fixture with annotated cursor positions, | ||
221 | /// and checks that: | ||
222 | /// * a diagnostic is produced | ||
223 | /// * this diagnostic touches the input cursor position | ||
224 | /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied | ||
225 | fn check_apply_diagnostic_fix_from_position(fixture: &str, after: &str) { | ||
226 | let (analysis, file_position) = analysis_and_position(fixture); | ||
227 | let diagnostic = analysis.diagnostics(file_position.file_id).unwrap().pop().unwrap(); | ||
228 | let mut fix = diagnostic.fix.unwrap(); | ||
229 | let edit = fix.source_file_edits.pop().unwrap().edit; | ||
230 | let target_file_contents = analysis.file_text(file_position.file_id).unwrap(); | ||
231 | let actual = edit.apply(&target_file_contents); | ||
232 | |||
233 | // Strip indent and empty lines from `after`, to match the behaviour of | ||
234 | // `parse_fixture` called from `analysis_and_position`. | ||
235 | let margin = fixture | ||
236 | .lines() | ||
237 | .filter(|it| it.trim_start().starts_with("//-")) | ||
238 | .map(|it| it.len() - it.trim_start().len()) | ||
239 | .next() | ||
240 | .expect("empty fixture"); | ||
241 | let after = join(after.lines().filter_map(|line| { | ||
242 | if line.len() > margin { | ||
243 | Some(&line[margin..]) | ||
244 | } else { | ||
245 | None | ||
246 | } | ||
247 | })) | ||
248 | .separator("\n") | ||
249 | .suffix("\n") | ||
250 | .to_string(); | ||
251 | |||
252 | assert_eq_text!(&after, &actual); | ||
253 | assert!( | ||
254 | diagnostic.range.start() <= file_position.offset | ||
255 | && diagnostic.range.end() >= file_position.offset, | ||
256 | "diagnostic range {} does not touch cursor position {}", | ||
257 | diagnostic.range, | ||
258 | file_position.offset | ||
259 | ); | ||
260 | } | ||
261 | |||
206 | fn check_apply_diagnostic_fix(before: &str, after: &str) { | 262 | fn check_apply_diagnostic_fix(before: &str, after: &str) { |
207 | let (analysis, file_id) = single_file(before); | 263 | let (analysis, file_id) = single_file(before); |
208 | let diagnostic = analysis.diagnostics(file_id).unwrap().pop().unwrap(); | 264 | let diagnostic = analysis.diagnostics(file_id).unwrap().pop().unwrap(); |
@@ -212,6 +268,14 @@ mod tests { | |||
212 | assert_eq_text!(after, &actual); | 268 | assert_eq_text!(after, &actual); |
213 | } | 269 | } |
214 | 270 | ||
271 | /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics | ||
272 | /// apply to the file containing the cursor. | ||
273 | fn check_no_diagnostic_for_target_file(fixture: &str) { | ||
274 | let (analysis, file_position) = analysis_and_position(fixture); | ||
275 | let diagnostics = analysis.diagnostics(file_position.file_id).unwrap(); | ||
276 | assert_eq!(diagnostics.len(), 0); | ||
277 | } | ||
278 | |||
215 | fn check_no_diagnostic(content: &str) { | 279 | fn check_no_diagnostic(content: &str) { |
216 | let (analysis, file_id) = single_file(content); | 280 | let (analysis, file_id) = single_file(content); |
217 | let diagnostics = analysis.diagnostics(file_id).unwrap(); | 281 | let diagnostics = analysis.diagnostics(file_id).unwrap(); |
@@ -219,6 +283,155 @@ mod tests { | |||
219 | } | 283 | } |
220 | 284 | ||
221 | #[test] | 285 | #[test] |
286 | fn test_wrap_return_type() { | ||
287 | let before = r#" | ||
288 | //- /main.rs | ||
289 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
290 | |||
291 | fn div(x: i32, y: i32) -> Result<i32, String> { | ||
292 | if y == 0 { | ||
293 | return Err("div by zero".into()); | ||
294 | } | ||
295 | x / y<|> | ||
296 | } | ||
297 | |||
298 | //- /std/lib.rs | ||
299 | pub mod string { | ||
300 | pub struct String { } | ||
301 | } | ||
302 | pub mod result { | ||
303 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
304 | } | ||
305 | "#; | ||
306 | let after = r#" | ||
307 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
308 | |||
309 | fn div(x: i32, y: i32) -> Result<i32, String> { | ||
310 | if y == 0 { | ||
311 | return Err("div by zero".into()); | ||
312 | } | ||
313 | Ok(x / y) | ||
314 | } | ||
315 | "#; | ||
316 | check_apply_diagnostic_fix_from_position(before, after); | ||
317 | } | ||
318 | |||
319 | #[test] | ||
320 | fn test_wrap_return_type_handles_generic_functions() { | ||
321 | let before = r#" | ||
322 | //- /main.rs | ||
323 | use std::result::Result::{self, Ok, Err}; | ||
324 | |||
325 | fn div<T>(x: T) -> Result<T, i32> { | ||
326 | if x == 0 { | ||
327 | return Err(7); | ||
328 | } | ||
329 | <|>x | ||
330 | } | ||
331 | |||
332 | //- /std/lib.rs | ||
333 | pub mod result { | ||
334 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
335 | } | ||
336 | "#; | ||
337 | let after = r#" | ||
338 | use std::result::Result::{self, Ok, Err}; | ||
339 | |||
340 | fn div<T>(x: T) -> Result<T, i32> { | ||
341 | if x == 0 { | ||
342 | return Err(7); | ||
343 | } | ||
344 | Ok(x) | ||
345 | } | ||
346 | "#; | ||
347 | check_apply_diagnostic_fix_from_position(before, after); | ||
348 | } | ||
349 | |||
350 | #[test] | ||
351 | fn test_wrap_return_type_handles_type_aliases() { | ||
352 | let before = r#" | ||
353 | //- /main.rs | ||
354 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
355 | |||
356 | type MyResult<T> = Result<T, String>; | ||
357 | |||
358 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
359 | if y == 0 { | ||
360 | return Err("div by zero".into()); | ||
361 | } | ||
362 | x <|>/ y | ||
363 | } | ||
364 | |||
365 | //- /std/lib.rs | ||
366 | pub mod string { | ||
367 | pub struct String { } | ||
368 | } | ||
369 | pub mod result { | ||
370 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
371 | } | ||
372 | "#; | ||
373 | let after = r#" | ||
374 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
375 | |||
376 | type MyResult<T> = Result<T, String>; | ||
377 | fn div(x: i32, y: i32) -> MyResult<i32> { | ||
378 | if y == 0 { | ||
379 | return Err("div by zero".into()); | ||
380 | } | ||
381 | Ok(x / y) | ||
382 | } | ||
383 | "#; | ||
384 | check_apply_diagnostic_fix_from_position(before, after); | ||
385 | } | ||
386 | |||
387 | #[test] | ||
388 | fn test_wrap_return_type_not_applicable_when_expr_type_does_not_match_ok_type() { | ||
389 | let content = r#" | ||
390 | //- /main.rs | ||
391 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
392 | |||
393 | fn foo() -> Result<String, i32> { | ||
394 | 0<|> | ||
395 | } | ||
396 | |||
397 | //- /std/lib.rs | ||
398 | pub mod string { | ||
399 | pub struct String { } | ||
400 | } | ||
401 | pub mod result { | ||
402 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
403 | } | ||
404 | "#; | ||
405 | check_no_diagnostic_for_target_file(content); | ||
406 | } | ||
407 | |||
408 | #[test] | ||
409 | fn test_wrap_return_type_not_applicable_when_return_type_is_not_result() { | ||
410 | let content = r#" | ||
411 | //- /main.rs | ||
412 | use std::{string::String, result::Result::{self, Ok, Err}}; | ||
413 | |||
414 | enum SomeOtherEnum { | ||
415 | Ok(i32), | ||
416 | Err(String), | ||
417 | } | ||
418 | |||
419 | fn foo() -> SomeOtherEnum { | ||
420 | 0<|> | ||
421 | } | ||
422 | |||
423 | //- /std/lib.rs | ||
424 | pub mod string { | ||
425 | pub struct String { } | ||
426 | } | ||
427 | pub mod result { | ||
428 | pub enum Result<T, E> { Ok(T), Err(E) } | ||
429 | } | ||
430 | "#; | ||
431 | check_no_diagnostic_for_target_file(content); | ||
432 | } | ||
433 | |||
434 | #[test] | ||
222 | fn test_fill_struct_fields_empty() { | 435 | fn test_fill_struct_fields_empty() { |
223 | let before = r" | 436 | let before = r" |
224 | struct TestStruct { | 437 | struct TestStruct { |
diff --git a/crates/ra_syntax/src/ptr.rs b/crates/ra_syntax/src/ptr.rs index d24660ac3..992034ef0 100644 --- a/crates/ra_syntax/src/ptr.rs +++ b/crates/ra_syntax/src/ptr.rs | |||
@@ -31,6 +31,13 @@ impl SyntaxNodePtr { | |||
31 | pub fn kind(self) -> SyntaxKind { | 31 | pub fn kind(self) -> SyntaxKind { |
32 | self.kind | 32 | self.kind |
33 | } | 33 | } |
34 | |||
35 | pub fn cast<N: AstNode>(self) -> Option<AstPtr<N>> { | ||
36 | if !N::can_cast(self.kind()) { | ||
37 | return None; | ||
38 | } | ||
39 | Some(AstPtr { raw: self, _ty: PhantomData }) | ||
40 | } | ||
34 | } | 41 | } |
35 | 42 | ||
36 | /// Like `SyntaxNodePtr`, but remembers the type of node | 43 | /// Like `SyntaxNodePtr`, but remembers the type of node |