use crate::parsing::resolution::ImportBinding;
use crate::parsing::{InheritanceResolver, ResolutionScope, ScopeLevel, ScopeType};
use crate::{FileId, SymbolId};
use std::collections::HashMap;
pub struct JavaScriptResolutionContext {
#[allow(dead_code)]
file_id: FileId,
local_scope: HashMap<String, SymbolId>,
hoisted_scope: HashMap<String, SymbolId>,
module_symbols: HashMap<String, SymbolId>,
imported_symbols: HashMap<String, SymbolId>,
global_symbols: HashMap<String, SymbolId>,
scope_stack: Vec<ScopeType>,
imports: Vec<(String, Option<String>)>,
qualified_names: HashMap<String, SymbolId>,
namespace_aliases: HashMap<String, String>,
import_bindings: HashMap<String, ImportBinding>,
}
impl JavaScriptResolutionContext {
pub fn new(file_id: FileId) -> Self {
Self {
file_id,
local_scope: HashMap::new(),
hoisted_scope: HashMap::new(),
module_symbols: HashMap::new(),
imported_symbols: HashMap::new(),
global_symbols: HashMap::new(),
scope_stack: Vec::new(),
imports: Vec::new(),
qualified_names: HashMap::new(),
namespace_aliases: HashMap::new(),
import_bindings: HashMap::new(),
}
}
pub fn add_import(&mut self, path: String, alias: Option<String>) {
self.imports.push((path, alias));
}
pub fn add_namespace_alias(&mut self, alias: String, target_module: String) {
self.namespace_aliases.insert(alias, target_module);
}
pub fn add_qualified_name(&mut self, qualified: String, symbol_id: SymbolId) {
self.qualified_names.insert(qualified, symbol_id);
}
pub fn add_import_symbol(&mut self, name: String, symbol_id: SymbolId, _is_type_only: bool) {
self.imported_symbols.insert(name, symbol_id);
}
pub fn add_symbol_with_context(
&mut self,
name: String,
symbol_id: SymbolId,
scope_context: Option<&crate::symbol::ScopeContext>,
) {
use crate::symbol::ScopeContext;
match scope_context {
Some(ScopeContext::Local { hoisted: true, .. }) => {
self.hoisted_scope.insert(name, symbol_id);
}
Some(ScopeContext::Local { hoisted: false, .. }) => {
self.local_scope.insert(name, symbol_id);
}
Some(ScopeContext::ClassMember { .. }) => {
self.local_scope.insert(name, symbol_id);
}
Some(ScopeContext::Parameter) => {
self.local_scope.insert(name, symbol_id);
}
Some(ScopeContext::Module) => {
self.module_symbols.insert(name, symbol_id);
}
Some(ScopeContext::Package) => {
self.imported_symbols.insert(name, symbol_id);
}
Some(ScopeContext::Global) => {
self.global_symbols.insert(name, symbol_id);
}
None => {
self.local_scope.insert(name, symbol_id);
}
}
}
}
impl ResolutionScope for JavaScriptResolutionContext {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
fn add_symbol(&mut self, name: String, symbol_id: SymbolId, scope_level: ScopeLevel) {
match scope_level {
ScopeLevel::Local => {
self.local_scope.insert(name, symbol_id);
}
ScopeLevel::Module => {
self.module_symbols.insert(name, symbol_id);
}
ScopeLevel::Package => {
self.imported_symbols.insert(name, symbol_id);
}
ScopeLevel::Global => {
self.global_symbols.insert(name, symbol_id);
}
}
}
fn resolve(&self, name: &str) -> Option<SymbolId> {
tracing::debug!("[javascript] resolve looking up name='{name}'");
if let Some(&id) = self.local_scope.get(name) {
tracing::debug!("[javascript] found in local_scope: {id:?}");
return Some(id);
}
if let Some(&id) = self.hoisted_scope.get(name) {
tracing::debug!("[javascript] found in hoisted_scope: {id:?}");
return Some(id);
}
if let Some(&id) = self.imported_symbols.get(name) {
tracing::debug!("[javascript] found in imported_symbols: {id:?}");
return Some(id);
}
if let Some(&id) = self.module_symbols.get(name) {
return Some(id);
}
if let Some(&id) = self.global_symbols.get(name) {
return Some(id);
}
if name.contains('.') {
if let Some(&id) = self.imported_symbols.get(name) {
return Some(id);
}
if let Some(&id) = self.module_symbols.get(name) {
return Some(id);
}
if let Some(&id) = self.global_symbols.get(name) {
return Some(id);
}
let parts: Vec<&str> = name.split('.').collect();
if parts.len() == 2 {
let class_or_module = parts[0];
let method_or_prop = parts[1];
if let Some(_module) = self.namespace_aliases.get(class_or_module) {
if let Some(&id) = self
.local_scope
.get(method_or_prop)
.or_else(|| self.hoisted_scope.get(method_or_prop))
.or_else(|| self.imported_symbols.get(method_or_prop))
.or_else(|| self.module_symbols.get(method_or_prop))
.or_else(|| self.global_symbols.get(method_or_prop))
{
return Some(id);
}
}
if self.resolve(class_or_module).is_some() {
return self.resolve(method_or_prop);
}
return None;
}
}
tracing::debug!("[javascript] resolve not found: '{name}'");
None
}
fn clear_local_scope(&mut self) {
self.local_scope.clear();
}
fn enter_scope(&mut self, scope_type: ScopeType) {
self.scope_stack.push(scope_type);
}
fn exit_scope(&mut self) {
self.scope_stack.pop();
if matches!(
self.scope_stack.last(),
None | Some(ScopeType::Module | ScopeType::Global)
) {
self.clear_local_scope();
self.hoisted_scope.clear();
}
}
fn symbols_in_scope(&self) -> Vec<(String, SymbolId, ScopeLevel)> {
let mut symbols = Vec::new();
for (name, &id) in &self.local_scope {
symbols.push((name.clone(), id, ScopeLevel::Local));
}
for (name, &id) in &self.hoisted_scope {
symbols.push((name.clone(), id, ScopeLevel::Local));
}
for (name, &id) in &self.imported_symbols {
symbols.push((name.clone(), id, ScopeLevel::Package));
}
for (name, &id) in &self.module_symbols {
symbols.push((name.clone(), id, ScopeLevel::Module));
}
for (name, &id) in &self.global_symbols {
symbols.push((name.clone(), id, ScopeLevel::Global));
}
symbols
}
fn resolve_relationship(
&self,
_from_name: &str,
to_name: &str,
kind: crate::RelationKind,
_from_file: FileId,
) -> Option<SymbolId> {
use crate::RelationKind;
match kind {
RelationKind::Extends => {
self.resolve(to_name)
}
RelationKind::Calls => {
if to_name.contains('.') {
if let Some(id) = self.resolve(to_name) {
return Some(id);
}
if let Some(last_part) = to_name.rsplit('.').next() {
return self.resolve(last_part);
}
}
self.resolve(to_name)
}
RelationKind::Uses => {
self.resolve(to_name)
}
_ => {
self.resolve(to_name)
}
}
}
fn is_compatible_relationship(
&self,
from_kind: crate::SymbolKind,
to_kind: crate::SymbolKind,
rel_kind: crate::RelationKind,
) -> bool {
use crate::RelationKind::*;
use crate::SymbolKind::*;
match rel_kind {
Calls => {
let caller_can_call = matches!(
from_kind,
Function | Method | Macro | Module | Constant | Variable
);
let callee_can_be_called = matches!(
to_kind,
Function | Method | Macro | Class | Constant | Variable
);
caller_can_call && callee_can_be_called
}
CalledBy => {
let caller_can_call = matches!(
to_kind,
Function | Method | Macro | Module | Constant | Variable
);
let callee_can_be_called = matches!(
from_kind,
Function | Method | Macro | Class | Constant | Variable
);
callee_can_be_called && caller_can_call
}
Extends => {
matches!(from_kind, Class) && matches!(to_kind, Class)
}
ExtendedBy => matches!(from_kind, Class) && matches!(to_kind, Class),
Uses => {
let can_use = matches!(
from_kind,
Function | Method | Struct | Class | Trait | Interface | Module | Enum
);
let can_be_used = matches!(
to_kind,
Struct
| Enum
| Class
| Trait
| Interface
| TypeAlias
| Constant
| Variable
| Function
| Method
);
can_use && can_be_used
}
UsedBy => {
let can_be_used = matches!(
from_kind,
Struct
| Enum
| Class
| Trait
| Interface
| TypeAlias
| Constant
| Variable
| Function
| Method
);
let can_use = matches!(
to_kind,
Function | Method | Struct | Class | Trait | Interface | Module | Enum
);
can_be_used && can_use
}
Defines => {
let container = matches!(
from_kind,
Trait | Interface | Module | Struct | Enum | Class
);
let member = matches!(to_kind, Method | Function | Constant | Field | Variable);
container && member
}
DefinedIn => {
let member = matches!(from_kind, Method | Function | Constant | Field | Variable);
let container =
matches!(to_kind, Trait | Interface | Module | Struct | Enum | Class);
member && container
}
References => true,
ReferencedBy => true,
Implements | ImplementedBy => false,
}
}
fn populate_imports(&mut self, imports: &[crate::parsing::Import]) {
for import in imports {
self.add_import(import.path.clone(), import.alias.clone());
}
}
fn register_import_binding(&mut self, binding: ImportBinding) {
self.import_bindings
.insert(binding.exposed_name.clone(), binding);
}
fn import_binding(&self, name: &str) -> Option<ImportBinding> {
self.import_bindings.get(name).cloned()
}
}
pub struct JavaScriptInheritanceResolver {
class_extends: HashMap<String, String>,
type_methods: HashMap<String, Vec<String>>,
}
impl Default for JavaScriptInheritanceResolver {
fn default() -> Self {
Self::new()
}
}
impl JavaScriptInheritanceResolver {
pub fn new() -> Self {
Self {
class_extends: HashMap::new(),
type_methods: HashMap::new(),
}
}
}
impl InheritanceResolver for JavaScriptInheritanceResolver {
fn add_inheritance(&mut self, child: String, parent: String, kind: &str) {
match kind {
"extends" => {
self.class_extends.insert(child, parent);
}
_ => {
}
}
}
fn resolve_method(&self, type_name: &str, method_name: &str) -> Option<String> {
if let Some(methods) = self.type_methods.get(type_name) {
if methods.iter().any(|m| m == method_name) {
return Some(type_name.to_string());
}
}
if let Some(parent) = self.class_extends.get(type_name) {
if let Some(resolved) = self.resolve_method(parent, method_name) {
return Some(resolved);
}
}
None
}
fn get_inheritance_chain(&self, type_name: &str) -> Vec<String> {
let mut chain = vec![type_name.to_string()];
let mut visited = std::collections::HashSet::new();
visited.insert(type_name.to_string());
if let Some(parent) = self.class_extends.get(type_name) {
if visited.insert(parent.clone()) {
chain.push(parent.clone());
for ancestor in self.get_inheritance_chain(parent) {
if visited.insert(ancestor.clone()) {
chain.push(ancestor);
}
}
}
}
chain
}
fn is_subtype(&self, child: &str, parent: &str) -> bool {
if let Some(direct_parent) = self.class_extends.get(child) {
if direct_parent == parent {
return true;
}
if self.is_subtype(direct_parent, parent) {
return true;
}
}
false
}
fn add_type_methods(&mut self, type_name: String, methods: Vec<String>) {
self.type_methods
.entry(type_name)
.or_default()
.extend(methods);
}
fn get_all_methods(&self, type_name: &str) -> Vec<String> {
let mut all_methods = Vec::new();
let mut visited = std::collections::HashSet::new();
fn collect_methods(
resolver: &JavaScriptInheritanceResolver,
type_name: &str,
all_methods: &mut Vec<String>,
visited: &mut std::collections::HashSet<String>,
) {
if !visited.insert(type_name.to_string()) {
return;
}
if let Some(methods) = resolver.type_methods.get(type_name) {
for method in methods {
if !all_methods.contains(method) {
all_methods.push(method.clone());
}
}
}
if let Some(parent) = resolver.class_extends.get(type_name) {
collect_methods(resolver, parent, all_methods, visited);
}
}
collect_methods(self, type_name, &mut all_methods, &mut visited);
all_methods
}
}
pub struct JavaScriptProjectEnhancer {
resolver: Option<crate::parsing::javascript::jsconfig::PathAliasResolver>,
}
impl JavaScriptProjectEnhancer {
pub fn new(rules: crate::project_resolver::persist::ResolutionRules) -> Self {
let resolver = if !rules.paths.is_empty() || rules.base_url.is_some() {
let config = crate::parsing::javascript::jsconfig::JsConfig {
extends: None,
compilerOptions: crate::parsing::javascript::jsconfig::CompilerOptions {
baseUrl: rules.base_url.clone(),
paths: rules.paths.clone(),
},
};
crate::parsing::javascript::jsconfig::PathAliasResolver::from_jsconfig(&config).ok()
} else {
None
};
Self { resolver }
}
}
impl crate::parsing::resolution::ProjectResolutionEnhancer for JavaScriptProjectEnhancer {
fn enhance_import_path(&self, import_path: &str, _from_file: FileId) -> Option<String> {
if import_path.starts_with("./") || import_path.starts_with("../") {
return None;
}
if let Some(ref resolver) = self.resolver {
let candidates = resolver.resolve_import(import_path);
candidates.into_iter().next()
} else {
None
}
}
fn get_import_candidates(&self, import_path: &str, _from_file: FileId) -> Vec<String> {
if import_path.starts_with("./") || import_path.starts_with("../") {
return vec![import_path.to_string()];
}
if let Some(ref resolver) = self.resolver {
let candidates = resolver.resolve_import(import_path);
if !candidates.is_empty() {
candidates
} else {
vec![import_path.to_string()]
}
} else {
vec![import_path.to_string()]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_javascript_function_can_call_constant() {
let context = JavaScriptResolutionContext::new(FileId::new(1).unwrap());
assert!(context.is_compatible_relationship(
crate::SymbolKind::Function,
crate::SymbolKind::Constant,
crate::RelationKind::Calls
));
}
#[test]
fn test_javascript_hoisting() {
let mut context = JavaScriptResolutionContext::new(FileId::new(1).unwrap());
context.add_symbol(
"function myFunc".to_string(),
SymbolId::new(1).unwrap(),
ScopeLevel::Local,
);
assert_eq!(
context.resolve("function myFunc"),
Some(SymbolId::new(1).unwrap())
);
}
#[test]
fn test_javascript_no_implements() {
let context = JavaScriptResolutionContext::new(FileId::new(1).unwrap());
assert!(!context.is_compatible_relationship(
crate::SymbolKind::Class,
crate::SymbolKind::Interface,
crate::RelationKind::Implements
));
}
#[test]
fn test_class_inheritance() {
let mut resolver = JavaScriptInheritanceResolver::new();
resolver.add_inheritance(
"ChildClass".to_string(),
"ParentClass".to_string(),
"extends",
);
assert!(resolver.is_subtype("ChildClass", "ParentClass"));
let chain = resolver.get_inheritance_chain("ChildClass");
assert!(chain.contains(&"ParentClass".to_string()));
}
}