#![deny(unsafe_code)]
#![warn(rust_2018_idioms)]
#![warn(missing_docs)]
#![warn(clippy::all)]
#[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 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>,
}
impl<'a> ModuleImportHead<'a> {
#[must_use]
pub fn require_form(&self) -> Option<RequireForm> {
self.require_form
}
}
#[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,
};
return Some(ModuleImportHead { kind, token, token_start, token_end, require_form: None });
}
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(inner) = rest_trimmed
.strip_prefix('"')
.and_then(|s| s.strip_suffix('"').or_else(|| s.split('"').next()))
.or_else(|| {
rest_trimmed
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\'').or_else(|| s.split('\'').next()))
})
{
let quote_char_len = 1usize; let token_start = after_keyword + quote_offset + quote_char_len;
let token_end = token_start + inner.len();
return Some(ModuleImportHead {
kind: ModuleImportKind::Require,
token: inner,
token_start,
token_end,
require_form: Some(RequireForm::FilePath),
});
}
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),
})
}
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, ';' | '(' | ')')
}
#[cfg(test)]
mod tests {
use super::{ModuleImportKind, parse_module_import_head};
#[test]
fn parses_use_statement_head() {
let parsed = parse_module_import_head("use Foo::Bar;");
assert!(parsed.is_some());
if let Some(head) = parsed {
assert_eq!(head.kind, ModuleImportKind::Use);
assert_eq!(head.token, "Foo::Bar");
assert_eq!(head.token_start, 4);
assert_eq!(head.token_end, 12);
}
}
#[test]
fn parses_require_statement_head() {
let parsed = parse_module_import_head(" require Foo::Bar;");
assert!(parsed.is_some());
if let Some(head) = parsed {
assert_eq!(head.kind, ModuleImportKind::Require);
assert_eq!(head.token, "Foo::Bar");
assert_eq!(head.token_start, 10);
assert_eq!(head.token_end, 18);
}
}
#[test]
fn classifies_parent_and_base_specializations() {
let parent = parse_module_import_head("use parent qw(Foo::Bar);");
let base = parse_module_import_head("use base 'Foo::Bar';");
assert!(parent.is_some());
if let Some(head) = parent {
assert_eq!(head.kind, ModuleImportKind::UseParent);
assert_eq!(head.token, "parent");
}
assert!(base.is_some());
if let Some(head) = base {
assert_eq!(head.kind, ModuleImportKind::UseBase);
assert_eq!(head.token, "base");
}
}
#[test]
fn rejects_non_keyword_boundaries() {
assert!(parse_module_import_head("user Foo::Bar;").is_none());
assert!(parse_module_import_head("required Foo::Bar;").is_none());
}
#[test]
fn rejects_missing_tokens() {
assert!(parse_module_import_head("use ;").is_none());
assert!(parse_module_import_head("require").is_none());
}
}