Skip to main content

perl_semantic_analyzer/analysis/semantic/
exporter_metadata.rs

1//! Per-file metadata extraction for `Exporter`-style modules.
2
3use crate::SourceLocation;
4use crate::ast::{Node, NodeKind};
5use std::collections::{HashMap, HashSet};
6
7/// A same-file subroutine that can be exported by an `Exporter` package.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ExportedSubroutine {
10    /// Subroutine name (without leading sigils).
11    pub name: String,
12    /// Source location of the defining `sub` declaration.
13    pub location: SourceLocation,
14}
15
16/// Export metadata captured for a single package declaration in a file.
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub struct PackageExportMetadata {
19    /// Package name that owns these exports.
20    pub package: String,
21    /// Default exports declared via `@EXPORT`.
22    pub exports: Vec<ExportedSubroutine>,
23    /// Optional exports declared via `@EXPORT_OK`.
24    pub export_ok: Vec<ExportedSubroutine>,
25    /// Tag-based exports declared via `%EXPORT_TAGS`.
26    pub export_tags: HashMap<String, Vec<ExportedSubroutine>>,
27}
28
29/// Export metadata extracted from one parsed source file.
30#[derive(Debug, Clone, PartialEq, Eq, Default)]
31pub struct FileExportMetadata {
32    /// Package-level export metadata entries discovered in this file.
33    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}