Skip to main content

hir_def/nameres/
mod_resolution.rs

1//! This module resolves `mod foo;` declaration to file.
2use arrayvec::ArrayVec;
3use base_db::{AnchoredPath, SourceDatabase};
4use hir_expand::{EditionedFileId, name::Name};
5
6use crate::HirFileId;
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 SourceDatabase,
62        file_id: HirFileId,
63        name: &Name,
64        attr_path: Option<&str>,
65    ) -> Result<(EditionedFileId, bool, ModDir), Box<[String]>> {
66        let name = name.as_str();
67
68        let mut candidate_files = ArrayVec::<_, 2>::new();
69        match attr_path {
70            Some(attr_path) => {
71                candidate_files.push(self.dir_path.join_attr(attr_path, self.root_non_dir_owner))
72            }
73            None => {
74                candidate_files.push(format!("{}{}.rs", self.dir_path.0, name));
75                candidate_files.push(format!("{}{}/mod.rs", self.dir_path.0, name));
76            }
77        };
78
79        let orig_file_id = file_id.original_file_respecting_includes(db);
80        for candidate in candidate_files.iter() {
81            let path = AnchoredPath { anchor: orig_file_id.file_id(db), path: candidate.as_str() };
82            if let Some(file_id) = db.resolve_path(path) {
83                let is_mod_rs = candidate.ends_with("/mod.rs");
84
85                let root_dir_owner = is_mod_rs || attr_path.is_some();
86                let dir_path = if root_dir_owner {
87                    DirPath::empty()
88                } else {
89                    DirPath::new(format!("{name}/"))
90                };
91                if let Some(mod_dir) = self.child(dir_path, !root_dir_owner) {
92                    return Ok((
93                        // FIXME: Edition, is this rightr?
94                        EditionedFileId::new(db, file_id, orig_file_id.edition(db)),
95                        is_mod_rs,
96                        mod_dir,
97                    ));
98                }
99            }
100        }
101        Err(candidate_files.into_iter().collect())
102    }
103}
104
105#[derive(Clone, Debug)]
106struct DirPath(String);
107
108impl DirPath {
109    fn assert_invariant(&self) {
110        assert!(self.0.is_empty() || self.0.ends_with('/'));
111    }
112    fn new(repr: String) -> DirPath {
113        let res = DirPath(repr);
114        res.assert_invariant();
115        res
116    }
117    fn empty() -> DirPath {
118        DirPath::new(String::new())
119    }
120    fn push(&mut self, name: &str) {
121        self.0.push_str(name);
122        self.0.push('/');
123        self.assert_invariant();
124    }
125    fn parent(&self) -> Option<&str> {
126        if self.0.is_empty() {
127            return None;
128        };
129        let idx =
130            self.0[..self.0.len() - '/'.len_utf8()].rfind('/').map_or(0, |it| it + '/'.len_utf8());
131        Some(&self.0[..idx])
132    }
133    /// So this is the case which doesn't really work I think if we try to be
134    /// 100% platform agnostic:
135    ///
136    /// ```ignore
137    /// mod a {
138    ///     #[path="C://sad/face"]
139    ///     mod b { mod c; }
140    /// }
141    /// ```
142    ///
143    /// Here, we need to join logical dir path to a string path from an
144    /// attribute. Ideally, we should somehow losslessly communicate the whole
145    /// construction to `FileLoader`.
146    fn join_attr(&self, mut attr: &str, relative_to_parent: bool) -> String {
147        let base = if relative_to_parent { self.parent().unwrap() } else { &self.0 };
148
149        if attr.starts_with("./") {
150            attr = &attr["./".len()..];
151        }
152        let attr = if attr.contains('\\') { &attr.replace('\\', "/") } else { attr };
153        let res = format!("{base}{attr}");
154        res
155    }
156}