Skip to content

Commit 95ea23c

Browse files
committed
Fix path comparison not comparing paths correctly with unequal lengths
1 parent 3f4e991 commit 95ea23c

File tree

1 file changed

+85
-48
lines changed

1 file changed

+85
-48
lines changed

crates/assists/src/utils/insert_use.rs

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ use std::{
44
iter::{self, successors},
55
};
66

7-
use ast::{
8-
edit::{AstNodeEdit, IndentLevel},
9-
PathSegmentKind, VisibilityOwner,
10-
};
7+
use itertools::{EitherOrBoth, Itertools};
118
use syntax::{
129
algo,
13-
ast::{self, make, AstNode},
10+
ast::{
11+
self,
12+
edit::{AstNodeEdit, IndentLevel},
13+
make, AstNode, PathSegmentKind, VisibilityOwner,
14+
},
1415
InsertPosition, SyntaxElement, SyntaxNode,
1516
};
1617

@@ -193,13 +194,13 @@ fn recursive_merge(
193194
false => None,
194195
})
195196
.collect::<Option<Vec<_>>>()?;
196-
use_trees.sort_unstable_by(|a, b| path_cmp_opt(a.path(), b.path()));
197+
use_trees.sort_unstable_by(|a, b| path_cmp_for_sort(a.path(), b.path()));
197198
for rhs_t in rhs.use_tree_list().into_iter().flat_map(|list| list.use_trees()) {
198199
if !merge.is_tree_allowed(&rhs_t) {
199200
return None;
200201
}
201202
let rhs_path = rhs_t.path();
202-
match use_trees.binary_search_by(|p| path_cmp_opt(p.path(), rhs_path.clone())) {
203+
match use_trees.binary_search_by(|p| path_cmp_bin_search(p.path(), rhs_path.clone())) {
203204
Ok(idx) => {
204205
let lhs_t = &mut use_trees[idx];
205206
let lhs_path = lhs_t.path()?;
@@ -307,39 +308,77 @@ fn path_len(path: ast::Path) -> usize {
307308

308309
/// Orders paths in the following way:
309310
/// the sole self token comes first, after that come uppercase identifiers, then lowercase identifiers
310-
// FIXME: rustfmt sort lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
311+
// FIXME: rustfmt sorts lowercase idents before uppercase, in general we want to have the same ordering rustfmt has
311312
// which is `self` and `super` first, then identifier imports with lowercase ones first, then glob imports and at last list imports.
312313
// Example foo::{self, foo, baz, Baz, Qux, *, {Bar}}
313-
fn path_cmp(a: &ast::Path, b: &ast::Path) -> Ordering {
314-
match (path_is_self(a), path_is_self(b)) {
315-
(true, true) => Ordering::Equal,
316-
(true, false) => Ordering::Less,
317-
(false, true) => Ordering::Greater,
318-
(false, false) => {
319-
let a = segment_iter(a);
320-
let b = segment_iter(b);
321-
// cmp_by would be useful for us here but that is currently unstable
322-
// cmp doesnt work due the lifetimes on text's return type
323-
a.zip(b)
324-
.flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
325-
.find_map(|(a, b)| match a.text().cmp(b.text()) {
326-
ord @ Ordering::Greater | ord @ Ordering::Less => Some(ord),
327-
Ordering::Equal => None,
328-
})
329-
.unwrap_or(Ordering::Equal)
330-
}
314+
fn path_cmp_for_sort(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering {
315+
match (a, b) {
316+
(None, None) => Ordering::Equal,
317+
(None, Some(_)) => Ordering::Less,
318+
(Some(_), None) => Ordering::Greater,
319+
(Some(ref a), Some(ref b)) => match (path_is_self(a), path_is_self(b)) {
320+
(true, true) => Ordering::Equal,
321+
(true, false) => Ordering::Less,
322+
(false, true) => Ordering::Greater,
323+
(false, false) => path_cmp_short(a, b),
324+
},
331325
}
332326
}
333327

334-
fn path_cmp_opt(a: Option<ast::Path>, b: Option<ast::Path>) -> Ordering {
335-
match (a, b) {
328+
/// Path comparison func for binary searching for merging.
329+
fn path_cmp_bin_search(lhs: Option<ast::Path>, rhs: Option<ast::Path>) -> Ordering {
330+
match (lhs, rhs) {
336331
(None, None) => Ordering::Equal,
337332
(None, Some(_)) => Ordering::Less,
338333
(Some(_), None) => Ordering::Greater,
339-
(Some(a), Some(b)) => path_cmp(&a, &b),
334+
(Some(ref a), Some(ref b)) => path_cmp_short(a, b),
340335
}
341336
}
342337

338+
/// Short circuiting comparison, if both paths are equal until one of them ends they are considered
339+
/// equal
340+
fn path_cmp_short(a: &ast::Path, b: &ast::Path) -> Ordering {
341+
let a = segment_iter(a);
342+
let b = segment_iter(b);
343+
// cmp_by would be useful for us here but that is currently unstable
344+
// cmp doesnt work due the lifetimes on text's return type
345+
a.zip(b)
346+
.find_map(|(a, b)| match path_segment_cmp(&a, &b) {
347+
Ordering::Equal => None,
348+
ord => Some(ord),
349+
})
350+
.unwrap_or(Ordering::Equal)
351+
}
352+
353+
/// Compares to paths, if one ends earlier than the other the has_tl parameters decide which is
354+
/// greater as a a path that has a tree list should be greater, while one that just ends without
355+
/// a tree list should be considered less.
356+
fn use_tree_path_cmp(a: &ast::Path, a_has_tl: bool, b: &ast::Path, b_has_tl: bool) -> Ordering {
357+
let a_segments = segment_iter(a);
358+
let b_segments = segment_iter(b);
359+
// cmp_by would be useful for us here but that is currently unstable
360+
// cmp doesnt work due the lifetimes on text's return type
361+
a_segments
362+
.zip_longest(b_segments)
363+
.find_map(|zipped| match zipped {
364+
EitherOrBoth::Both(ref a, ref b) => match path_segment_cmp(a, b) {
365+
Ordering::Equal => None,
366+
ord => Some(ord),
367+
},
368+
EitherOrBoth::Left(_) if !b_has_tl => Some(Ordering::Greater),
369+
EitherOrBoth::Left(_) => Some(Ordering::Less),
370+
EitherOrBoth::Right(_) if !a_has_tl => Some(Ordering::Less),
371+
EitherOrBoth::Right(_) => Some(Ordering::Greater),
372+
})
373+
.unwrap_or(Ordering::Equal)
374+
}
375+
376+
fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
377+
let a = a.name_ref();
378+
let b = b.name_ref();
379+
a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
380+
}
381+
343382
/// What type of merges are allowed.
344383
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
345384
pub enum MergeBehaviour {
@@ -389,7 +428,6 @@ impl ImportGroup {
389428
PathSegmentKind::Name(name) => match name.text().as_str() {
390429
"std" => ImportGroup::Std,
391430
"core" => ImportGroup::Std,
392-
// FIXME: can be ThisModule as well
393431
_ => ImportGroup::ExternCrate,
394432
},
395433
PathSegmentKind::Type { .. } => unreachable!(),
@@ -415,30 +453,30 @@ fn find_insert_position(
415453
.as_syntax_node()
416454
.children()
417455
.filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
418-
.flat_map(|(use_, node)| use_.use_tree().and_then(|tree| tree.path()).zip(Some(node)));
456+
.flat_map(|(use_, node)| {
457+
let tree = use_.use_tree()?;
458+
let path = tree.path()?;
459+
let has_tl = tree.use_tree_list().is_some();
460+
Some((path, has_tl, node))
461+
});
419462
// Iterator that discards anything thats not in the required grouping
420463
// This implementation allows the user to rearrange their import groups as this only takes the first group that fits
421464
let group_iter = path_node_iter
422465
.clone()
423-
.skip_while(|(path, _)| ImportGroup::new(path) != group)
424-
.take_while(|(path, _)| ImportGroup::new(path) == group);
466+
.skip_while(|(path, ..)| ImportGroup::new(path) != group)
467+
.take_while(|(path, ..)| ImportGroup::new(path) == group);
425468

426-
let segments = segment_iter(&insert_path);
427469
// track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place
428470
let mut last = None;
429471
// find the element that would come directly after our new import
430-
let post_insert =
431-
group_iter.inspect(|(_, node)| last = Some(node.clone())).find(|(path, _)| {
432-
let check_segments = segment_iter(&path);
433-
segments
434-
.clone()
435-
.zip(check_segments)
436-
.flat_map(|(seg, seg2)| seg.name_ref().zip(seg2.name_ref()))
437-
.all(|(l, r)| l.text() <= r.text())
438-
});
472+
let post_insert = group_iter.inspect(|(.., node)| last = Some(node.clone())).find(
473+
|&(ref path, has_tl, _)| {
474+
use_tree_path_cmp(&insert_path, false, path, has_tl) != Ordering::Greater
475+
},
476+
);
439477
match post_insert {
440478
// insert our import before that element
441-
Some((_, node)) => (InsertPosition::Before(node.into()), AddBlankLine::After),
479+
Some((.., node)) => (InsertPosition::Before(node.into()), AddBlankLine::After),
442480
// there is no element after our new import, so append it to the end of the group
443481
None => match last {
444482
Some(node) => (InsertPosition::After(node.into()), AddBlankLine::Before),
@@ -448,10 +486,10 @@ fn find_insert_position(
448486
let mut last = None;
449487
// find the group that comes after where we want to insert
450488
let post_group = path_node_iter
451-
.inspect(|(_, node)| last = Some(node.clone()))
452-
.find(|(p, _)| ImportGroup::new(p) > group);
489+
.inspect(|(.., node)| last = Some(node.clone()))
490+
.find(|(p, ..)| ImportGroup::new(p) > group);
453491
match post_group {
454-
Some((_, node)) => {
492+
Some((.., node)) => {
455493
(InsertPosition::Before(node.into()), AddBlankLine::AfterTwice)
456494
}
457495
// there is no such group, so append after the last one
@@ -844,7 +882,6 @@ use foo::bar::baz::Qux;",
844882
}
845883

846884
#[test]
847-
#[ignore] // FIXME: Order changes when switching lhs and rhs
848885
fn skip_merge_last_too_long2() {
849886
check_last(
850887
"foo::bar::baz::Qux",

0 commit comments

Comments
 (0)