1use std::path::Path;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct UseLibPath {
11 pub path: String,
13 pub from_findbin: bool,
15}
16
17pub fn extract_use_lib_paths(source: &str) -> Vec<UseLibPath> {
39 let mut paths = Vec::new();
40
41 for line in source.lines() {
42 let trimmed = line.trim();
43 if let Some(rest) = strip_use_lib_prefix(trimmed) {
44 extract_paths_from_args(rest, &mut paths);
45 }
46 }
47
48 paths
49}
50
51pub fn resolve_use_lib_paths(
69 use_lib_paths: &[UseLibPath],
70 workspace_root: &Path,
71 file_dir: Option<&Path>,
72) -> Vec<String> {
73 let mut result = Vec::new();
74
75 for ulp in use_lib_paths {
76 let path_str = &ulp.path;
77
78 if ulp.from_findbin {
79 let base = file_dir.unwrap_or(workspace_root);
80 let resolved = base.join(path_str);
81 if let Some(s) = path_to_relative_string(&resolved, workspace_root) {
82 if !result.contains(&s) {
83 result.push(s);
84 }
85 }
86 } else {
87 let p = Path::new(path_str);
88 if p.is_absolute() {
89 if let Ok(rel) = p.strip_prefix(workspace_root) {
90 let s = rel.to_string_lossy().to_string();
91 if !result.contains(&s) {
92 result.push(s);
93 }
94 }
95 } else {
96 let s = path_str.to_string();
97 if !result.contains(&s) {
98 result.push(s);
99 }
100 }
101 }
102 }
103
104 result
105}
106
107fn strip_use_lib_prefix(trimmed: &str) -> Option<&str> {
108 let rest = trimmed.strip_prefix("use")?;
109 if !rest.starts_with(|c: char| c.is_whitespace()) {
110 return None;
111 }
112 let rest = rest.trim_start();
113 let rest = rest.strip_prefix("lib")?;
114 if !rest.starts_with(|c: char| c.is_whitespace() || c == '(' || c == ';') {
115 return None;
116 }
117 Some(rest.trim_start())
118}
119
120fn extract_paths_from_args(args: &str, out: &mut Vec<UseLibPath>) {
121 let args = args.trim_end_matches(';').trim();
122
123 if let Some(rest) = args.strip_prefix("qw") {
124 extract_qw_paths(rest.trim_start(), out);
125 return;
126 }
127
128 if let Some(inner) = strip_parens(args) {
129 extract_quoted_list(inner, out);
130 return;
131 }
132
133 extract_quoted_list(args, out);
134}
135
136fn extract_qw_paths(rest: &str, out: &mut Vec<UseLibPath>) {
137 let (open, close) = match rest.chars().next() {
138 Some('(') => ('(', ')'),
139 Some('/') => ('/', '/'),
140 Some('{') => ('{', '}'),
141 Some('[') => ('[', ']'),
142 Some('<') => ('<', '>'),
143 Some('!') => ('!', '!'),
144 _ => return,
145 };
146
147 let inner = &rest[open.len_utf8()..];
148 let end = inner.find(close).unwrap_or(inner.len());
149 let content = &inner[..end];
150
151 for word in content.split_whitespace() {
152 out.push(UseLibPath { path: word.to_string(), from_findbin: false });
153 }
154}
155
156fn strip_parens(s: &str) -> Option<&str> {
157 let s = s.trim();
158 let inner = s.strip_prefix('(')?;
159 let inner = inner.trim_end().strip_suffix(')')?;
160 Some(inner)
161}
162
163fn extract_quoted_list(s: &str, out: &mut Vec<UseLibPath>) {
164 let mut remaining = s.trim();
165
166 while !remaining.is_empty() {
167 remaining = remaining.trim_start_matches(|c: char| c == ',' || c.is_whitespace());
168 if remaining.is_empty() {
169 break;
170 }
171
172 if let Some((path, from_findbin, rest)) = extract_one_quoted(remaining) {
173 out.push(UseLibPath { path, from_findbin });
174 remaining = rest.trim_start_matches(|c: char| c == ',' || c.is_whitespace());
175 } else {
176 break;
177 }
178 }
179}
180
181fn extract_one_quoted(s: &str) -> Option<(String, bool, &str)> {
182 let s = s.trim();
183 let quote = match s.chars().next()? {
184 '\'' => '\'',
185 '"' => '"',
186 _ => return None,
187 };
188
189 let inner = &s[1..];
190 let end = inner.find(quote)?;
191 let content = &inner[..end];
192 let rest = &inner[end + 1..];
193
194 let (path, from_findbin) = resolve_findbin_in_string(content);
195 Some((path, from_findbin, rest))
196}
197
198fn resolve_findbin_in_string(s: &str) -> (String, bool) {
199 let findbin_vars =
200 ["$FindBin::Bin", "$FindBin::RealBin", "${FindBin::Bin}", "${FindBin::RealBin}"];
201
202 for var in &findbin_vars {
203 if let Some(rest) = s.strip_prefix(var) {
204 let path = rest.strip_prefix('/').unwrap_or(rest);
205 if path.is_empty() {
206 return (".".to_string(), true);
207 }
208 return (path.to_string(), true);
209 }
210 }
211
212 (s.to_string(), false)
213}
214
215fn path_to_relative_string(path: &Path, workspace_root: &Path) -> Option<String> {
216 if let Ok(rel) = path.strip_prefix(workspace_root) {
217 let s = normalize_relative_path_string(rel.to_string_lossy().as_ref());
218 if s.is_empty() { Some(".".to_string()) } else { Some(s) }
219 } else {
220 let s = normalize_relative_path_string(path.to_string_lossy().as_ref());
221 Some(s)
222 }
223}
224
225fn normalize_relative_path_string(path: &str) -> String {
226 path.replace('\\', "/")
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn single_quoted_lib() {
235 let paths = extract_use_lib_paths("use lib 'lib';");
236 assert_eq!(paths, vec![UseLibPath { path: "lib".into(), from_findbin: false }]);
237 }
238
239 #[test]
240 fn double_quoted_lib() {
241 let paths = extract_use_lib_paths("use lib \"lib\";");
242 assert_eq!(paths, vec![UseLibPath { path: "lib".into(), from_findbin: false }]);
243 }
244
245 #[test]
246 fn single_quoted_with_subdir() {
247 let paths = extract_use_lib_paths("use lib 'local/lib/perl5';");
248 assert_eq!(paths, vec![UseLibPath { path: "local/lib/perl5".into(), from_findbin: false }]);
249 }
250
251 #[test]
252 fn qw_parens_multiple_paths() {
253 let paths = extract_use_lib_paths("use lib qw(lib t/lib);");
254 assert_eq!(paths.len(), 2);
255 assert_eq!(paths[0].path, "lib");
256 assert_eq!(paths[1].path, "t/lib");
257 }
258
259 #[test]
260 fn qw_slash_delimiter() {
261 let paths = extract_use_lib_paths("use lib qw/lib t-lib/;");
262 assert_eq!(paths.len(), 2);
263 assert_eq!(paths[0].path, "lib");
264 assert_eq!(paths[1].path, "t-lib");
265 }
266
267 #[test]
268 fn qw_curly_delimiter() {
269 let paths = extract_use_lib_paths("use lib qw{lib};");
270 assert_eq!(paths.len(), 1);
271 assert_eq!(paths[0].path, "lib");
272 }
273
274 #[test]
275 fn qw_bracket_delimiter() {
276 let paths = extract_use_lib_paths("use lib qw[lib t/lib];");
277 assert_eq!(paths.len(), 2);
278 assert_eq!(paths[0].path, "lib");
279 assert_eq!(paths[1].path, "t/lib");
280 }
281
282 #[test]
283 fn paren_list_single() {
284 let paths = extract_use_lib_paths("use lib ('lib');");
285 assert_eq!(paths, vec![UseLibPath { path: "lib".into(), from_findbin: false }]);
286 }
287
288 #[test]
289 fn paren_list_multiple() {
290 let paths = extract_use_lib_paths("use lib ('lib', 't/lib');");
291 assert_eq!(paths.len(), 2);
292 assert_eq!(paths[0].path, "lib");
293 assert_eq!(paths[1].path, "t/lib");
294 }
295
296 #[test]
297 fn findbin_bin_with_lib() {
298 let paths = extract_use_lib_paths("use lib \"$FindBin::Bin/lib\";");
299 assert_eq!(paths.len(), 1);
300 assert_eq!(paths[0].path, "lib");
301 assert!(paths[0].from_findbin);
302 }
303
304 #[test]
305 fn findbin_bin_with_parent_lib() {
306 let paths = extract_use_lib_paths("use lib \"$FindBin::Bin/../lib\";");
307 assert_eq!(paths.len(), 1);
308 assert_eq!(paths[0].path, "../lib");
309 assert!(paths[0].from_findbin);
310 }
311
312 #[test]
313 fn findbin_realbin() {
314 let paths = extract_use_lib_paths("use lib \"$FindBin::RealBin/lib\";");
315 assert_eq!(paths.len(), 1);
316 assert_eq!(paths[0].path, "lib");
317 assert!(paths[0].from_findbin);
318 }
319
320 #[test]
321 fn findbin_braced_form() {
322 let paths = extract_use_lib_paths("use lib \"${FindBin::Bin}/lib\";");
323 assert_eq!(paths.len(), 1);
324 assert_eq!(paths[0].path, "lib");
325 assert!(paths[0].from_findbin);
326 }
327
328 #[test]
329 fn findbin_bare_bin() {
330 let paths = extract_use_lib_paths("use lib \"$FindBin::Bin\";");
331 assert_eq!(paths.len(), 1);
332 assert_eq!(paths[0].path, ".");
333 assert!(paths[0].from_findbin);
334 }
335
336 #[test]
337 fn leading_whitespace() {
338 let paths = extract_use_lib_paths(" use lib 'lib';");
339 assert_eq!(paths.len(), 1);
340 assert_eq!(paths[0].path, "lib");
341 }
342
343 #[test]
344 fn multiple_use_lib_statements() {
345 let source = "use lib 'lib';\nuse lib 't/lib';\n";
346 let paths = extract_use_lib_paths(source);
347 assert_eq!(paths.len(), 2);
348 assert_eq!(paths[0].path, "lib");
349 assert_eq!(paths[1].path, "t/lib");
350 }
351
352 #[test]
353 fn non_use_lib_lines_ignored() {
354 let source = "use strict;\nuse warnings;\nuse lib 'lib';\nuse Foo::Bar;\n";
355 let paths = extract_use_lib_paths(source);
356 assert_eq!(paths.len(), 1);
357 assert_eq!(paths[0].path, "lib");
358 }
359
360 #[test]
361 fn use_library_not_confused_with_use_lib() {
362 let paths = extract_use_lib_paths("use library 'foo';");
363 assert!(paths.is_empty());
364 }
365
366 #[test]
367 fn empty_source() {
368 let paths = extract_use_lib_paths("");
369 assert!(paths.is_empty());
370 }
371
372 #[test]
373 fn no_use_lib() {
374 let paths = extract_use_lib_paths("use strict;\nuse warnings;\n");
375 assert!(paths.is_empty());
376 }
377
378 #[test]
379 fn resolve_relative_path() {
380 let paths = vec![UseLibPath { path: "lib".into(), from_findbin: false }];
381 let resolved = resolve_use_lib_paths(&paths, Path::new("/project"), None);
382 assert_eq!(resolved, vec!["lib"]);
383 }
384
385 #[test]
386 fn resolve_findbin_path_with_file_dir() {
387 let paths = vec![UseLibPath { path: "lib".into(), from_findbin: true }];
388 let resolved =
389 resolve_use_lib_paths(&paths, Path::new("/project"), Some(Path::new("/project/bin")));
390 assert_eq!(resolved, vec!["bin/lib"]);
391 }
392
393 #[test]
394 fn resolve_findbin_path_without_file_dir() {
395 let paths = vec![UseLibPath { path: "lib".into(), from_findbin: true }];
396 let resolved = resolve_use_lib_paths(&paths, Path::new("/project"), None);
397 assert_eq!(resolved, vec!["lib"]);
398 }
399
400 #[test]
401 fn resolve_absolute_path_inside_workspace() -> Result<(), Box<dyn std::error::Error>> {
402 let workspace = tempfile::tempdir()?;
403 let inside = workspace.path().join("lib");
404 let paths =
405 vec![UseLibPath { path: inside.to_string_lossy().to_string(), from_findbin: false }];
406 let resolved = resolve_use_lib_paths(&paths, workspace.path(), None);
407 assert_eq!(resolved, vec!["lib"]);
408 Ok(())
409 }
410
411 #[test]
412 fn resolve_absolute_path_outside_workspace_ignored() -> Result<(), Box<dyn std::error::Error>> {
413 let workspace = tempfile::tempdir()?;
414 let outside = tempfile::tempdir()?;
415 let paths = vec![UseLibPath {
416 path: outside.path().join("lib").to_string_lossy().to_string(),
417 from_findbin: false,
418 }];
419 let resolved = resolve_use_lib_paths(&paths, workspace.path(), None);
420 assert!(resolved.is_empty());
421 Ok(())
422 }
423
424 #[test]
425 fn resolve_deduplicates_paths() {
426 let paths = vec![
427 UseLibPath { path: "lib".into(), from_findbin: false },
428 UseLibPath { path: "lib".into(), from_findbin: false },
429 ];
430 let resolved = resolve_use_lib_paths(&paths, Path::new("/project"), None);
431 assert_eq!(resolved, vec!["lib"]);
432 }
433}