perl_semantic_analyzer/analysis/semantic/
exporter_metadata.rs1use crate::SourceLocation;
4use crate::ast::{Node, NodeKind};
5use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ExportedSubroutine {
10 pub name: String,
12 pub location: SourceLocation,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct PackageExportMetadata {
19 pub package: String,
21 pub exports: Vec<ExportedSubroutine>,
23 pub export_ok: Vec<ExportedSubroutine>,
25 pub export_tags: HashMap<String, Vec<ExportedSubroutine>>,
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub struct FileExportMetadata {
32 pub packages: Vec<PackageExportMetadata>,
34}
35
36#[derive(Default)]
37struct PendingPackageExports {
38 uses_exporter: bool,
39 export_names: Vec<String>,
40 export_ok_names: Vec<String>,
41 export_tag_names: HashMap<String, Vec<String>>,
42 subroutines: HashMap<String, SourceLocation>,
43}
44
45pub(super) struct ExportMetadataBuilder {
46 current_package: String,
47 current: PendingPackageExports,
48 packages: Vec<PackageExportMetadata>,
49}
50
51impl Default for ExportMetadataBuilder {
52 fn default() -> Self {
53 Self::new()
54 }
55}
56
57impl ExportMetadataBuilder {
58 pub(super) fn new() -> Self {
59 Self {
60 current_package: "main".to_string(),
61 current: PendingPackageExports::default(),
62 packages: Vec::new(),
63 }
64 }
65
66 pub(super) fn build(mut self, root: &Node) -> FileExportMetadata {
67 self.visit(root);
68 self.flush_current_package();
69 FileExportMetadata { packages: self.packages }
70 }
71
72 fn flush_current_package(&mut self) {
73 if !self.current.uses_exporter {
74 self.current = PendingPackageExports::default();
75 return;
76 }
77
78 let mut seen = HashSet::new();
79 let resolve_names = |names: &[String],
80 subroutines: &HashMap<String, SourceLocation>,
81 seen: &mut HashSet<String>| {
82 let mut resolved = Vec::new();
83 for name in names {
84 if let Some(location) = subroutines.get(name)
85 && seen.insert(name.clone())
86 {
87 resolved.push(ExportedSubroutine { name: name.clone(), location: *location });
88 }
89 }
90 resolved
91 };
92
93 let exports =
94 resolve_names(&self.current.export_names, &self.current.subroutines, &mut seen);
95 let export_ok =
96 resolve_names(&self.current.export_ok_names, &self.current.subroutines, &mut seen);
97
98 let mut export_tags = HashMap::new();
99 for (tag, names) in &self.current.export_tag_names {
100 let mut local_seen = HashSet::new();
101 let mut resolved = Vec::new();
102 for name in names {
103 if let Some(location) = self.current.subroutines.get(name)
104 && local_seen.insert(name.clone())
105 {
106 resolved.push(ExportedSubroutine { name: name.clone(), location: *location });
107 }
108 }
109 if !resolved.is_empty() {
110 export_tags.insert(tag.clone(), resolved);
111 }
112 }
113
114 if !(exports.is_empty() && export_ok.is_empty() && export_tags.is_empty()) {
115 self.packages.push(PackageExportMetadata {
116 package: self.current_package.clone(),
117 exports,
118 export_ok,
119 export_tags,
120 });
121 }
122
123 self.current = PendingPackageExports::default();
124 }
125
126 fn visit_statement_list(&mut self, statements: &[Node]) {
127 for statement in statements {
128 self.visit(statement);
129 }
130 }
131
132 fn visit(&mut self, node: &Node) {
133 match &node.kind {
134 NodeKind::Program { statements } => self.visit_statement_list(statements),
135 NodeKind::Block { statements, .. } => self.visit_statement_list(statements),
136 NodeKind::Package { name, block, .. } => {
137 self.flush_current_package();
138 self.current_package = name.clone();
139 if let Some(block) = block {
140 self.visit(block);
141 }
142 }
143 NodeKind::Use { module, args, .. } => {
144 if module == "Exporter"
145 || ((module == "parent" || module == "base")
146 && args
147 .iter()
148 .any(|arg| parse_argument_names(arg).iter().any(|i| i == "Exporter")))
149 {
150 self.current.uses_exporter = true;
151 }
152 }
153 NodeKind::VariableDeclaration { variable, initializer, .. } => {
154 if let NodeKind::Variable { sigil, name } = &variable.kind
155 && let Some(initializer) = initializer
156 {
157 self.capture_export_assignment(sigil, name, initializer);
158 }
159 }
160 NodeKind::Assignment { lhs, rhs, .. } => {
161 if let NodeKind::Variable { sigil, name } = &lhs.kind {
162 self.capture_export_assignment(sigil, name, rhs);
163 }
164 }
165 NodeKind::Subroutine { name, body, .. } => {
166 if let Some(sub_name) = name {
167 self.current.subroutines.insert(sub_name.clone(), node.location);
168 }
169 self.visit(body);
170 }
171 NodeKind::ExpressionStatement { expression } => self.visit(expression),
172 _ => {}
173 }
174 }
175
176 fn capture_export_assignment(&mut self, sigil: &str, name: &str, rhs: &Node) {
177 match (sigil, name) {
178 ("@", "ISA") => {
179 if let Some(items) = parse_name_list(rhs)
180 && items.iter().any(|item| item == "Exporter")
181 {
182 self.current.uses_exporter = true;
183 }
184 }
185 ("@", "EXPORT") => {
186 if let Some(items) = parse_name_list(rhs) {
187 self.current.export_names.extend(items);
188 }
189 }
190 ("@", "EXPORT_OK") => {
191 if let Some(items) = parse_name_list(rhs) {
192 self.current.export_ok_names.extend(items);
193 }
194 }
195 ("%", "EXPORT_TAGS") => {
196 if let Some(tags) = parse_export_tags(rhs) {
197 for (tag, names) in tags {
198 self.current.export_tag_names.entry(tag).or_default().extend(names);
199 }
200 }
201 }
202 _ => {}
203 }
204 }
205}
206
207fn parse_export_tags(node: &Node) -> Option<HashMap<String, Vec<String>>> {
208 let NodeKind::HashLiteral { pairs } = &node.kind else {
209 return None;
210 };
211
212 let mut tags = HashMap::new();
213 for (key_node, value_node) in pairs {
214 let mut key_names = parse_name_list(key_node)?;
215 let tag = key_names.pop()?;
216 let members = parse_name_list(value_node)?;
217 tags.insert(tag, members);
218 }
219 Some(tags)
220}
221
222fn parse_name_list(node: &Node) -> Option<Vec<String>> {
223 match &node.kind {
224 NodeKind::String { value, .. } => {
225 let list = parse_string_value(value);
226 if list.is_empty() { None } else { Some(list) }
227 }
228 NodeKind::Identifier { name } => {
229 let list = parse_string_value(name);
230 if list.is_empty() { None } else { Some(list) }
231 }
232 NodeKind::ArrayLiteral { elements } => {
233 let mut out = Vec::new();
234 for element in elements {
235 out.extend(parse_name_list(element)?);
236 }
237 Some(out)
238 }
239 _ => None,
240 }
241}
242
243fn parse_string_value(raw: &str) -> Vec<String> {
244 let trimmed = raw.trim();
245
246 if trimmed.starts_with("qw") {
247 return parse_qw_list(trimmed);
248 }
249
250 normalize_name(trimmed).into_iter().collect()
251}
252
253fn parse_qw_list(raw: &str) -> Vec<String> {
254 if raw.len() < 4 {
255 return Vec::new();
256 }
257
258 let mut chars = raw.chars();
259 let _q = chars.next();
260 let _w = chars.next();
261 let open = chars.next().unwrap_or(' ');
262 let close = match open {
263 '(' => ')',
264 '[' => ']',
265 '{' => '}',
266 '<' => '>',
267 c => c,
268 };
269
270 let Some(start) = raw.find(open) else {
271 return Vec::new();
272 };
273 let Some(end) = raw.rfind(close) else {
274 return Vec::new();
275 };
276 if start >= end {
277 return Vec::new();
278 }
279
280 raw[start + 1..end].split_whitespace().filter_map(normalize_name).collect()
281}
282
283fn parse_argument_names(raw: &str) -> Vec<String> {
284 parse_string_value(raw)
285}
286
287fn normalize_name(value: &str) -> Option<String> {
288 let name = value.trim().trim_matches('"').trim_matches('\'').trim();
289 if name.is_empty() { None } else { Some(name.to_string()) }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::Parser;
296
297 fn parse_export_metadata(
298 source: &str,
299 ) -> Result<FileExportMetadata, Box<dyn std::error::Error>> {
300 let mut parser = Parser::new(source);
301 let ast = parser.parse()?;
302 Ok(ExportMetadataBuilder::new().build(&ast))
303 }
304
305 #[test]
306 fn captures_simple_export_array() -> Result<(), Box<dyn std::error::Error>> {
307 let metadata = parse_export_metadata(
308 "package Demo;\nuse Exporter 'import';\nour @EXPORT = qw(foo bar);\nsub foo {}\nsub bar {}\n1;",
309 )?;
310
311 let package = &metadata.packages[0];
312 assert_eq!(package.package, "Demo");
313 assert_eq!(
314 package.exports.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
315 vec!["foo", "bar"]
316 );
317 Ok(())
318 }
319
320 #[test]
321 fn captures_export_ok_and_ignores_missing_definitions() -> Result<(), Box<dyn std::error::Error>>
322 {
323 let metadata = parse_export_metadata(
324 "package Demo;\nuse Exporter 'import';\nour @EXPORT_OK = qw(alpha missing);\nsub alpha {}\n1;",
325 )?;
326
327 let package = &metadata.packages[0];
328 assert_eq!(
329 package.export_ok.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(),
330 vec!["alpha"]
331 );
332 Ok(())
333 }
334
335 #[test]
336 fn captures_export_tags_hash_literal() -> Result<(), Box<dyn std::error::Error>> {
337 let metadata = parse_export_metadata(
338 "package Demo;\nuse parent 'Exporter';\nour %EXPORT_TAGS = (\n core => [qw(one two)],\n extra => ['three'],\n);\nsub one {}\nsub two {}\nsub three {}\n1;",
339 )?;
340
341 let package = &metadata.packages[0];
342 assert_eq!(
343 package.export_tags["core"].iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
344 vec!["one", "two"]
345 );
346 assert_eq!(
347 package.export_tags["extra"].iter().map(|item| item.name.as_str()).collect::<Vec<_>>(),
348 vec!["three"]
349 );
350 Ok(())
351 }
352}