use super::js_workspace::JsWorkspace;
use crate::models::{ClassInfo, ConstantInfo, EnumInfo, FileInfo, FunctionInfo, InterfaceInfo};
use crate::parsers::registry;
use aho_corasick::{AhoCorasick, MatchKind};
use rayon::prelude::*;
use std::collections::{BTreeMap, HashMap, HashSet};
fn get_separator(language: &str) -> &'static str {
registry::edge_sep(language)
}
pub struct ContainsEdge {
pub parent: String,
pub child: String,
}
pub struct ImportEdge {
pub file_path: String,
pub module: String,
}
pub struct FileImportEdge {
pub source: String,
pub target: String,
pub import_count: i64,
}
pub struct UsesTypeEdge {
pub function: String,
pub type_name: String,
pub target_node_type: &'static str,
pub position: &'static str,
}
pub struct FfiExposesEdge {
pub module_fn: String,
pub target_qname: String,
pub target_type: &'static str,
pub py_name: String,
}
pub struct ModuleContainsFileEdge {
pub module: String,
pub file_path: String,
}
pub fn build_module_contains_file_edges(files: &[FileInfo]) -> Vec<ModuleContainsFileEdge> {
files
.iter()
.filter(|f| !f.module_path.is_empty())
.map(|f| ModuleContainsFileEdge {
module: f.module_path.clone(),
file_path: f.path.clone(),
})
.collect()
}
pub fn build_contains_edges(files: &[FileInfo]) -> Vec<ContainsEdge> {
let mut out = Vec::new();
let mut implicit_seen = HashSet::new();
for f in files {
if registry::has_implicit_module_hierarchy(&f.language) {
let sep = registry::module_sep(&f.language);
let parts: Vec<_> = f.module_path.split(sep).collect();
for end in 2..=parts.len() {
let parent = parts[..end - 1].join(sep);
let child = parts[..end].join(sep);
if implicit_seen.insert((parent.clone(), child.clone())) {
out.push(ContainsEdge { parent, child });
}
}
}
let sep = get_separator(&f.language);
for sub in &f.submodule_declarations {
out.push(ContainsEdge {
parent: f.module_path.clone(),
child: format!("{}{}{}", f.module_path, sep, sub),
});
}
}
out
}
fn normalize_import_path(base: &str, raw: &str) -> Option<String> {
let mut parts: Vec<&str> = base.split('/').filter(|part| !part.is_empty()).collect();
for part in raw.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop()?;
}
value => parts.push(value),
}
}
(!parts.is_empty()).then(|| parts.join("/"))
}
fn path_import_candidates(file: &FileInfo, raw: &str) -> Vec<String> {
if !registry::uses_path_imports(&file.language) {
return Vec::new();
}
let trimmed = raw.trim();
if trimmed.is_empty()
|| trimmed.starts_with('<')
|| trimmed.starts_with('#')
|| trimmed.starts_with("//")
|| trimmed.contains("://")
|| trimmed.to_ascii_lowercase().starts_with("data:")
{
return Vec::new();
}
let without_suffix = trimmed
.split(['?', '#'])
.next()
.unwrap_or("")
.replace('\\', "/");
if without_suffix.is_empty() {
return Vec::new();
}
let mut candidates = Vec::new();
let root_path = without_suffix.trim_start_matches('/');
let root_absolute = without_suffix.starts_with('/');
let parent = file.path.rsplit_once('/').map_or("", |(parent, _)| parent);
if !root_absolute {
if let Some(candidate) = normalize_import_path(parent, root_path) {
candidates.push(candidate);
}
}
if !matches!(file.language.as_str(), "c" | "cpp") {
if let Some(candidate) = normalize_import_path("", root_path) {
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
}
if root_absolute && matches!(file.language.as_str(), "html" | "css") {
if let Some(candidate) = normalize_import_path(parent, root_path) {
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
}
if file.language == "css" {
let extensionless: Vec<_> = candidates
.iter()
.filter(|candidate| {
candidate
.rsplit_once('/')
.map_or(candidate.as_str(), |(_, name)| name)
.rsplit_once('.')
.is_none()
})
.map(|candidate| format!("{candidate}.css"))
.collect();
candidates.extend(extensionless);
}
candidates
}
const MODULE_PATH_EXTENSIONS: &[&str] =
&[".tsx", ".ts", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".js"];
fn module_path_import_candidates(
file: &FileInfo,
raw: &str,
workspace: &JsWorkspace,
) -> Vec<String> {
if !registry::uses_module_path_imports(&file.language) {
return Vec::new();
}
let trimmed = raw.trim();
let parent = file.path.rsplit_once('/').map_or("", |(parent, _)| parent);
let raw_paths: Vec<String> = if trimmed.starts_with("./")
|| trimmed.starts_with("../")
|| trimmed == "."
|| trimmed == ".."
{
normalize_import_path(parent, trimmed).into_iter().collect()
} else {
let mut paths = workspace.alias_targets(parent, trimmed);
paths.extend(workspace.package_targets(trimmed));
paths
};
let mut candidates: Vec<String> = Vec::new();
let mut push = |candidate: String| {
if !candidate.is_empty() && !candidates.contains(&candidate) {
candidates.push(candidate);
}
};
for path in raw_paths {
let stripped = MODULE_PATH_EXTENSIONS
.iter()
.find_map(|ext| path.strip_suffix(ext))
.unwrap_or(path.as_str())
.to_string();
let without_index = stripped.strip_suffix("/index").map(str::to_string);
push(stripped);
if let Some(without_index) = without_index {
push(without_index);
}
}
candidates
}
fn module_path_root_prefix(file: &FileInfo, sep: &str) -> Option<(String, Vec<String>)> {
let mut segments: Vec<String> = file
.path
.split('/')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
if let Some(last) = segments.last_mut() {
if let Some((stem, _ext)) = last.rsplit_once('.') {
if !stem.is_empty() {
*last = stem.to_string();
}
}
}
if segments.last().map(String::as_str) == Some("__init__") {
segments.pop();
}
let module = file.module_path.as_str();
let tail = segments.join(sep);
let dirs: Vec<String> = segments
.split_last()
.map_or_else(Vec::new, |(_, rest)| rest.to_vec());
if tail.is_empty() {
return (!module.is_empty()).then(|| (module.to_string(), dirs));
}
if module == tail {
return None;
}
module
.strip_suffix(&format!("{sep}{tail}"))
.filter(|prefix| !prefix.is_empty())
.map(|prefix| (prefix.to_string(), dirs))
}
fn src_layout_dirs(files: &[FileInfo]) -> HashSet<String> {
let mut out = HashSet::new();
for file in files {
if file.language != "python" {
continue;
}
let segments: Vec<&str> = file.path.split('/').collect();
let dir_count = segments.len().saturating_sub(1);
for (i, segment) in segments.iter().take(dir_count).enumerate() {
if *segment == "src" {
out.insert(segments[..i].join("/"));
}
}
}
out
}
fn rust_import_candidates(file: &FileInfo, raw: &str) -> Vec<String> {
if file.language != "rust" {
return Vec::new();
}
let own: Vec<&str> = file.module_path.split("::").collect();
let root_len = own
.iter()
.rposition(|seg| *seg == "src")
.map(|i| i + 1)
.unwrap_or(1);
let trimmed = raw.trim();
let rebased: Option<Vec<String>> = if let Some(rest) = trimmed.strip_prefix("crate::") {
Some(
own[..root_len]
.iter()
.map(|s| s.to_string())
.chain(rest.split("::").map(str::to_string))
.collect(),
)
} else if trimmed == "crate" {
Some(own[..root_len].iter().map(|s| s.to_string()).collect())
} else if trimmed.starts_with("super::")
|| trimmed == "super"
|| trimmed.starts_with("self::")
|| trimmed == "self"
{
let mut base: Vec<String> = own.iter().map(|s| s.to_string()).collect();
let mut rest = trimmed;
while let Some(r) =
rest.strip_prefix("super::")
.or(if rest == "super" { Some("") } else { None })
{
if base.len() > root_len {
base.pop();
}
rest = r;
}
if let Some(r) = rest.strip_prefix("self::") {
rest = r;
} else if rest == "self" {
rest = "";
}
if !rest.is_empty() {
base.extend(rest.split("::").map(str::to_string));
}
Some(base)
} else {
Some(
own[..root_len]
.iter()
.map(|s| s.to_string())
.chain(trimmed.split("::").map(str::to_string))
.collect(),
)
};
let mut out = Vec::new();
if let Some(parts) = rebased {
for end in (root_len..=parts.len()).rev() {
let candidate = parts[..end].join("::");
if !out.contains(&candidate) {
out.push(candidate);
}
}
}
out
}
fn python_relative_import_candidates(file: &FileInfo, raw: &str) -> Vec<String> {
if file.language != "python" {
return Vec::new();
}
let trimmed = raw.trim();
if !trimmed.starts_with('.') {
return Vec::new();
}
let dots = trimmed.chars().take_while(|c| *c == '.').count();
let rest = &trimmed[dots..];
let mut base: Vec<String> = file
.module_path
.split('.')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let leaf = file.path.rsplit('/').next().unwrap_or(&file.path);
let is_init = matches!(leaf, "__init__.py" | "__init__.pyi");
if !is_init && base.len() > 1 {
base.pop();
}
for _ in 1..dots {
if base.len() > 1 {
base.pop();
}
}
let anchor_len = base.len();
base.extend(
rest.split('.')
.filter(|s| !s.is_empty())
.map(str::to_string),
);
let mut out = Vec::new();
for end in (anchor_len..=base.len()).rev() {
let candidate = base[..end].join(".");
if !candidate.is_empty() && !out.contains(&candidate) {
out.push(candidate);
}
}
out
}
fn module_path_prefix_candidates(
file: &FileInfo,
raw: &str,
sep: &str,
src_dirs: &HashSet<String>,
) -> Vec<String> {
if file.language != "python" {
return Vec::new();
}
let trimmed = raw.trim();
if trimmed.is_empty() || trimmed.starts_with('.') {
return Vec::new();
}
let Some((prefix, dirs)) = module_path_root_prefix(file, sep) else {
return Vec::new();
};
let mut roots: Vec<String> = (0..=dirs.len())
.map(|depth| {
if depth == 0 {
prefix.clone()
} else {
format!("{prefix}{sep}{}", dirs[..depth].join(sep))
}
})
.collect();
let src_roots: Vec<String> = (0..=dirs.len())
.filter(|depth| src_dirs.contains(&dirs[..*depth].join("/")))
.map(|depth| format!("{}{sep}src", roots[depth]))
.collect();
roots.extend(src_roots);
let parts: Vec<&str> = trimmed.split(sep).collect();
let mut out = Vec::new();
for end in (1..=parts.len()).rev() {
let specifier = parts[..end].join(sep);
for root in &roots {
let candidate = format!("{root}{sep}{specifier}");
if !out.contains(&candidate) {
out.push(candidate);
}
}
}
out
}
fn resolve_path_import(
file: &FileInfo,
raw: &str,
file_to_module: &HashMap<&str, &str>,
) -> Option<(String, String)> {
for candidate in path_import_candidates(file, raw) {
if let Some(module) = file_to_module.get(candidate.as_str()) {
return Some((candidate, (*module).to_string()));
}
}
None
}
pub fn build_import_edges(
files: &[FileInfo],
known_modules: &HashSet<String>,
workspace: &JsWorkspace,
) -> Vec<ImportEdge> {
let mut out = Vec::new();
let src_dirs = src_layout_dirs(files);
let file_to_module: HashMap<&str, &str> = if files
.iter()
.any(|file| registry::uses_path_imports(&file.language))
{
files
.iter()
.map(|file| (file.path.as_str(), file.module_path.as_str()))
.collect()
} else {
HashMap::new()
};
for f in files {
let sep = get_separator(&f.language);
for use_path in &f.imports {
if use_path.starts_with('<') {
continue;
}
if let Some((_, module)) = resolve_path_import(f, use_path, &file_to_module) {
out.push(ImportEdge {
file_path: f.path.clone(),
module,
});
continue;
}
if let Some(module) = module_path_import_candidates(f, use_path, workspace)
.into_iter()
.find(|candidate| known_modules.contains(candidate))
{
out.push(ImportEdge {
file_path: f.path.clone(),
module,
});
continue;
}
let rust_candidates = rust_import_candidates(f, use_path);
if !rust_candidates.is_empty() {
if let Some(module) = rust_candidates
.into_iter()
.find(|candidate| known_modules.contains(candidate))
{
if module != f.module_path {
out.push(ImportEdge {
file_path: f.path.clone(),
module,
});
}
}
continue;
}
let relative_candidates = python_relative_import_candidates(f, use_path);
if !relative_candidates.is_empty() {
if let Some(module) = relative_candidates
.into_iter()
.find(|candidate| known_modules.contains(candidate))
{
if module != f.module_path {
out.push(ImportEdge {
file_path: f.path.clone(),
module,
});
}
}
continue;
}
let parts: Vec<&str> = use_path.split(sep).collect();
let min_end = match f.language.as_str() {
"agc" => parts.len(),
"java" | "csharp" | "php" | "swift" | "go" => parts.len().saturating_sub(1).max(1),
_ => 1,
};
let mut resolved = false;
for end in (min_end..=parts.len()).rev() {
let candidate = parts[..end].join(sep);
if candidate.is_empty() {
continue;
}
if known_modules.contains(&candidate) {
out.push(ImportEdge {
file_path: f.path.clone(),
module: candidate,
});
resolved = true;
break;
}
}
if resolved {
continue;
}
if let Some(module) = module_path_prefix_candidates(f, use_path, sep, &src_dirs)
.into_iter()
.find(|candidate| known_modules.contains(candidate))
{
out.push(ImportEdge {
file_path: f.path.clone(),
module,
});
}
}
}
out
}
pub fn build_file_import_edges(
files: &[FileInfo],
module_to_file: &HashMap<String, String>,
workspace: &JsWorkspace,
) -> Vec<FileImportEdge> {
let mut counts: BTreeMap<(String, String), i64> = BTreeMap::new();
let src_dirs = src_layout_dirs(files);
let file_to_module: HashMap<&str, &str> = if files
.iter()
.any(|file| registry::uses_path_imports(&file.language))
{
files
.iter()
.map(|file| (file.path.as_str(), file.module_path.as_str()))
.collect()
} else {
HashMap::new()
};
for f in files {
let sep = get_separator(&f.language);
for use_path in &f.imports {
if use_path.starts_with('<') {
continue;
}
if let Some((target_file, _)) = resolve_path_import(f, use_path, &file_to_module) {
if target_file != f.path {
*counts.entry((f.path.clone(), target_file)).or_insert(0) += 1;
}
continue;
}
if let Some(target_file) = module_path_import_candidates(f, use_path, workspace)
.into_iter()
.find_map(|candidate| module_to_file.get(&candidate))
{
if target_file != &f.path {
*counts
.entry((f.path.clone(), target_file.clone()))
.or_insert(0) += 1;
}
continue;
}
let rust_candidates = rust_import_candidates(f, use_path);
if !rust_candidates.is_empty() {
if let Some(target_file) = rust_candidates
.into_iter()
.find_map(|candidate| module_to_file.get(&candidate))
{
if target_file != &f.path {
*counts
.entry((f.path.clone(), target_file.clone()))
.or_insert(0) += 1;
}
}
continue;
}
let relative_candidates = python_relative_import_candidates(f, use_path);
if !relative_candidates.is_empty() {
if let Some(target_file) = relative_candidates
.into_iter()
.find_map(|candidate| module_to_file.get(&candidate))
{
if target_file != &f.path {
*counts
.entry((f.path.clone(), target_file.clone()))
.or_insert(0) += 1;
}
}
continue;
}
let file_anchored = matches!(f.language.as_str(), "python" | "agc" | "dart");
let parts: Vec<&str> = use_path.split(sep).collect();
let file_min_end = if f.language == "agc" { parts.len() } else { 1 };
let mut resolved = false;
if file_anchored {
for end in (file_min_end..=parts.len()).rev() {
let candidate = parts[..end].join(sep);
if candidate.is_empty() {
continue;
}
if let Some(target_file) = module_to_file.get(&candidate) {
if target_file != &f.path {
*counts
.entry((f.path.clone(), target_file.clone()))
.or_insert(0) += 1;
}
resolved = true;
break;
}
}
}
if resolved {
continue;
}
if !file_anchored {
continue;
}
if let Some(target_file) = module_path_prefix_candidates(f, use_path, sep, &src_dirs)
.into_iter()
.find_map(|candidate| module_to_file.get(&candidate))
{
if target_file != &f.path {
*counts
.entry((f.path.clone(), target_file.clone()))
.or_insert(0) += 1;
}
}
}
}
counts
.into_iter()
.map(|((source, target), import_count)| FileImportEdge {
source,
target,
import_count,
})
.collect()
}
pub fn build_uses_type_edges(
functions: &[FunctionInfo],
classes: &[ClassInfo],
enums: &[EnumInfo],
interfaces: &[InterfaceInfo],
) -> BTreeMap<&'static str, Vec<UsesTypeEdge>> {
let mut type_lookup: HashMap<String, (String, &'static str)> = HashMap::new();
for c in classes {
if c.name.chars().count() > 1 {
let target = super::class_node_type(&c.kind);
type_lookup.insert(c.name.clone(), (c.qualified_name.clone(), target));
}
}
for e in enums {
if e.name.chars().count() > 1 {
type_lookup.insert(e.name.clone(), (e.qualified_name.clone(), "Enum"));
}
}
for i in interfaces {
if i.name.chars().count() > 1 {
let target = match i.kind.as_str() {
"trait" => "Trait",
"protocol" => "Protocol",
_ => "Interface",
};
type_lookup.insert(i.name.clone(), (i.qualified_name.clone(), target));
}
}
if type_lookup.is_empty() {
return BTreeMap::new();
}
let mut names: Vec<String> = type_lookup.keys().cloned().collect();
names.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
let pattern_meta: Vec<(String, &'static str)> = names
.iter()
.map(|n| {
let (q, t) = type_lookup.get(n).unwrap();
(q.clone(), *t)
})
.collect();
let ac = match AhoCorasick::builder()
.match_kind(MatchKind::LeftmostLongest)
.build(&names)
{
Ok(ac) => ac,
Err(_) => return BTreeMap::new(),
};
const POS_PARAM: u8 = 1 << 0;
const POS_RETURN: u8 = 1 << 1;
const POS_SIGNATURE: u8 = 1 << 2;
const POS_RECEIVER: u8 = 1 << 3;
let per_fn: Vec<Vec<(u32, &'static str, String, &'static str)>> = functions
.par_iter()
.map(|fn_info| {
let mut seen: HashMap<u32, u8> = HashMap::new();
let scan = |text: &str, pos_bit: u8, seen: &mut HashMap<u32, u8>| {
if text.is_empty() {
return;
}
let bytes = text.as_bytes();
for m in ac.find_iter(text) {
let start = m.start();
let end = m.end();
let before_ok = start == 0
|| !bytes[start - 1].is_ascii_alphanumeric() && bytes[start - 1] != b'_';
let after_ok = end == text.len()
|| !bytes[end].is_ascii_alphanumeric() && bytes[end] != b'_';
if !before_ok || !after_ok {
continue;
}
let pat_id = m.pattern().as_usize() as u32;
*seen.entry(pat_id).or_insert(0) |= pos_bit;
}
};
for p in &fn_info.parameters {
if let Some(t) = &p.type_annotation {
let pos_bit = if p.kind == crate::models::ParameterKind::Receiver {
POS_RECEIVER
} else {
POS_PARAM
};
scan(t, pos_bit, &mut seen);
}
}
if let Some(rt) = &fn_info.return_type {
scan(rt, POS_RETURN, &mut seen);
}
let has_param_types = fn_info
.parameters
.iter()
.any(|p| p.type_annotation.is_some());
if !has_param_types && !fn_info.signature.is_empty() {
scan(&fn_info.signature, POS_SIGNATURE, &mut seen);
}
let mut matches: Vec<_> = seen
.into_iter()
.map(|(pat_id, bits)| {
let semantic_count = (bits & POS_PARAM != 0) as u8
+ (bits & POS_RETURN != 0) as u8
+ (bits & POS_RECEIVER != 0) as u8;
let position = if semantic_count >= 2 {
"both"
} else if bits & POS_RECEIVER != 0 {
"receiver"
} else if bits & POS_PARAM != 0 {
"parameter"
} else if bits & POS_RETURN != 0 {
"return"
} else if bits & POS_SIGNATURE != 0 {
"signature"
} else {
unreachable!("at least one position bit must be set");
};
let (qname, target) = &pattern_meta[pat_id as usize];
(pat_id, *target, qname.clone(), position)
})
.collect();
matches.sort_unstable_by_key(|(pat_id, _, _, _)| *pat_id);
matches
})
.collect();
let mut by_target_type: BTreeMap<&'static str, Vec<UsesTypeEdge>> = BTreeMap::new();
for (fn_info, matches) in functions.iter().zip(per_fn) {
for (_pat_id, target, qname, position) in matches {
by_target_type
.entry(target)
.or_default()
.push(UsesTypeEdge {
function: fn_info.qualified_name.clone(),
type_name: qname,
target_node_type: target,
position,
});
}
}
by_target_type
}
pub struct ReferencesEdge {
pub function: String,
pub constant: String,
pub line: u32,
pub reference_lines: Option<String>,
pub reference_count: Option<i64>,
pub opcodes: Option<String>,
pub accesses: Option<String>,
pub has_read: Option<bool>,
pub has_write: Option<bool>,
pub has_address: Option<bool>,
}
pub struct ReferencesFnEdge {
pub caller: String,
pub callee: String,
pub line: u32,
}
pub struct DecoratesEdge {
pub decorator: String,
pub function: String,
pub decorator_name: String,
}
pub fn build_references_edges(
functions: &[FunctionInfo],
constants: &[ConstantInfo],
) -> Vec<ReferencesEdge> {
if constants.is_empty() {
return Vec::new();
}
let mut by_name: HashMap<&str, Vec<&str>> = HashMap::new();
for c in constants {
by_name
.entry(c.name.as_str())
.or_default()
.push(c.qualified_name.as_str());
}
let mut out: Vec<ReferencesEdge> = Vec::new();
for f in functions {
if f.references.is_empty() {
continue;
}
let mut seen: HashSet<&str> = HashSet::new();
for (ident, line) in &f.references {
let Some(matches) = by_name.get(ident.as_str()) else {
continue;
};
for &qname in matches {
if seen.insert(qname) {
out.push(ReferencesEdge {
function: f.qualified_name.clone(),
constant: qname.to_string(),
line: *line,
reference_lines: None,
reference_count: None,
opcodes: None,
accesses: None,
has_read: None,
has_write: None,
has_address: None,
});
}
}
}
}
out
}
pub fn build_references_fn_edges(functions: &[FunctionInfo]) -> Vec<ReferencesFnEdge> {
if functions.is_empty() {
return Vec::new();
}
let mut by_name: HashMap<&str, Vec<&str>> = HashMap::new();
let mut nested_by_file: HashMap<&str, HashMap<&str, Vec<&str>>> = HashMap::new();
for f in functions {
let bucket = if super::call_edges::is_nested_function(f) {
nested_by_file
.entry(f.file_path.as_str())
.or_default()
.entry(f.name.as_str())
.or_default()
} else {
by_name.entry(f.name.as_str()).or_default()
};
bucket.push(f.qualified_name.as_str());
}
let mut out: Vec<ReferencesFnEdge> = Vec::new();
for f in functions {
if f.function_refs.is_empty() {
continue;
}
let caller = f.qualified_name.as_str();
let local = nested_by_file.get(f.file_path.as_str());
let mut seen: HashSet<&str> = HashSet::new();
for (ident, line) in &f.function_refs {
let mut storage: Vec<&str> = Vec::new();
let matches = super::call_edges::merge_candidates(
by_name.get(ident.as_str()),
local.and_then(|m| m.get(ident.as_str())),
&mut storage,
);
if matches.len() != 1 {
continue;
}
let target = matches[0];
if target == caller {
continue;
}
if seen.insert(target) {
out.push(ReferencesFnEdge {
caller: caller.to_string(),
callee: target.to_string(),
line: *line,
});
}
}
}
out
}
pub struct PyO3BindsEdge {
pub py_function: String,
pub rust_function: String,
}
pub fn build_pyo3_binds_edges(functions: &[FunctionInfo]) -> Vec<PyO3BindsEdge> {
let mut rust_idx: HashMap<(String, String), Vec<&str>> = HashMap::new();
for f in functions {
if !f
.metadata
.get("is_pymethod")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
continue;
}
let parts: Vec<&str> = f.qualified_name.split("::").collect();
if parts.len() < 2 {
continue;
}
let parent = parts[parts.len() - 2].to_string();
let method = parts[parts.len() - 1].to_string();
rust_idx
.entry((parent, method))
.or_default()
.push(f.qualified_name.as_str());
}
let mut out = Vec::new();
for f in functions {
if !f.qualified_name.contains('.') || !f.is_method {
continue;
}
let parts: Vec<&str> = f.qualified_name.split('.').collect();
if parts.len() < 3 {
continue;
}
let py_class = parts[parts.len() - 2].to_string();
let py_method = parts[parts.len() - 1].to_string();
let Some(matches) = rust_idx.get(&(py_class, py_method)) else {
continue;
};
if matches.len() != 1 {
continue; }
out.push(PyO3BindsEdge {
py_function: f.qualified_name.clone(),
rust_function: matches[0].to_string(),
});
}
out
}
pub fn build_decorates_edges(functions: &[FunctionInfo]) -> Vec<DecoratesEdge> {
if functions.is_empty() {
return Vec::new();
}
let mut by_name: HashMap<&str, Vec<&str>> = HashMap::new();
let mut nested_by_file: HashMap<&str, HashMap<&str, Vec<&str>>> = HashMap::new();
for f in functions {
let bucket = if super::call_edges::is_nested_function(f) {
nested_by_file
.entry(f.file_path.as_str())
.or_default()
.entry(f.name.as_str())
.or_default()
} else {
by_name.entry(f.name.as_str()).or_default()
};
bucket.push(f.qualified_name.as_str());
}
let mut out: Vec<DecoratesEdge> = Vec::new();
for f in functions {
if f.decorators.is_empty() {
continue;
}
let function_qname = f.qualified_name.as_str();
let local = nested_by_file.get(f.file_path.as_str());
let mut seen: HashSet<&str> = HashSet::new();
for raw in &f.decorators {
let head = raw.split('(').next().unwrap_or(raw).trim();
if head.is_empty() {
continue;
}
let bare = head
.rsplit_once("::")
.map(|(_, t)| t)
.or_else(|| head.rsplit_once('.').map(|(_, t)| t))
.unwrap_or(head);
let mut storage: Vec<&str> = Vec::new();
let candidates = super::call_edges::merge_candidates(
by_name.get(bare),
local.and_then(|m| m.get(bare)),
&mut storage,
);
if candidates.len() != 1 {
continue; }
let target = candidates[0];
if target == function_qname {
continue; }
if seen.insert(target) {
out.push(DecoratesEdge {
decorator: target.to_string(),
function: function_qname.to_string(),
decorator_name: raw.clone(),
});
}
}
}
out
}
pub fn build_ffi_exposes_edges(
functions: &[FunctionInfo],
classes: &[ClassInfo],
) -> Vec<FfiExposesEdge> {
let pymodule_fns: Vec<&FunctionInfo> = functions
.iter()
.filter(|f| {
f.metadata
.get("is_pymodule")
.and_then(|v| v.as_bool())
.unwrap_or(false)
})
.collect();
if pymodule_fns.is_empty() {
return Vec::new();
}
let pyclass_items: Vec<(&ClassInfo, String)> = classes
.iter()
.filter(|c| {
c.metadata
.get("is_pyclass")
.and_then(|v| v.as_bool())
.unwrap_or(false)
})
.map(|c| {
let py_name = c
.metadata
.get("py_name")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| c.name.clone());
(c, py_name)
})
.collect();
let pyfunc_items: Vec<(&FunctionInfo, String)> = functions
.iter()
.filter(|f| {
!f.is_method
&& !f
.metadata
.get("is_pymodule")
.and_then(|v| v.as_bool())
.unwrap_or(false)
&& f.metadata.get("ffi_kind").and_then(|v| v.as_str()) == Some("pyo3")
})
.map(|f| {
let py_name = f
.metadata
.get("py_name")
.and_then(|v| v.as_str())
.map(str::to_string)
.unwrap_or_else(|| f.name.clone());
(f, py_name)
})
.collect();
let mut out = Vec::new();
for mod_fn in &pymodule_fns {
for (c, py_name) in &pyclass_items {
out.push(FfiExposesEdge {
module_fn: mod_fn.qualified_name.clone(),
target_qname: c.qualified_name.clone(),
target_type: "Struct",
py_name: py_name.clone(),
});
}
for (f, py_name) in &pyfunc_items {
out.push(FfiExposesEdge {
module_fn: mod_fn.qualified_name.clone(),
target_qname: f.qualified_name.clone(),
target_type: "Function",
py_name: py_name.clone(),
});
}
}
out
}
#[cfg(test)]
mod determinism_tests {
use super::*;
use crate::models::{ClassInfo, FileInfo, FunctionInfo, ParameterInfo};
fn source_file(path: &str, module: &str, language: &str, imports: &[&str]) -> FileInfo {
FileInfo {
path: path.into(),
module_path: module.into(),
language: language.into(),
imports: imports.iter().map(|value| (*value).to_string()).collect(),
..FileInfo::default()
}
}
#[test]
fn implicit_module_hierarchy_uses_parser_module_separator() {
let files = vec![
source_file("Foo/Bar.php", "Foo\\Bar", "php", &[]),
source_file("Foo/Other.php", "Foo\\Bar", "php", &[]),
source_file("src/net/client.c", "src/net/client", "c", &[]),
];
let pairs: Vec<_> = build_contains_edges(&files)
.into_iter()
.map(|edge| (edge.parent, edge.child))
.collect();
assert_eq!(
pairs,
vec![
("Foo".into(), "Foo\\Bar".into()),
("src".into(), "src/net".into()),
("src/net".into(), "src/net/client".into()),
]
);
}
#[test]
fn path_imports_resolve_relative_root_and_extensionless_css() {
let files = vec![
source_file(
"examples/main.c",
"examples/main",
"c",
&["../include/header.h"],
),
source_file("include/header.h", "include/header", "c", &[]),
source_file(
"index.html",
"site.index",
"html",
&["styles/site.css?rev=1", "https://cdn.example/x.css"],
),
source_file("styles/site.css", "site.site", "css", &["theme"]),
source_file("styles/theme.css", "site.theme", "css", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|file| file.module_path.clone()).collect();
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
let module_pairs: Vec<_> = module_edges
.iter()
.map(|edge| (edge.file_path.as_str(), edge.module.as_str()))
.collect();
assert_eq!(
module_pairs,
vec![
("examples/main.c", "include/header"),
("index.html", "site.site"),
("styles/site.css", "site.theme"),
]
);
let module_to_file = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let file_edges = build_file_import_edges(&files, &module_to_file, &JsWorkspace::default());
let file_pairs: Vec<_> = file_edges
.iter()
.map(|edge| (edge.source.as_str(), edge.target.as_str()))
.collect();
assert_eq!(
file_pairs,
vec![
("examples/main.c", "include/header.h"),
("index.html", "styles/site.css"),
("styles/site.css", "styles/theme.css"),
]
);
}
#[test]
fn cpp_quoted_includes_resolve_dir_first_and_angle_includes_never_do() {
let files = vec![
source_file(
"main.cpp",
"main",
"cpp",
&["local.h", "util/helper.h", "<vector>", "<local.h>"],
),
source_file("local.h", "local", "cpp", &[]),
source_file("helper.h", "helper", "cpp", &[]),
source_file(
"util/helper.cpp",
"util/helper",
"cpp",
&["helper.h", "../local.h"],
),
source_file("util/helper.h", "util/helper", "cpp", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|file| file.module_path.clone()).collect();
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
let module_pairs: Vec<_> = module_edges
.iter()
.map(|edge| (edge.file_path.as_str(), edge.module.as_str()))
.collect();
assert_eq!(
module_pairs,
vec![
("main.cpp", "local"),
("main.cpp", "util/helper"),
("util/helper.cpp", "util/helper"),
("util/helper.cpp", "local"),
]
);
let module_to_file = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let file_edges = build_file_import_edges(&files, &module_to_file, &JsWorkspace::default());
let file_pairs: Vec<_> = file_edges
.iter()
.map(|edge| (edge.source.as_str(), edge.target.as_str()))
.collect();
assert_eq!(
file_pairs,
vec![
("main.cpp", "local.h"),
("main.cpp", "util/helper.h"),
("util/helper.cpp", "local.h"),
("util/helper.cpp", "util/helper.h"),
]
);
}
#[test]
fn a_leading_dot_specifier_never_resolves_to_the_empty_module() {
let jl = vec![
source_file("src/Main.jl", "pkg.src.Main", "julia", &[".Geometry"]),
source_file("src/geometry.jl", "pkg.src.geometry", "julia", &[]),
];
let mut known_modules: HashSet<String> =
jl.iter().map(|file| file.module_path.clone()).collect();
known_modules.insert(String::new());
assert!(build_import_edges(&jl, &known_modules, &JsWorkspace::default()).is_empty());
let dart = vec![
source_file("lib/a.dart", "pkg.lib.a", "dart", &[".Geometry"]),
source_file("lib/b.dart", "pkg.lib.b", "dart", &[]),
];
let mut module_to_file: HashMap<String, String> = dart
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
module_to_file.insert(String::new(), "lib/b.dart".to_string());
assert!(
build_file_import_edges(&dart, &module_to_file, &JsWorkspace::default()).is_empty()
);
}
#[test]
fn cpp_quoted_includes_never_fall_back_to_the_project_root() {
let files = vec![
source_file("net/socket.c", "net/socket", "c", &["config.h"]),
source_file("config.h", "config", "c", &[]),
source_file("gui/window.cpp", "gui/window", "cpp", &["theme.hpp"]),
source_file("theme.hpp", "theme", "cpp", &[]),
];
assert_eq!(
path_import_candidates(&files[0], "config.h"),
vec!["net/config.h".to_string()]
);
assert_eq!(
path_import_candidates(&files[2], "theme.hpp"),
vec!["gui/theme.hpp".to_string()]
);
let known_modules: HashSet<_> = files.iter().map(|file| file.module_path.clone()).collect();
assert!(build_import_edges(&files, &known_modules, &JsWorkspace::default()).is_empty());
let module_to_file: HashMap<String, String> = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
assert!(
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default()).is_empty()
);
let r_run = source_file("analysis/run.R", "analysis/run", "r", &["lib/helpers.R"]);
assert_eq!(
path_import_candidates(&r_run, "lib/helpers.R"),
vec![
"analysis/lib/helpers.R".to_string(),
"lib/helpers.R".to_string()
]
);
}
#[test]
fn root_absolute_web_refs_also_try_the_linking_files_own_directory() {
let html = source_file(
"dist/index.html",
"site.dist.index",
"html",
&["/_astro/app.css", "/shared/reset.css"],
);
assert_eq!(
path_import_candidates(&html, "/_astro/app.css"),
vec![
"_astro/app.css".to_string(),
"dist/_astro/app.css".to_string()
],
"project root first, served root second"
);
let files = vec![
html,
source_file("dist/_astro/app.css", "site.dist._astro.app", "css", &[]),
source_file("shared/reset.css", "site.shared.reset", "css", &[]),
];
let module_to_file: HashMap<String, String> = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let pairs: Vec<_> =
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.source, edge.target))
.collect();
assert_eq!(
pairs,
vec![
(
"dist/index.html".to_string(),
"dist/_astro/app.css".to_string()
),
(
"dist/index.html".to_string(),
"shared/reset.css".to_string()
),
]
);
let c = source_file("net/socket.c", "net/socket", "c", &["/etc/config.h"]);
assert!(path_import_candidates(&c, "/etc/config.h").is_empty());
assert_eq!(
path_import_candidates(&files[0], "css/app.css"),
vec!["dist/css/app.css".to_string(), "css/app.css".to_string()]
);
}
#[test]
fn julia_includes_resolve_but_using_references_never_claim_files() {
let files = vec![
source_file(
"src/Main.jl",
"pkg.src.Main",
"julia",
&["geometry.jl", "util.jl", "Downloads", ".Geometry"],
),
source_file(
"src/geometry.jl",
"pkg.src.geometry",
"julia",
&["shapes/circle.jl"],
),
source_file(
"src/shapes/circle.jl",
"pkg.src.shapes.circle",
"julia",
&[],
),
source_file("src/util.jl", "pkg.src.util", "julia", &[]),
source_file("src/Downloads.jl", "pkg.src.Downloads", "julia", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|file| file.module_path.clone()).collect();
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
let module_pairs: Vec<_> = module_edges
.iter()
.map(|edge| (edge.file_path.as_str(), edge.module.as_str()))
.collect();
assert_eq!(
module_pairs,
vec![
("src/Main.jl", "pkg.src.geometry"),
("src/Main.jl", "pkg.src.util"),
("src/geometry.jl", "pkg.src.shapes.circle"),
]
);
let module_to_file = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let file_edges = build_file_import_edges(&files, &module_to_file, &JsWorkspace::default());
let file_pairs: Vec<_> = file_edges
.iter()
.map(|edge| (edge.source.as_str(), edge.target.as_str()))
.collect();
assert_eq!(
file_pairs,
vec![
("src/Main.jl", "src/geometry.jl"),
("src/Main.jl", "src/util.jl"),
("src/geometry.jl", "src/shapes/circle.jl"),
]
);
assert!(file_pairs.iter().all(|(_, t)| *t != "src/Downloads.jl"));
assert!(module_pairs.iter().all(|(_, m)| *m != "pkg.src.Downloads"));
}
#[test]
fn ts_relative_imports_resolve_against_the_module_set() {
let files = vec![
source_file(
"packages/core/src/util.ts",
"packages/core/src/util",
"typescript",
&[],
),
source_file(
"packages/core/src/nested/deep.ts",
"packages/core/src/nested/deep",
"typescript",
&[],
),
source_file(
"packages/core/src/index.ts",
"packages/core/src",
"typescript",
&["./util", "./nested/deep"],
),
source_file(
"packages/core/src/consumer.ts",
"packages/core/src/consumer",
"typescript",
&["./util.js"],
),
source_file(
"packages/app/src/main.ts",
"packages/app/src/main",
"typescript",
&["../../core/src/index"],
),
source_file(
"packages/app/src/widget.tsx",
"packages/app/src/widget",
"typescript",
&["../../core/src"],
),
source_file(
"packages/app/src/dangling.ts",
"packages/app/src/dangling",
"typescript",
&["./does-not-exist", "../../core/src/nope"],
),
source_file(
"packages/app/src/bare.ts",
"packages/app/src/bare",
"typescript",
&["zod", "@scope/core"],
),
];
let known_modules: HashSet<_> = files.iter().map(|file| file.module_path.clone()).collect();
let module_pairs: Vec<_> =
build_import_edges(&files, &known_modules, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.file_path, edge.module))
.collect();
assert_eq!(
module_pairs,
vec![
(
"packages/core/src/index.ts".to_string(),
"packages/core/src/util".to_string()
),
(
"packages/core/src/index.ts".to_string(),
"packages/core/src/nested/deep".to_string()
),
(
"packages/core/src/consumer.ts".to_string(),
"packages/core/src/util".to_string()
),
(
"packages/app/src/main.ts".to_string(),
"packages/core/src".to_string()
),
(
"packages/app/src/widget.tsx".to_string(),
"packages/core/src".to_string()
),
]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let file_pairs: Vec<_> =
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.source, edge.target))
.collect();
assert_eq!(
file_pairs,
vec![
(
"packages/app/src/main.ts".to_string(),
"packages/core/src/index.ts".to_string()
),
(
"packages/app/src/widget.tsx".to_string(),
"packages/core/src/index.ts".to_string()
),
(
"packages/core/src/consumer.ts".to_string(),
"packages/core/src/util.ts".to_string()
),
(
"packages/core/src/index.ts".to_string(),
"packages/core/src/nested/deep.ts".to_string()
),
(
"packages/core/src/index.ts".to_string(),
"packages/core/src/util.ts".to_string()
),
]
);
}
#[test]
fn ts_alias_and_workspace_specifiers_resolve() {
use super::super::js_workspace::TsPathsConfig;
let files = vec![
source_file(
"packages/core/src/util.ts",
"packages/core/src/util",
"typescript",
&[],
),
source_file(
"packages/core/src/index.ts",
"packages/core/src",
"typescript",
&[],
),
source_file(
"packages/app/src/main.ts",
"packages/app/src/main",
"typescript",
&[],
),
source_file(
"packages/app/src/consumer.ts",
"packages/app/src/consumer",
"typescript",
&[
"@/main",
"@scope/core",
"@scope/core/util",
"@scope/nope/thing",
"@/ghost",
],
),
];
let workspace = JsWorkspace::from_raw(
&[(
"packages/app",
TsPathsConfig {
base: "packages/app".into(),
paths: [("@/*".to_string(), vec!["./src/*".to_string()])]
.into_iter()
.collect(),
},
)],
&[("@scope/core", "packages/core")],
);
let known_modules: HashSet<_> = files
.iter()
.map(|file| file.module_path.clone())
.chain(["packages/core".to_string(), "packages/app".to_string()])
.collect();
let module_pairs: Vec<_> = build_import_edges(&files, &known_modules, &workspace)
.into_iter()
.map(|edge| (edge.file_path, edge.module))
.collect();
assert_eq!(
module_pairs,
vec![
(
"packages/app/src/consumer.ts".to_string(),
"packages/app/src/main".to_string()
),
(
"packages/app/src/consumer.ts".to_string(),
"packages/core".to_string()
),
(
"packages/app/src/consumer.ts".to_string(),
"packages/core/src/util".to_string()
),
]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|file| (file.module_path.clone(), file.path.clone()))
.collect();
let file_pairs: Vec<_> = build_file_import_edges(&files, &module_to_file, &workspace)
.into_iter()
.map(|edge| (edge.source, edge.target))
.collect();
assert_eq!(
file_pairs,
vec![
(
"packages/app/src/consumer.ts".to_string(),
"packages/app/src/main.ts".to_string()
),
(
"packages/app/src/consumer.ts".to_string(),
"packages/core/src/index.ts".to_string()
),
(
"packages/app/src/consumer.ts".to_string(),
"packages/core/src/util.ts".to_string()
),
]
);
}
#[test]
fn alias_and_package_specifiers_need_the_workspace_table() {
let files = vec![
source_file(
"packages/core/src/util.ts",
"packages/core/src/util",
"typescript",
&[],
),
source_file(
"packages/app/src/consumer.ts",
"packages/app/src/consumer",
"typescript",
&["@/main", "@scope/core/util"],
),
];
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
assert!(build_import_edges(&files, &known_modules, &JsWorkspace::default()).is_empty());
}
#[test]
fn python_absolute_imports_resolve_across_the_root_prefix() {
let files = vec![
source_file("pkg/app.py", "root.pkg.app", "python", &["pkg.util"]),
source_file("pkg/util.py", "root.pkg.util", "python", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_pairs: Vec<_> =
build_import_edges(&files, &known_modules, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.file_path, edge.module))
.collect();
assert_eq!(
module_pairs,
vec![("pkg/app.py".to_string(), "root.pkg.util".to_string())]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
let file_pairs: Vec<_> =
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.source, edge.target, edge.import_count))
.collect();
assert_eq!(
file_pairs,
vec![("pkg/app.py".to_string(), "pkg/util.py".to_string(), 1)]
);
}
#[test]
fn python_absolute_imports_do_not_manufacture_targets() {
let files = vec![
source_file(
"pkg/app.py",
"root.pkg.app",
"python",
&["pkg.nonexistent", "functools"],
),
source_file("pkg/util.py", "root.pkg.util", "python", &[]),
];
let mut known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
known_modules.insert("root".into());
known_modules.insert("root.pkg".into());
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
assert_eq!(
module_edges
.iter()
.map(|e| e.module.as_str())
.collect::<Vec<_>>(),
vec!["root.pkg"]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
assert!(
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default()).is_empty()
);
}
#[test]
fn python_src_layout_imports_resolve() {
let files = vec![
source_file(
"src/mypkg/app.py",
"proj.src.mypkg.app",
"python",
&["mypkg.util"],
),
source_file("src/mypkg/util.py", "proj.src.mypkg.util", "python", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_pairs: Vec<_> =
build_import_edges(&files, &known_modules, &JsWorkspace::default())
.into_iter()
.map(|edge| edge.module)
.collect();
assert_eq!(module_pairs, vec!["proj.src.mypkg.util".to_string()]);
}
#[test]
fn python_relative_imports_resolve_against_the_importers_package() {
let files = vec![
source_file("pkg/__init__.py", "root.pkg", "python", &[".util"]),
source_file(
"pkg/a.py",
"root.pkg.a",
"python",
&[".util", ".util.helper", ".sub.deeper.deep_thing"],
),
source_file("pkg/sub/__init__.py", "root.pkg.sub", "python", &[]),
source_file(
"pkg/sub/deeper.py",
"root.pkg.sub.deeper",
"python",
&["..util.helper"],
),
source_file("pkg/util.py", "root.pkg.util", "python", &[]),
];
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_pairs: Vec<_> =
build_import_edges(&files, &known_modules, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.file_path, edge.module))
.collect();
assert_eq!(
module_pairs,
vec![
("pkg/__init__.py".to_string(), "root.pkg.util".to_string()),
("pkg/a.py".to_string(), "root.pkg.util".to_string()),
("pkg/a.py".to_string(), "root.pkg.util".to_string()),
("pkg/a.py".to_string(), "root.pkg.sub.deeper".to_string()),
("pkg/sub/deeper.py".to_string(), "root.pkg.util".to_string()),
]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
let file_pairs: Vec<_> =
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.source, edge.target, edge.import_count))
.collect();
assert_eq!(
file_pairs,
vec![
("pkg/__init__.py".to_string(), "pkg/util.py".to_string(), 1),
("pkg/a.py".to_string(), "pkg/sub/deeper.py".to_string(), 1),
("pkg/a.py".to_string(), "pkg/util.py".to_string(), 2),
(
"pkg/sub/deeper.py".to_string(),
"pkg/util.py".to_string(),
1
),
]
);
}
#[test]
fn python_relative_imports_floor_at_the_anchor_and_never_self_target() {
let files = vec![
source_file("pkg/__init__.py", "root.pkg", "python", &[".some_symbol"]),
source_file(
"pkg/a.py",
"root.pkg.a",
"python",
&[".other_symbol", "...................nowhere"],
),
];
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_pairs: Vec<_> =
build_import_edges(&files, &known_modules, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.file_path, edge.module))
.collect();
assert_eq!(
module_pairs,
vec![("pkg/a.py".to_string(), "root.pkg".to_string())]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
let file_pairs: Vec<_> =
build_file_import_edges(&files, &module_to_file, &JsWorkspace::default())
.into_iter()
.map(|edge| (edge.source, edge.target, edge.import_count))
.collect();
assert_eq!(
file_pairs,
vec![("pkg/a.py".to_string(), "pkg/__init__.py".to_string(), 1)]
);
}
#[test]
fn python_clone_layout_resolves_exactly_once() {
let files = vec![
source_file(
"xarray/core/dataset.py",
"xarray.core.dataset",
"python",
&["xarray.core"],
),
source_file("xarray/core/__init__.py", "xarray.core", "python", &[]),
];
assert!(module_path_root_prefix(&files[0], ".").is_none());
assert!(
module_path_prefix_candidates(&files[0], "xarray.core", ".", &HashSet::new())
.is_empty()
);
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
assert_eq!(module_edges.len(), 1);
assert_eq!(module_edges[0].module, "xarray.core");
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
let file_edges = build_file_import_edges(&files, &module_to_file, &JsWorkspace::default());
assert_eq!(
file_edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str(), e.import_count))
.collect::<Vec<_>>(),
vec![("xarray/core/dataset.py", "xarray/core/__init__.py", 1)]
);
}
#[test]
fn src_layout_dirs_are_derived_from_the_file_set() {
let files = vec![
source_file("src/pkg/util.py", "root.src.pkg.util", "python", &[]),
source_file(
"apps/api/src/svc/db.py",
"root.apps.api.src.svc.db",
"python",
&[],
),
source_file("tests/test_util.py", "root.tests.test_util", "python", &[]),
source_file("tools/src", "root.tools.src", "python", &[]),
source_file("web/src/app.ts", "web/src/app", "typescript", &[]),
];
let dirs = src_layout_dirs(&files);
assert_eq!(
dirs,
HashSet::from(["".to_string(), "apps/api".to_string()]),
"only directories holding a Python-bearing `src/`"
);
}
#[test]
fn python_src_layout_absolute_import_resolves_through_the_src_root() {
let files = vec![
source_file("src/pkg/__init__.py", "root.src.pkg", "python", &[]),
source_file("src/pkg/util.py", "root.src.pkg.util", "python", &[]),
source_file(
"tests/test_util.py",
"root.tests.test_util",
"python",
&["pkg.util"],
),
];
let importer = &files[2];
let src_dirs = src_layout_dirs(&files);
assert_eq!(
module_path_prefix_candidates(importer, "pkg.util", ".", &HashSet::new()),
vec![
"root.pkg.util".to_string(),
"root.tests.pkg.util".to_string(),
"root.pkg".to_string(),
"root.tests.pkg".to_string(),
]
);
assert_eq!(
module_path_prefix_candidates(importer, "pkg.util", ".", &src_dirs),
vec![
"root.pkg.util".to_string(),
"root.tests.pkg.util".to_string(),
"root.src.pkg.util".to_string(),
"root.pkg".to_string(),
"root.tests.pkg".to_string(),
"root.src.pkg".to_string(),
]
);
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
let module_edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
assert_eq!(
module_edges
.iter()
.map(|e| (e.file_path.as_str(), e.module.as_str()))
.collect::<Vec<_>>(),
vec![("tests/test_util.py", "root.src.pkg.util")]
);
let module_to_file: HashMap<String, String> = files
.iter()
.map(|f| (f.module_path.clone(), f.path.clone()))
.collect();
let file_edges = build_file_import_edges(&files, &module_to_file, &JsWorkspace::default());
assert_eq!(
file_edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str(), e.import_count))
.collect::<Vec<_>>(),
vec![("tests/test_util.py", "src/pkg/util.py", 1)]
);
}
#[test]
fn no_src_directory_in_the_file_set_means_no_src_root() {
let files = vec![
source_file(
"tests/test_util.py",
"root.tests.test_util",
"python",
&["pkg.util"],
),
source_file("other/pkg/util.py", "root.other.pkg.util", "python", &[]),
];
let src_dirs = src_layout_dirs(&files);
assert!(src_dirs.is_empty());
assert!(
!module_path_prefix_candidates(&files[0], "pkg.util", ".", &src_dirs)
.iter()
.any(|c| c.contains(".src.")),
"no `src/` in the file set, no `src` root"
);
let known_modules: HashSet<_> = files.iter().map(|f| f.module_path.clone()).collect();
assert!(build_import_edges(&files, &known_modules, &JsWorkspace::default()).is_empty());
}
#[test]
fn module_path_candidates_are_language_gated() {
let ts = source_file("a/b.ts", "a/b", "typescript", &["./c"]);
assert_eq!(
module_path_import_candidates(&ts, "./c", &JsWorkspace::default()),
vec!["a/c"]
);
let py = source_file("a/b.py", "root.a.b", "python", &["./c"]);
assert!(module_path_import_candidates(&py, "./c", &JsWorkspace::default()).is_empty());
assert!(module_path_prefix_candidates(&py, "./c", ".", &HashSet::new()).is_empty());
assert_eq!(
module_path_prefix_candidates(&py, "c.d", ".", &HashSet::new()),
vec![
"root.c.d".to_string(),
"root.a.c.d".to_string(),
"root.c".to_string(),
"root.a.c".to_string(),
]
);
assert!(module_path_prefix_candidates(&ts, "a/c", "/", &HashSet::new()).is_empty());
let js = source_file("a/b.js", "a/b", "javascript", &["../d/index.js"]);
assert_eq!(
module_path_import_candidates(&js, "../d/index.js", &JsWorkspace::default()),
vec!["d/index", "d"]
);
assert!(module_path_import_candidates(&ts, "zod", &JsWorkspace::default()).is_empty());
assert!(
module_path_import_candidates(&ts, "@scope/core", &JsWorkspace::default()).is_empty()
);
assert!(
module_path_import_candidates(&ts, "../../../outside", &JsWorkspace::default())
.is_empty()
);
}
#[test]
fn swift_target_import_resolves_as_a_namespace() {
let files = vec![source_file(
"Sources/Helpers/Test.swift",
"Helpers.Test",
"swift",
&["ArgumentParser"],
)];
let known_modules = HashSet::from(["ArgumentParser".to_string()]);
let edges = build_import_edges(&files, &known_modules, &JsWorkspace::default());
assert_eq!(edges.len(), 1);
assert_eq!(edges[0].module, "ArgumentParser");
}
#[test]
fn module_walk_trim_depth_is_bounded_by_language_semantics() {
let mut known = HashSet::new();
known.insert("com".to_string());
known.insert("com.example".to_string());
let java = FileInfo {
path: "src/App.java".into(),
language: "java".into(),
module_path: "com.example.app".into(),
imports: vec!["com.fasterxml.jackson.core.JsonFactory".into()],
..FileInfo::default()
};
let edges = build_import_edges(&[java], &known, &JsWorkspace::default());
assert!(
edges.is_empty(),
"an external namespace import must not land on an ancestor Module: {:?}",
edges.iter().map(|e| e.module.as_str()).collect::<Vec<_>>()
);
let mut known = HashSet::new();
known.insert("com.example.util".to_string());
let java_ok = FileInfo {
path: "src/App.java".into(),
language: "java".into(),
module_path: "com.example.app".into(),
imports: vec!["com.example.util.Helper".into()],
..FileInfo::default()
};
let edges = build_import_edges(&[java_ok], &known, &JsWorkspace::default());
assert_eq!(
edges.len(),
1,
"one trim to the real package must still resolve"
);
assert_eq!(edges[0].module, "com.example.util");
let mut known = HashSet::new();
known.insert("Luminary099".to_string());
let agc = FileInfo {
path: "Luminary099/MAIN.agc".into(),
language: "agc".into(),
module_path: "Luminary099/MAIN".into(),
imports: vec!["Luminary099/TRIM_GIMBAL_CNTROL_SYSTEM".into()],
..FileInfo::default()
};
let edges = build_import_edges(&[agc], &known, &JsWorkspace::default());
assert!(
edges.is_empty(),
"a typo'd AGC directive must resolve to nothing, not the parent program"
);
}
#[test]
fn namespace_languages_get_no_file_edges_from_the_raw_walk() {
let java = FileInfo {
path: "src/App.java".into(),
language: "java".into(),
module_path: "com.example.app".into(),
imports: vec!["com.example.util.Helper".into()],
..FileInfo::default()
};
let module_to_file = HashMap::from([(
"com.example.util".to_string(),
"src/util/Unrelated.java".to_string(),
)]);
let edges = build_file_import_edges(&[java], &module_to_file, &JsWorkspace::default());
assert!(
edges.is_empty(),
"a namespace hit must not become a file edge (got {} edge(s), first: {} -> {})",
edges.len(),
edges.first().map(|e| e.source.as_str()).unwrap_or("-"),
edges.first().map(|e| e.target.as_str()).unwrap_or("-"),
);
let python = FileInfo {
path: "pkg/app.py".into(),
language: "python".into(),
module_path: "proj.pkg.app".into(),
imports: vec!["proj.pkg.util".into()],
..FileInfo::default()
};
let module_to_file =
HashMap::from([("proj.pkg.util".to_string(), "pkg/util.py".to_string())]);
let edges = build_file_import_edges(&[python], &module_to_file, &JsWorkspace::default());
assert_eq!(
edges.len(),
1,
"file-anchored python must keep its raw-walk resolution"
);
}
#[test]
fn rust_bare_path_resolves_locally_and_never_falls_through() {
use crate::parsers::rust_lang::RustParser;
use std::path::Path;
let root = Path::new("");
let mp = |p: &str| RustParser::file_to_module_path(Path::new(p), root);
let importer = FileInfo {
path: "src/lib.rs".into(),
language: "rust".into(),
module_path: mp("src/lib.rs"),
imports: vec![
"api::Reader".into(), "codingest::build".into(), ],
..FileInfo::default()
};
let module_to_file = HashMap::from([
(mp("src/api.rs"), "src/api.rs".to_string()),
(
"codingest".to_string(),
"codingest/__init__.pyi".to_string(),
),
]);
let edges = build_file_import_edges(&[importer], &module_to_file, &JsWorkspace::default());
let pairs: Vec<_> = edges
.iter()
.map(|e| (e.source.as_str(), e.target.as_str()))
.collect();
assert_eq!(
pairs,
vec![("src/lib.rs", "src/api.rs")],
"bare local path must resolve; external-crate name must produce \
no edge even when a same-named foreign module exists"
);
}
#[test]
fn file_import_edges_are_key_sorted() {
use crate::parsers::rust_lang::RustParser;
use std::path::Path;
let root = Path::new("");
let mp = |p: &str| RustParser::file_to_module_path(Path::new(p), root);
let source = FileInfo {
path: "src/lib.rs".into(),
language: "rust".into(),
module_path: mp("src/lib.rs"),
imports: vec!["crate::beta".into(), "crate::alpha".into()],
..FileInfo::default()
};
let module_to_file = HashMap::from([
(mp("src/alpha.rs"), "src/alpha.rs".to_string()),
(mp("src/beta.rs"), "src/beta.rs".to_string()),
]);
let edges = build_file_import_edges(&[source], &module_to_file, &JsWorkspace::default());
let pairs: Vec<_> = edges
.iter()
.map(|edge| (edge.source.as_str(), edge.target.as_str()))
.collect();
assert_eq!(
pairs,
vec![
("src/lib.rs", "src/alpha.rs"),
("src/lib.rs", "src/beta.rs"),
]
);
}
#[test]
fn uses_type_edges_are_pattern_sorted() {
let function = FunctionInfo {
qualified_name: "crate::combine".into(),
parameters: vec![
ParameterInfo {
type_annotation: Some("Beta".into()),
..ParameterInfo::default()
},
ParameterInfo {
type_annotation: Some("Alpha".into()),
..ParameterInfo::default()
},
],
..FunctionInfo::default()
};
let classes = vec![
ClassInfo {
name: "Beta".into(),
qualified_name: "crate::beta::Beta".into(),
kind: "struct".into(),
..ClassInfo::default()
},
ClassInfo {
name: "Alpha".into(),
qualified_name: "crate::alpha::Alpha".into(),
kind: "struct".into(),
..ClassInfo::default()
},
];
let edges = build_uses_type_edges(&[function], &classes, &[], &[]);
let targets: Vec<_> = edges["Struct"]
.iter()
.map(|edge| edge.type_name.as_str())
.collect();
assert_eq!(targets, vec!["crate::alpha::Alpha", "crate::beta::Beta"]);
}
}
#[cfg(test)]
mod nested_visibility_tests {
use super::*;
use crate::models::FunctionInfo;
fn top_level(qname: &str, file: &str, name: &str) -> FunctionInfo {
FunctionInfo {
name: name.into(),
qualified_name: qname.into(),
file_path: file.into(),
..FunctionInfo::default()
}
}
fn nested(qname: &str, file: &str, name: &str) -> FunctionInfo {
let mut f = top_level(qname, file, name);
f.metadata
.insert("nesting_depth".into(), serde_json::json!(1));
f
}
#[test]
fn a_nested_definition_is_not_a_cross_file_references_fn_target() {
let mut referrer = top_level("b.consume", "b.py", "consume");
referrer.function_refs = vec![("wrapper".into(), 4)];
let functions = vec![referrer, nested("a.deco.wrapper", "a.py", "wrapper")];
assert!(
build_references_fn_edges(&functions).is_empty(),
"a closure-scoped `wrapper` in another file is not referable"
);
}
#[test]
fn a_nested_definition_is_a_same_file_references_fn_target() {
let mut referrer = top_level("a.deco", "a.py", "deco");
referrer.function_refs = vec![("wrapper".into(), 4)];
let functions = vec![referrer, nested("a.deco.wrapper", "a.py", "wrapper")];
let pairs: Vec<_> = build_references_fn_edges(&functions)
.into_iter()
.map(|e| (e.caller, e.callee))
.collect();
assert_eq!(pairs, vec![("a.deco".into(), "a.deco.wrapper".into())]);
}
#[test]
fn a_nested_definition_is_not_a_cross_file_decorator() {
let mut decorated = top_level("b.handler", "b.py", "handler");
decorated.decorators = vec!["wrapper".into()];
let functions = vec![decorated, nested("a.deco.wrapper", "a.py", "wrapper")];
assert!(
build_decorates_edges(&functions).is_empty(),
"a closure-scoped `wrapper` in another file cannot decorate"
);
}
#[test]
fn a_nested_definition_still_decorates_within_its_file() {
let mut decorated = top_level("a.handler", "a.py", "handler");
decorated.decorators = vec!["wrapper".into()];
let functions = vec![decorated, nested("a.deco.wrapper", "a.py", "wrapper")];
let pairs: Vec<_> = build_decorates_edges(&functions)
.into_iter()
.map(|e| (e.decorator, e.function))
.collect();
assert_eq!(pairs, vec![("a.deco.wrapper".into(), "a.handler".into())]);
}
#[test]
fn a_nested_name_does_not_disturb_the_global_index() {
let mut referrer = top_level("b.consume", "b.py", "consume");
referrer.function_refs = vec![("wrapper".into(), 4)];
let functions = vec![
referrer,
top_level("c.wrapper", "c.py", "wrapper"),
nested("a.deco.wrapper", "a.py", "wrapper"),
];
let pairs: Vec<_> = build_references_fn_edges(&functions)
.into_iter()
.map(|e| (e.caller, e.callee))
.collect();
assert_eq!(pairs, vec![("b.consume".into(), "c.wrapper".into())]);
}
}