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;
7use std::ops::Range;
8
9mod args;
10mod conditional;
11mod features;
12mod map;
13mod range_builder;
14mod version;
15
16pub use map::{
17    CompileTimePragmaEnvironment, PragmaEntry, PragmaMap, PragmaQueryCursor, PragmaStateQuery,
18};
19pub use version::{
20    PerlVersion, features_enabled_by_version, parse_perl_version, version_implies_strict,
21    version_implies_warnings,
22};
23
24pub(crate) use args::{
25    add_disabled_warning_category, apply_builtin_imports, builtin_import_names,
26    normalized_pragma_token, pragma_arg_items, remove_builtin_imports,
27};
28pub(crate) use conditional::conditional_pragma_target;
29pub(crate) use features::{apply_feature_state, canonical_feature_query};
30pub(crate) use map::normalize_state;
31pub(crate) use version::enable_effective_version_semantics;
32
33/// Pragma state at a given point in the code
34#[derive(Debug, Clone, Default, PartialEq)]
35pub struct PragmaState {
36    /// Whether strict vars is enabled
37    pub strict_vars: bool,
38    /// Whether strict subs is enabled
39    pub strict_subs: bool,
40    /// Whether strict refs is enabled
41    pub strict_refs: bool,
42    /// Whether warnings are enabled (globally)
43    pub warnings: bool,
44    /// Whether `use utf8` is enabled.
45    pub utf8: bool,
46    /// Active source encoding from `use encoding`.
47    pub encoding: Option<String>,
48    /// Whether `use feature 'unicode_strings'` or a matching feature bundle is enabled.
49    pub unicode_strings: bool,
50    /// Whether locale-sensitive behavior is enabled.
51    pub locale: bool,
52    /// Locale scope from `use locale`, if any.
53    pub locale_scope: Option<String>,
54    /// Warning categories explicitly disabled via `no warnings 'CATEGORY'`.
55    ///
56    /// When `no warnings` is used with specific category arguments (e.g.
57    /// `no warnings 'uninitialized'`), the global `warnings` flag stays `true`
58    /// and the disabled categories are recorded here. Only bare `no warnings`
59    /// (no arguments) clears the global `warnings` flag.
60    pub disabled_warning_categories: Vec<String>,
61    /// Whether explicit `use feature 'signatures'` currently implies strictness.
62    ///
63    /// This is tracked separately from the raw strict flags so `no feature
64    /// 'signatures'` can unwind the feature-driven implication without
65    /// clobbering explicit `use strict` or version-implied strict state.
66    pub signatures_strict: bool,
67    /// Effective language features enabled in the current lexical scope.
68    ///
69    /// This starts with any features implied by `use vX.Y` declarations and is
70    /// then updated by explicit `use feature` / `no feature` pragmas.
71    pub features: Vec<&'static str>,
72    /// Lexically imported builtin short names from `use builtin`.
73    pub builtin_imports: Vec<String>,
74}
75
76/// Immutable compile-time snapshot of pragma state.
77///
78/// This is the stable value object returned by position queries, and the same
79/// type used by lexical save/restore operations while building an environment.
80#[derive(Debug, Clone, Default, PartialEq)]
81pub struct PragmaSnapshot {
82    state: PragmaState,
83}
84
85impl PragmaSnapshot {
86    /// Create a snapshot from a concrete state value.
87    #[must_use]
88    pub fn from_state(state: PragmaState) -> Self {
89        Self { state }
90    }
91
92    /// Borrow the underlying state.
93    #[must_use]
94    pub fn state(&self) -> &PragmaState {
95        &self.state
96    }
97
98    /// Whether all strict categories are active in this snapshot.
99    #[must_use]
100    pub fn strict_enabled(&self) -> bool {
101        self.state.strict_vars && self.state.strict_subs && self.state.strict_refs
102    }
103
104    /// Whether warnings are globally active in this snapshot.
105    #[must_use]
106    pub fn warnings_enabled(&self) -> bool {
107        self.state.warnings
108    }
109
110    /// Whether a feature is enabled in this snapshot.
111    #[must_use]
112    pub fn has_feature(&self, feature: &str) -> bool {
113        self.state.has_feature(feature)
114    }
115
116    /// Returns true if warnings are active for the given category.
117    #[must_use]
118    pub fn is_warning_active(&self, category: &str) -> bool {
119        self.state.is_warning_active(category)
120    }
121}
122
123impl From<PragmaState> for PragmaSnapshot {
124    fn from(state: PragmaState) -> Self {
125        Self::from_state(state)
126    }
127}
128
129impl From<PragmaSnapshot> for PragmaState {
130    fn from(snapshot: PragmaSnapshot) -> Self {
131        snapshot.state
132    }
133}
134
135impl PragmaState {
136    /// Create a new pragma state with all strict modes enabled
137    pub fn all_strict() -> Self {
138        Self {
139            strict_vars: true,
140            strict_subs: true,
141            strict_refs: true,
142            warnings: false,
143            utf8: false,
144            encoding: None,
145            unicode_strings: false,
146            locale: false,
147            locale_scope: None,
148            disabled_warning_categories: Vec::new(),
149            signatures_strict: false,
150            features: Vec::new(),
151            builtin_imports: Vec::new(),
152        }
153    }
154
155    /// Returns `true` if warnings are active for the given category.
156    ///
157    /// Warnings for a category are active when:
158    /// - The global `warnings` flag is `true`, **and**
159    /// - The category is not listed in `disabled_warning_categories`.
160    ///
161    /// If the global `warnings` flag is `false` (i.e. `no warnings` with no
162    /// arguments was used), all categories are considered inactive regardless of
163    /// the `disabled_warning_categories` list.
164    #[must_use]
165    pub fn is_warning_active(&self, category: &str) -> bool {
166        self.warnings && !self.disabled_warning_categories.iter().any(|c| c == category)
167    }
168
169    /// Returns `true` if the given feature name is currently enabled.
170    #[must_use]
171    pub fn has_feature(&self, feature: &str) -> bool {
172        let feature = canonical_feature_query(feature);
173        self.features.contains(&feature)
174    }
175
176    /// Returns `true` when a builtin short name was lexically imported in scope.
177    #[must_use]
178    pub fn has_builtin_import(&self, name: &str) -> bool {
179        self.builtin_imports.iter().any(|import| import == name)
180    }
181}
182
183/// Tracks pragma state throughout a Perl file
184pub struct PragmaTracker;
185
186impl PragmaTracker {
187    /// Build a range-indexed pragma map from an AST
188    pub fn build(ast: &Node) -> Vec<(Range<usize>, PragmaState)> {
189        CompileTimePragmaEnvironment::build(ast)
190            .as_map()
191            .iter()
192            .map(|(range, snapshot)| (range.clone(), snapshot.clone().into()))
193            .collect()
194    }
195
196    /// Get the pragma state at a specific byte offset
197    pub fn state_for_offset(
198        pragma_map: &[(Range<usize>, PragmaState)],
199        offset: usize,
200    ) -> PragmaState {
201        let idx = pragma_map.partition_point(|(range, _)| range.start <= offset);
202        let state = if idx > 0 { pragma_map[idx - 1].1.clone() } else { PragmaState::default() };
203
204        normalize_state(state)
205    }
206
207    /// Get the final top-level pragma state after all lexical scopes close.
208    #[must_use]
209    pub fn final_state(pragma_map: &[(Range<usize>, PragmaState)]) -> PragmaState {
210        let state = pragma_map.last().map_or_else(PragmaState::default, |(_, s)| s.clone());
211
212        normalize_state(state)
213    }
214}