hir_def/nameres/
mod_resolution.rs

1//! This module resolves `mod foo;` declaration to file.
2use arrayvec::ArrayVec;
3use base_db::{AnchoredPath, Crate};
4use hir_expand::{EditionedFileId, name::Name};
5
6use crate::{HirFileId, db::DefDatabase};
7
8const MOD_DEPTH_LIMIT: usize = 32;
9
10#[derive(Clone, Debug)]
11pub(super) struct ModDir {
12    /// `` for `mod.rs`, `lib.rs`
13    /// `foo/` for `foo.rs`
14    /// `foo/bar/` for `mod bar { mod x; }` nested in `foo.rs`
15    /// Invariant: path.is_empty() || path.ends_with('/')
16    dir_path: DirPath,
17    /// inside `./foo.rs`, mods with `#[path]` should *not* be relative to `./foo/`
18    root_non_dir_owner: bool,
19    depth: u32,
20}
21
22impl ModDir {
23    pub(super) fn root() -> ModDir {
24        ModDir { dir_path: DirPath::empty(), root_non_dir_owner: false, depth: 0 }
25    }
26
27    pub(super) fn descend_into_definition(
28        &self,
29        name: &Name,
30        attr_path: Option<&str>,
31    ) -> Option<ModDir> {
32        let path = match attr_path {
33            None => {
34                let mut path = self.dir_path.clone();
35                path.push(name.as_str());
36                path
37            }
38            Some(attr_path) => {
39                let mut path = self.dir_path.join_attr(attr_path, self.root_non_dir_owner);
40                if !(path.is_empty() || path.ends_with('/')) {
41                    path.push('/')
42                }
43                DirPath::new(path)
44            }
45        };
46        self.child(path, false)
47    }
48
49    fn child(&self, dir_path: DirPath, root_non_dir_owner: bool) -> Option<ModDir> {
50        let depth = self.depth + 1;
51        if depth as usize > MOD_DEPTH_LIMIT {
52            tracing::error!("MOD_DEPTH_LIMIT exceeded");
53            cov_mark::hit!(circular_mods);
54            return None;
55        }
56        Some(ModDir { dir_path, root_non_dir_owner, depth })
57    }
58
59    pub(super) fn resolve_declaration(
60        &self,
61        db: &dyn DefDatabase,
62        file_id: HirFileId,
63        name: &Name,
64        attr_path: Option<&str>,
65        krate: Crate,
66    ) -> Result<(EditionedFileId, bool, ModDir), Box<[String]>> {
67        let name = name.as_str();
68
69        let mut candidate_files = ArrayVec::<_, 2>::new();
70        match attr_path {
71            Some(attr_path) => {
72                candidate_files.push(self.dir_path.join_attr(attr_path, self.root_non_dir_owner))
73            }
74            None => {
75                candidate_files.push(format!("{}{}.rs", self.dir_path.0, name));
76                candidate_files.push(format!("{}{}/mod.rs", self.dir_path.0, name));
77            }
78        };
79
80        let orig_file_id = file_id.original_file_respecting_includes(db);
81        for candidate in candidate_files.iter() {
82            let path = AnchoredPath { anchor: orig_file_id.file_id(db), path: candidate.as_str() };
83            if let Some(file_id) = db.resolve_path(path) {
84                let is_mod_rs = candidate.ends_with("/mod.rs");
85
86                let root_dir_owner = is_mod_rs || attr_path.is_some();
87                let dir_path = if root_dir_owner {
88                    DirPath::empty()
89                } else {
90                    DirPath::new(format!("{name}/"))
91                };
92                if let Some(mod_dir) = self.child(dir_path, !root_dir_owner) {
93                    return Ok((
94                        // FIXME: Edition, is this rightr?
95                        EditionedFileId::new(db, file_id, orig_file_id.edition(db), krate),
96                        is_mod_rs,
97                        mod_dir,
98                    ));
99                }
100            }
101        }
102        Err(candidate_files.into_iter().collect())
103    }
104}
105
106#[derive(Clone, Debug)]
107struct DirPath(String);
108
109impl DirPath {
110    fn assert_invariant(&self) {
111        assert!(self.0.is_empty() || self.0.ends_with('/'));
112    }
113    fn new(repr: String) -> DirPath {
114        let res = DirPath(repr);
115        res.assert_invariant();
116        res
117    }
118    fn empty() -> DirPath {
119        DirPath::new(String::new())
120    }
121    fn push(&mut self, name: &str) {
122        self.0.push_str(name);
123        self.0.push('/');
124        self.assert_invariant();
125    }
126    fn parent(&self) -> Option<&str> {
127        if self.0.is_empty() {
128            return None;
129        };
130        let idx =
131            self.0[..self.0.len() - '/'.len_utf8()].rfind('/').map_or(0, |it| it + '/'.len_utf8());
132        Some(&self.0[..idx])
133    }
134    /// So this is the case which doesn't really work I think if we try to be
135    /// 100% platform agnostic:
136    ///
137    /// ```ignore
138    /// mod a {
139    ///     #[path="C://sad/face"]
140    ///     mod b { mod c; }
141    /// }
142    /// ```
143    ///
144    /// Here, we need to join logical dir path to a string path from an
145    /// attribute. Ideally, we should somehow losslessly communicate the whole
146    /// construction to `FileLoader`.
147    fn join_attr(&self, mut attr: &str, relative_to_parent: bool) -> String {
148        let base = if relative_to_parent { self.parent().unwrap() } else { &self.0 };
149
150        if attr.starts_with("./") {
151            attr = &attr["./".len()..];
152        }
153        let tmp;
154        let attr = if attr.contains('\\') {
155            tmp = attr.replace('\\', "/");
156            &tmp
157        } else {
158            attr
159        };
160        let res = format!("{base}{attr}");
161        res
162    }
163}