relationships.rs•19.8 kB
//! Relationship analysis and learning between semantic concepts
#[cfg(feature = "napi-bindings")]
use napi_derive::napi;
use crate::types::SemanticConcept;
use std::collections::HashMap;
/// Analyzer for learning and discovering relationships between code concepts
#[cfg_attr(feature = "napi-bindings", napi)]
pub struct RelationshipLearner;
#[cfg_attr(feature = "napi-bindings", napi)]
impl RelationshipLearner {
#[cfg_attr(feature = "napi-bindings", napi(constructor))]
pub fn new() -> Self {
RelationshipLearner
}
/// Learn relationships between concepts and update the relationships map
pub fn learn_concept_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
Self::analyze_spatial_relationships(concepts, relationships);
Self::analyze_naming_relationships(concepts, relationships);
Self::analyze_type_relationships(concepts, relationships);
Self::analyze_file_relationships(concepts, relationships);
}
/// Analyze relationships based on proximity in source code
fn analyze_spatial_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
// Group concepts by file for spatial analysis
let mut file_concepts: HashMap<String, Vec<&SemanticConcept>> = HashMap::new();
for concept in concepts {
file_concepts
.entry(concept.file_path.clone())
.or_default()
.push(concept);
}
// Analyze spatial relationships within each file
for concepts_in_file in file_concepts.values() {
for (i, concept1) in concepts_in_file.iter().enumerate() {
for concept2 in concepts_in_file.iter().skip(i + 1) {
let distance = Self::calculate_line_distance(concept1, concept2);
// Consider concepts close if within 10 lines
if distance <= 10 {
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"spatial_proximity",
);
Self::add_relationship(
relationships,
&concept2.id,
&concept1.id,
"spatial_proximity",
);
}
}
}
}
}
/// Analyze relationships based on naming patterns
fn analyze_naming_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
for concept1 in concepts {
for concept2 in concepts {
if concept1.id == concept2.id {
continue;
}
let similarity = Self::calculate_name_similarity(&concept1.name, &concept2.name);
// Consider names similar if they share significant prefixes/suffixes
if similarity > 0.6 {
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"naming_similarity",
);
}
// Check for common naming patterns
if Self::has_naming_relationship(&concept1.name, &concept2.name) {
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"naming_pattern",
);
}
}
}
}
/// Analyze relationships based on concept types
fn analyze_type_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
// Group concepts by type
let mut type_groups: HashMap<String, Vec<&SemanticConcept>> = HashMap::new();
for concept in concepts {
type_groups
.entry(concept.concept_type.clone())
.or_default()
.push(concept);
}
// Analyze relationships within type groups
for (_concept_type, group_concepts) in type_groups {
for (i, concept1) in group_concepts.iter().enumerate() {
for concept2 in group_concepts.iter().skip(i + 1) {
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"same_type",
);
Self::add_relationship(
relationships,
&concept2.id,
&concept1.id,
"same_type",
);
}
}
}
// Analyze cross-type relationships (e.g., functions in classes)
Self::analyze_cross_type_relationships(concepts, relationships);
}
/// Analyze relationships between different concept types
fn analyze_cross_type_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
for concept in concepts {
match concept.concept_type.as_str() {
"function" | "method" => {
// Look for classes or interfaces this function might belong to
for other_concept in concepts {
if (other_concept.concept_type == "class" || other_concept.concept_type == "interface") && Self::is_function_in_class(concept, other_concept) {
Self::add_relationship(
relationships,
&concept.id,
&other_concept.id,
"member_of",
);
Self::add_relationship(
relationships,
&other_concept.id,
&concept.id,
"contains",
);
}
}
}
"variable" | "field" => {
// Look for functions or classes this variable might belong to
for other_concept in concepts {
if (other_concept.concept_type == "function" || other_concept.concept_type == "class") && Self::is_variable_in_scope(concept, other_concept) {
Self::add_relationship(
relationships,
&concept.id,
&other_concept.id,
"scoped_in",
);
}
}
}
_ => {}
}
}
}
/// Analyze relationships based on file organization
fn analyze_file_relationships(
concepts: &Vec<SemanticConcept>,
relationships: &mut HashMap<String, Vec<String>>,
) {
// Group concepts by file
let mut file_groups: HashMap<String, Vec<&SemanticConcept>> = HashMap::new();
for concept in concepts {
file_groups
.entry(concept.file_path.clone())
.or_default()
.push(concept);
}
// Analyze relationships within files
for concepts_in_file in file_groups.values() {
for (i, concept1) in concepts_in_file.iter().enumerate() {
for concept2 in concepts_in_file.iter().skip(i + 1) {
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"same_file",
);
}
}
}
// Analyze cross-file relationships based on import/export patterns
Self::analyze_import_relationships(&file_groups, relationships);
}
/// Analyze import/export relationships between files
fn analyze_import_relationships(
file_groups: &HashMap<String, Vec<&SemanticConcept>>,
relationships: &mut HashMap<String, Vec<String>>,
) {
// This is a simplified analysis - in practice, you'd parse import statements
for (file1, concepts1) in file_groups {
for (file2, concepts2) in file_groups {
if file1 == file2 {
continue;
}
// Look for potential import relationships based on naming
for concept1 in concepts1 {
for concept2 in concepts2 {
if (concept1.concept_type == "class" || concept1.concept_type == "interface") && concept2.metadata.contains_key("imports") {
if let Some(imports) = concept2.metadata.get("imports") {
if imports.contains(&concept1.name) {
Self::add_relationship(
relationships,
&concept2.id,
&concept1.id,
"imports",
);
Self::add_relationship(
relationships,
&concept1.id,
&concept2.id,
"imported_by",
);
}
}
}
}
}
}
}
}
/// Calculate distance between concepts in lines
fn calculate_line_distance(concept1: &SemanticConcept, concept2: &SemanticConcept) -> u32 {
if concept1.file_path != concept2.file_path {
return u32::MAX; // Different files have infinite distance
}
let start1 = concept1.line_range.start;
let end1 = concept1.line_range.end;
let start2 = concept2.line_range.start;
let end2 = concept2.line_range.end;
if end1 < start2 {
start2 - end1
} else if end2 < start1 {
start1.saturating_sub(end2)
} else {
0 // Overlapping ranges
}
}
/// Calculate similarity between two names
fn calculate_name_similarity(name1: &str, name2: &str) -> f64 {
if name1 == name2 {
return 1.0;
}
let len1 = name1.len();
let len2 = name2.len();
let max_len = len1.max(len2);
if max_len == 0 {
return 1.0;
}
// Simple longest common subsequence similarity
let common = Self::longest_common_subsequence(name1, name2);
common as f64 / max_len as f64
}
/// Calculate longest common subsequence length
fn longest_common_subsequence(s1: &str, s2: &str) -> usize {
let chars1: Vec<char> = s1.chars().collect();
let chars2: Vec<char> = s2.chars().collect();
let len1 = chars1.len();
let len2 = chars2.len();
let mut dp = vec![vec![0; len2 + 1]; len1 + 1];
for i in 1..=len1 {
for j in 1..=len2 {
if chars1[i - 1] == chars2[j - 1] {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
}
}
}
dp[len1][len2]
}
/// Check if two names have a naming relationship (e.g., getter/setter)
fn has_naming_relationship(name1: &str, name2: &str) -> bool {
let name1_lower = name1.to_lowercase();
let name2_lower = name2.to_lowercase();
// Check for getter/setter patterns
if name1_lower.starts_with("get") && name2_lower.starts_with("set") {
let suffix1 = &name1_lower[3..];
let suffix2 = &name2_lower[3..];
return suffix1 == suffix2;
}
// Check for test/implementation patterns
if name1_lower.contains("test") || name2_lower.contains("test") {
let clean1 = name1_lower.replace("test", "");
let clean2 = name2_lower.replace("test", "");
return clean1 == clean2 || clean1.is_empty() || clean2.is_empty();
}
false
}
/// Check if a function is likely a member of a class
fn is_function_in_class(function: &SemanticConcept, class: &SemanticConcept) -> bool {
// Same file check
if function.file_path != class.file_path {
return false;
}
// Check if function is within class line range (with some tolerance)
function.line_range.start >= class.line_range.start
&& function.line_range.end <= class.line_range.end + 5
}
/// Check if a variable is in scope of another concept
fn is_variable_in_scope(variable: &SemanticConcept, scope: &SemanticConcept) -> bool {
// Same file check
if variable.file_path != scope.file_path {
return false;
}
// Check if variable is within scope line range
variable.line_range.start >= scope.line_range.start
&& variable.line_range.end <= scope.line_range.end
}
/// Add a relationship to the relationships map
fn add_relationship(
relationships: &mut HashMap<String, Vec<String>>,
from_id: &str,
to_id: &str,
relationship_type: &str,
) {
let entry = relationships.entry(from_id.to_string()).or_default();
let relationship = format!("{}:{}", relationship_type, to_id);
if !entry.contains(&relationship) {
entry.push(relationship);
}
}
}
impl Default for RelationshipLearner {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::LineRange;
fn create_test_concept(
id: &str,
name: &str,
concept_type: &str,
file_path: &str,
start: u32,
end: u32,
) -> SemanticConcept {
SemanticConcept {
id: id.to_string(),
name: name.to_string(),
concept_type: concept_type.to_string(),
confidence: 0.8,
file_path: file_path.to_string(),
line_range: LineRange { start, end },
relationships: HashMap::new(),
metadata: HashMap::new(),
}
}
#[test]
fn test_relationship_learner_creation() {
let _learner = RelationshipLearner::new();
assert!(true); // Constructor should work
}
#[test]
fn test_spatial_relationships() {
let concepts = vec![
create_test_concept("1", "func1", "function", "test.rs", 1, 5),
create_test_concept("2", "func2", "function", "test.rs", 8, 12),
create_test_concept("3", "func3", "function", "test.rs", 50, 60),
];
let mut relationships = HashMap::new();
RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships);
// func1 and func2 should be spatially related (within 10 lines)
assert!(relationships.contains_key("1"));
assert!(relationships.contains_key("2"));
let func1_rels = relationships.get("1").unwrap();
assert!(func1_rels.iter().any(|r| r.contains("spatial_proximity:2")));
}
#[test]
fn test_naming_relationships() {
let concepts = vec![
create_test_concept("1", "getUserName", "function", "test.rs", 1, 5),
create_test_concept("2", "setUserName", "function", "test.rs", 10, 15),
create_test_concept("3", "getData", "function", "test.rs", 20, 25),
];
let mut relationships = HashMap::new();
RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships);
// getUserName and setUserName should have naming relationship
let get_rels = relationships.get("1").unwrap();
assert!(get_rels.iter().any(|r| r.contains("naming_pattern:2")));
}
#[test]
fn test_type_relationships() {
let concepts = vec![
create_test_concept("1", "func1", "function", "test.rs", 1, 5),
create_test_concept("2", "func2", "function", "test.rs", 10, 15),
create_test_concept("3", "Class1", "class", "test.rs", 20, 30),
];
let mut relationships = HashMap::new();
RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships);
// Functions should be related by same type
let func1_rels = relationships.get("1").unwrap();
assert!(func1_rels.iter().any(|r| r.contains("same_type:2")));
}
#[test]
fn test_cross_type_relationships() {
let concepts = vec![
create_test_concept("1", "TestClass", "class", "test.rs", 1, 30),
create_test_concept("2", "method1", "function", "test.rs", 5, 10),
create_test_concept("3", "field1", "variable", "test.rs", 15, 15),
];
let mut relationships = HashMap::new();
RelationshipLearner::learn_concept_relationships(&concepts, &mut relationships);
// Method should be member of class
let method_rels = relationships.get("2").unwrap();
assert!(method_rels.iter().any(|r| r.contains("member_of:1")));
// Class should contain method
let class_rels = relationships.get("1").unwrap();
assert!(class_rels.iter().any(|r| r.contains("contains:2")));
}
#[test]
fn test_calculate_line_distance() {
let concept1 = create_test_concept("1", "func1", "function", "test.rs", 1, 5);
let concept2 = create_test_concept("2", "func2", "function", "test.rs", 10, 15);
let concept3 = create_test_concept("3", "func3", "function", "other.rs", 1, 5);
assert_eq!(RelationshipLearner::calculate_line_distance(&concept1, &concept2), 5);
assert_eq!(RelationshipLearner::calculate_line_distance(&concept1, &concept3), u32::MAX);
}
#[test]
fn test_name_similarity() {
assert_eq!(RelationshipLearner::calculate_name_similarity("test", "test"), 1.0);
assert!(RelationshipLearner::calculate_name_similarity("test", "testing") > 0.7);
assert!(RelationshipLearner::calculate_name_similarity("abc", "xyz") < 0.3);
}
#[test]
fn test_naming_patterns() {
assert!(RelationshipLearner::has_naming_relationship("getName", "setName"));
assert!(RelationshipLearner::has_naming_relationship("testFunction", "function"));
assert!(!RelationshipLearner::has_naming_relationship("foo", "bar"));
}
#[test]
fn test_function_in_class_detection() {
let class = create_test_concept("1", "TestClass", "class", "test.rs", 1, 30);
let method_inside = create_test_concept("2", "method1", "function", "test.rs", 5, 10);
let method_outside = create_test_concept("3", "method2", "function", "test.rs", 35, 40);
assert!(RelationshipLearner::is_function_in_class(&method_inside, &class));
assert!(!RelationshipLearner::is_function_in_class(&method_outside, &class));
}
}