aboutsummaryrefslogtreecommitdiff
path: root/crates/ide_db
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-05-20 09:27:16 +0100
committerGitHub <[email protected]>2021-05-20 09:27:16 +0100
commit8bb37737c9e8b7a390ae29d5fb0daaeeb495d2b5 (patch)
tree0f8d45124abe86151e1954c1766534116e174209 /crates/ide_db
parent764241e38e46316b6370977e8b51e841e93e84b9 (diff)
parent2bf720900f94e36969af44ff8ac52470faf9af4b (diff)
Merge #8873
8873: Implement import-granularity guessing r=matklad a=Veykril This renames our `MergeBehavior` to `ImportGranularity` as rustfmt has it as the purpose of them are basically the same. `ImportGranularity::Preserve` currently has no specific purpose for us as we don't have an organize imports assist yet, so it currently acts the same as `ImportGranularity::Item`. We now try to guess the import style on a per file basis and fall back to the user granularity setting if the file has no specific style yet or where it is ambiguous. This can be turned off by setting `import.enforceGranularity` to `true`. Closes https://github.com/rust-analyzer/rust-analyzer/issues/8870 Co-authored-by: Lukas Tobias Wirth <[email protected]>
Diffstat (limited to 'crates/ide_db')
-rw-r--r--crates/ide_db/src/helpers/insert_use.rs105
-rw-r--r--crates/ide_db/src/helpers/insert_use/tests.rs141
-rw-r--r--crates/ide_db/src/helpers/merge_imports.rs6
3 files changed, 237 insertions, 15 deletions
diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs
index 55cdc4da3..aa61c5bcb 100644
--- a/crates/ide_db/src/helpers/insert_use.rs
+++ b/crates/ide_db/src/helpers/insert_use.rs
@@ -4,20 +4,36 @@ use std::cmp::Ordering;
4use hir::Semantics; 4use hir::Semantics;
5use syntax::{ 5use syntax::{
6 algo, 6 algo,
7 ast::{self, make, AstNode, PathSegmentKind}, 7 ast::{self, make, AstNode, AttrsOwner, ModuleItemOwner, PathSegmentKind, VisibilityOwner},
8 ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken, 8 ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken,
9}; 9};
10 10
11use crate::{ 11use crate::{
12 helpers::merge_imports::{try_merge_imports, use_tree_path_cmp, MergeBehavior}, 12 helpers::merge_imports::{
13 common_prefix, eq_attrs, eq_visibility, try_merge_imports, use_tree_path_cmp, MergeBehavior,
14 },
13 RootDatabase, 15 RootDatabase,
14}; 16};
15 17
16pub use hir::PrefixKind; 18pub use hir::PrefixKind;
17 19
20/// How imports should be grouped into use statements.
21#[derive(Copy, Clone, Debug, PartialEq, Eq)]
22pub enum ImportGranularity {
23 /// Do not change the granularity of any imports and preserve the original structure written by the developer.
24 Preserve,
25 /// Merge imports from the same crate into a single use statement.
26 Crate,
27 /// Merge imports from the same module into a single use statement.
28 Module,
29 /// Flatten imports so that each has its own use statement.
30 Item,
31}
32
18#[derive(Clone, Copy, Debug, PartialEq, Eq)] 33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct InsertUseConfig { 34pub struct InsertUseConfig {
20 pub merge: Option<MergeBehavior>, 35 pub granularity: ImportGranularity,
36 pub enforce_granularity: bool,
21 pub prefix_kind: PrefixKind, 37 pub prefix_kind: PrefixKind,
22 pub group: bool, 38 pub group: bool,
23} 39}
@@ -65,15 +81,96 @@ impl ImportScope {
65 ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()), 81 ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()),
66 } 82 }
67 } 83 }
84
85 fn guess_granularity_from_scope(&self) -> ImportGranularityGuess {
86 // The idea is simple, just check each import as well as the import and its precedent together for
87 // whether they fulfill a granularity criteria.
88 let use_stmt = |item| match item {
89 ast::Item::Use(use_) => {
90 let use_tree = use_.use_tree()?;
91 Some((use_tree, use_.visibility(), use_.attrs()))
92 }
93 _ => None,
94 };
95 let mut use_stmts = match self {
96 ImportScope::File(f) => f.items(),
97 ImportScope::Module(m) => m.items(),
98 }
99 .filter_map(use_stmt);
100 let mut res = ImportGranularityGuess::Unknown;
101 let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
102 Some(it) => it,
103 None => return res,
104 };
105 loop {
106 if let Some(use_tree_list) = prev.use_tree_list() {
107 if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
108 // Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
109 break ImportGranularityGuess::Crate;
110 } else {
111 // Could still be crate-style so continue looking.
112 res = ImportGranularityGuess::CrateOrModule;
113 }
114 }
115
116 let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
117 Some(it) => it,
118 None => break res,
119 };
120 if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone())
121 {
122 if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
123 if let Some(_) = common_prefix(&prev_path, &curr_path) {
124 if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
125 // Same prefix but no use tree lists so this has to be of item style.
126 break ImportGranularityGuess::Item; // this overwrites CrateOrModule, technically the file doesn't adhere to anything here.
127 } else {
128 // Same prefix with item tree lists, has to be module style as it
129 // can't be crate style since the trees wouldn't share a prefix then.
130 break ImportGranularityGuess::Module;
131 }
132 }
133 }
134 }
135 prev = curr;
136 prev_vis = curr_vis;
137 prev_attrs = curr_attrs;
138 }
139 }
140}
141
142#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
143enum ImportGranularityGuess {
144 Unknown,
145 Item,
146 Module,
147 Crate,
148 CrateOrModule,
68} 149}
69 150
70/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur. 151/// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
71pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) { 152pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) {
72 let _p = profile::span("insert_use"); 153 let _p = profile::span("insert_use");
154 let mut mb = match cfg.granularity {
155 ImportGranularity::Crate => Some(MergeBehavior::Crate),
156 ImportGranularity::Module => Some(MergeBehavior::Module),
157 ImportGranularity::Item | ImportGranularity::Preserve => None,
158 };
159 if !cfg.enforce_granularity {
160 let file_granularity = scope.guess_granularity_from_scope();
161 mb = match file_granularity {
162 ImportGranularityGuess::Unknown => mb,
163 ImportGranularityGuess::Item => None,
164 ImportGranularityGuess::Module => Some(MergeBehavior::Module),
165 ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
166 ImportGranularityGuess::CrateOrModule => mb.or(Some(MergeBehavior::Crate)),
167 };
168 }
169
73 let use_item = 170 let use_item =
74 make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update(); 171 make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update();
75 // merge into existing imports if possible 172 // merge into existing imports if possible
76 if let Some(mb) = cfg.merge { 173 if let Some(mb) = mb {
77 for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) { 174 for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) {
78 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) { 175 if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
79 ted::replace(existing_use.syntax(), merged.syntax()); 176 ted::replace(existing_use.syntax(), merged.syntax());
diff --git a/crates/ide_db/src/helpers/insert_use/tests.rs b/crates/ide_db/src/helpers/insert_use/tests.rs
index 248227d29..78a2a87b3 100644
--- a/crates/ide_db/src/helpers/insert_use/tests.rs
+++ b/crates/ide_db/src/helpers/insert_use/tests.rs
@@ -21,7 +21,7 @@ use crate::bar::A;
21use self::bar::A; 21use self::bar::A;
22use super::bar::A; 22use super::bar::A;
23use external_crate2::bar::A;", 23use external_crate2::bar::A;",
24 None, 24 ImportGranularity::Item,
25 false, 25 false,
26 false, 26 false,
27 ); 27 );
@@ -36,7 +36,7 @@ fn insert_not_group_empty() {
36 r"use external_crate2::bar::A; 36 r"use external_crate2::bar::A;
37 37
38", 38",
39 None, 39 ImportGranularity::Item,
40 false, 40 false,
41 false, 41 false,
42 ); 42 );
@@ -281,7 +281,7 @@ fn insert_empty_module() {
281 r"{ 281 r"{
282 use foo::bar; 282 use foo::bar;
283}", 283}",
284 None, 284 ImportGranularity::Item,
285 true, 285 true,
286 true, 286 true,
287 ) 287 )
@@ -631,11 +631,121 @@ fn merge_last_fail3() {
631 ); 631 );
632} 632}
633 633
634#[test]
635fn guess_empty() {
636 check_guess("", ImportGranularityGuess::Unknown);
637}
638
639#[test]
640fn guess_single() {
641 check_guess(r"use foo::{baz::{qux, quux}, bar};", ImportGranularityGuess::Crate);
642 check_guess(r"use foo::bar;", ImportGranularityGuess::Unknown);
643 check_guess(r"use foo::bar::{baz, qux};", ImportGranularityGuess::CrateOrModule);
644}
645
646#[test]
647fn guess_unknown() {
648 check_guess(
649 r"
650use foo::bar::baz;
651use oof::rab::xuq;
652",
653 ImportGranularityGuess::Unknown,
654 );
655}
656
657#[test]
658fn guess_item() {
659 check_guess(
660 r"
661use foo::bar::baz;
662use foo::bar::qux;
663",
664 ImportGranularityGuess::Item,
665 );
666}
667
668#[test]
669fn guess_module() {
670 check_guess(
671 r"
672use foo::bar::baz;
673use foo::bar::{qux, quux};
674",
675 ImportGranularityGuess::Module,
676 );
677 // this is a rather odd case, technically this file isn't following any style properly.
678 check_guess(
679 r"
680use foo::bar::baz;
681use foo::{baz::{qux, quux}, bar};
682",
683 ImportGranularityGuess::Module,
684 );
685}
686
687#[test]
688fn guess_crate_or_module() {
689 check_guess(
690 r"
691use foo::bar::baz;
692use oof::bar::{qux, quux};
693",
694 ImportGranularityGuess::CrateOrModule,
695 );
696}
697
698#[test]
699fn guess_crate() {
700 check_guess(
701 r"
702use frob::bar::baz;
703use foo::{baz::{qux, quux}, bar};
704",
705 ImportGranularityGuess::Crate,
706 );
707}
708
709#[test]
710fn guess_skips_differing_vis() {
711 check_guess(
712 r"
713use foo::bar::baz;
714pub use foo::bar::qux;
715",
716 ImportGranularityGuess::Unknown,
717 );
718}
719
720#[test]
721fn guess_skips_differing_attrs() {
722 check_guess(
723 r"
724pub use foo::bar::baz;
725#[doc(hidden)]
726pub use foo::bar::qux;
727",
728 ImportGranularityGuess::Unknown,
729 );
730}
731
732#[test]
733fn guess_grouping_matters() {
734 check_guess(
735 r"
736use foo::bar::baz;
737use oof::bar::baz;
738use foo::bar::qux;
739",
740 ImportGranularityGuess::Unknown,
741 );
742}
743
634fn check( 744fn check(
635 path: &str, 745 path: &str,
636 ra_fixture_before: &str, 746 ra_fixture_before: &str,
637 ra_fixture_after: &str, 747 ra_fixture_after: &str,
638 mb: Option<MergeBehavior>, 748 granularity: ImportGranularity,
639 module: bool, 749 module: bool,
640 group: bool, 750 group: bool,
641) { 751) {
@@ -651,21 +761,30 @@ fn check(
651 .find_map(ast::Path::cast) 761 .find_map(ast::Path::cast)
652 .unwrap(); 762 .unwrap();
653 763
654 insert_use(&file, path, InsertUseConfig { merge: mb, prefix_kind: PrefixKind::Plain, group }); 764 insert_use(
765 &file,
766 path,
767 InsertUseConfig {
768 granularity,
769 enforce_granularity: true,
770 prefix_kind: PrefixKind::Plain,
771 group,
772 },
773 );
655 let result = file.as_syntax_node().to_string(); 774 let result = file.as_syntax_node().to_string();
656 assert_eq_text!(ra_fixture_after, &result); 775 assert_eq_text!(ra_fixture_after, &result);
657} 776}
658 777
659fn check_crate(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 778fn check_crate(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
660 check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Crate), false, true) 779 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Crate, false, true)
661} 780}
662 781
663fn check_module(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 782fn check_module(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
664 check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Module), false, true) 783 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Module, false, true)
665} 784}
666 785
667fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) { 786fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
668 check(path, ra_fixture_before, ra_fixture_after, None, false, true) 787 check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Item, false, true)
669} 788}
670 789
671fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior) { 790fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior) {
@@ -686,3 +805,9 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior
686 let result = try_merge_imports(&use0, &use1, mb); 805 let result = try_merge_imports(&use0, &use1, mb);
687 assert_eq!(result.map(|u| u.to_string()), None); 806 assert_eq!(result.map(|u| u.to_string()), None);
688} 807}
808
809fn check_guess(ra_fixture: &str, expected: ImportGranularityGuess) {
810 let syntax = ast::SourceFile::parse(ra_fixture).tree().syntax().clone();
811 let file = super::ImportScope::from(syntax).unwrap();
812 assert_eq!(file.guess_granularity_from_scope(), expected);
813}
diff --git a/crates/ide_db/src/helpers/merge_imports.rs b/crates/ide_db/src/helpers/merge_imports.rs
index 8fb40e837..697e8bcff 100644
--- a/crates/ide_db/src/helpers/merge_imports.rs
+++ b/crates/ide_db/src/helpers/merge_imports.rs
@@ -181,7 +181,7 @@ fn recursive_merge(
181} 181}
182 182
183/// Traverses both paths until they differ, returning the common prefix of both. 183/// Traverses both paths until they differ, returning the common prefix of both.
184fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> { 184pub fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
185 let mut res = None; 185 let mut res = None;
186 let mut lhs_curr = lhs.first_qualifier_or_self(); 186 let mut lhs_curr = lhs.first_qualifier_or_self();
187 let mut rhs_curr = rhs.first_qualifier_or_self(); 187 let mut rhs_curr = rhs.first_qualifier_or_self();
@@ -289,7 +289,7 @@ fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
289 a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text)) 289 a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
290} 290}
291 291
292fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool { 292pub fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
293 match (vis0, vis1) { 293 match (vis0, vis1) {
294 (None, None) => true, 294 (None, None) => true,
295 // FIXME: Don't use the string representation to check for equality 295 // FIXME: Don't use the string representation to check for equality
@@ -299,7 +299,7 @@ fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -
299 } 299 }
300} 300}
301 301
302fn eq_attrs( 302pub fn eq_attrs(
303 attrs0: impl Iterator<Item = ast::Attr>, 303 attrs0: impl Iterator<Item = ast::Attr>,
304 attrs1: impl Iterator<Item = ast::Attr>, 304 attrs1: impl Iterator<Item = ast::Attr>,
305) -> bool { 305) -> bool {