Skip to main content

frs/cmds/rsl/rules/common/
fn_path_resolution.rs

1use std::collections::HashSet;
2
3use super::import_resolution::imported_binding;
4use super::module_idx::ModuleIdx;
5use super::module_idx::ScopeInfo;
6use super::module_idx::is_local_module_path;
7use super::path_resolution::is_associated_fn_path;
8use super::path_resolution::is_non_fn_call_path;
9use super::path_resolution::normalize_path;
10use super::path_resolution::path_parts;
11
12pub struct FnCallSuggestion {
13    pub expected_path: String,
14    pub required_import: Option<String>,
15}
16
17pub fn expected_fn_path(
18    idx: &ModuleIdx<'_>,
19    current_module: &[String],
20    local_bindings: &[HashSet<String>],
21    path: &syn::Path,
22) -> Option<FnCallSuggestion> {
23    let parts = path_parts(path)?;
24    if is_non_fn_call_path(&parts) || is_associated_fn_path(&parts) {
25        return None;
26    }
27
28    if parts.len() == 1 && parts.first().is_some_and(|name| is_local_binding(local_bindings, name)) {
29        return None;
30    }
31
32    if local_fn(idx, current_module, path).is_some() && fn_path_tail(&parts).len() <= 2 {
33        return None;
34    }
35
36    let name = parts.last()?;
37    if parts.len() == 1 {
38        // TODO: Resolve bare calls whose definitions are outside the current file.
39        return imported_fn_path(idx, current_module, name);
40    }
41
42    let qualified = fn_path_tail(&parts);
43    if qualified.len() <= 2 || !can_use_shortened_module(idx, current_module, &parts) {
44        return None;
45    }
46
47    let expected_path = qualified
48        .get(qualified.len().saturating_sub(2)..)
49        .map(|parts| parts.join("::"))?;
50    let fn_idx = parts.len().saturating_sub(1);
51    let target_module = normalize_path(current_module, parts.get(..fn_idx).unwrap_or_default());
52
53    Some(FnCallSuggestion {
54        expected_path,
55        required_import: required_module_import(idx, current_module, &parts, &target_module),
56    })
57}
58
59fn is_local_binding(local_bindings: &[HashSet<String>], name: &str) -> bool {
60    local_bindings.iter().rev().any(|bindings| bindings.contains(name))
61}
62
63fn imported_fn_path(idx: &ModuleIdx<'_>, current_module: &[String], name: &str) -> Option<FnCallSuggestion> {
64    let binding = imported_binding(idx, current_module, name)?;
65    let source_path = &binding.path;
66    if !can_use_imported_module(idx, current_module, source_path) {
67        return None;
68    }
69
70    let expected_path = (source_path.len() >= 2)
71        .then(|| source_path.get(source_path.len().saturating_sub(2)..))
72        .flatten()
73        .map(|parts| parts.join("::"))?;
74    let fn_idx = source_path.len().saturating_sub(1);
75    let target_module = source_path.get(..fn_idx)?;
76
77    Some(FnCallSuggestion {
78        expected_path,
79        required_import: required_module_import(idx, current_module, &binding.source_path, target_module),
80    })
81}
82
83fn required_module_import(
84    idx: &ModuleIdx<'_>,
85    current_module: &[String],
86    source_path: &[String],
87    target_module: &[String],
88) -> Option<String> {
89    let module_name = target_module.last()?;
90    if module_is_available(idx, current_module, module_name, target_module) {
91        return None;
92    }
93
94    let source_module_path = source_path.get(..source_path.len().saturating_sub(1))?;
95    let import_path = match source_module_path.first().map(String::as_str) {
96        Some("crate" | "self" | "super") => crate_path(target_module),
97        Some(_) if source_module_path.len() == 1 && !idx.modules.contains(target_module) => return None,
98        Some(_) if idx.modules.contains(target_module) => crate_path(target_module),
99        Some(_) => source_module_path.join("::"),
100        None => return None,
101    };
102
103    Some(format!("use {import_path};"))
104}
105
106fn module_is_available(
107    idx: &ModuleIdx<'_>,
108    current_module: &[String],
109    module_name: &str,
110    target_module: &[String],
111) -> bool {
112    let Some(scope) = idx.info.get(current_module) else {
113        return false;
114    };
115    if scope
116        .imports
117        .iter()
118        .any(|binding| binding.name == module_name && binding.path == target_module)
119    {
120        return true;
121    }
122
123    let mut local_module = current_module.to_vec();
124    local_module.push(module_name.to_owned());
125    scope.definitions.contains(module_name) && local_module == target_module && idx.modules.contains(&local_module)
126}
127
128fn crate_path(parts: &[String]) -> String {
129    std::iter::once("crate".to_owned())
130        .chain(parts.iter().cloned())
131        .collect::<Vec<_>>()
132        .join("::")
133}
134
135fn can_use_shortened_module(idx: &ModuleIdx<'_>, current_module: &[String], parts: &[String]) -> bool {
136    let Some(fn_idx) = parts.len().checked_sub(1) else {
137        return false;
138    };
139    let Some(module_name) = parts.get(fn_idx.saturating_sub(1)) else {
140        return false;
141    };
142    let target_module = normalize_path(current_module, parts.get(..fn_idx).unwrap_or_default());
143
144    can_use_module_name(idx, current_module, module_name, &target_module)
145}
146
147fn can_use_imported_module(idx: &ModuleIdx<'_>, current_module: &[String], source_path: &[String]) -> bool {
148    let Some(fn_idx) = source_path.len().checked_sub(1) else {
149        return false;
150    };
151    let Some(module_name) = source_path.get(fn_idx.saturating_sub(1)) else {
152        return false;
153    };
154    let target_module = source_path.get(..fn_idx).unwrap_or_default();
155
156    // The explicit import resolves the callable; defer only the module-name conflict to future glob resolution.
157    if idx.info.get(current_module).is_some_and(|scope| scope.unknown_imports) {
158        return true;
159    }
160
161    can_use_module_name(idx, current_module, module_name, target_module)
162}
163
164fn can_use_module_name(idx: &ModuleIdx<'_>, current_module: &[String], name: &str, target_module: &[String]) -> bool {
165    let Some(scope) = idx.info.get(current_module) else {
166        return false;
167    };
168    // TODO: Resolve glob imports before deciding whether the shortened module name conflicts.
169    if scope.unknown_imports {
170        return false;
171    }
172
173    let mut local_module = current_module.to_vec();
174    local_module.push(name.to_owned());
175    if scope.definitions.contains(name) && (!idx.modules.contains(&local_module) || local_module != target_module) {
176        return false;
177    }
178
179    scope
180        .imports
181        .iter()
182        .filter(|binding| binding.name == name)
183        .all(|binding| binding.path == target_module)
184}
185
186fn fn_path_tail(parts: &[String]) -> &[String] {
187    let mut first = 0;
188    while matches!(parts.get(first).map(String::as_str), Some("crate" | "self" | "super")) {
189        first = first.saturating_add(1);
190    }
191    parts.get(first..).unwrap_or_default()
192}
193
194fn local_fn(idx: &ModuleIdx<'_>, current_module: &[String], path: &syn::Path) -> Option<(Vec<String>, String)> {
195    let parts = path_parts(path)?;
196    let name = parts.last()?.clone();
197    let last_item = parts.len().saturating_sub(1);
198    let mut module_path = current_module.to_vec();
199    let mut first_item = 0;
200
201    match parts.first()?.as_str() {
202        "crate" => {
203            module_path.clear();
204            first_item = 1;
205        }
206        "self" => first_item = 1,
207        "super" => {
208            while parts.get(first_item).is_some_and(|part| part == "super") {
209                module_path.pop()?;
210                first_item = first_item.saturating_add(1);
211            }
212        }
213        _ => {}
214    }
215
216    if first_item > last_item {
217        return None;
218    }
219    module_path.extend(parts.get(first_item..last_item)?.iter().cloned());
220    if !is_local_module_path(idx, &module_path) {
221        return None;
222    }
223
224    let scope = idx.info.get(&module_path)?;
225    if scope.imports.iter().any(|binding| binding.name == name) {
226        return None;
227    }
228    if scope.fns.contains(&name) {
229        return Some((module_path, name));
230    }
231    if let Some(glob_module) = glob_imported_fn(idx, scope, &name) {
232        return Some((glob_module, name));
233    }
234    if scope.unknown_imports {
235        return None;
236    }
237    None
238}
239
240fn glob_imported_fn(idx: &ModuleIdx<'_>, scope: &ScopeInfo, name: &str) -> Option<Vec<String>> {
241    let mut candidate = None;
242    for glob_module in &scope.glob_imports {
243        let Some(glob_scope) = idx.info.get(glob_module) else {
244            // TODO: Resolve glob imports from modules outside this file.
245            continue;
246        };
247        if !glob_scope.fns.contains(name) {
248            continue;
249        }
250        if candidate.is_some() {
251            // TODO: Resolve ambiguous names imported from multiple glob sources.
252            return None;
253        }
254        candidate = Some(glob_module.clone());
255    }
256    candidate
257}