perl_module/resolution/
uri.rs1use 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#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum ModuleUriResolution {
50 Resolved(String),
52 NotFound,
54 TimedOut,
56}
57
58#[must_use]
65pub fn resolve_module_uri(
66 module_name: &str,
67 open_document_uris: &[String],
68 workspace_folders: &[String],
69 include_paths: &[String],
70 use_system_inc: bool,
71 system_inc: &[PathBuf],
72 timeout: Duration,
73) -> ModuleUriResolution {
74 let mut effective_inc_roots = Vec::new();
75 let mut seen_include_paths = HashSet::new();
76
77 for include_path in include_paths {
78 let Some(path) = normalize_inc_path_string(include_path) else {
79 continue;
80 };
81 if !seen_include_paths.insert(path.clone()) {
82 continue;
83 }
84
85 let kind = if path.is_absolute() {
86 IncRootKind::ExternalAbsolute
87 } else {
88 IncRootKind::WorkspaceRelative
89 };
90 effective_inc_roots.push(IncRoot {
91 kind,
92 path,
93 precedence: effective_inc_roots.len(),
94 source: "includePaths".to_string(),
95 });
96 }
97
98 if use_system_inc {
99 let mut seen_system_paths = HashSet::new();
100
101 for path in system_inc {
102 let Some(path) = normalize_system_inc_path(path) else {
103 continue;
104 };
105 if !seen_system_paths.insert(path.clone()) {
106 continue;
107 }
108
109 effective_inc_roots.push(IncRoot {
110 kind: IncRootKind::InterpreterStartup,
111 path,
112 precedence: effective_inc_roots.len(),
113 source: "interpreter-startup-inc".to_string(),
114 });
115 }
116 }
117
118 resolve_module_uri_with_effective_inc(
119 module_name,
120 open_document_uris,
121 workspace_folders,
122 &effective_inc_roots,
123 timeout,
124 )
125}
126
127#[must_use]
129pub fn resolve_module_uri_with_effective_inc(
130 module_name: &str,
131 open_document_uris: &[String],
132 workspace_folders: &[String],
133 effective_inc_roots: &[IncRoot],
134 timeout: Duration,
135) -> ModuleUriResolution {
136 let start_time = Instant::now();
137 let relative_path = module_name_to_path(module_name);
138
139 for uri in open_document_uris {
140 if uri.ends_with(&relative_path) {
141 return ModuleUriResolution::Resolved(uri.clone());
142 }
143 }
144
145 let mut ordered_roots = effective_inc_roots.to_vec();
146 ordered_roots.sort_by_key(|r| r.precedence);
147
148 for workspace_folder in workspace_folders {
149 if start_time.elapsed() > timeout {
150 return ModuleUriResolution::TimedOut;
151 }
152
153 let workspace_path = workspace_folder_to_path(workspace_folder);
154
155 for inc_root in &ordered_roots {
156 if !matches!(
157 inc_root.kind,
158 IncRootKind::FileLocalLexical | IncRootKind::WorkspaceRelative
159 ) {
160 continue;
161 }
162 if start_time.elapsed() > timeout {
163 return ModuleUriResolution::TimedOut;
164 }
165
166 let full_path = full_path_for_root(inc_root, &workspace_path, &relative_path);
167 let Some(full_path) = full_path else { continue };
168
169 if full_path.is_file()
170 && let Ok(url) = Url::from_file_path(&full_path)
171 {
172 return ModuleUriResolution::Resolved(url.to_string());
173 }
174 }
175 }
176
177 for inc_root in &ordered_roots {
178 if !matches!(
179 inc_root.kind,
180 IncRootKind::ExternalAbsolute
181 | IncRootKind::Perl5LibEnv
182 | IncRootKind::InterpreterStartup
183 | IncRootKind::RuntimeDerived
184 ) {
185 continue;
186 }
187 if start_time.elapsed() > timeout {
188 return ModuleUriResolution::TimedOut;
189 }
190
191 let full_path = inc_root.path.join(&relative_path);
192 if full_path.is_file()
193 && let Ok(url) = Url::from_file_path(&full_path)
194 {
195 return ModuleUriResolution::Resolved(url.to_string());
196 }
197 }
198
199 ModuleUriResolution::NotFound
200}
201
202fn normalize_inc_path_string(input: &str) -> Option<PathBuf> {
203 let trimmed = input.trim();
204 if trimmed.is_empty() {
205 return None;
206 }
207
208 Some(normalize_path_for_dedupe(Path::new(trimmed)))
209}
210
211fn normalize_system_inc_path(input: &Path) -> Option<PathBuf> {
212 let trimmed = input.to_string_lossy().trim().to_string();
213 if trimmed.is_empty() {
214 return None;
215 }
216
217 let normalized = normalize_path_for_dedupe(Path::new(&trimmed));
218 if normalized == Path::new(".") {
219 return None;
220 }
221
222 Some(normalized)
223}
224
225fn normalize_path_for_dedupe(path: &Path) -> PathBuf {
226 let mut normalized = PathBuf::new();
227 for component in path.components() {
228 if component == Component::CurDir {
229 continue;
230 }
231 normalized.push(component.as_os_str());
232 }
233
234 if normalized.as_os_str().is_empty() { PathBuf::from(".") } else { normalized }
235}
236
237fn full_path_for_root(
238 inc_root: &IncRoot,
239 workspace_path: &Path,
240 relative_path: &str,
241) -> Option<PathBuf> {
242 match inc_root.kind {
243 IncRootKind::FileLocalLexical | IncRootKind::WorkspaceRelative => {
244 if inc_root.path == Path::new(".") {
245 let full_path = workspace_path.join(relative_path);
246 validate_workspace_path(&full_path, workspace_path).ok()
247 } else if inc_root.path.is_absolute() {
248 Some(inc_root.path.join(relative_path))
249 } else {
250 let full_path = workspace_path.join(&inc_root.path).join(relative_path);
251 validate_workspace_path(&full_path, workspace_path).ok()
252 }
253 }
254 IncRootKind::ExternalAbsolute
255 | IncRootKind::Perl5LibEnv
256 | IncRootKind::InterpreterStartup
257 | IncRootKind::RuntimeDerived => Some(inc_root.path.join(relative_path)),
258 }
259}