Skip to main content

perl_semantic_analyzer/analysis/
import_extractor.rs

1//! Import specification extractor for `use` and `require` statements.
2//!
3//! Walks the AST to extract [`ImportSpec`] entries from Perl `use` and `require`
4//! statements, classifying each import site by its syntactic shape
5//! ([`ImportKind`]) and symbol selection policy ([`ImportSymbols`]).
6//!
7//! # Supported Patterns
8//!
9//! | Perl source                              | `ImportKind`          | `ImportSymbols`              |
10//! |------------------------------------------|-----------------------|------------------------------|
11//! | `use Module qw(a b)`                     | `UseExplicitList`     | `Explicit(["a", "b"])`       |
12//! | `use Module ()`                          | `UseEmpty`            | `None`                       |
13//! | `use Module ':tag'`                      | `UseTag`              | `Tags(["tag"])`              |
14//! | `use Module` (bare)                      | `Use`                 | `Default`                    |
15//! | `use constant { FOO => 1 }`              | `UseConstant`         | `Explicit(["FOO"])`          |
16//! | `use constant PI => 3.14`                | `UseConstant`         | `Explicit(["PI"])`           |
17//! | `require Module`                         | `Require`             | `Default`                    |
18//! | `require Module; Module->import(...)`    | `RequireThenImport`   | `Explicit([...])` / `Default`|
19//! | `require $var`                           | `DynamicRequire`      | `Dynamic`                    |
20
21use crate::ast::{Node, NodeKind};
22use perl_semantic_facts::{
23    AnchorId, Confidence, FileId, ImportKind, ImportSpec, ImportSymbols, Provenance,
24};
25
26/// Extractor that walks an AST to produce [`ImportSpec`] entries for each
27/// `use` and `require` statement found.
28pub struct ImportExtractor;
29
30impl ImportExtractor {
31    /// Walk the entire AST and return one [`ImportSpec`] per `use` or
32    /// `require` statement.
33    ///
34    /// Each spec carries the supplied `file_id` and an `anchor_id` derived from
35    /// the statement's byte-offset span.
36    pub fn extract(ast: &Node, file_id: FileId) -> Vec<ImportSpec> {
37        let mut specs = Vec::new();
38        Self::walk(ast, file_id, &mut specs);
39        specs
40    }
41
42    // ── AST walker ──────────────────────────────────────────────────────
43
44    fn walk(node: &Node, file_id: FileId, out: &mut Vec<ImportSpec>) {
45        // Handle `use` statements directly.
46        if let NodeKind::Use { module, args, .. } = &node.kind {
47            if let Some(spec) = Self::classify_use(module, args, file_id, node) {
48                out.push(spec);
49            }
50        }
51
52        // For statement-list containers (Program, Block, Package), scan
53        // consecutive statements to detect `require Module; Module->import(...)`
54        // pairs and standalone `require` statements.
55        match &node.kind {
56            NodeKind::Program { statements } | NodeKind::Block { statements } => {
57                Self::walk_statements(statements, file_id, out);
58            }
59            NodeKind::Package { block: Some(block), .. } => {
60                if let NodeKind::Block { statements } = &block.kind {
61                    Self::walk_statements(statements, file_id, out);
62                }
63            }
64            _ => {}
65        }
66
67        for child in node.children() {
68            Self::walk(child, file_id, out);
69        }
70    }
71
72    /// Scan a list of sibling statements for `require` patterns.
73    ///
74    /// Detects:
75    /// - `require Module; Module->import(...)` → `RequireThenImport`
76    /// - `require Module` (standalone) → `Require`
77    /// - `require $var` → `DynamicRequire`
78    ///
79    /// Statements that are part of a `require + import` pair are recorded
80    /// once (not duplicated by the per-node walk).
81    fn walk_statements(statements: &[Node], file_id: FileId, out: &mut Vec<ImportSpec>) {
82        // Track which statement indices have been consumed as part of a
83        // require-then-import pair so the per-node walk does not re-emit them.
84        let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
85
86        for (i, stmt) in statements.iter().enumerate() {
87            if consumed.contains(&i) {
88                continue;
89            }
90
91            // Unwrap ExpressionStatement to get the inner expression.
92            let expr = Self::unwrap_expression_statement(stmt);
93
94            // Check for `require <something>`.
95            let (require_node, require_args) = match &expr.kind {
96                NodeKind::FunctionCall { name, args } if name == "require" => (stmt, args),
97                _ => continue,
98            };
99
100            // Dynamic require: `require $var`
101            if Self::is_dynamic_require(require_args) {
102                out.push(Self::make_dynamic_require(file_id, require_node));
103                consumed.insert(i);
104                continue;
105            }
106
107            // Static require: extract module name.
108            let module_name = match Self::extract_require_module_name(require_args) {
109                Some(name) => name,
110                None => continue,
111            };
112
113            // Look ahead for `Module->import(...)` in the next statement.
114            let import_spec = if let Some(next_stmt) = statements.get(i + 1) {
115                let next_expr = Self::unwrap_expression_statement(next_stmt);
116                Self::try_match_import_call(next_expr, &module_name)
117            } else {
118                None
119            };
120
121            if let Some((symbols, import_node)) = import_spec {
122                // `require Module; Module->import(...)` → RequireThenImport
123                //
124                // Use the require statement's anchor for the spec.
125                let anchor_id = Self::anchor_from_node(require_node);
126                let confidence = Self::confidence_for_symbols(&symbols);
127                out.push(ImportSpec {
128                    module: module_name,
129                    kind: ImportKind::RequireThenImport,
130                    symbols,
131                    provenance: Provenance::ExactAst,
132                    confidence,
133                    file_id: Some(file_id),
134                    anchor_id: Some(anchor_id),
135                    scope_id: None,
136                });
137                consumed.insert(i);
138                consumed.insert(i + 1);
139                // Also record the import call node index so the per-node
140                // walk does not process it.
141                let _ = import_node;
142            } else {
143                // Standalone `require Module` → Require
144                let anchor_id = Self::anchor_from_node(require_node);
145                out.push(ImportSpec {
146                    module: module_name,
147                    kind: ImportKind::Require,
148                    symbols: ImportSymbols::Default,
149                    provenance: Provenance::ExactAst,
150                    confidence: Confidence::High,
151                    file_id: Some(file_id),
152                    anchor_id: Some(anchor_id),
153                    scope_id: None,
154                });
155                consumed.insert(i);
156            }
157        }
158    }
159
160    // ── Require helpers ────────────────────────────────────────────────
161
162    /// Unwrap an `ExpressionStatement` to get the inner expression node.
163    /// Returns the node itself if it is not an `ExpressionStatement`.
164    fn unwrap_expression_statement(node: &Node) -> &Node {
165        match &node.kind {
166            NodeKind::ExpressionStatement { expression } => expression,
167            _ => node,
168        }
169    }
170
171    /// Check whether a `require` call's arguments indicate a dynamic require
172    /// (i.e. `require $var`).
173    fn is_dynamic_require(args: &[Node]) -> bool {
174        match args.first() {
175            Some(arg) => matches!(&arg.kind, NodeKind::Variable { .. }),
176            None => false,
177        }
178    }
179
180    /// Extract the module name from a `require` call's arguments.
181    ///
182    /// Handles:
183    /// - `require Foo::Bar` → `"Foo::Bar"` (Identifier)
184    /// - `require "Foo/Bar.pm"` → `"Foo::Bar"` (String, path-to-module conversion)
185    fn extract_require_module_name(args: &[Node]) -> Option<String> {
186        let arg = args.first()?;
187        match &arg.kind {
188            NodeKind::Identifier { name } => Some(name.clone()),
189            NodeKind::String { value, .. } => {
190                // "Foo/Bar.pm" → "Foo::Bar"
191                let cleaned = value.trim_matches('\'').trim_matches('"').trim();
192                let module = cleaned.trim_end_matches(".pm").replace('/', "::");
193                Some(module)
194            }
195            _ => None,
196        }
197    }
198
199    /// Build an [`ImportSpec`] for `require $var` (dynamic require).
200    fn make_dynamic_require(file_id: FileId, node: &Node) -> ImportSpec {
201        let anchor_id = Self::anchor_from_node(node);
202        ImportSpec {
203            module: String::new(),
204            kind: ImportKind::DynamicRequire,
205            symbols: ImportSymbols::Dynamic,
206            provenance: Provenance::ExactAst,
207            confidence: Confidence::Low,
208            file_id: Some(file_id),
209            anchor_id: Some(anchor_id),
210            scope_id: None,
211        }
212    }
213
214    /// Try to match a `Module->import(...)` method call node.
215    ///
216    /// Returns `Some((symbols, node))` if the node is a `MethodCall` with
217    /// method `"import"` and the object matches `expected_module`.
218    fn try_match_import_call<'a>(
219        node: &'a Node,
220        expected_module: &str,
221    ) -> Option<(ImportSymbols, &'a Node)> {
222        let (object, method, args) = match &node.kind {
223            NodeKind::MethodCall { object, method, args } => (object, method, args),
224            _ => return None,
225        };
226
227        if method != "import" {
228            return None;
229        }
230
231        // The object must be an Identifier matching the module name.
232        let obj_name = match &object.kind {
233            NodeKind::Identifier { name } => name.as_str(),
234            _ => return None,
235        };
236
237        if obj_name != expected_module {
238            return None;
239        }
240
241        // Extract imported symbols from the argument list.
242        let symbols = Self::extract_import_call_symbols(args);
243        Some((symbols, node))
244    }
245
246    /// Extract [`ImportSymbols`] from the argument list of a `Module->import(...)` call.
247    fn extract_import_call_symbols(args: &[Node]) -> ImportSymbols {
248        if args.is_empty() {
249            return ImportSymbols::Default;
250        }
251
252        let mut names: Vec<String> = Vec::new();
253        let mut tags: Vec<String> = Vec::new();
254        let mut has_dynamic_arg = false;
255
256        for arg in args {
257            has_dynamic_arg |= Self::collect_import_arg_symbols(arg, &mut names, &mut tags);
258        }
259
260        if has_dynamic_arg {
261            return ImportSymbols::Dynamic;
262        }
263
264        if names.is_empty() && tags.is_empty() {
265            return ImportSymbols::Default;
266        }
267
268        if !tags.is_empty() && names.is_empty() {
269            return ImportSymbols::Tags(tags);
270        }
271
272        if !tags.is_empty() && !names.is_empty() {
273            return ImportSymbols::Mixed { tags, names };
274        }
275
276        ImportSymbols::Explicit(names)
277    }
278
279    /// Collect symbol names and tags from a single argument node of an
280    /// `import(...)` call.
281    ///
282    /// Returns `true` when the argument is dynamic or unsupported and should
283    /// prevent the import site from claiming exact symbol names.
284    fn collect_import_arg_symbols(
285        arg: &Node,
286        names: &mut Vec<String>,
287        tags: &mut Vec<String>,
288    ) -> bool {
289        match &arg.kind {
290            NodeKind::String { value, .. } => {
291                let bare = value.trim_matches('\'').trim_matches('"');
292                if let Some(tag) = bare.strip_prefix(':') {
293                    tags.push(tag.to_string());
294                } else if !bare.is_empty() {
295                    names.push(bare.to_string());
296                }
297                false
298            }
299            NodeKind::Identifier { name } => {
300                // Handle qw(...) stored as raw identifier string.
301                if let Some(inner) = Self::parse_qw_content(name) {
302                    for word in inner.split_whitespace() {
303                        if let Some(tag) = word.strip_prefix(':') {
304                            tags.push(tag.to_string());
305                        } else {
306                            names.push(word.to_string());
307                        }
308                    }
309                } else if let Some(tag) = name.strip_prefix(':') {
310                    tags.push(tag.to_string());
311                } else if !name.is_empty() {
312                    names.push(name.clone());
313                }
314                false
315            }
316            NodeKind::Variable { .. } => {
317                // `Foo->import(@names)` / `Foo->import($name)` is dynamic:
318                // do not guess exact imported symbols.
319                true
320            }
321            NodeKind::ArrayLiteral { elements } => {
322                // qw(...) in expression context → ArrayLiteral of String nodes
323                let mut has_dynamic_arg = false;
324                for el in elements {
325                    has_dynamic_arg |= Self::collect_import_arg_symbols(el, names, tags);
326                }
327                has_dynamic_arg
328            }
329            _ => true,
330        }
331    }
332
333    fn confidence_for_symbols(symbols: &ImportSymbols) -> Confidence {
334        if matches!(symbols, ImportSymbols::Dynamic) { Confidence::Low } else { Confidence::High }
335    }
336
337    // ── Classification ──────────────────────────────────────────────────
338
339    /// Classify a single `use` statement into an [`ImportSpec`].
340    ///
341    /// Returns `None` for version pragmas (`use 5.036;`, `use v5.38;`) and
342    /// other non-module-import statements that should not produce import facts.
343    fn classify_use(
344        module: &str,
345        args: &[String],
346        file_id: FileId,
347        node: &Node,
348    ) -> Option<ImportSpec> {
349        // Skip version pragmas — they are not module imports.
350        if Self::is_version_pragma(module) {
351            return None;
352        }
353
354        let anchor_id = Self::anchor_from_node(node);
355
356        // `use constant` is a special pragma that defines constants.
357        if module == "constant" {
358            return Some(Self::classify_use_constant(args, file_id, anchor_id));
359        }
360
361        // Classify by argument shape.
362        let (kind, symbols) = Self::classify_args(args, module, node);
363
364        Some(ImportSpec {
365            module: module.to_string(),
366            kind,
367            symbols,
368            provenance: Provenance::ExactAst,
369            confidence: Confidence::High,
370            file_id: Some(file_id),
371            anchor_id: Some(anchor_id),
372            scope_id: None,
373        })
374    }
375
376    /// Classify the argument list of a non-constant `use` statement.
377    fn classify_args(args: &[String], module: &str, node: &Node) -> (ImportKind, ImportSymbols) {
378        if args.is_empty() {
379            // Distinguish `use Module;` from `use Module ()`.
380            //
381            // The parser produces empty args for both forms. We use a span-length
382            // heuristic: `use Module;` occupies `"use " + module + ";"` bytes,
383            // while `use Module ()` is longer due to the parentheses.
384            let bare_len = "use ".len() + module.len() + 1; // +1 for ';'
385            let span_len = node.location.end.saturating_sub(node.location.start);
386            if span_len > bare_len {
387                // The source text is longer than a bare `use Module;`, so there
388                // were likely empty parentheses.
389                return (ImportKind::UseEmpty, ImportSymbols::None);
390            }
391            // `use Module;` — bare import, triggers default @EXPORT.
392            return (ImportKind::Use, ImportSymbols::Default);
393        }
394
395        // Collect explicit names, tags, and detect qw() forms.
396        let mut explicit_names: Vec<String> = Vec::new();
397        let mut tags: Vec<String> = Vec::new();
398
399        for arg in args {
400            let trimmed = arg.trim();
401
402            // qw(...) form: "qw(a b c)"
403            if let Some(inner) = Self::parse_qw_content(trimmed) {
404                let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
405                for word in words {
406                    if let Some(tag) = word.strip_prefix(':') {
407                        tags.push(tag.to_string());
408                    } else {
409                        explicit_names.push(word);
410                    }
411                }
412                continue;
413            }
414
415            // Tag argument: ':tag' or ":tag" (with or without quotes)
416            let unquoted = Self::unquote(trimmed);
417            if let Some(tag) = unquoted.strip_prefix(':') {
418                tags.push(tag.to_string());
419                continue;
420            }
421
422            // Skip fat-arrow values and punctuation that are part of overload-style
423            // key-value pairs (e.g. `use overload '""' => \&stringify`).
424            if trimmed == "=>" || trimmed == "," || trimmed == "\\" {
425                continue;
426            }
427
428            // Regular symbol name.
429            if Self::looks_like_symbol_name(trimmed) {
430                explicit_names.push(Self::unquote(trimmed).to_string());
431            }
432        }
433
434        // Empty parens: `use Module ()`
435        if explicit_names.is_empty() && tags.is_empty() && !args.is_empty() {
436            // The parser consumed `()` but produced no meaningful args.
437            // However, args may contain punctuation tokens from complex use statements.
438            // If all args are non-symbol tokens, treat as empty import.
439            let has_any_symbol = args.iter().any(|a| {
440                let t = a.trim();
441                Self::looks_like_symbol_name(t) || Self::parse_qw_content(t).is_some()
442            });
443            if !has_any_symbol {
444                return (ImportKind::UseEmpty, ImportSymbols::None);
445            }
446        }
447
448        // Tags only.
449        if !tags.is_empty() && explicit_names.is_empty() {
450            return (ImportKind::UseTag, ImportSymbols::Tags(tags));
451        }
452
453        // Mixed tags and names.
454        if !tags.is_empty() && !explicit_names.is_empty() {
455            return (
456                ImportKind::UseExplicitList,
457                ImportSymbols::Mixed { tags, names: explicit_names },
458            );
459        }
460
461        // Explicit symbol list.
462        if !explicit_names.is_empty() {
463            return (ImportKind::UseExplicitList, ImportSymbols::Explicit(explicit_names));
464        }
465
466        // Fallback: bare use with unrecognised args.
467        (ImportKind::Use, ImportSymbols::Default)
468    }
469
470    /// Classify `use constant` statements.
471    fn classify_use_constant(args: &[String], file_id: FileId, anchor_id: AnchorId) -> ImportSpec {
472        let mut constant_names: Vec<String> = Vec::new();
473
474        if args.is_empty() {
475            // `use constant;` — degenerate, no constants defined.
476            return ImportSpec {
477                module: "constant".to_string(),
478                kind: ImportKind::UseConstant,
479                symbols: ImportSymbols::None,
480                provenance: Provenance::ExactAst,
481                confidence: Confidence::High,
482                file_id: Some(file_id),
483                anchor_id: Some(anchor_id),
484                scope_id: None,
485            };
486        }
487
488        // Hash-ref form: `use constant { FOO => 1, BAR => 2 }`
489        // Args look like: ["{", "FOO", "=>", "1", "BAR", "=>", "2", "}"]
490        if args.first().map(|a| a.as_str()) == Some("{") {
491            let mut i = 1; // skip opening brace
492            while i < args.len() {
493                let token = args[i].trim();
494                if token == "}" || token == "=>" || token == "," {
495                    i += 1;
496                    continue;
497                }
498                // After a name, skip the => and value
499                if i + 1 < args.len() && args[i + 1].trim() == "=>" {
500                    constant_names.push(token.to_string());
501                    // Skip => and value
502                    i += 3;
503                } else {
504                    i += 1;
505                }
506            }
507        }
508        // qw() form: `use constant qw(ONE TWO THREE)`
509        else if let Some(inner) = args.first().and_then(|a| Self::parse_qw_content(a.trim())) {
510            let words: Vec<String> = inner.split_whitespace().map(|w| w.to_string()).collect();
511            constant_names.extend(words);
512        }
513        // Scalar form: `use constant PI => 3.14`
514        // Args look like: ["PI", "3.14"] or ["PI", "=>", "3.14"]
515        else if let Some(name) = args.first() {
516            let trimmed = name.trim();
517            if Self::looks_like_constant_name(trimmed) {
518                constant_names.push(trimmed.to_string());
519            }
520        }
521
522        // Deduplicate while preserving order.
523        let mut seen = std::collections::HashSet::new();
524        constant_names.retain(|n| seen.insert(n.clone()));
525
526        let symbols = if constant_names.is_empty() {
527            ImportSymbols::None
528        } else {
529            ImportSymbols::Explicit(constant_names)
530        };
531
532        ImportSpec {
533            module: "constant".to_string(),
534            kind: ImportKind::UseConstant,
535            symbols,
536            provenance: Provenance::ExactAst,
537            confidence: Confidence::High,
538            file_id: Some(file_id),
539            anchor_id: Some(anchor_id),
540            scope_id: None,
541        }
542    }
543
544    // ── Helpers ─────────────────────────────────────────────────────────
545
546    /// Derive an [`AnchorId`] from a node's byte-offset span.
547    fn anchor_from_node(node: &Node) -> AnchorId {
548        // Use the start byte offset as a deterministic anchor ID.
549        // This is unique per use-statement within a file.
550        AnchorId(node.location.start as u64)
551    }
552
553    /// Check whether a module string is a version pragma (e.g. `5.036`, `v5.38`).
554    fn is_version_pragma(module: &str) -> bool {
555        // Numeric version: 5.036, 5.10
556        if module.chars().next().is_some_and(|c| c.is_ascii_digit()) {
557            return true;
558        }
559        // v-string: v5.38, v5.12.0
560        if module.starts_with('v')
561            && module.len() > 1
562            && module[1..].chars().all(|c| c.is_ascii_digit() || c == '.')
563        {
564            return true;
565        }
566        false
567    }
568
569    /// Extract the inner content of a `qw(...)` string.
570    ///
571    /// Returns `Some("a b c")` for `"qw(a b c)"`, `None` otherwise.
572    fn parse_qw_content(s: &str) -> Option<&str> {
573        let rest = s.strip_prefix("qw")?;
574        // The parser normalises all qw delimiters to parentheses.
575        let inner = rest.strip_prefix('(')?.strip_suffix(')')?;
576        Some(inner)
577    }
578
579    /// Remove surrounding single or double quotes from a string.
580    fn unquote(s: &str) -> &str {
581        if (s.starts_with('\'') && s.ends_with('\'')) || (s.starts_with('"') && s.ends_with('"')) {
582            if s.len() >= 2 {
583                return &s[1..s.len() - 1];
584            }
585        }
586        s
587    }
588
589    /// Heuristic: does this string look like a Perl symbol name?
590    fn looks_like_symbol_name(s: &str) -> bool {
591        let s = Self::unquote(s);
592        if s.is_empty() {
593            return false;
594        }
595        // Tags start with ':'
596        if s.starts_with(':') {
597            return true;
598        }
599        // Sigiled variables: $foo, @bar, %baz, &sub, *glob
600        if s.starts_with('$')
601            || s.starts_with('@')
602            || s.starts_with('%')
603            || s.starts_with('&')
604            || s.starts_with('*')
605        {
606            return true;
607        }
608        // Bare word: starts with letter or underscore
609        s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
610    }
611
612    /// Heuristic: does this string look like a constant name?
613    ///
614    /// Constants are typically UPPER_CASE identifiers.
615    fn looks_like_constant_name(s: &str) -> bool {
616        if s.is_empty() {
617            return false;
618        }
619        s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::Parser;
627
628    /// Parse Perl source and extract import specs.
629    fn parse_and_extract(code: &str) -> Vec<ImportSpec> {
630        let mut parser = Parser::new(code);
631        let ast = match parser.parse() {
632            Ok(ast) => ast,
633            Err(_) => return Vec::new(),
634        };
635        ImportExtractor::extract(&ast, FileId(1))
636    }
637
638    // ── use Module qw(a b) → UseExplicitList ────────────────────────────
639
640    #[test]
641    fn test_use_explicit_list_qw() -> Result<(), String> {
642        let specs = parse_and_extract("use List::Util qw(first reduce any);");
643        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
644
645        assert_eq!(spec.module, "List::Util");
646        assert_eq!(spec.kind, ImportKind::UseExplicitList);
647        if let ImportSymbols::Explicit(names) = &spec.symbols {
648            assert!(names.contains(&"first".to_string()), "missing 'first' in {names:?}");
649            assert!(names.contains(&"reduce".to_string()), "missing 'reduce' in {names:?}");
650            assert!(names.contains(&"any".to_string()), "missing 'any' in {names:?}");
651        } else {
652            return Err(format!("expected Explicit, got {:?}", spec.symbols));
653        }
654        assert_eq!(spec.file_id, Some(FileId(1)));
655        assert!(spec.anchor_id.is_some());
656        Ok(())
657    }
658
659    #[test]
660    fn test_use_explicit_list_quoted_strings() -> Result<(), String> {
661        let specs = parse_and_extract("use Exporter 'import';");
662        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
663
664        assert_eq!(spec.module, "Exporter");
665        assert_eq!(spec.kind, ImportKind::UseExplicitList);
666        if let ImportSymbols::Explicit(names) = &spec.symbols {
667            assert!(names.contains(&"import".to_string()), "missing 'import' in {names:?}");
668        } else {
669            return Err(format!("expected Explicit, got {:?}", spec.symbols));
670        }
671        Ok(())
672    }
673
674    // ── use Module () → UseEmpty ────────────────────────────────────────
675    //
676    // NOTE: The current parser represents both `use Module;` and `use Module ()`
677    // with empty args. We detect empty-parens by checking for an AST node whose
678    // source text contains `()`. When the parser cannot distinguish the two
679    // forms, both are classified as bare `Use`/`Default`.
680
681    #[test]
682    fn test_use_empty_parens() -> Result<(), String> {
683        let specs = parse_and_extract("use POSIX ();");
684        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
685
686        assert_eq!(spec.module, "POSIX");
687        // The parser produces empty args for both `use POSIX;` and `use POSIX ()`.
688        // We detect the empty-parens form by inspecting the source span length
689        // relative to the module name length.
690        assert_eq!(spec.kind, ImportKind::UseEmpty);
691        assert_eq!(spec.symbols, ImportSymbols::None);
692        Ok(())
693    }
694
695    // ── use Module ':tag' → UseTag ──────────────────────────────────────
696
697    #[test]
698    fn test_use_tag_single() -> Result<(), String> {
699        let specs = parse_and_extract("use POSIX ':sys_wait_h';");
700        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
701
702        assert_eq!(spec.module, "POSIX");
703        assert_eq!(spec.kind, ImportKind::UseTag);
704        if let ImportSymbols::Tags(tags) = &spec.symbols {
705            assert!(tags.contains(&"sys_wait_h".to_string()), "missing tag in {tags:?}");
706        } else {
707            return Err(format!("expected Tags, got {:?}", spec.symbols));
708        }
709        Ok(())
710    }
711
712    #[test]
713    fn test_use_tag_in_qw() -> Result<(), String> {
714        let specs = parse_and_extract("use Fcntl qw(:flock);");
715        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
716
717        assert_eq!(spec.module, "Fcntl");
718        assert_eq!(spec.kind, ImportKind::UseTag);
719        if let ImportSymbols::Tags(tags) = &spec.symbols {
720            assert!(tags.contains(&"flock".to_string()), "missing tag in {tags:?}");
721        } else {
722            return Err(format!("expected Tags, got {:?}", spec.symbols));
723        }
724        Ok(())
725    }
726
727    // ── use Module (bare) → Use/Default ─────────────────────────────────
728
729    #[test]
730    fn test_use_bare() -> Result<(), String> {
731        let specs = parse_and_extract("use strict;");
732        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
733
734        assert_eq!(spec.module, "strict");
735        assert_eq!(spec.kind, ImportKind::Use);
736        assert_eq!(spec.symbols, ImportSymbols::Default);
737        Ok(())
738    }
739
740    #[test]
741    fn test_use_bare_qualified() -> Result<(), String> {
742        let specs = parse_and_extract("use Data::Dumper;");
743        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
744
745        assert_eq!(spec.module, "Data::Dumper");
746        assert_eq!(spec.kind, ImportKind::Use);
747        assert_eq!(spec.symbols, ImportSymbols::Default);
748        Ok(())
749    }
750
751    // ── use constant → UseConstant ──────────────────────────────────────
752
753    #[test]
754    fn test_use_constant_scalar() -> Result<(), String> {
755        let specs = parse_and_extract("use constant PI => 3.14;");
756        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
757
758        assert_eq!(spec.module, "constant");
759        assert_eq!(spec.kind, ImportKind::UseConstant);
760        if let ImportSymbols::Explicit(names) = &spec.symbols {
761            assert!(names.contains(&"PI".to_string()), "missing 'PI' in {names:?}");
762        } else {
763            return Err(format!("expected Explicit, got {:?}", spec.symbols));
764        }
765        Ok(())
766    }
767
768    #[test]
769    fn test_use_constant_hash_ref() -> Result<(), String> {
770        let specs = parse_and_extract("use constant { FOO => 1, BAR => 2 };");
771        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
772
773        assert_eq!(spec.module, "constant");
774        assert_eq!(spec.kind, ImportKind::UseConstant);
775        if let ImportSymbols::Explicit(names) = &spec.symbols {
776            assert!(names.contains(&"FOO".to_string()), "missing 'FOO' in {names:?}");
777            assert!(names.contains(&"BAR".to_string()), "missing 'BAR' in {names:?}");
778        } else {
779            return Err(format!("expected Explicit, got {:?}", spec.symbols));
780        }
781        Ok(())
782    }
783
784    #[test]
785    fn test_use_constant_empty() -> Result<(), String> {
786        let specs = parse_and_extract("use constant;");
787        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
788
789        assert_eq!(spec.module, "constant");
790        assert_eq!(spec.kind, ImportKind::UseConstant);
791        assert_eq!(spec.symbols, ImportSymbols::None);
792        Ok(())
793    }
794
795    // ── Version pragmas are skipped ─────────────────────────────────────
796
797    #[test]
798    fn test_version_pragma_skipped() -> Result<(), String> {
799        let specs = parse_and_extract("use 5.036;");
800        assert!(specs.is_empty(), "version pragma should not produce ImportSpec");
801        Ok(())
802    }
803
804    #[test]
805    fn test_vstring_pragma_skipped() -> Result<(), String> {
806        let specs = parse_and_extract("use v5.38;");
807        assert!(specs.is_empty(), "v-string pragma should not produce ImportSpec");
808        Ok(())
809    }
810
811    // ── Multiple use statements ─────────────────────────────────────────
812
813    #[test]
814    fn test_multiple_use_statements() -> Result<(), String> {
815        let code = r#"
816use strict;
817use warnings;
818use List::Util qw(first any);
819use POSIX ();
820use constant MAX => 100;
821"#;
822        let specs = parse_and_extract(code);
823        assert_eq!(specs.len(), 5, "expected 5 ImportSpecs, got {}", specs.len());
824
825        // strict — bare
826        assert_eq!(specs[0].module, "strict");
827        assert_eq!(specs[0].kind, ImportKind::Use);
828
829        // warnings — bare
830        assert_eq!(specs[1].module, "warnings");
831        assert_eq!(specs[1].kind, ImportKind::Use);
832
833        // List::Util — explicit list
834        assert_eq!(specs[2].module, "List::Util");
835        assert_eq!(specs[2].kind, ImportKind::UseExplicitList);
836
837        // POSIX — empty
838        assert_eq!(specs[3].module, "POSIX");
839        assert_eq!(specs[3].kind, ImportKind::UseEmpty);
840
841        // constant — use constant
842        assert_eq!(specs[4].module, "constant");
843        assert_eq!(specs[4].kind, ImportKind::UseConstant);
844
845        Ok(())
846    }
847
848    // ── Anchor and file_id are populated ────────────────────────────────
849
850    #[test]
851    fn test_anchor_and_file_id_populated() -> Result<(), String> {
852        let specs = parse_and_extract("use Foo::Bar qw(baz);");
853        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
854
855        assert_eq!(spec.file_id, Some(FileId(1)));
856        assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
857        assert_eq!(spec.provenance, Provenance::ExactAst);
858        assert_eq!(spec.confidence, Confidence::High);
859        Ok(())
860    }
861
862    // ── Nested use in package block ─────────────────────────────────────
863
864    #[test]
865    fn test_use_inside_package_block() -> Result<(), String> {
866        let code = r#"
867package MyModule;
868use Exporter 'import';
869our @EXPORT = qw(foo);
8701;
871"#;
872        let specs = parse_and_extract(code);
873        let exporter_spec =
874            specs.iter().find(|s| s.module == "Exporter").ok_or("expected Exporter ImportSpec")?;
875
876        assert_eq!(exporter_spec.kind, ImportKind::UseExplicitList);
877        if let ImportSymbols::Explicit(names) = &exporter_spec.symbols {
878            assert!(names.contains(&"import".to_string()));
879        } else {
880            return Err(format!("expected Explicit, got {:?}", exporter_spec.symbols));
881        }
882        Ok(())
883    }
884
885    // ── Mixed tags and names ────────────────────────────────────────────
886
887    #[test]
888    fn test_use_mixed_tags_and_names() -> Result<(), String> {
889        let specs = parse_and_extract("use Fcntl qw(:flock LOCK_EX LOCK_NB);");
890        let spec = specs.first().ok_or("expected at least one ImportSpec")?;
891
892        assert_eq!(spec.module, "Fcntl");
893        assert_eq!(spec.kind, ImportKind::UseExplicitList);
894        if let ImportSymbols::Mixed { tags, names } = &spec.symbols {
895            assert!(tags.contains(&"flock".to_string()), "missing tag 'flock' in {tags:?}");
896            assert!(names.contains(&"LOCK_EX".to_string()), "missing 'LOCK_EX' in {names:?}");
897            assert!(names.contains(&"LOCK_NB".to_string()), "missing 'LOCK_NB' in {names:?}");
898        } else {
899            return Err(format!("expected Mixed, got {:?}", spec.symbols));
900        }
901        Ok(())
902    }
903
904    // ── require Module → Require ────────────────────────────────────────
905
906    #[test]
907    fn test_require_bare_module() -> Result<(), String> {
908        let specs = parse_and_extract("require Foo::Bar;");
909        let spec = specs
910            .iter()
911            .find(|s| s.module == "Foo::Bar")
912            .ok_or("expected ImportSpec for Foo::Bar")?;
913
914        assert_eq!(spec.kind, ImportKind::Require);
915        assert_eq!(spec.symbols, ImportSymbols::Default);
916        assert_eq!(spec.provenance, Provenance::ExactAst);
917        assert_eq!(spec.confidence, Confidence::High);
918        assert_eq!(spec.file_id, Some(FileId(1)));
919        assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
920        Ok(())
921    }
922
923    // ── require Module; Module->import(...) → RequireThenImport ─────────
924
925    #[test]
926    fn test_require_then_import_with_qw() -> Result<(), String> {
927        let code = r#"
928require Foo::Bar;
929Foo::Bar->import(qw(alpha beta));
930"#;
931        let specs = parse_and_extract(code);
932        let spec = specs
933            .iter()
934            .find(|s| s.module == "Foo::Bar")
935            .ok_or("expected ImportSpec for Foo::Bar")?;
936
937        assert_eq!(spec.kind, ImportKind::RequireThenImport);
938        if let ImportSymbols::Explicit(names) = &spec.symbols {
939            assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
940            assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
941        } else {
942            return Err(format!("expected Explicit, got {:?}", spec.symbols));
943        }
944        assert_eq!(spec.provenance, Provenance::ExactAst);
945        assert_eq!(spec.confidence, Confidence::High);
946        Ok(())
947    }
948
949    #[test]
950    fn test_require_then_import_bare() -> Result<(), String> {
951        let code = r#"
952require Some::Module;
953Some::Module->import();
954"#;
955        let specs = parse_and_extract(code);
956        let spec = specs
957            .iter()
958            .find(|s| s.module == "Some::Module")
959            .ok_or("expected ImportSpec for Some::Module")?;
960
961        assert_eq!(spec.kind, ImportKind::RequireThenImport);
962        assert_eq!(spec.symbols, ImportSymbols::Default);
963        Ok(())
964    }
965
966    #[test]
967    fn test_require_then_import_quoted_strings() -> Result<(), String> {
968        let code = r#"
969require Foo::Bar;
970Foo::Bar->import('alpha', 'beta');
971"#;
972        let specs = parse_and_extract(code);
973        let spec = specs
974            .iter()
975            .find(|s| s.module == "Foo::Bar")
976            .ok_or("expected ImportSpec for Foo::Bar")?;
977
978        assert_eq!(spec.kind, ImportKind::RequireThenImport);
979        if let ImportSymbols::Explicit(names) = &spec.symbols {
980            assert!(names.contains(&"alpha".to_string()), "missing 'alpha' in {names:?}");
981            assert!(names.contains(&"beta".to_string()), "missing 'beta' in {names:?}");
982        } else {
983            return Err(format!("expected Explicit, got {:?}", spec.symbols));
984        }
985        assert_eq!(spec.confidence, Confidence::High);
986        Ok(())
987    }
988
989    #[test]
990    fn test_require_then_import_dynamic_symbol_list() -> Result<(), String> {
991        let code = r#"
992require Foo::Bar;
993Foo::Bar->import(@names);
994"#;
995        let specs = parse_and_extract(code);
996        let spec = specs
997            .iter()
998            .find(|s| s.module == "Foo::Bar")
999            .ok_or("expected ImportSpec for Foo::Bar")?;
1000
1001        assert_eq!(spec.kind, ImportKind::RequireThenImport);
1002        assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1003        assert_eq!(spec.confidence, Confidence::Low);
1004        Ok(())
1005    }
1006
1007    // ── require $var → DynamicRequire ───────────────────────────────────
1008
1009    #[test]
1010    fn test_require_dynamic_variable() -> Result<(), String> {
1011        let specs = parse_and_extract("require $module;");
1012        let spec = specs
1013            .iter()
1014            .find(|s| s.kind == ImportKind::DynamicRequire)
1015            .ok_or("expected DynamicRequire ImportSpec")?;
1016
1017        assert_eq!(spec.module, "");
1018        assert_eq!(spec.symbols, ImportSymbols::Dynamic);
1019        assert_eq!(spec.provenance, Provenance::ExactAst);
1020        assert_eq!(spec.confidence, Confidence::Low);
1021        assert_eq!(spec.file_id, Some(FileId(1)));
1022        assert!(spec.anchor_id.is_some(), "anchor_id should be populated");
1023        Ok(())
1024    }
1025
1026    // ── Mixed use and require statements ────────────────────────────────
1027
1028    #[test]
1029    fn test_mixed_use_and_require() -> Result<(), String> {
1030        let code = r#"
1031use strict;
1032use warnings;
1033require Foo::Bar;
1034Foo::Bar->import(qw(baz));
1035require $dynamic;
1036"#;
1037        let specs = parse_and_extract(code);
1038
1039        // strict — bare use
1040        let strict_spec =
1041            specs.iter().find(|s| s.module == "strict").ok_or("expected strict ImportSpec")?;
1042        assert_eq!(strict_spec.kind, ImportKind::Use);
1043
1044        // warnings — bare use
1045        let warnings_spec =
1046            specs.iter().find(|s| s.module == "warnings").ok_or("expected warnings ImportSpec")?;
1047        assert_eq!(warnings_spec.kind, ImportKind::Use);
1048
1049        // Foo::Bar — require then import
1050        let foo_spec =
1051            specs.iter().find(|s| s.module == "Foo::Bar").ok_or("expected Foo::Bar ImportSpec")?;
1052        assert_eq!(foo_spec.kind, ImportKind::RequireThenImport);
1053        if let ImportSymbols::Explicit(names) = &foo_spec.symbols {
1054            assert!(names.contains(&"baz".to_string()), "missing 'baz' in {names:?}");
1055        } else {
1056            return Err(format!("expected Explicit, got {:?}", foo_spec.symbols));
1057        }
1058
1059        // dynamic require
1060        let dyn_spec = specs
1061            .iter()
1062            .find(|s| s.kind == ImportKind::DynamicRequire)
1063            .ok_or("expected DynamicRequire ImportSpec")?;
1064        assert_eq!(dyn_spec.symbols, ImportSymbols::Dynamic);
1065
1066        Ok(())
1067    }
1068
1069    // ── require with string path → Require ──────────────────────────────
1070
1071    #[test]
1072    fn test_require_string_path() -> Result<(), String> {
1073        let specs = parse_and_extract(r#"require "Foo/Bar.pm";"#);
1074        let spec = specs
1075            .iter()
1076            .find(|s| s.module == "Foo::Bar")
1077            .ok_or("expected ImportSpec for Foo::Bar")?;
1078
1079        assert_eq!(spec.kind, ImportKind::Require);
1080        assert_eq!(spec.symbols, ImportSymbols::Default);
1081        Ok(())
1082    }
1083}