use crate::parsing::MethodCall;
use crate::parsing::paths::{strip_extension, strip_source_root};
use crate::parsing::resolution::{
GenericInheritanceResolver, GenericResolutionContext, ImportBinding, ImportOrigin,
InheritanceResolver, PipelineSymbolCache, ResolutionScope, ScopeLevel,
};
use crate::relationship::RelationKind;
use crate::storage::DocumentIndex;
use crate::{FileId, Symbol, SymbolId, SymbolKind, Visibility};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use tree_sitter::Language;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationRole {
From,
To,
}
pub trait LanguageBehavior: Send + Sync {
fn language_id(&self) -> crate::parsing::registry::LanguageId;
fn format_module_path(&self, base_path: &str, symbol_name: &str) -> String;
fn parse_visibility(&self, signature: &str) -> Visibility;
fn module_separator(&self) -> &'static str;
fn source_roots(&self) -> &'static [&'static str] {
&["src"]
}
fn format_path_as_module(&self, components: &[&str]) -> Option<String>;
fn supports_traits(&self) -> bool {
false
}
fn supports_inherent_methods(&self) -> bool {
false
}
fn get_language(&self) -> Language;
fn validate_node_kind(&self, node_kind: &str) -> bool {
self.get_language().id_for_node_kind(node_kind, true) != 0
}
fn get_abi_version(&self) -> usize {
self.get_language().abi_version()
}
fn normalize_caller_name(&self, name: &str, _file_id: crate::FileId) -> String {
name.to_string()
}
fn configure_symbol(&self, symbol: &mut Symbol, module_path: Option<&str>) {
if let Some(path) = module_path {
let full_path = self.format_module_path(path, &symbol.name);
symbol.module_path = Some(full_path.into());
}
if let Some(ref sig) = symbol.signature {
symbol.visibility = self.parse_visibility(sig);
}
}
fn module_path_from_file(
&self,
file_path: &Path,
workspace_root: &Path,
extensions: &[&str],
) -> Option<String> {
let relative_path = file_path.strip_prefix(workspace_root).ok()?;
let path_without_src = strip_source_root(relative_path, self.source_roots());
let path_str = path_without_src.to_str()?;
let path_without_ext = strip_extension(path_str, extensions);
let components: Vec<&str> = path_without_ext
.split(std::path::MAIN_SEPARATOR)
.filter(|s| !s.is_empty())
.collect();
self.format_path_as_module(&components)
}
fn create_resolution_context(&self, file_id: FileId) -> Box<dyn ResolutionScope> {
Box::new(GenericResolutionContext::new(file_id))
}
fn create_inheritance_resolver(&self) -> Box<dyn InheritanceResolver> {
Box::new(GenericInheritanceResolver::new())
}
fn add_import(&self, _import: crate::parsing::Import) {
}
fn register_file(&self, _path: PathBuf, _file_id: FileId, _module_path: String) {
}
fn add_trait_impl(&self, _type_name: String, _trait_name: String, _file_id: FileId) {
}
fn add_inherent_methods(&self, _type_name: String, _methods: Vec<String>) {
}
fn add_trait_methods(&self, _trait_name: String, _methods: Vec<String>) {
}
fn resolve_method_trait(&self, _type_name: &str, _method: &str) -> Option<&str> {
None
}
fn format_method_call(&self, receiver: &str, method: &str) -> String {
format!("{}{}{}", receiver, self.module_separator(), method)
}
fn resolve_instance_method(
&self,
type_name: &str,
method_name: &str,
context: &dyn ResolutionScope,
document_index: &DocumentIndex,
) -> Option<SymbolId> {
let type_id = match context.resolve(type_name) {
Some(id) => {
tracing::debug!("[resolve_instance_method] resolved type '{type_name}' to {id:?}");
id
}
None => {
tracing::debug!("[resolve_instance_method] failed to resolve type '{type_name}'");
return None;
}
};
let defined_symbols =
match document_index.get_relationships_from(type_id, RelationKind::Defines) {
Ok(rels) => {
tracing::debug!(
"[resolve_instance_method] found {} Defines relationships from {type_id:?}",
rels.len()
);
rels
}
Err(e) => {
tracing::debug!(
"[resolve_instance_method] error getting Defines from {type_id:?}: {e}"
);
return None;
}
};
for (_, to_id, _) in defined_symbols {
if let Ok(Some(symbol)) = document_index.find_symbol_by_id(to_id) {
tracing::debug!(
"[resolve_instance_method] checking defined symbol: '{}' vs '{method_name}'",
symbol.name.as_ref()
);
if symbol.name.as_ref() == method_name {
tracing::debug!(
"[resolve_instance_method] found method '{method_name}' at {to_id:?}"
);
return Some(to_id);
}
}
}
tracing::debug!(
"[resolve_instance_method] method '{method_name}' not found in type '{type_name}'"
);
None
}
fn resolve_method_call(
&self,
method_call: &MethodCall,
receiver_types: &HashMap<String, String>,
context: &dyn ResolutionScope,
document_index: &DocumentIndex,
) -> Option<SymbolId> {
let method_name = &method_call.method_name;
match (&method_call.receiver, method_call.is_static) {
(Some(type_name), true) => {
let qualified = format!("{type_name}{}{method_name}", self.module_separator());
tracing::debug!("[resolve_method_call] static call: {qualified}");
context.resolve(&qualified)
}
(Some(receiver), false) if receiver != "self" => {
let type_name = match receiver_types.get(receiver) {
Some(t) => t,
None => {
tracing::debug!(
"[resolve_method_call] no type found for receiver '{receiver}'"
);
return None;
}
};
tracing::debug!(
"[resolve_method_call] instance call: {receiver}.{method_name} (type: {type_name})"
);
self.resolve_instance_method(type_name, method_name, context, document_index)
}
(Some(receiver), false) if receiver == "self" => {
let self_method = format!("self.{method_name}");
tracing::debug!("[resolve_method_call] self call: {self_method}");
context.resolve(&self_method).or_else(|| {
context.resolve(method_name)
})
}
(None, _) => {
tracing::debug!("[resolve_method_call] plain function call: {method_name}");
context.resolve(method_name)
}
_ => {
tracing::debug!("[resolve_method_call] unhandled case: {:?}", method_call);
None
}
}
}
fn inheritance_relation_name(&self) -> &'static str {
if self.supports_traits() {
"implements"
} else {
"extends"
}
}
fn map_relationship(&self, language_specific: &str) -> RelationKind {
match language_specific {
"extends" => RelationKind::Extends,
"implements" => RelationKind::Implements,
"inherits" => RelationKind::Extends,
"uses" => RelationKind::Uses,
"calls" => RelationKind::Calls,
"defines" => RelationKind::Defines,
_ => RelationKind::References,
}
}
fn build_resolution_context_with_pipeline_cache(
&self,
file_id: FileId,
imports: &[crate::parsing::Import],
cache: &dyn PipelineSymbolCache,
extensions: &[&str],
) -> (Box<dyn ResolutionScope>, Vec<crate::parsing::Import>) {
let _ = extensions;
let mut context = self.create_resolution_context(file_id);
let importing_module = self.get_module_path_for_file(file_id);
let separator = self.module_separator();
let enhanced_imports: Vec<crate::parsing::Import> = imports
.iter()
.map(|import| {
let enhanced_path = if import.path.starts_with("./") {
import.path.trim_start_matches("./").replace('/', separator)
} else if import.path.starts_with("../") {
import.path.replace('/', separator)
} else {
import.path.replace('/', separator)
};
crate::parsing::Import {
path: enhanced_path,
file_id: import.file_id,
alias: import.alias.clone(),
is_glob: import.is_glob,
is_type_only: import.is_type_only,
}
})
.collect();
context.populate_imports(&enhanced_imports);
let caller = crate::parsing::CallerContext::from_file(file_id, self.language_id());
for import in &enhanced_imports {
let separator = self.module_separator();
let symbol_name = import.path.split(separator).last().unwrap_or(&import.path);
let result = cache.resolve(
symbol_name,
&caller,
None, imports,
);
let resolved_symbol = match result {
crate::parsing::ResolveResult::Found(id) => Some(id),
crate::parsing::ResolveResult::Ambiguous(ids) => ids.first().copied(),
crate::parsing::ResolveResult::NotFound => None,
};
let origin = if let Some(id) = resolved_symbol {
if let Some(sym) = cache.get(id) {
if sym.language_id.as_ref() == Some(&self.language_id()) {
if let Some(ref module_path) = sym.module_path {
if self.import_matches_symbol(
&import.path,
module_path.as_ref(),
importing_module.as_deref(),
) {
ImportOrigin::Internal
} else {
ImportOrigin::External
}
} else {
ImportOrigin::Internal }
} else {
ImportOrigin::External
}
} else {
ImportOrigin::Unknown
}
} else {
ImportOrigin::External };
let mut binding_names: Vec<String> = Vec::new();
if let Some(alias) = &import.alias {
binding_names.push(alias.clone());
}
if !binding_names.contains(&symbol_name.to_string()) {
binding_names.push(symbol_name.to_string());
}
let import_clone = import.clone();
for name in &binding_names {
context.register_import_binding(ImportBinding {
import: import_clone.clone(),
exposed_name: name.clone(),
origin,
resolved_symbol,
});
}
if let (ImportOrigin::Internal, Some(symbol_id)) = (origin, resolved_symbol) {
let primary_name = binding_names
.first()
.cloned()
.unwrap_or_else(|| symbol_name.to_string());
context.add_symbol(primary_name, symbol_id, ScopeLevel::Module);
}
}
for symbol_id in cache.symbols_in_file(file_id) {
if let Some(symbol) = cache.get(symbol_id) {
if self.is_resolvable_symbol(&symbol) {
context.add_symbol(symbol.name.to_string(), symbol.id, ScopeLevel::Module);
if let Some(module_path) = &symbol.module_path {
context.add_symbol(module_path.to_string(), symbol.id, ScopeLevel::Module);
}
}
}
}
self.initialize_resolution_context(context.as_mut(), file_id);
(context, enhanced_imports)
}
fn is_resolvable_symbol(&self, symbol: &Symbol) -> bool {
use crate::SymbolKind;
if let Some(ref scope_context) = symbol.scope_context {
use crate::symbol::ScopeContext;
match scope_context {
ScopeContext::Module | ScopeContext::Global | ScopeContext::Package => true,
ScopeContext::Local { .. } | ScopeContext::Parameter => false,
ScopeContext::ClassMember { .. } => {
matches!(symbol.visibility, Visibility::Public)
}
}
} else {
matches!(
symbol.kind,
SymbolKind::Function
| SymbolKind::Method
| SymbolKind::Struct
| SymbolKind::Trait
| SymbolKind::Interface
| SymbolKind::Class
| SymbolKind::TypeAlias
| SymbolKind::Enum
| SymbolKind::Constant
)
}
}
fn is_symbol_visible_from_file(&self, symbol: &Symbol, from_file: FileId) -> bool {
if symbol.file_id == from_file {
return true;
}
matches!(symbol.visibility, Visibility::Public)
}
fn get_imports_for_file(&self, _file_id: FileId) -> Vec<crate::parsing::Import> {
Vec::new()
}
fn import_matches_symbol(
&self,
import_path: &str,
symbol_module_path: &str,
_importing_module: Option<&str>,
) -> bool {
import_path == symbol_module_path
}
fn get_module_path_for_file(&self, _file_id: FileId) -> Option<String> {
None
}
fn get_file_path(&self, _file_id: FileId) -> Option<PathBuf> {
None
}
fn load_project_rules_for_file(
&self,
file_id: FileId,
) -> Option<crate::project_resolver::persist::ResolutionRules> {
use crate::project_resolver::persist::ResolutionPersistence;
use std::cell::RefCell;
use std::time::{Duration, Instant};
thread_local! {
static RULES_CACHE: RefCell<Option<(Instant, String, crate::project_resolver::persist::ResolutionIndex)>> = const { RefCell::new(None) };
}
let language_id = self.language_id().as_str().to_string();
RULES_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
let needs_reload = if let Some((timestamp, cached_lang, _)) = &*cache {
timestamp.elapsed() >= Duration::from_secs(1) || cached_lang != &language_id
} else {
true
};
if needs_reload {
let persistence =
ResolutionPersistence::new(Path::new(crate::init::local_dir_name()));
if let Ok(index) = persistence.load(&language_id) {
*cache = Some((Instant::now(), language_id.clone(), index));
} else {
return None;
}
}
if let Some((_, _, ref index)) = *cache {
if let Some(file_path) = self.get_file_path(file_id) {
if let Some(config_path) = index.get_config_for_file(&file_path) {
return index.rules.get(config_path).cloned();
}
}
index.rules.values().next().cloned()
} else {
None
}
})
}
fn register_expression_types(&self, _file_id: FileId, _entries: &[(String, String)]) {}
fn initialize_resolution_context(&self, _context: &mut dyn ResolutionScope, _file_id: FileId) {}
fn is_compatible_relationship(
&self,
from_kind: crate::SymbolKind,
to_kind: crate::SymbolKind,
rel_kind: crate::RelationKind,
file_id: FileId,
) -> bool {
let context = self.create_resolution_context(file_id);
context.is_compatible_relationship(from_kind, to_kind, rel_kind)
}
fn disambiguate_symbol(
&self,
_name: &str,
candidates: &[(SymbolId, SymbolKind)],
_rel_kind: RelationKind,
_role: RelationRole,
) -> Option<SymbolId> {
candidates.first().map(|(id, _)| *id)
}
fn is_valid_relationship(
&self,
from_kind: SymbolKind,
to_kind: SymbolKind,
rel_kind: RelationKind,
) -> bool {
default_relationship_compatibility(from_kind, to_kind, rel_kind)
}
}
pub fn default_relationship_compatibility(
from_kind: SymbolKind,
to_kind: SymbolKind,
rel_kind: RelationKind,
) -> bool {
use RelationKind::*;
use SymbolKind::*;
match rel_kind {
Calls | CalledBy => {
let caller = matches!(
from_kind,
Function | Method | Macro | Module | Constant | Variable
);
let callee = matches!(
to_kind,
Function | Method | Macro | Class | Constant | Variable
);
match rel_kind {
Calls => caller && callee,
CalledBy => callee && caller,
_ => unreachable!(),
}
}
Implements | ImplementedBy => {
let implementor = matches!(from_kind, Struct | Enum | Class);
let interface = matches!(to_kind, Trait | Interface);
match rel_kind {
Implements => implementor && interface,
ImplementedBy => interface && implementor,
_ => unreachable!(),
}
}
Extends | ExtendedBy => {
let extendable = matches!(from_kind, Class | Interface | Trait | Struct | Enum);
let base = matches!(to_kind, Class | Interface | Trait | Struct | Enum);
extendable && base
}
Uses | UsedBy => {
true
}
Defines | DefinedIn => {
let container = matches!(
from_kind,
Class | Struct | Enum | Trait | Interface | Module
);
let member = matches!(to_kind, Function | Method | Field | Constant | Variable);
match rel_kind {
Defines => container && member,
DefinedIn => member && container,
_ => unreachable!(),
}
}
References | ReferencedBy => {
true
}
}
}
#[derive(Debug, Clone)]
pub struct LanguageMetadata {
pub abi_version: usize,
pub node_kind_count: usize,
pub field_count: usize,
}
impl LanguageMetadata {
pub fn from_language(language: Language) -> Self {
Self {
abi_version: language.abi_version(),
node_kind_count: language.node_kind_count(),
field_count: language.field_count(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{RelationKind, SymbolKind};
struct TestBehavior;
impl LanguageBehavior for TestBehavior {
fn language_id(&self) -> crate::parsing::registry::LanguageId {
crate::parsing::registry::LanguageId::new("test")
}
fn format_module_path(&self, base_path: &str, _symbol_name: &str) -> String {
base_path.to_string()
}
fn parse_visibility(&self, _signature: &str) -> crate::Visibility {
crate::Visibility::Public
}
fn module_separator(&self) -> &'static str {
"."
}
fn format_path_as_module(&self, components: &[&str]) -> Option<String> {
if components.is_empty() {
None
} else {
Some(components.join("."))
}
}
fn get_language(&self) -> tree_sitter::Language {
tree_sitter_rust::LANGUAGE.into()
}
}
#[test]
fn test_default_compatibility_function_calls_function() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Function,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_function_calls_method() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Method,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_method_calls_function() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Method,
SymbolKind::Function,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_function_calls_class() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Class,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_function_cannot_call_constant() {
let behavior = TestBehavior;
assert!(!behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Constant,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_function_cannot_call_variable() {
let behavior = TestBehavior;
assert!(!behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Variable,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_macro_can_be_called() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Macro,
RelationKind::Calls,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_class_extends_class() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Class,
SymbolKind::Class,
RelationKind::Extends,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_trait_extends_trait() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Trait,
SymbolKind::Trait,
RelationKind::Extends,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_class_implements_trait() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Class,
SymbolKind::Trait,
RelationKind::Implements,
FileId::new(1).unwrap()
));
}
#[test]
fn test_default_compatibility_uses_always_valid() {
let behavior = TestBehavior;
assert!(behavior.is_compatible_relationship(
SymbolKind::Function,
SymbolKind::Struct,
RelationKind::Uses,
FileId::new(1).unwrap()
));
assert!(behavior.is_compatible_relationship(
SymbolKind::Method,
SymbolKind::Enum,
RelationKind::Uses,
FileId::new(1).unwrap()
));
}
}