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}