use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::types::{ColumnNumber, Count, Language, LineNumber, Node, NodeKind, ReferenceKind};
use crate::utils::line_number_at_byte;
use super::go_module::GoModule;
use super::path_aliases::AliasMap;
use super::workspace_packages::WorkspacePackages;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UnresolvedRef {
pub from_node_id: String,
pub reference_name: String,
pub reference_kind: ReferenceKind,
pub line: LineNumber,
pub column: ColumnNumber,
pub file_path: String,
pub language: Language,
#[serde(skip_serializing_if = "Option::is_none")]
pub candidates: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResolvedBy {
#[serde(rename = "exact-match")]
ExactMatch,
#[serde(rename = "import")]
Import,
#[serde(rename = "qualified-name")]
QualifiedName,
#[serde(rename = "framework")]
Framework,
#[serde(rename = "fuzzy")]
Fuzzy,
#[serde(rename = "instance-method")]
InstanceMethod,
#[serde(rename = "file-path")]
FilePath,
#[serde(rename = "function-ref")]
FunctionRef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolvedRef {
pub original: UnresolvedRef,
pub target_node_id: String,
pub confidence: f64,
pub resolved_by: ResolvedBy,
}
impl ResolvedRef {
pub fn framework(
original: &UnresolvedRef,
target_node_id: impl Into<String>,
confidence: f64,
) -> Self {
Self {
original: original.clone(),
target_node_id: target_node_id.into(),
confidence,
resolved_by: ResolvedBy::Framework,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolutionStats {
pub total: Count,
pub resolved: Count,
pub unresolved: Count,
pub by_method: HashMap<String, Count>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolutionResult {
pub resolved: Vec<ResolvedRef>,
pub unresolved: Vec<UnresolvedRef>,
pub stats: ResolutionStats,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum ReExport {
Named {
#[serde(rename = "exportedName")]
exported_name: String,
#[serde(rename = "originalName")]
original_name: String,
source: String,
},
Wildcard {
source: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ImportMapping {
pub local_name: String,
pub exported_name: String,
pub source: String,
pub is_default: bool,
pub is_namespace: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved_path: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FrameworkExtractionResult {
pub nodes: Vec<Node>,
pub references: Vec<UnresolvedRef>,
}
pub trait ResolutionContext {
fn get_nodes_in_file(&mut self, file_path: &str) -> Vec<Node>;
fn get_nodes_by_name(&mut self, name: &str) -> Vec<Node>;
fn get_nodes_by_qualified_name(&mut self, qualified_name: &str) -> Vec<Node>;
fn get_nodes_by_kind(&mut self, kind: NodeKind) -> Vec<Node>;
fn file_exists(&mut self, file_path: &str) -> bool;
fn read_file(&mut self, file_path: &str) -> Option<String>;
fn get_project_root(&self) -> String;
fn get_all_files(&mut self) -> Vec<String>;
fn get_nodes_by_lower_name(&mut self, lower_name: &str) -> Vec<Node>;
fn get_import_mappings(&mut self, file_path: &str, language: Language) -> Vec<ImportMapping>;
fn get_supertypes(&mut self, _type_name: &str, _language: Language) -> Vec<String> {
Vec::new()
}
fn get_node_by_id(&mut self, _id: &str) -> Option<Node> {
None
}
fn get_project_aliases(&mut self) -> Option<AliasMap> {
None
}
fn get_go_module(&mut self) -> Option<GoModule> {
None
}
fn get_workspace_packages(&mut self) -> Option<WorkspacePackages> {
None
}
fn get_re_exports(&mut self, _file_path: &str, _language: Language) -> Vec<ReExport> {
Vec::new()
}
fn list_directories(&mut self, _relative_path: &str) -> Vec<String> {
Vec::new()
}
fn get_cpp_include_dirs(&mut self) -> Vec<String> {
Vec::new()
}
}
pub trait FrameworkResolver: Send + Sync {
fn name(&self) -> &str;
fn languages(&self) -> Option<&[Language]> {
None
}
fn detect(&self, context: &mut dyn ResolutionContext) -> bool;
fn resolve(
&self,
reference: &UnresolvedRef,
context: &mut dyn ResolutionContext,
) -> Option<ResolvedRef>;
fn claims_reference(&self, _name: &str) -> bool {
false
}
fn extract(&self, _file_path: &str, _content: &str) -> FrameworkExtractionResult {
FrameworkExtractionResult::default()
}
fn post_extract(&self, _context: &mut dyn ResolutionContext) -> Vec<Node> {
Vec::new()
}
}
pub fn language_name(language: Language) -> &'static str {
match language {
Language::TypeScript => "typescript",
Language::JavaScript => "javascript",
Language::Tsx => "tsx",
Language::Jsx => "jsx",
Language::Python => "python",
Language::Go => "go",
Language::Rust => "rust",
Language::Java => "java",
Language::C => "c",
Language::Cpp => "cpp",
Language::CSharp => "csharp",
Language::Razor => "razor",
Language::Php => "php",
Language::Ruby => "ruby",
Language::Swift => "swift",
Language::Kotlin => "kotlin",
Language::Dart => "dart",
Language::Svelte => "svelte",
Language::Vue => "vue",
Language::Astro => "astro",
Language::Liquid => "liquid",
Language::Pascal => "pascal",
Language::Scala => "scala",
Language::Lua => "lua",
Language::Luau => "luau",
Language::ObjC => "objc",
Language::R => "r",
Language::Yaml => "yaml",
Language::Twig => "twig",
Language::Xml => "xml",
Language::Properties => "properties",
Language::Unknown => "unknown",
}
}
pub fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis() as i64)
.unwrap_or(0)
}
pub fn line_for_byte(content: &str, byte_index: usize) -> LineNumber {
line_number_at_byte(content, byte_index) as LineNumber
}
pub fn language_for_path(file_path: &str) -> Language {
let lower = file_path.to_ascii_lowercase();
if lower.ends_with(".tsx") {
Language::Tsx
} else if lower.ends_with(".ts") {
Language::TypeScript
} else if lower.ends_with(".jsx") {
Language::Jsx
} else if lower.ends_with(".js") || lower.ends_with(".mjs") || lower.ends_with(".cjs") {
Language::JavaScript
} else if lower.ends_with(".py") {
Language::Python
} else if lower.ends_with(".go") {
Language::Go
} else if lower.ends_with(".rs") {
Language::Rust
} else if lower.ends_with(".java") {
Language::Java
} else if lower.ends_with(".kt") {
Language::Kotlin
} else if lower.ends_with(".cshtml") || lower.ends_with(".razor") {
Language::Razor
} else if lower.ends_with(".cs") {
Language::CSharp
} else if lower.ends_with(".php") {
Language::Php
} else if lower.ends_with(".rb") {
Language::Ruby
} else if lower.ends_with(".swift") {
Language::Swift
} else if lower.ends_with(".dart") {
Language::Dart
} else if lower.ends_with(".svelte") {
Language::Svelte
} else if lower.ends_with(".vue") {
Language::Vue
} else if lower.ends_with(".astro") {
Language::Astro
} else if lower.ends_with(".yaml") || lower.ends_with(".yml") {
Language::Yaml
} else if lower.ends_with(".properties") {
Language::Properties
} else {
Language::Unknown
}
}
pub fn resolve_by_name_and_kind(
name: &str,
kinds: &[NodeKind],
preferred_dirs: &[&str],
context: &mut dyn ResolutionContext,
) -> Option<String> {
let candidates = context
.get_nodes_by_name(name)
.into_iter()
.filter(|node| kinds.contains(&node.kind))
.collect::<Vec<_>>();
if candidates.is_empty() {
return None;
}
if let Some(preferred) = candidates.iter().find(|node| {
preferred_dirs
.iter()
.any(|dir| node.file_path.contains(dir))
}) {
return Some(preferred.id.clone());
}
candidates.first().map(|node| node.id.clone())
}
#[allow(clippy::too_many_arguments)]
pub fn make_node(
id: impl Into<String>,
kind: NodeKind,
name: impl Into<String>,
qualified_name: impl Into<String>,
file_path: impl Into<String>,
language: Language,
line: LineNumber,
signature: Option<String>,
docstring: Option<String>,
) -> Node {
let name = name.into();
Node {
id: id.into(),
kind,
name: name.clone(),
qualified_name: qualified_name.into(),
file_path: file_path.into(),
language,
start_line: line,
end_line: line,
start_column: 0,
end_column: name.len() as ColumnNumber,
docstring,
signature,
visibility: None,
is_exported: Some(true),
is_async: None,
is_static: None,
is_abstract: None,
decorators: None,
type_parameters: None,
return_type: None,
updated_at: now_ms(),
}
}
pub fn make_reference(
from_node_id: impl Into<String>,
reference_name: impl Into<String>,
reference_kind: ReferenceKind,
line: LineNumber,
column: ColumnNumber,
file_path: impl Into<String>,
language: Language,
) -> UnresolvedRef {
UnresolvedRef {
from_node_id: from_node_id.into(),
reference_name: reference_name.into(),
reference_kind,
line,
column,
file_path: file_path.into(),
language,
candidates: None,
}
}