perl_semantic_analyzer/analysis/semantic/
query_facade.rs1use 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#[derive(Debug)]
15pub struct SemanticQueryFacade {
16 model: SemanticModel,
17 pragma_map: Vec<(Range<usize>, PragmaState)>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
22#[non_exhaustive]
23pub struct ResolvedSymbol {
24 pub name: String,
26 pub qualified_name: String,
28 pub kind: SymbolKind,
30 pub location: SourceLocation,
32 pub declaration: Option<String>,
34 pub documentation: Option<String>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct DefinitionLocation {
42 pub uri: Option<String>,
44 pub location: SourceLocation,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
50#[non_exhaustive]
51pub struct VisibleImport {
52 pub module_name: String,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub struct ParentChain {
60 pub class_name: String,
62 pub ancestors: Vec<String>,
64}
65
66#[derive(Debug, Clone, PartialEq)]
68#[non_exhaustive]
69pub struct EffectivePragmaState {
70 pub offset: usize,
72 pub state: PragmaState,
74}
75
76impl SemanticQueryFacade {
77 pub fn build(root: &Node, source: &str) -> Self {
79 Self { model: SemanticModel::build(root, source), pragma_map: PragmaTracker::build(root) }
80 }
81
82 pub fn semantic_model(&self) -> &SemanticModel {
84 &self.model
85 }
86
87 pub fn resolved_symbol_at(&self, position: usize) -> Option<ResolvedSymbol> {
89 self.model.definition_at(position).map(ResolvedSymbol::from)
90 }
91
92 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 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 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 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 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 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 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 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 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 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 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}