Skip to main content

perl_semantic_analyzer/analysis/semantic/
query_facade.rs

1//! Read-only semantic query facade over parser, semantic, and workspace layers.
2
3use std::ops::Range;
4
5use crate::SourceLocation;
6use crate::ast::Node;
7use crate::pragma_tracker::{PragmaState, PragmaTracker};
8use crate::symbol::{Symbol, SymbolKind};
9use crate::workspace_index::WorkspaceIndex;
10
11use super::SemanticModel;
12
13/// Stable read-only semantic query surface for incremental consumer adoption.
14#[derive(Debug)]
15pub struct SemanticQueryFacade {
16    model: SemanticModel,
17    pragma_map: Vec<(Range<usize>, PragmaState)>,
18}
19
20/// Read-only symbol projection returned by semantic query lookups.
21#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct ResolvedSymbol {
24    /// Symbol name (without sigil for variables).
25    pub name: String,
26    /// Fully qualified symbol name when known.
27    pub qualified_name: String,
28    /// Symbol kind.
29    pub kind: SymbolKind,
30    /// Definition source location.
31    pub location: SourceLocation,
32    /// Variable declaration type (`my`, `our`, `local`, `state`) when known.
33    pub declaration: Option<String>,
34    /// Extracted POD or comment documentation when present.
35    pub documentation: Option<String>,
36}
37
38/// Definition location that may include a workspace URI.
39#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct DefinitionLocation {
42    /// File URI if known for this definition.
43    pub uri: Option<String>,
44    /// Byte-range location inside the source file.
45    pub location: SourceLocation,
46}
47
48/// Imported module visible to the current document.
49#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct VisibleImport {
52    /// Imported module name.
53    pub module_name: String,
54}
55
56/// Ordered inheritance information for a class.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct ParentChain {
60    /// Class/package name that owns the chain.
61    pub class_name: String,
62    /// Ancestors in method-resolution order.
63    pub ancestors: Vec<String>,
64}
65
66/// Effective pragma state at a byte offset.
67#[derive(Debug, Clone, PartialEq)]
68#[non_exhaustive]
69pub struct EffectivePragmaState {
70    /// Byte offset where state was requested.
71    pub offset: usize,
72    /// Effective tracked pragma state.
73    pub state: PragmaState,
74}
75
76impl SemanticQueryFacade {
77    /// Build a read-only query facade from parser output and source text.
78    pub fn build(root: &Node, source: &str) -> Self {
79        Self { model: SemanticModel::build(root, source), pragma_map: PragmaTracker::build(root) }
80    }
81
82    /// Access the underlying semantic model for incremental migration.
83    pub fn semantic_model(&self) -> &SemanticModel {
84        &self.model
85    }
86
87    /// Resolve a symbol definition at `position`.
88    pub fn resolved_symbol_at(&self, position: usize) -> Option<ResolvedSymbol> {
89        self.model.definition_at(position).map(ResolvedSymbol::from)
90    }
91
92    /// Resolve a definition location at `position`.
93    pub fn definition_location_at(
94        &self,
95        position: usize,
96        current_uri: Option<&str>,
97    ) -> Option<DefinitionLocation> {
98        self.model.definition_at(position).map(|symbol| DefinitionLocation {
99            uri: current_uri.map(std::string::ToString::to_string),
100            location: symbol.location,
101        })
102    }
103
104    /// Return imports visible to `uri` from workspace indexing.
105    pub fn visible_imports(&self, workspace: &WorkspaceIndex, uri: &str) -> Vec<VisibleImport> {
106        let mut imports: Vec<_> = workspace
107            .file_dependencies(uri)
108            .into_iter()
109            .map(|module_name| VisibleImport { module_name })
110            .collect();
111        imports.sort_by(|left, right| left.module_name.cmp(&right.module_name));
112        imports
113    }
114
115    /// Return class parent chain in analyzer-configured resolution order.
116    pub fn parent_chain(&self, class_name: &str) -> Option<ParentChain> {
117        self.model
118            .parent_chain(class_name)
119            .map(|ancestors| ParentChain { class_name: class_name.to_string(), ancestors })
120    }
121
122    /// Resolve inherited method origin for a class and method name.
123    pub fn inherited_origin(
124        &self,
125        class_name: &str,
126        method_name: &str,
127        current_uri: Option<&str>,
128    ) -> Option<DefinitionLocation> {
129        self.model.resolve_inherited_method_location(class_name, method_name).map(|location| {
130            DefinitionLocation { uri: current_uri.map(std::string::ToString::to_string), location }
131        })
132    }
133
134    /// Return effective tracked pragma state for `offset`.
135    pub fn effective_pragma_state(&self, offset: usize) -> EffectivePragmaState {
136        EffectivePragmaState {
137            offset,
138            state: PragmaTracker::state_for_offset(&self.pragma_map, offset),
139        }
140    }
141}
142
143impl From<&Symbol> for ResolvedSymbol {
144    fn from(value: &Symbol) -> Self {
145        Self {
146            name: value.name.clone(),
147            qualified_name: value.qualified_name.clone(),
148            kind: value.kind,
149            location: value.location,
150            declaration: value.declaration.clone(),
151            documentation: value.documentation.clone(),
152        }
153    }
154}
155
156#[cfg(test)]
157mod tests {
158    use perl_tdd_support::{must, must_some};
159
160    use super::*;
161    use crate::parser::Parser;
162    use crate::workspace_index;
163
164    #[test]
165    fn query_facade_resolves_symbol_and_pragmas() {
166        let code = "use strict;\nmy $value = 1;\n$value;\n";
167        let mut parser = Parser::new(code);
168        let ast = must(parser.parse());
169
170        let facade = SemanticQueryFacade::build(&ast, code);
171        let usage_offset = must_some(code.rfind("$value"));
172
173        let symbol = must_some(facade.resolved_symbol_at(usage_offset));
174        assert_eq!(symbol.name, "value");
175
176        let pragma_state = facade.effective_pragma_state(usage_offset);
177        assert!(pragma_state.state.strict_vars);
178    }
179
180    #[test]
181    fn query_facade_reads_workspace_imports_and_parent_chain() {
182        let code = "package Child;\nuse parent 'Base';\n1;\n";
183        let mut parser = Parser::new(code);
184        let ast = must(parser.parse());
185
186        let facade = SemanticQueryFacade::build(&ast, code);
187
188        let index = workspace_index::WorkspaceIndex::new();
189        must(index.index_file_str("file:///test/child.pm", code));
190
191        let imports = facade.visible_imports(&index, "file:///test/child.pm");
192        assert!(imports.iter().any(|import| import.module_name == "Base"));
193
194        let chain = must_some(facade.parent_chain("Child"));
195        assert_eq!(chain.ancestors, vec!["Base"]);
196    }
197
198    #[test]
199    fn query_facade_resolved_symbol_carries_declaration_and_docs() {
200        // Verify that declaration and documentation fields are propagated
201        // from Symbol through the From impl — not silently dropped.
202        let code = "my $x = 1;\n$x;\n";
203        let mut parser = Parser::new(code);
204        let ast = must(parser.parse());
205        let facade = SemanticQueryFacade::build(&ast, code);
206
207        let usage_offset = must_some(code.rfind("$x"));
208        let symbol = must_some(facade.resolved_symbol_at(usage_offset));
209        // `my $x` should produce declaration = Some("my")
210        assert_eq!(
211            symbol.declaration.as_deref(),
212            Some("my"),
213            "declaration field must be propagated from Symbol, not silently dropped"
214        );
215    }
216
217    #[test]
218    fn query_facade_resolved_symbol_at_past_end_returns_none() {
219        // Out-of-range offset must return None gracefully — no panic.
220        let code = "my $x = 1;\n";
221        let mut parser = Parser::new(code);
222        let ast = must(parser.parse());
223        let facade = SemanticQueryFacade::build(&ast, code);
224
225        assert!(
226            facade.resolved_symbol_at(usize::MAX).is_none(),
227            "out-of-range offset must return None, not panic"
228        );
229    }
230
231    #[test]
232    fn query_facade_parent_chain_unknown_class_returns_none() {
233        // A class that doesn't appear in class models must return None.
234        let code = "package Foo; sub bar {} 1;\n";
235        let mut parser = Parser::new(code);
236        let ast = must(parser.parse());
237        let facade = SemanticQueryFacade::build(&ast, code);
238
239        assert!(
240            facade.parent_chain("NonExistentClass").is_none(),
241            "unknown class must return None from parent_chain"
242        );
243    }
244
245    #[test]
246    fn query_facade_effective_pragma_state_no_pragmas() {
247        // A file with no pragmas must return a default (all-false) pragma state.
248        let code = "my $x = 1;\n";
249        let mut parser = Parser::new(code);
250        let ast = must(parser.parse());
251        let facade = SemanticQueryFacade::build(&ast, code);
252
253        let state = facade.effective_pragma_state(0);
254        assert!(!state.state.strict_vars, "strict_vars must be false when no use strict");
255        assert!(!state.state.strict_subs, "strict_subs must be false when no use strict");
256        assert!(!state.state.strict_refs, "strict_refs must be false when no use strict");
257        assert!(!state.state.warnings, "warnings must be false when no use warnings");
258    }
259
260    #[test]
261    fn query_facade_visible_imports_empty_file_returns_empty() {
262        // A file with no use statements must return an empty import list.
263        let code = "my $x = 1;\n";
264        let mut parser = Parser::new(code);
265        let ast = must(parser.parse());
266        let facade = SemanticQueryFacade::build(&ast, code);
267
268        let index = workspace_index::WorkspaceIndex::new();
269        must(index.index_file_str("file:///test/empty.pm", code));
270
271        let imports = facade.visible_imports(&index, "file:///test/empty.pm");
272        assert!(imports.is_empty(), "file with no use statements must produce no visible imports");
273    }
274}