Skip to main content

perl_module/resolution/
use_lib.rs

1//! Extract include paths from `use lib` and `FindBin` statements.
2//!
3//! Scans Perl source text for `use lib` pragmas and recognizes common
4//! `FindBin` patterns to discover additional module include directories.
5
6use std::path::Path;
7
8mod extract;
9mod resolve;
10mod statements;
11
12use extract::extract_paths_from_args;
13pub use resolve::resolve_use_lib_paths;
14use statements::{split_perl_statements, strip_no_lib_prefix, strip_use_lib_prefix};
15
16/// A discovered include path from a `use lib` statement.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct UseLibPath {
19    /// The resolved directory path (relative or absolute).
20    pub path: String,
21    /// Whether this path was derived from a `FindBin` variable.
22    pub from_findbin: bool,
23}
24
25/// A `use lib` / `no lib` operation extracted from source in lexical order.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum UseLibAction {
28    /// Add paths to the effective include stack.
29    Add(Vec<UseLibPath>),
30    /// Remove paths from the effective include stack.
31    Remove(Vec<UseLibPath>),
32}
33
34/// Extract include paths from `use lib` statements in Perl source text.
35///
36/// Handles the following patterns:
37/// - `use lib 'path';`
38/// - `use lib "path";`
39/// - `use lib qw(path1 path2);`
40/// - `use lib qw/path1 path2/;`
41/// - `use lib ("path1", "path2");`
42/// - `use lib '$FindBin::Bin/path'` and `"$FindBin::Bin/path"`
43/// - `use lib '$Bin/path'` and `"$RealBin/path"` (from `FindBin` exports)
44///
45/// Returns extracted paths in order of appearance.
46pub fn extract_use_lib_paths(source: &str) -> Vec<UseLibPath> {
47    let mut paths = Vec::new();
48
49    for statement in split_perl_statements(source) {
50        let trimmed = statement.trim();
51        if let Some(rest) = strip_use_lib_prefix(trimmed) {
52            extract_paths_from_args(rest, &mut paths);
53        }
54    }
55
56    paths
57}
58
59/// Extract ordered `use lib` and `no lib` operations from source text.
60#[must_use]
61pub fn extract_use_lib_operations(source: &str) -> Vec<UseLibAction> {
62    let mut ops = Vec::new();
63
64    for statement in split_perl_statements(source) {
65        let trimmed = statement.trim();
66        if let Some(rest) = strip_use_lib_prefix(trimmed) {
67            let mut paths = Vec::new();
68            extract_paths_from_args(rest, &mut paths);
69            if !paths.is_empty() {
70                ops.push(UseLibAction::Add(paths));
71            }
72            continue;
73        }
74
75        if let Some(rest) = strip_no_lib_prefix(trimmed) {
76            let mut paths = Vec::new();
77            extract_paths_from_args(rest, &mut paths);
78            if !paths.is_empty() {
79                ops.push(UseLibAction::Remove(paths));
80            }
81        }
82    }
83
84    ops
85}
86
87/// Resolve effective include paths from lexical `use lib` / `no lib` operations.
88#[must_use]
89pub fn resolve_use_lib_paths_from_source(
90    source: &str,
91    workspace_root: &Path,
92    file_dir: Option<&Path>,
93) -> Vec<String> {
94    resolve_use_lib_paths_from_source_at_offset(source, source.len(), workspace_root, file_dir)
95}
96
97/// Resolve effective include paths from lexical `use lib` / `no lib` operations,
98/// considering only source text up to the provided byte offset.
99#[must_use]
100pub fn resolve_use_lib_paths_from_source_at_offset(
101    source: &str,
102    offset: usize,
103    workspace_root: &Path,
104    file_dir: Option<&Path>,
105) -> Vec<String> {
106    let mut resolved = Vec::new();
107    let source_prefix = source.get(..offset).unwrap_or(source);
108    for op in extract_use_lib_operations(source_prefix) {
109        match op {
110            UseLibAction::Add(paths) => {
111                let added = resolve_use_lib_paths(&paths, workspace_root, file_dir);
112                for path in added.into_iter().rev() {
113                    resolved.retain(|existing| existing != &path);
114                    resolved.insert(0, path);
115                }
116            }
117            UseLibAction::Remove(paths) => {
118                for path in resolve_use_lib_paths(&paths, workspace_root, file_dir) {
119                    resolved.retain(|existing| existing != &path);
120                }
121            }
122        }
123    }
124    resolved
125}
126
127/// Compute the set of paths that are currently excluded from `@INC` at a given
128/// source offset due to `no lib` operations.
129///
130/// Returns the resolved path strings that have been explicitly removed by `no lib`
131/// and not subsequently re-added by a later `use lib` before the given offset.
132/// Callers should use this set to filter out matching entries from configured
133/// include paths, so that `no lib 'lib'` cancels both lexical AND configured
134/// `lib` entries that would otherwise survive the lexical scan.
135///
136/// # Example
137///
138/// For the source `use lib 'lib'; no lib 'lib'; use GoneModule;` at an offset
139/// within `use GoneModule;`, this function returns `["lib"]` because `lib` was
140/// added then removed before the offset.
141#[must_use]
142pub fn no_lib_cancelled_paths_at_offset(
143    source: &str,
144    offset: usize,
145    workspace_root: &Path,
146    file_dir: Option<&Path>,
147) -> Vec<String> {
148    let mut effective = Vec::<String>::new();
149    let mut cancelled = Vec::<String>::new();
150    let source_prefix = source.get(..offset).unwrap_or(source);
151    for op in extract_use_lib_operations(source_prefix) {
152        match op {
153            UseLibAction::Add(paths) => {
154                let added = resolve_use_lib_paths(&paths, workspace_root, file_dir);
155                for path in &added {
156                    // If it was cancelled, re-adding it removes the cancellation.
157                    cancelled.retain(|c| c != path);
158                }
159                for path in added.into_iter().rev() {
160                    effective.retain(|e| e != &path);
161                    effective.insert(0, path);
162                }
163            }
164            UseLibAction::Remove(paths) => {
165                let removed = resolve_use_lib_paths(&paths, workspace_root, file_dir);
166                for path in removed {
167                    effective.retain(|e| e != &path);
168                    if !cancelled.contains(&path) {
169                        cancelled.push(path);
170                    }
171                }
172            }
173        }
174    }
175    cancelled
176}