Skip to main content

perl_pragma/
lib.rs

1//! Pragma tracker for Perl code analysis
2//!
3//! Tracks `use` and `no` pragmas throughout the codebase to determine
4//! effective pragma state at any point in the code.
5
6use perl_ast::ast::{Node, NodeKind};
7use std::ops::Range;
8
9/// Pragma state at a given point in the code
10#[derive(Debug, Clone, Default, PartialEq)]
11pub struct PragmaState {
12    /// Whether strict vars is enabled
13    pub strict_vars: bool,
14    /// Whether strict subs is enabled  
15    pub strict_subs: bool,
16    /// Whether strict refs is enabled
17    pub strict_refs: bool,
18    /// Whether warnings are enabled
19    pub warnings: bool,
20}
21
22impl PragmaState {
23    /// Create a new pragma state with all strict modes enabled
24    pub fn all_strict() -> Self {
25        Self { strict_vars: true, strict_subs: true, strict_refs: true, warnings: false }
26    }
27}
28
29/// Tracks pragma state throughout a Perl file
30pub struct PragmaTracker;
31
32impl PragmaTracker {
33    /// Build a range-indexed pragma map from an AST
34    pub fn build(ast: &Node) -> Vec<(Range<usize>, PragmaState)> {
35        let mut ranges = Vec::new();
36        let mut current_state = PragmaState::default();
37
38        // Build the pragma map by walking the AST
39        Self::build_ranges(ast, &mut current_state, &mut ranges);
40
41        // Sort by start offset
42        ranges.sort_by_key(|(range, _)| range.start);
43
44        ranges
45    }
46
47    /// Get the pragma state at a specific byte offset
48    pub fn state_for_offset(
49        pragma_map: &[(Range<usize>, PragmaState)],
50        offset: usize,
51    ) -> PragmaState {
52        // Find the last pragma state that starts before this offset.
53        // pragma_map is sorted by start offset (guaranteed by build()).
54        // We use partition_point to find the first element where start > offset,
55        // then take the element before it.
56        let idx = pragma_map.partition_point(|(range, _)| range.start <= offset);
57
58        if idx > 0 { pragma_map[idx - 1].1.clone() } else { PragmaState::default() }
59    }
60
61    fn build_ranges(
62        node: &Node,
63        current_state: &mut PragmaState,
64        ranges: &mut Vec<(Range<usize>, PragmaState)>,
65    ) {
66        match &node.kind {
67            NodeKind::Use { module, args, .. } => {
68                // Handle use statements
69                match module.as_str() {
70                    "strict" => {
71                        if args.is_empty() {
72                            // use strict; enables all categories
73                            current_state.strict_vars = true;
74                            current_state.strict_subs = true;
75                            current_state.strict_refs = true;
76                        } else {
77                            // Parse specific categories
78                            for arg in args {
79                                match arg.as_str() {
80                                    "vars" | "'vars'" | "\"vars\"" => {
81                                        current_state.strict_vars = true
82                                    }
83                                    "subs" | "'subs'" | "\"subs\"" => {
84                                        current_state.strict_subs = true
85                                    }
86                                    "refs" | "'refs'" | "\"refs\"" => {
87                                        current_state.strict_refs = true
88                                    }
89                                    _ => {}
90                                }
91                            }
92                        }
93
94                        // Record the state change at this location
95                        ranges
96                            .push((node.location.start..node.location.end, current_state.clone()));
97                    }
98                    "warnings" => {
99                        current_state.warnings = true;
100                        ranges
101                            .push((node.location.start..node.location.end, current_state.clone()));
102                    }
103                    _ => {}
104                }
105            }
106            NodeKind::No { module, args, .. } => {
107                // Handle no statements
108                match module.as_str() {
109                    "strict" => {
110                        if args.is_empty() {
111                            // no strict; disables all categories
112                            current_state.strict_vars = false;
113                            current_state.strict_subs = false;
114                            current_state.strict_refs = false;
115                        } else {
116                            // Parse specific categories
117                            for arg in args {
118                                match arg.as_str() {
119                                    "vars" | "'vars'" | "\"vars\"" => {
120                                        current_state.strict_vars = false
121                                    }
122                                    "subs" | "'subs'" | "\"subs\"" => {
123                                        current_state.strict_subs = false
124                                    }
125                                    "refs" | "'refs'" | "\"refs\"" => {
126                                        current_state.strict_refs = false
127                                    }
128                                    _ => {}
129                                }
130                            }
131                        }
132
133                        // Record the state change at this location
134                        ranges
135                            .push((node.location.start..node.location.end, current_state.clone()));
136                    }
137                    "warnings" => {
138                        current_state.warnings = false;
139                        ranges
140                            .push((node.location.start..node.location.end, current_state.clone()));
141                    }
142                    _ => {}
143                }
144            }
145            NodeKind::Block { statements } => {
146                // Save current state
147                let saved_state = current_state.clone();
148
149                // Process statements in the block
150                for stmt in statements {
151                    Self::build_ranges(stmt, current_state, ranges);
152                }
153
154                // Restore state after block
155                *current_state = saved_state;
156            }
157            NodeKind::Program { statements } => {
158                // Process all top-level statements
159                for stmt in statements {
160                    Self::build_ranges(stmt, current_state, ranges);
161                }
162            }
163            // For subroutines and other container nodes, recurse into their bodies
164            NodeKind::Subroutine { body, .. } => {
165                Self::build_ranges(body, current_state, ranges);
166            }
167            NodeKind::If { then_branch, else_branch, .. } => {
168                Self::build_ranges(then_branch, current_state, ranges);
169                if let Some(else_b) = else_branch {
170                    Self::build_ranges(else_b, current_state, ranges);
171                }
172            }
173            NodeKind::While { body, .. }
174            | NodeKind::For { body, .. }
175            | NodeKind::Foreach { body, .. } => {
176                Self::build_ranges(body, current_state, ranges);
177            }
178            // Other node types don't contain use/no statements
179            _ => {}
180        }
181    }
182}