Skip to main content

stern4rust/finding/model/
import_path.rs

1// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
2// Licensed under the MIT License
3// SPDX-License-Identifier: MIT
4
5// The imports whose order rustfmt decides rather than the alphabet.
6//
7// rustfmt sorts `self`, `super` and `crate` ahead of every other path. It also
8// treats case as significant, and -- measured, because it is not what anyone
9// would guess -- in opposite directions at the two levels: an uppercase-initial
10// crate goes behind every lowercase one, while an uppercase-initial segment
11// later in a path goes ahead of its lowercase siblings. `Bbb::gamma` sorts after
12// `zzz::last`; `serde_json::Value` sorts before `serde_json::from_str`.
13//
14// None of that matches a plain alphabetic sort, and unlike every other
15// disagreement this tool could have with a formatter, this one has no
16// resolution: `cargo fmt` writes one order, the rule would demand another, and
17// stage 1 runs the formatter first. A file caught between the two cannot be
18// fixed by hand at all -- each run undoes the last.
19//
20// So the structure rule stands down on exactly those pairs. Everything else in
21// the import list is still ordered, because among segments of the same case
22// rustfmt's comparator and the alphabet agree.
23//
24// The case rule is pairwise rather than a property of one import, and that
25// distinction is the whole of it. `use serde_json::Value` and
26// `use serde_json::from_str` share a first segment and part company at the
27// second, where one is uppercase and one is not. Neither path is remarkable on
28// its own; only the pair is.
29pub struct ImportPath;
30
31impl ImportPath {
32    // Whether rustfmt, rather than the alphabet, decides this pair's order.
33    pub fn decides_order(previous: &str, item: &str) -> bool {
34        Self::is_specially_ordered(previous)
35            || Self::is_specially_ordered(item)
36            || Self::diverges_by_case(previous, item)
37    }
38
39    pub fn is_specially_ordered(import: &str) -> bool {
40        let first = Self::first_segment(import);
41        matches!(first, "self" | "super" | "crate")
42            || first.chars().next().is_some_and(char::is_uppercase)
43    }
44
45    // Where two paths first differ, and whether the segments there are of
46    // different case. That is the one place the two comparators can disagree:
47    // before it the paths are identical, and after it nothing is compared.
48    fn diverges_by_case(previous: &str, item: &str) -> bool {
49        Self::segments(previous)
50            .into_iter()
51            .zip(Self::segments(item))
52            .find(|(left, right)| left != right)
53            .is_some_and(|(left, right)| Self::is_uppercase(left) != Self::is_uppercase(right))
54    }
55
56    // The first segment, not a prefix: a crate genuinely named `crateful` sorts
57    // alphabetically like anything else.
58    fn first_segment(import: &str) -> &str {
59        Self::segments(import).first().copied().unwrap_or_default()
60    }
61
62    fn is_uppercase(segment: &str) -> bool {
63        segment.chars().next().is_some_and(char::is_uppercase)
64    }
65
66    fn segments(import: &str) -> Vec<&str> {
67        import
68            .trim()
69            .trim_start_matches("pub ")
70            .trim_start_matches("use ")
71            .trim_start()
72            .trim_end_matches(';')
73            .trim_start_matches("::")
74            .split("::")
75            .map(str::trim)
76            .collect()
77    }
78}