diff options
author | bors[bot] <26634292+bors[bot]@users.noreply.github.com> | 2020-01-11 22:42:39 +0000 |
---|---|---|
committer | GitHub <[email protected]> | 2020-01-11 22:42:39 +0000 |
commit | bcfd297f4910bbf2305ec859d7cf42b7dca25f57 (patch) | |
tree | c6b53cd774e7baacf213e91b295734852a83f40b /crates/ra_assists/src | |
parent | e90aa86fbfa716c4028f38d0d22654065011a964 (diff) | |
parent | ccb75f7c979b56bc62b61fadd81903e11a7f5d74 (diff) |
Merge #2727
2727: Qualify paths in 'add impl members' r=flodiebold a=flodiebold
This makes the 'add impl members' assist qualify paths, so that they should resolve to the same thing as in the definition. To do that, it adds an algorithm that finds a path to refer to any item from any module (if possible), which is actually probably the more important part of this PR :smile: It handles visibility, reexports, renamed crates, prelude etc.; I think the only thing that's missing is support for local items. I'm not sure about the performance, since it takes into account every location where the target item has been `pub use`d, and then recursively goes up the module tree; there's probably potential for optimization by memoizing more, but I think the general shape of the algorithm is necessary to handle every case in Rust's module system.
~The 'find path' part is actually pretty complete, I think; I'm still working on the assist (hence the failing tests).~
Fixes #1943.
Co-authored-by: Florian Diebold <[email protected]>
Co-authored-by: Florian Diebold <[email protected]>
Diffstat (limited to 'crates/ra_assists/src')
-rw-r--r-- | crates/ra_assists/src/assists/add_missing_impl_members.rs | 251 | ||||
-rw-r--r-- | crates/ra_assists/src/ast_transform.rs | 179 | ||||
-rw-r--r-- | crates/ra_assists/src/lib.rs | 1 |
3 files changed, 364 insertions, 67 deletions
diff --git a/crates/ra_assists/src/assists/add_missing_impl_members.rs b/crates/ra_assists/src/assists/add_missing_impl_members.rs index bc49e71fe..bf1136193 100644 --- a/crates/ra_assists/src/assists/add_missing_impl_members.rs +++ b/crates/ra_assists/src/assists/add_missing_impl_members.rs | |||
@@ -1,12 +1,13 @@ | |||
1 | use std::collections::HashMap; | 1 | use hir::{db::HirDatabase, HasSource, InFile}; |
2 | |||
3 | use hir::{db::HirDatabase, HasSource}; | ||
4 | use ra_syntax::{ | 2 | use ra_syntax::{ |
5 | ast::{self, edit, make, AstNode, NameOwner}, | 3 | ast::{self, edit, make, AstNode, NameOwner}, |
6 | SmolStr, | 4 | SmolStr, |
7 | }; | 5 | }; |
8 | 6 | ||
9 | use crate::{Assist, AssistCtx, AssistId}; | 7 | use crate::{ |
8 | ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams}, | ||
9 | Assist, AssistCtx, AssistId, | ||
10 | }; | ||
10 | 11 | ||
11 | #[derive(PartialEq)] | 12 | #[derive(PartialEq)] |
12 | enum AddMissingImplMembersMode { | 13 | enum AddMissingImplMembersMode { |
@@ -134,25 +135,23 @@ fn add_missing_impl_members_inner( | |||
134 | return None; | 135 | return None; |
135 | } | 136 | } |
136 | 137 | ||
137 | let file_id = ctx.frange.file_id; | ||
138 | let db = ctx.db; | 138 | let db = ctx.db; |
139 | let file_id = ctx.frange.file_id; | ||
140 | let trait_file_id = trait_.source(db).file_id; | ||
139 | 141 | ||
140 | ctx.add_assist(AssistId(assist_id), label, |edit| { | 142 | ctx.add_assist(AssistId(assist_id), label, |edit| { |
141 | let n_existing_items = impl_item_list.impl_items().count(); | 143 | let n_existing_items = impl_item_list.impl_items().count(); |
142 | let substs = get_syntactic_substs(impl_node).unwrap_or_default(); | 144 | let module = hir::SourceAnalyzer::new( |
143 | let generic_def: hir::GenericDef = trait_.into(); | 145 | db, |
144 | let substs_by_param: HashMap<_, _> = generic_def | 146 | hir::InFile::new(file_id.into(), impl_node.syntax()), |
145 | .params(db) | 147 | None, |
146 | .into_iter() | 148 | ) |
147 | // this is a trait impl, so we need to skip the first type parameter -- this is a bit hacky | 149 | .module(); |
148 | .skip(1) | 150 | let ast_transform = QualifyPaths::new(db, module) |
149 | .zip(substs.into_iter()) | 151 | .or(SubstituteTypeParams::for_trait_impl(db, trait_, impl_node)); |
150 | .collect(); | ||
151 | let items = missing_items | 152 | let items = missing_items |
152 | .into_iter() | 153 | .into_iter() |
153 | .map(|it| { | 154 | .map(|it| ast_transform::apply(&*ast_transform, InFile::new(trait_file_id, it))) |
154 | substitute_type_params(db, hir::InFile::new(file_id.into(), it), &substs_by_param) | ||
155 | }) | ||
156 | .map(|it| match it { | 155 | .map(|it| match it { |
157 | ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)), | 156 | ast::ImplItem::FnDef(def) => ast::ImplItem::FnDef(add_body(def)), |
158 | _ => it, | 157 | _ => it, |
@@ -177,56 +176,6 @@ fn add_body(fn_def: ast::FnDef) -> ast::FnDef { | |||
177 | } | 176 | } |
178 | } | 177 | } |
179 | 178 | ||
180 | // FIXME: It would probably be nicer if we could get this via HIR (i.e. get the | ||
181 | // trait ref, and then go from the types in the substs back to the syntax) | ||
182 | // FIXME: This should be a general utility (not even just for assists) | ||
183 | fn get_syntactic_substs(impl_block: ast::ImplBlock) -> Option<Vec<ast::TypeRef>> { | ||
184 | let target_trait = impl_block.target_trait()?; | ||
185 | let path_type = match target_trait { | ||
186 | ast::TypeRef::PathType(path) => path, | ||
187 | _ => return None, | ||
188 | }; | ||
189 | let type_arg_list = path_type.path()?.segment()?.type_arg_list()?; | ||
190 | let mut result = Vec::new(); | ||
191 | for type_arg in type_arg_list.type_args() { | ||
192 | let type_arg: ast::TypeArg = type_arg; | ||
193 | result.push(type_arg.type_ref()?); | ||
194 | } | ||
195 | Some(result) | ||
196 | } | ||
197 | |||
198 | // FIXME: This should be a general utility (not even just for assists) | ||
199 | fn substitute_type_params<N: AstNode>( | ||
200 | db: &impl HirDatabase, | ||
201 | node: hir::InFile<N>, | ||
202 | substs: &HashMap<hir::TypeParam, ast::TypeRef>, | ||
203 | ) -> N { | ||
204 | let type_param_replacements = node | ||
205 | .value | ||
206 | .syntax() | ||
207 | .descendants() | ||
208 | .filter_map(ast::TypeRef::cast) | ||
209 | .filter_map(|n| { | ||
210 | let path = match &n { | ||
211 | ast::TypeRef::PathType(path_type) => path_type.path()?, | ||
212 | _ => return None, | ||
213 | }; | ||
214 | let analyzer = hir::SourceAnalyzer::new(db, node.with_value(n.syntax()), None); | ||
215 | let resolution = analyzer.resolve_path(db, &path)?; | ||
216 | match resolution { | ||
217 | hir::PathResolution::TypeParam(tp) => Some((n, substs.get(&tp)?.clone())), | ||
218 | _ => None, | ||
219 | } | ||
220 | }) | ||
221 | .collect::<Vec<_>>(); | ||
222 | |||
223 | if type_param_replacements.is_empty() { | ||
224 | node.value | ||
225 | } else { | ||
226 | edit::replace_descendants(&node.value, type_param_replacements.into_iter()) | ||
227 | } | ||
228 | } | ||
229 | |||
230 | /// Given an `ast::ImplBlock`, resolves the target trait (the one being | 179 | /// Given an `ast::ImplBlock`, resolves the target trait (the one being |
231 | /// implemented) to a `ast::TraitDef`. | 180 | /// implemented) to a `ast::TraitDef`. |
232 | fn resolve_target_trait_def( | 181 | fn resolve_target_trait_def( |
@@ -401,6 +350,174 @@ impl Foo for S { | |||
401 | } | 350 | } |
402 | 351 | ||
403 | #[test] | 352 | #[test] |
353 | fn test_qualify_path_1() { | ||
354 | check_assist( | ||
355 | add_missing_impl_members, | ||
356 | " | ||
357 | mod foo { | ||
358 | pub struct Bar; | ||
359 | trait Foo { fn foo(&self, bar: Bar); } | ||
360 | } | ||
361 | struct S; | ||
362 | impl foo::Foo for S { <|> }", | ||
363 | " | ||
364 | mod foo { | ||
365 | pub struct Bar; | ||
366 | trait Foo { fn foo(&self, bar: Bar); } | ||
367 | } | ||
368 | struct S; | ||
369 | impl foo::Foo for S { | ||
370 | <|>fn foo(&self, bar: foo::Bar) { unimplemented!() } | ||
371 | }", | ||
372 | ); | ||
373 | } | ||
374 | |||
375 | #[test] | ||
376 | fn test_qualify_path_generic() { | ||
377 | check_assist( | ||
378 | add_missing_impl_members, | ||
379 | " | ||
380 | mod foo { | ||
381 | pub struct Bar<T>; | ||
382 | trait Foo { fn foo(&self, bar: Bar<u32>); } | ||
383 | } | ||
384 | struct S; | ||
385 | impl foo::Foo for S { <|> }", | ||
386 | " | ||
387 | mod foo { | ||
388 | pub struct Bar<T>; | ||
389 | trait Foo { fn foo(&self, bar: Bar<u32>); } | ||
390 | } | ||
391 | struct S; | ||
392 | impl foo::Foo for S { | ||
393 | <|>fn foo(&self, bar: foo::Bar<u32>) { unimplemented!() } | ||
394 | }", | ||
395 | ); | ||
396 | } | ||
397 | |||
398 | #[test] | ||
399 | fn test_qualify_path_and_substitute_param() { | ||
400 | check_assist( | ||
401 | add_missing_impl_members, | ||
402 | " | ||
403 | mod foo { | ||
404 | pub struct Bar<T>; | ||
405 | trait Foo<T> { fn foo(&self, bar: Bar<T>); } | ||
406 | } | ||
407 | struct S; | ||
408 | impl foo::Foo<u32> for S { <|> }", | ||
409 | " | ||
410 | mod foo { | ||
411 | pub struct Bar<T>; | ||
412 | trait Foo<T> { fn foo(&self, bar: Bar<T>); } | ||
413 | } | ||
414 | struct S; | ||
415 | impl foo::Foo<u32> for S { | ||
416 | <|>fn foo(&self, bar: foo::Bar<u32>) { unimplemented!() } | ||
417 | }", | ||
418 | ); | ||
419 | } | ||
420 | |||
421 | #[test] | ||
422 | fn test_substitute_param_no_qualify() { | ||
423 | // when substituting params, the substituted param should not be qualified! | ||
424 | check_assist( | ||
425 | add_missing_impl_members, | ||
426 | " | ||
427 | mod foo { | ||
428 | trait Foo<T> { fn foo(&self, bar: T); } | ||
429 | pub struct Param; | ||
430 | } | ||
431 | struct Param; | ||
432 | struct S; | ||
433 | impl foo::Foo<Param> for S { <|> }", | ||
434 | " | ||
435 | mod foo { | ||
436 | trait Foo<T> { fn foo(&self, bar: T); } | ||
437 | pub struct Param; | ||
438 | } | ||
439 | struct Param; | ||
440 | struct S; | ||
441 | impl foo::Foo<Param> for S { | ||
442 | <|>fn foo(&self, bar: Param) { unimplemented!() } | ||
443 | }", | ||
444 | ); | ||
445 | } | ||
446 | |||
447 | #[test] | ||
448 | fn test_qualify_path_associated_item() { | ||
449 | check_assist( | ||
450 | add_missing_impl_members, | ||
451 | " | ||
452 | mod foo { | ||
453 | pub struct Bar<T>; | ||
454 | impl Bar<T> { type Assoc = u32; } | ||
455 | trait Foo { fn foo(&self, bar: Bar<u32>::Assoc); } | ||
456 | } | ||
457 | struct S; | ||
458 | impl foo::Foo for S { <|> }", | ||
459 | " | ||
460 | mod foo { | ||
461 | pub struct Bar<T>; | ||
462 | impl Bar<T> { type Assoc = u32; } | ||
463 | trait Foo { fn foo(&self, bar: Bar<u32>::Assoc); } | ||
464 | } | ||
465 | struct S; | ||
466 | impl foo::Foo for S { | ||
467 | <|>fn foo(&self, bar: foo::Bar<u32>::Assoc) { unimplemented!() } | ||
468 | }", | ||
469 | ); | ||
470 | } | ||
471 | |||
472 | #[test] | ||
473 | fn test_qualify_path_nested() { | ||
474 | check_assist( | ||
475 | add_missing_impl_members, | ||
476 | " | ||
477 | mod foo { | ||
478 | pub struct Bar<T>; | ||
479 | pub struct Baz; | ||
480 | trait Foo { fn foo(&self, bar: Bar<Baz>); } | ||
481 | } | ||
482 | struct S; | ||
483 | impl foo::Foo for S { <|> }", | ||
484 | " | ||
485 | mod foo { | ||
486 | pub struct Bar<T>; | ||
487 | pub struct Baz; | ||
488 | trait Foo { fn foo(&self, bar: Bar<Baz>); } | ||
489 | } | ||
490 | struct S; | ||
491 | impl foo::Foo for S { | ||
492 | <|>fn foo(&self, bar: foo::Bar<foo::Baz>) { unimplemented!() } | ||
493 | }", | ||
494 | ); | ||
495 | } | ||
496 | |||
497 | #[test] | ||
498 | fn test_qualify_path_fn_trait_notation() { | ||
499 | check_assist( | ||
500 | add_missing_impl_members, | ||
501 | " | ||
502 | mod foo { | ||
503 | pub trait Fn<Args> { type Output; } | ||
504 | trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } | ||
505 | } | ||
506 | struct S; | ||
507 | impl foo::Foo for S { <|> }", | ||
508 | " | ||
509 | mod foo { | ||
510 | pub trait Fn<Args> { type Output; } | ||
511 | trait Foo { fn foo(&self, bar: dyn Fn(u32) -> i32); } | ||
512 | } | ||
513 | struct S; | ||
514 | impl foo::Foo for S { | ||
515 | <|>fn foo(&self, bar: dyn Fn(u32) -> i32) { unimplemented!() } | ||
516 | }", | ||
517 | ); | ||
518 | } | ||
519 | |||
520 | #[test] | ||
404 | fn test_empty_trait() { | 521 | fn test_empty_trait() { |
405 | check_assist_not_applicable( | 522 | check_assist_not_applicable( |
406 | add_missing_impl_members, | 523 | add_missing_impl_members, |
diff --git a/crates/ra_assists/src/ast_transform.rs b/crates/ra_assists/src/ast_transform.rs new file mode 100644 index 000000000..cbddc50ac --- /dev/null +++ b/crates/ra_assists/src/ast_transform.rs | |||
@@ -0,0 +1,179 @@ | |||
1 | //! `AstTransformer`s are functions that replace nodes in an AST and can be easily combined. | ||
2 | use rustc_hash::FxHashMap; | ||
3 | |||
4 | use hir::{db::HirDatabase, InFile, PathResolution}; | ||
5 | use ra_syntax::ast::{self, make, AstNode}; | ||
6 | |||
7 | pub trait AstTransform<'a> { | ||
8 | fn get_substitution( | ||
9 | &self, | ||
10 | node: InFile<&ra_syntax::SyntaxNode>, | ||
11 | ) -> Option<ra_syntax::SyntaxNode>; | ||
12 | |||
13 | fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a>; | ||
14 | fn or<T: AstTransform<'a> + 'a>(self, other: T) -> Box<dyn AstTransform<'a> + 'a> | ||
15 | where | ||
16 | Self: Sized + 'a, | ||
17 | { | ||
18 | self.chain_before(Box::new(other)) | ||
19 | } | ||
20 | } | ||
21 | |||
22 | struct NullTransformer; | ||
23 | |||
24 | impl<'a> AstTransform<'a> for NullTransformer { | ||
25 | fn get_substitution( | ||
26 | &self, | ||
27 | _node: InFile<&ra_syntax::SyntaxNode>, | ||
28 | ) -> Option<ra_syntax::SyntaxNode> { | ||
29 | None | ||
30 | } | ||
31 | fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> { | ||
32 | other | ||
33 | } | ||
34 | } | ||
35 | |||
36 | pub struct SubstituteTypeParams<'a, DB: HirDatabase> { | ||
37 | db: &'a DB, | ||
38 | substs: FxHashMap<hir::TypeParam, ast::TypeRef>, | ||
39 | previous: Box<dyn AstTransform<'a> + 'a>, | ||
40 | } | ||
41 | |||
42 | impl<'a, DB: HirDatabase> SubstituteTypeParams<'a, DB> { | ||
43 | pub fn for_trait_impl( | ||
44 | db: &'a DB, | ||
45 | trait_: hir::Trait, | ||
46 | impl_block: ast::ImplBlock, | ||
47 | ) -> SubstituteTypeParams<'a, DB> { | ||
48 | let substs = get_syntactic_substs(impl_block).unwrap_or_default(); | ||
49 | let generic_def: hir::GenericDef = trait_.into(); | ||
50 | let substs_by_param: FxHashMap<_, _> = generic_def | ||
51 | .params(db) | ||
52 | .into_iter() | ||
53 | // this is a trait impl, so we need to skip the first type parameter -- this is a bit hacky | ||
54 | .skip(1) | ||
55 | .zip(substs.into_iter()) | ||
56 | .collect(); | ||
57 | return SubstituteTypeParams { | ||
58 | db, | ||
59 | substs: substs_by_param, | ||
60 | previous: Box::new(NullTransformer), | ||
61 | }; | ||
62 | |||
63 | // FIXME: It would probably be nicer if we could get this via HIR (i.e. get the | ||
64 | // trait ref, and then go from the types in the substs back to the syntax) | ||
65 | fn get_syntactic_substs(impl_block: ast::ImplBlock) -> Option<Vec<ast::TypeRef>> { | ||
66 | let target_trait = impl_block.target_trait()?; | ||
67 | let path_type = match target_trait { | ||
68 | ast::TypeRef::PathType(path) => path, | ||
69 | _ => return None, | ||
70 | }; | ||
71 | let type_arg_list = path_type.path()?.segment()?.type_arg_list()?; | ||
72 | let mut result = Vec::new(); | ||
73 | for type_arg in type_arg_list.type_args() { | ||
74 | let type_arg: ast::TypeArg = type_arg; | ||
75 | result.push(type_arg.type_ref()?); | ||
76 | } | ||
77 | Some(result) | ||
78 | } | ||
79 | } | ||
80 | fn get_substitution_inner( | ||
81 | &self, | ||
82 | node: InFile<&ra_syntax::SyntaxNode>, | ||
83 | ) -> Option<ra_syntax::SyntaxNode> { | ||
84 | let type_ref = ast::TypeRef::cast(node.value.clone())?; | ||
85 | let path = match &type_ref { | ||
86 | ast::TypeRef::PathType(path_type) => path_type.path()?, | ||
87 | _ => return None, | ||
88 | }; | ||
89 | let analyzer = hir::SourceAnalyzer::new(self.db, node, None); | ||
90 | let resolution = analyzer.resolve_path(self.db, &path)?; | ||
91 | match resolution { | ||
92 | hir::PathResolution::TypeParam(tp) => Some(self.substs.get(&tp)?.syntax().clone()), | ||
93 | _ => None, | ||
94 | } | ||
95 | } | ||
96 | } | ||
97 | |||
98 | impl<'a, DB: HirDatabase> AstTransform<'a> for SubstituteTypeParams<'a, DB> { | ||
99 | fn get_substitution( | ||
100 | &self, | ||
101 | node: InFile<&ra_syntax::SyntaxNode>, | ||
102 | ) -> Option<ra_syntax::SyntaxNode> { | ||
103 | self.get_substitution_inner(node).or_else(|| self.previous.get_substitution(node)) | ||
104 | } | ||
105 | fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> { | ||
106 | Box::new(SubstituteTypeParams { previous: other, ..self }) | ||
107 | } | ||
108 | } | ||
109 | |||
110 | pub struct QualifyPaths<'a, DB: HirDatabase> { | ||
111 | db: &'a DB, | ||
112 | from: Option<hir::Module>, | ||
113 | previous: Box<dyn AstTransform<'a> + 'a>, | ||
114 | } | ||
115 | |||
116 | impl<'a, DB: HirDatabase> QualifyPaths<'a, DB> { | ||
117 | pub fn new(db: &'a DB, from: Option<hir::Module>) -> Self { | ||
118 | Self { db, from, previous: Box::new(NullTransformer) } | ||
119 | } | ||
120 | |||
121 | fn get_substitution_inner( | ||
122 | &self, | ||
123 | node: InFile<&ra_syntax::SyntaxNode>, | ||
124 | ) -> Option<ra_syntax::SyntaxNode> { | ||
125 | // FIXME handle value ns? | ||
126 | let from = self.from?; | ||
127 | let p = ast::Path::cast(node.value.clone())?; | ||
128 | if p.segment().and_then(|s| s.param_list()).is_some() { | ||
129 | // don't try to qualify `Fn(Foo) -> Bar` paths, they are in prelude anyway | ||
130 | return None; | ||
131 | } | ||
132 | let analyzer = hir::SourceAnalyzer::new(self.db, node, None); | ||
133 | let resolution = analyzer.resolve_path(self.db, &p)?; | ||
134 | match resolution { | ||
135 | PathResolution::Def(def) => { | ||
136 | let found_path = from.find_use_path(self.db, def)?; | ||
137 | let args = p | ||
138 | .segment() | ||
139 | .and_then(|s| s.type_arg_list()) | ||
140 | .map(|arg_list| apply(self, node.with_value(arg_list))); | ||
141 | Some(make::path_with_type_arg_list(path_to_ast(found_path), args).syntax().clone()) | ||
142 | } | ||
143 | PathResolution::Local(_) | ||
144 | | PathResolution::TypeParam(_) | ||
145 | | PathResolution::SelfType(_) => None, | ||
146 | PathResolution::Macro(_) => None, | ||
147 | PathResolution::AssocItem(_) => None, | ||
148 | } | ||
149 | } | ||
150 | } | ||
151 | |||
152 | pub fn apply<'a, N: AstNode>(transformer: &dyn AstTransform<'a>, node: InFile<N>) -> N { | ||
153 | let syntax = node.value.syntax(); | ||
154 | let result = ra_syntax::algo::replace_descendants(syntax, &|element| match element { | ||
155 | ra_syntax::SyntaxElement::Node(n) => { | ||
156 | let replacement = transformer.get_substitution(node.with_value(&n))?; | ||
157 | Some(replacement.into()) | ||
158 | } | ||
159 | _ => None, | ||
160 | }); | ||
161 | N::cast(result).unwrap() | ||
162 | } | ||
163 | |||
164 | impl<'a, DB: HirDatabase> AstTransform<'a> for QualifyPaths<'a, DB> { | ||
165 | fn get_substitution( | ||
166 | &self, | ||
167 | node: InFile<&ra_syntax::SyntaxNode>, | ||
168 | ) -> Option<ra_syntax::SyntaxNode> { | ||
169 | self.get_substitution_inner(node).or_else(|| self.previous.get_substitution(node)) | ||
170 | } | ||
171 | fn chain_before(self, other: Box<dyn AstTransform<'a> + 'a>) -> Box<dyn AstTransform<'a> + 'a> { | ||
172 | Box::new(QualifyPaths { previous: other, ..self }) | ||
173 | } | ||
174 | } | ||
175 | |||
176 | fn path_to_ast(path: hir::ModPath) -> ast::Path { | ||
177 | let parse = ast::SourceFile::parse(&path.to_string()); | ||
178 | parse.tree().syntax().descendants().find_map(ast::Path::cast).unwrap() | ||
179 | } | ||
diff --git a/crates/ra_assists/src/lib.rs b/crates/ra_assists/src/lib.rs index 98fb20b22..712ff6f6a 100644 --- a/crates/ra_assists/src/lib.rs +++ b/crates/ra_assists/src/lib.rs | |||
@@ -11,6 +11,7 @@ mod marks; | |||
11 | mod doc_tests; | 11 | mod doc_tests; |
12 | #[cfg(test)] | 12 | #[cfg(test)] |
13 | mod test_db; | 13 | mod test_db; |
14 | pub mod ast_transform; | ||
14 | 15 | ||
15 | use hir::db::HirDatabase; | 16 | use hir::db::HirDatabase; |
16 | use ra_db::FileRange; | 17 | use ra_db::FileRange; |