#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadTiming {
CompileTime,
Runtime,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportBehavior {
CallsImport,
NoImport,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DispatchSemantics {
pub load_timing: LoadTiming,
pub import_behavior: ImportBehavior,
}
impl DispatchSemantics {
#[must_use]
pub fn hover_description(&self) -> &'static str {
match (self.load_timing, self.import_behavior) {
(LoadTiming::CompileTime, ImportBehavior::CallsImport) => {
"compile-time load; calls import()"
}
(LoadTiming::Runtime, ImportBehavior::NoImport) => "runtime load; no import() call",
(LoadTiming::CompileTime, ImportBehavior::NoImport) => {
"compile-time load; no import() call"
}
(LoadTiming::Runtime, ImportBehavior::CallsImport) => "runtime load; calls import()",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportListForm {
Default,
Empty,
Explicit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequireForm {
ModuleName,
FilePath,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleImportKind {
Use,
Require,
UseParent,
UseBase,
}
impl ModuleImportKind {
#[must_use]
pub fn dispatch_semantics(self) -> DispatchSemantics {
match self {
ModuleImportKind::Use | ModuleImportKind::UseParent | ModuleImportKind::UseBase => {
DispatchSemantics {
load_timing: LoadTiming::CompileTime,
import_behavior: ImportBehavior::CallsImport,
}
}
ModuleImportKind::Require => DispatchSemantics {
load_timing: LoadTiming::Runtime,
import_behavior: ImportBehavior::NoImport,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModuleImportHead<'a> {
pub kind: ModuleImportKind,
pub token: &'a str,
pub token_start: usize,
pub token_end: usize,
require_form: Option<RequireForm>,
pub import_list: Option<ImportListForm>,
}
#[must_use]
pub fn resolve_known_export_tag(module: &str, tag: &str) -> Option<&'static [&'static str]> {
let normalized_tag = tag.strip_prefix(':').unwrap_or(tag);
match (module, normalized_tag) {
("POSIX", "sys_wait_h") => Some(&["WIFEXITED", "WEXITSTATUS", "WIFSIGNALED", "WTERMSIG"]),
("POSIX", "fcntl_h") => Some(&["F_GETFL", "F_SETFL", "F_SETFD", "F_GETFD"]),
("POSIX", "termios_h") => Some(&["TCSANOW", "TCSADRAIN", "TCSAFLUSH", "B9600"]),
("File::Find", "find") => Some(&["find", "finddepth"]),
("Fcntl", "seek") => Some(&["SEEK_SET", "SEEK_CUR", "SEEK_END"]),
("Fcntl", "lock") => Some(&["LOCK_SH", "LOCK_EX", "LOCK_NB", "LOCK_UN"]),
("Encode", "fallback") => Some(&["FB_DEFAULT", "FB_CROAK", "FB_QUIET", "FB_WARN"]),
_ => None,
}
}
impl<'a> ModuleImportHead<'a> {
#[must_use]
pub fn require_form(&self) -> Option<RequireForm> {
self.require_form
}
#[must_use]
pub fn token_as_module_name(&self) -> String {
if self.require_form == Some(RequireForm::FilePath) && self.token.ends_with(".pm") {
crate::path::module_path_to_name(self.token)
} else {
self.token.to_owned()
}
}
}
#[must_use]
pub fn parse_module_import_head(line: &str) -> Option<ModuleImportHead<'_>> {
if let Some((token, token_start, token_end)) = parse_statement_head(line, "use") {
let kind = match token {
"parent" => ModuleImportKind::UseParent,
"base" => ModuleImportKind::UseBase,
_ => ModuleImportKind::Use,
};
let import_list = match kind {
ModuleImportKind::Use => Some(classify_use_import_list(&line[token_end..])),
ModuleImportKind::UseParent | ModuleImportKind::UseBase => None,
ModuleImportKind::Require => None,
};
return Some(ModuleImportHead {
kind,
token,
token_start,
token_end,
require_form: None,
import_list,
});
}
if let Some(result) = parse_require_head(line) {
return Some(result);
}
None
}
fn parse_require_head(line: &str) -> Option<ModuleImportHead<'_>> {
let trimmed = line.trim_start();
let leading = line.len().saturating_sub(trimmed.len());
let rest = trimmed.strip_prefix("require")?;
if !rest.chars().next().is_some_and(char::is_whitespace) {
return None;
}
let after_keyword = leading + "require".len();
let rest_trimmed = rest.trim_start();
let quote_offset = rest.len() - rest_trimmed.len();
if let Some(quote_char) = rest_trimmed.chars().next().filter(|ch| *ch == '"' || *ch == '\'') {
let quoted = &rest_trimmed[quote_char.len_utf8()..];
let close_idx = quoted.find(quote_char)?;
let inner = "ed[..close_idx];
let token_start = after_keyword + quote_offset + quote_char.len_utf8();
let token_end = token_start + inner.len();
return Some(ModuleImportHead {
kind: ModuleImportKind::Require,
token: inner,
token_start,
token_end,
require_form: Some(RequireForm::FilePath),
import_list: None,
});
}
let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
let token_start = after_keyword + token_rel_start;
let token_end = after_keyword + token_rel_end;
Some(ModuleImportHead {
kind: ModuleImportKind::Require,
token,
token_start,
token_end,
require_form: Some(RequireForm::ModuleName),
import_list: None,
})
}
fn classify_use_import_list(rest: &str) -> ImportListForm {
let trimmed = rest.trim_start();
if trimmed.is_empty() || trimmed.starts_with(';') {
return ImportListForm::Default;
}
if let Some(after_open) = trimmed.strip_prefix('(')
&& let Some(close_idx) = after_open.find(')')
&& after_open[..close_idx].trim().is_empty()
{
let after_close = after_open[close_idx + 1..].trim_start();
if after_close.is_empty() || after_close.starts_with(';') || after_close.starts_with('#') {
return ImportListForm::Empty;
}
}
ImportListForm::Explicit
}
fn parse_statement_head<'a>(line: &'a str, keyword: &str) -> Option<(&'a str, usize, usize)> {
let trimmed = line.trim_start();
let leading = line.len().saturating_sub(trimmed.len());
let rest = trimmed.strip_prefix(keyword)?;
if !rest.chars().next().is_some_and(char::is_whitespace) {
return None;
}
let (token, token_rel_start, token_rel_end) = first_token_with_range(rest)?;
let token_start = leading + keyword.len() + token_rel_start;
let token_end = leading + keyword.len() + token_rel_end;
Some((token, token_start, token_end))
}
fn first_token_with_range(input: &str) -> Option<(&str, usize, usize)> {
let mut token_start = None;
for (idx, ch) in input.char_indices() {
match token_start {
None => {
if is_token_delimiter(ch) {
continue;
}
token_start = Some(idx);
}
Some(start) => {
if is_token_delimiter(ch) {
if start == idx {
return None;
}
return Some((&input[start..idx], start, idx));
}
}
}
}
if let Some(start) = token_start {
if start < input.len() { Some((&input[start..], start, input.len())) } else { None }
} else {
None
}
}
fn is_token_delimiter(ch: char) -> bool {
ch.is_whitespace() || matches!(ch, ';' | '(' | ')')
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequireImportEntry {
pub module: String,
pub symbol: String,
pub require_byte_offset: usize,
pub import_byte_offset: usize,
}
#[must_use]
pub fn extract_require_import_symbols(source: &str) -> Vec<RequireImportEntry> {
let mut entries = Vec::new();
let lines: Vec<(usize, &str)> = {
let mut v = Vec::new();
let mut offset = 0usize;
for line in source.split('\n') {
let trimmed = line.trim();
if !trimmed.is_empty() {
let leading = line.len().saturating_sub(line.trim_start().len());
v.push((offset + leading, trimmed));
}
offset += line.len() + 1; }
v
};
for (i, &(req_offset, req_line)) in lines.iter().enumerate() {
let parsed_require = match parse_literal_require_line(req_line) {
Some(parsed_require) => parsed_require,
None => continue,
};
let module = parsed_require.module;
if collect_literal_import_entries(
&mut entries,
module,
req_offset,
req_offset + parsed_require.tail_start,
parsed_require.tail,
) {
continue;
}
let window_end = (i + 1 + 5).min(lines.len());
for &(imp_offset, imp_line) in &lines[i + 1..window_end] {
if collect_literal_import_entries(
&mut entries,
module,
req_offset,
imp_offset,
imp_line,
) {
break;
}
if is_statement_terminator(imp_line) {
break;
}
}
}
entries
}
struct ParsedLiteralRequire<'a> {
module: &'a str,
tail_start: usize,
tail: &'a str,
}
fn parse_literal_require_line(line: &str) -> Option<ParsedLiteralRequire<'_>> {
let rest = line.strip_prefix("require")?;
if !rest.starts_with(|c: char| c.is_whitespace()) {
return None;
}
let leading_after_keyword = rest.len().saturating_sub(rest.trim_start().len());
let rest = rest.trim_start();
if rest.starts_with('$') || rest.starts_with('"') || rest.starts_with('\'') {
return None;
}
let module_end = rest.find(|c: char| c == ';' || c.is_whitespace()).unwrap_or(rest.len());
let module = &rest[..module_end];
if !is_valid_bareword_module_name(module) {
return None;
}
let after_module = &rest[module_end..];
let semicolon_offset = after_module.find(';')?;
let tail_start = "require".len() + leading_after_keyword + module_end + semicolon_offset + 1;
Some(ParsedLiteralRequire { module, tail_start, tail: &line[tail_start..] })
}
fn is_valid_bareword_module_name(module: &str) -> bool {
if module.is_empty() {
return false;
}
module.split("::").all(|part| {
!part.is_empty()
&& part.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
&& part.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
})
}
fn collect_literal_import_entries(
entries: &mut Vec<RequireImportEntry>,
module: &str,
require_byte_offset: usize,
import_byte_offset: usize,
candidate: &str,
) -> bool {
let leading = candidate.len().saturating_sub(candidate.trim_start().len());
let candidate = candidate.trim_start();
if let Some(symbols) = parse_literal_import_call(candidate, module) {
for symbol in symbols {
entries.push(RequireImportEntry {
module: module.to_string(),
symbol,
require_byte_offset,
import_byte_offset: import_byte_offset + leading,
});
}
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_as_module_name_keeps_non_pm_require_tokens() -> Result<(), String> {
let bare = parse_module_import_head("require Local::Util;")
.ok_or_else(|| "expected bare require head".to_string())?;
assert_eq!(bare.token_as_module_name(), "Local::Util");
let script = parse_module_import_head(r#"require "script.pl";"#)
.ok_or_else(|| "expected quoted script require head".to_string())?;
assert_eq!(script.token_as_module_name(), "script.pl");
Ok(())
}
}
fn parse_literal_import_call(line: &str, expected_module: &str) -> Option<Vec<String>> {
let after_module = line.strip_prefix(expected_module)?.trim_start();
let after_arrow = after_module.strip_prefix("->")?.trim_start();
let after_method = after_arrow.strip_prefix("import")?.trim_start();
let after_open = after_method.strip_prefix('(')?;
let close_idx = after_open.rfind(')')?;
let args_src = &after_open[..close_idx];
if args_src.contains('@') || args_src.contains('$') {
return None;
}
let symbols = parse_literal_arg_list(args_src)?;
Some(symbols)
}
fn parse_literal_arg_list(args: &str) -> Option<Vec<String>> {
let trimmed = args.trim();
if trimmed.is_empty() {
return Some(Vec::new());
}
if let Some(words) = parse_qw_arg_list(trimmed) {
return Some(words);
}
let mut symbols = Vec::new();
for part in trimmed.split(',') {
let p = part.trim();
if p.is_empty() {
continue;
}
if let Some(inner) = p.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')) {
if inner.is_empty() {
continue;
}
symbols.push(inner.to_string());
continue;
}
if let Some(inner) = p.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
if inner.is_empty() {
continue;
}
symbols.push(inner.to_string());
continue;
}
return None;
}
Some(symbols)
}
fn parse_qw_arg_list(trimmed: &str) -> Option<Vec<String>> {
let after_operator = trimmed.strip_prefix("qw")?;
let delimiter = after_operator.chars().next()?;
if delimiter.is_ascii_alphanumeric() || delimiter == '_' || delimiter.is_whitespace() {
return None;
}
let closing = match delimiter {
'(' => ')',
'[' => ']',
'{' => '}',
'<' => '>',
other => other,
};
let inner_start = "qw".len() + delimiter.len_utf8();
let inner_end = trimmed.len().checked_sub(closing.len_utf8())?;
if inner_start > inner_end || !trimmed.ends_with(closing) {
return None;
}
let inner = &trimmed[inner_start..inner_end];
Some(inner.split_whitespace().filter(|word| !word.is_empty()).map(str::to_string).collect())
}
fn is_statement_terminator(line: &str) -> bool {
line.starts_with("use ")
|| line.starts_with("require ")
|| line.starts_with("sub ")
|| line.starts_with("package ")
|| line.starts_with("my ")
|| line.starts_with("our ")
|| line.starts_with("local ")
}