frs/cmds/rsl/rules/common/
fn_path_resolution.rs1use 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 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 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 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 continue;
246 };
247 if !glob_scope.fns.contains(name) {
248 continue;
249 }
250 if candidate.is_some() {
251 return None;
253 }
254 candidate = Some(glob_module.clone());
255 }
256 candidate
257}