1use crate::path::module_name_to_path;
6use perl_parser_core::path_security::validate_workspace_path;
7use perl_workspace::folder::workspace_folder_to_path;
8use std::collections::HashSet;
9use std::path::{Component, Path, PathBuf};
10use std::time::{Duration, Instant};
11use url::Url;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum IncRootKind {
16 FileLocalLexical,
18 WorkspaceRelative,
20 ExternalAbsolute,
22 Perl5LibEnv,
28 InterpreterStartup,
30 RuntimeDerived,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct IncRoot {
37 pub kind: IncRootKind,
39 pub path: PathBuf,
41 pub precedence: usize,
43 pub source: String,
45}
46
47#[must_use]
54pub fn build_effective_inc_roots(
55 include_paths: &[String],
56 perl5lib_paths: &[String],
57 use_perl5lib: bool,
58 lexical_paths: &[String],
59 system_paths: &[PathBuf],
60) -> Vec<IncRoot> {
61 let perl5lib_set: HashSet<String> =
62 if use_perl5lib { perl5lib_paths.iter().cloned().collect() } else { HashSet::new() };
63
64 let mut roots = Vec::new();
65 let mut seen = HashSet::new();
66 let mut precedence = 0usize;
67
68 for path in lexical_paths {
69 let path_buf = PathBuf::from(path);
70 let kind = if path_buf.is_absolute() {
71 IncRootKind::ExternalAbsolute
72 } else {
73 IncRootKind::FileLocalLexical
74 };
75 if !seen.insert(normalized_inc_key(&path_buf)) {
76 continue;
77 }
78 roots.push(IncRoot {
79 kind,
80 path: path_buf,
81 precedence,
82 source: "use-lib-lexical".to_string(),
83 });
84 precedence += 1;
85 }
86
87 for path in include_paths {
88 let path_buf = PathBuf::from(path);
89 if !seen.insert(normalized_inc_key(&path_buf)) {
90 continue;
91 }
92 let (kind, source) = if perl5lib_set.contains(path) {
93 (IncRootKind::Perl5LibEnv, "perl5lib-env")
94 } else if path_buf.is_absolute() {
95 (IncRootKind::ExternalAbsolute, "workspace-include-paths")
96 } else {
97 (IncRootKind::WorkspaceRelative, "workspace-include-paths")
98 };
99 roots.push(IncRoot { kind, path: path_buf, precedence, source: source.to_string() });
100 precedence += 1;
101 }
102
103 for path in system_paths {
104 if !seen.insert(normalized_inc_key(path)) {
105 continue;
106 }
107 roots.push(IncRoot {
108 kind: IncRootKind::InterpreterStartup,
109 path: path.clone(),
110 precedence,
111 source: "interpreter-startup-inc".to_string(),
112 });
113 precedence += 1;
114 }
115
116 roots
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum ModuleUriResolution {
122 Resolved(String),
124 NotFound,
126 TimedOut,
128}
129
130#[must_use]
137pub fn resolve_module_uri(
138 module_name: &str,
139 open_document_uris: &[String],
140 workspace_folders: &[String],
141 include_paths: &[String],
142 use_system_inc: bool,
143 system_inc: &[PathBuf],
144 timeout: Duration,
145) -> ModuleUriResolution {
146 let mut effective_inc_roots = Vec::new();
147 let mut seen_include_paths = HashSet::new();
148
149 for include_path in include_paths {
150 let Some(path) = normalize_inc_path_string(include_path) else {
151 continue;
152 };
153 if !seen_include_paths.insert(path.clone()) {
154 continue;
155 }
156
157 let kind = if path.is_absolute() {
158 IncRootKind::ExternalAbsolute
159 } else {
160 IncRootKind::WorkspaceRelative
161 };
162 effective_inc_roots.push(IncRoot {
163 kind,
164 path,
165 precedence: effective_inc_roots.len(),
166 source: "includePaths".to_string(),
167 });
168 }
169
170 if use_system_inc {
171 let mut seen_system_paths = HashSet::new();
172
173 for path in system_inc {
174 let Some(path) = normalize_system_inc_path(path) else {
175 continue;
176 };
177 if !seen_system_paths.insert(path.clone()) {
178 continue;
179 }
180
181 effective_inc_roots.push(IncRoot {
182 kind: IncRootKind::InterpreterStartup,
183 path,
184 precedence: effective_inc_roots.len(),
185 source: "interpreter-startup-inc".to_string(),
186 });
187 }
188 }
189
190 resolve_module_uri_with_effective_inc(
191 module_name,
192 open_document_uris,
193 workspace_folders,
194 &effective_inc_roots,
195 timeout,
196 )
197}
198
199#[must_use]
201pub fn resolve_module_uri_with_effective_inc(
202 module_name: &str,
203 open_document_uris: &[String],
204 workspace_folders: &[String],
205 effective_inc_roots: &[IncRoot],
206 timeout: Duration,
207) -> ModuleUriResolution {
208 let start_time = Instant::now();
209 let relative_path = module_name_to_path(module_name);
210
211 for uri in open_document_uris {
212 if uri.ends_with(&relative_path) {
213 return ModuleUriResolution::Resolved(uri.clone());
214 }
215 }
216
217 let mut ordered_roots = effective_inc_roots.to_vec();
218 ordered_roots.sort_by_key(|r| r.precedence);
219
220 for workspace_folder in workspace_folders {
221 if start_time.elapsed() > timeout {
222 return ModuleUriResolution::TimedOut;
223 }
224
225 let workspace_path = workspace_folder_to_path(workspace_folder);
226
227 for inc_root in &ordered_roots {
228 if !matches!(
229 inc_root.kind,
230 IncRootKind::FileLocalLexical | IncRootKind::WorkspaceRelative
231 ) {
232 continue;
233 }
234 if start_time.elapsed() > timeout {
235 return ModuleUriResolution::TimedOut;
236 }
237
238 let full_path = full_path_for_root(inc_root, &workspace_path, &relative_path);
239 let Some(full_path) = full_path else { continue };
240
241 if full_path.is_file()
242 && let Ok(url) = Url::from_file_path(&full_path)
243 {
244 return ModuleUriResolution::Resolved(url.to_string());
245 }
246 }
247 }
248
249 for inc_root in &ordered_roots {
250 if !matches!(
251 inc_root.kind,
252 IncRootKind::ExternalAbsolute
253 | IncRootKind::Perl5LibEnv
254 | IncRootKind::InterpreterStartup
255 | IncRootKind::RuntimeDerived
256 ) {
257 continue;
258 }
259 if start_time.elapsed() > timeout {
260 return ModuleUriResolution::TimedOut;
261 }
262
263 let full_path = inc_root.path.join(&relative_path);
264 if full_path.is_file()
265 && let Ok(url) = Url::from_file_path(&full_path)
266 {
267 return ModuleUriResolution::Resolved(url.to_string());
268 }
269 }
270
271 ModuleUriResolution::NotFound
272}
273
274fn normalize_inc_path_string(input: &str) -> Option<PathBuf> {
275 let trimmed = input.trim();
276 if trimmed.is_empty() {
277 return None;
278 }
279
280 Some(normalize_path_for_dedupe(Path::new(trimmed)))
281}
282
283fn normalize_system_inc_path(input: &Path) -> Option<PathBuf> {
284 let trimmed = input.to_string_lossy().trim().to_string();
285 if trimmed.is_empty() {
286 return None;
287 }
288
289 let normalized = normalize_path_for_dedupe(Path::new(&trimmed));
290 if normalized == Path::new(".") {
291 return None;
292 }
293
294 Some(normalized)
295}
296
297fn normalize_path_for_dedupe(path: &Path) -> PathBuf {
298 let mut normalized = PathBuf::new();
299 for component in path.components() {
300 if component == Component::CurDir {
301 continue;
302 }
303 normalized.push(component.as_os_str());
304 }
305
306 if normalized.as_os_str().is_empty() { PathBuf::from(".") } else { normalized }
307}
308
309fn normalized_inc_key(path: &Path) -> String {
310 let normalized = path.to_string_lossy().replace('\\', "/");
311 if normalized == "/" { normalized } else { normalized.trim_end_matches('/').to_string() }
312}
313
314fn full_path_for_root(
315 inc_root: &IncRoot,
316 workspace_path: &Path,
317 relative_path: &str,
318) -> Option<PathBuf> {
319 match inc_root.kind {
320 IncRootKind::FileLocalLexical | IncRootKind::WorkspaceRelative => {
321 if inc_root.path == Path::new(".") {
322 let full_path = workspace_path.join(relative_path);
323 validate_workspace_path(&full_path, workspace_path).ok()
324 } else if inc_root.path.is_absolute() {
325 Some(inc_root.path.join(relative_path))
326 } else {
327 let full_path = workspace_path.join(&inc_root.path).join(relative_path);
328 validate_workspace_path(&full_path, workspace_path).ok()
329 }
330 }
331 IncRootKind::ExternalAbsolute
332 | IncRootKind::Perl5LibEnv
333 | IncRootKind::InterpreterStartup
334 | IncRootKind::RuntimeDerived => Some(inc_root.path.join(relative_path)),
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::{IncRootKind, build_effective_inc_roots};
341 use std::path::PathBuf;
342
343 #[test]
344 fn effective_inc_roots_dedupes_normalized_sources() {
345 let include_paths = vec!["lib".to_string(), "lib/".to_string(), "other".to_string()];
346 let lexical_paths = vec!["lib\\".to_string()];
347 let system_paths = vec![PathBuf::from("other/"), PathBuf::from("syslib")];
348
349 let roots =
350 build_effective_inc_roots(&include_paths, &[], false, &lexical_paths, &system_paths);
351 let root_paths: Vec<String> =
352 roots.iter().map(|root| root.path.to_string_lossy().replace('\\', "/")).collect();
353
354 assert_eq!(root_paths, vec!["lib/".to_string(), "other".to_string(), "syslib".to_string()]);
355 assert_eq!(roots[0].source, "use-lib-lexical");
356 assert_eq!(roots[1].source, "workspace-include-paths");
357 assert_eq!(roots[2].source, "interpreter-startup-inc");
358 }
359
360 #[test]
361 fn effective_inc_roots_preserves_first_source_precedence() {
362 let include_paths = vec!["dup".to_string(), "late".to_string()];
363 let lexical_paths = vec!["dup".to_string()];
364 let system_paths = vec![PathBuf::from("late"), PathBuf::from("sys")];
365
366 let roots =
367 build_effective_inc_roots(&include_paths, &[], false, &lexical_paths, &system_paths);
368
369 assert_eq!(roots.len(), 3);
370 assert_eq!(roots[0].path, PathBuf::from("dup"));
371 assert_eq!(roots[0].kind, IncRootKind::FileLocalLexical);
372 assert_eq!(roots[1].path, PathBuf::from("late"));
373 assert_eq!(roots[1].kind, IncRootKind::WorkspaceRelative);
374 assert_eq!(roots[2].path, PathBuf::from("sys"));
375 assert_eq!(roots[2].kind, IncRootKind::InterpreterStartup);
376 assert_eq!(roots[0].precedence, 0);
377 assert_eq!(roots[1].precedence, 1);
378 assert_eq!(roots[2].precedence, 2);
379 }
380
381 #[test]
382 fn effective_inc_roots_labels_perl5lib_only_when_enabled() {
383 let perl5lib_path = "perl5lib".to_string();
384 let include_paths = vec![perl5lib_path.clone(), "lib".to_string()];
385
386 let enabled = build_effective_inc_roots(
387 &include_paths,
388 std::slice::from_ref(&perl5lib_path),
389 true,
390 &[],
391 &[],
392 );
393 assert_eq!(enabled[0].kind, IncRootKind::Perl5LibEnv);
394 assert_eq!(enabled[0].source, "perl5lib-env");
395 assert_eq!(enabled[1].kind, IncRootKind::WorkspaceRelative);
396
397 let disabled = build_effective_inc_roots(&include_paths, &[perl5lib_path], false, &[], &[]);
398 assert_eq!(disabled[0].kind, IncRootKind::WorkspaceRelative);
399 assert_eq!(disabled[0].source, "workspace-include-paths");
400 assert_eq!(disabled[1].kind, IncRootKind::WorkspaceRelative);
401 }
402}