use std::ops::Range;
use crate::SourceLocation;
use crate::ast::Node;
use crate::pragma_tracker::{PragmaState, PragmaTracker};
use crate::symbol::{Symbol, SymbolKind};
use crate::workspace_index::WorkspaceIndex;
use super::SemanticModel;
#[derive(Debug)]
pub struct SemanticQueryFacade {
model: SemanticModel,
pragma_map: Vec<(Range<usize>, PragmaState)>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ResolvedSymbol {
pub name: String,
pub qualified_name: String,
pub kind: SymbolKind,
pub location: SourceLocation,
pub declaration: Option<String>,
pub documentation: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct DefinitionLocation {
pub uri: Option<String>,
pub location: SourceLocation,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct VisibleImport {
pub module_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ParentChain {
pub class_name: String,
pub ancestors: Vec<String>,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EffectivePragmaState {
pub offset: usize,
pub state: PragmaState,
}
impl SemanticQueryFacade {
pub fn build(root: &Node, source: &str) -> Self {
Self { model: SemanticModel::build(root, source), pragma_map: PragmaTracker::build(root) }
}
pub fn semantic_model(&self) -> &SemanticModel {
&self.model
}
pub fn resolved_symbol_at(&self, position: usize) -> Option<ResolvedSymbol> {
self.model.definition_at(position).map(ResolvedSymbol::from)
}
pub fn definition_location_at(
&self,
position: usize,
current_uri: Option<&str>,
) -> Option<DefinitionLocation> {
self.model.definition_at(position).map(|symbol| DefinitionLocation {
uri: current_uri.map(std::string::ToString::to_string),
location: symbol.location,
})
}
pub fn visible_imports(&self, workspace: &WorkspaceIndex, uri: &str) -> Vec<VisibleImport> {
let mut imports: Vec<_> = workspace
.file_dependencies(uri)
.into_iter()
.map(|module_name| VisibleImport { module_name })
.collect();
imports.sort_by(|left, right| left.module_name.cmp(&right.module_name));
imports
}
pub fn parent_chain(&self, class_name: &str) -> Option<ParentChain> {
self.model
.parent_chain(class_name)
.map(|ancestors| ParentChain { class_name: class_name.to_string(), ancestors })
}
pub fn inherited_origin(
&self,
class_name: &str,
method_name: &str,
current_uri: Option<&str>,
) -> Option<DefinitionLocation> {
self.model.resolve_inherited_method_location(class_name, method_name).map(|location| {
DefinitionLocation { uri: current_uri.map(std::string::ToString::to_string), location }
})
}
pub fn effective_pragma_state(&self, offset: usize) -> EffectivePragmaState {
EffectivePragmaState {
offset,
state: PragmaTracker::state_for_offset(&self.pragma_map, offset),
}
}
}
impl From<&Symbol> for ResolvedSymbol {
fn from(value: &Symbol) -> Self {
Self {
name: value.name.clone(),
qualified_name: value.qualified_name.clone(),
kind: value.kind,
location: value.location,
declaration: value.declaration.clone(),
documentation: value.documentation.clone(),
}
}
}
#[cfg(test)]
mod tests {
use perl_tdd_support::{must, must_some};
use super::*;
use crate::parser::Parser;
use crate::workspace_index;
#[test]
fn query_facade_resolves_symbol_and_pragmas() {
let code = "use strict;\nmy $value = 1;\n$value;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
let usage_offset = must_some(code.rfind("$value"));
let symbol = must_some(facade.resolved_symbol_at(usage_offset));
assert_eq!(symbol.name, "value");
let pragma_state = facade.effective_pragma_state(usage_offset);
assert!(pragma_state.state.strict_vars);
}
#[test]
fn query_facade_reads_workspace_imports_and_parent_chain() {
let code = "package Child;\nuse parent 'Base';\n1;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
let index = workspace_index::WorkspaceIndex::new();
must(index.index_file_str("file:///test/child.pm", code));
let imports = facade.visible_imports(&index, "file:///test/child.pm");
assert!(imports.iter().any(|import| import.module_name == "Base"));
let chain = must_some(facade.parent_chain("Child"));
assert_eq!(chain.ancestors, vec!["Base"]);
}
#[test]
fn query_facade_resolved_symbol_carries_declaration_and_docs() {
let code = "my $x = 1;\n$x;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
let usage_offset = must_some(code.rfind("$x"));
let symbol = must_some(facade.resolved_symbol_at(usage_offset));
assert_eq!(
symbol.declaration.as_deref(),
Some("my"),
"declaration field must be propagated from Symbol, not silently dropped"
);
}
#[test]
fn query_facade_resolved_symbol_at_past_end_returns_none() {
let code = "my $x = 1;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
assert!(
facade.resolved_symbol_at(usize::MAX).is_none(),
"out-of-range offset must return None, not panic"
);
}
#[test]
fn query_facade_parent_chain_unknown_class_returns_none() {
let code = "package Foo; sub bar {} 1;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
assert!(
facade.parent_chain("NonExistentClass").is_none(),
"unknown class must return None from parent_chain"
);
}
#[test]
fn query_facade_effective_pragma_state_no_pragmas() {
let code = "my $x = 1;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
let state = facade.effective_pragma_state(0);
assert!(!state.state.strict_vars, "strict_vars must be false when no use strict");
assert!(!state.state.strict_subs, "strict_subs must be false when no use strict");
assert!(!state.state.strict_refs, "strict_refs must be false when no use strict");
assert!(!state.state.warnings, "warnings must be false when no use warnings");
}
#[test]
fn query_facade_visible_imports_empty_file_returns_empty() {
let code = "my $x = 1;\n";
let mut parser = Parser::new(code);
let ast = must(parser.parse());
let facade = SemanticQueryFacade::build(&ast, code);
let index = workspace_index::WorkspaceIndex::new();
must(index.index_file_str("file:///test/empty.pm", code));
let imports = facade.visible_imports(&index, "file:///test/empty.pm");
assert!(imports.is_empty(), "file with no use statements must produce no visible imports");
}
}