perl_parser_core/syntax/
qualified_name.rs1use core::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum QualifiedNameError {
11 EmptyName,
13 LeadingSigil(char),
15 EmptySegment {
17 index: usize,
19 },
20 InvalidSegment {
22 index: usize,
24 },
25}
26
27impl fmt::Display for QualifiedNameError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::EmptyName => write!(f, "name is empty"),
31 Self::LeadingSigil(sigil) => {
32 write!(f, "qualified name cannot start with sigil '{sigil}'")
33 }
34 Self::EmptySegment { index } => {
35 write!(f, "segment {index} is empty (leading/trailing/double separator)")
36 }
37 Self::InvalidSegment { index } => {
38 write!(f, "segment {index} is not a valid identifier")
39 }
40 }
41 }
42}
43
44impl std::error::Error for QualifiedNameError {}
45
46#[must_use]
51pub fn split_qualified_name(name: &str) -> (Option<&str>, &str) {
52 if let Some(idx) = name.rfind("::") {
53 (Some(&name[..idx]), &name[idx + 2..])
54 } else {
55 (None, name)
56 }
57}
58
59#[must_use]
64pub fn container_name(name: &str) -> Option<&str> {
65 split_qualified_name(name).0
66}
67
68pub fn validate_perl_qualified_name(name: &str) -> Result<(), QualifiedNameError> {
75 if name.is_empty() {
76 return Err(QualifiedNameError::EmptyName);
77 }
78
79 if name.starts_with(['$', '@', '%', '&', '*']) {
80 let sigil = name.chars().next().unwrap_or_default();
81 return Err(QualifiedNameError::LeadingSigil(sigil));
82 }
83
84 for (idx, part) in name.split("::").enumerate() {
85 if part.is_empty() {
86 return Err(QualifiedNameError::EmptySegment { index: idx });
87 }
88
89 if !is_valid_identifier_part(part) {
90 return Err(QualifiedNameError::InvalidSegment { index: idx });
91 }
92 }
93
94 Ok(())
95}
96
97#[must_use]
99pub fn is_valid_identifier_part(s: &str) -> bool {
100 let mut chars = s.chars();
101 match chars.next() {
102 Some(c) if c.is_alphabetic() || c == '_' => chars.all(|c| c.is_alphanumeric() || c == '_'),
103 _ => false,
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::{
110 container_name, is_valid_identifier_part, split_qualified_name,
111 validate_perl_qualified_name,
112 };
113
114 #[test]
115 fn splits_qualified_and_unqualified_names() {
116 assert_eq!(split_qualified_name("Foo::Bar"), (Some("Foo"), "Bar"));
117 assert_eq!(split_qualified_name("process"), (None, "process"));
118 }
119
120 #[test]
121 fn extracts_container_name() {
122 assert_eq!(container_name("Foo::Bar::baz"), Some("Foo::Bar"));
123 assert_eq!(container_name("MyClass::new"), Some("MyClass"));
124 assert_eq!(container_name("toplevel"), None);
125 assert_eq!(container_name(""), None);
126 assert_eq!(container_name("Package::"), Some("Package"));
127 }
128
129 #[test]
130 fn validates_simple_qualified_names() {
131 assert!(validate_perl_qualified_name("Package").is_ok());
132 assert!(validate_perl_qualified_name("Package::Sub").is_ok());
133 assert!(validate_perl_qualified_name("Müller::Util").is_ok());
134 assert!(validate_perl_qualified_name("日本::パッケージ").is_ok());
135 }
136
137 #[test]
138 fn rejects_invalid_qualified_names() {
139 assert!(validate_perl_qualified_name("").is_err());
140 assert!(validate_perl_qualified_name("Package::").is_err());
141 assert!(validate_perl_qualified_name("Foo::::Bar").is_err());
142 assert!(validate_perl_qualified_name("$foo").is_err());
143 }
144
145 #[test]
146 fn validates_identifier_part_syntax() {
147 assert!(is_valid_identifier_part("MyPkg"));
148 assert!(is_valid_identifier_part("π"));
149 assert!(!is_valid_identifier_part("1abc"));
150 assert!(!is_valid_identifier_part(""));
151 }
152}