use crate::edit::ResolvedEditChange;
use crate::graph::ProgramDependenceGraph;
use crate::validation::Location;
use crate::validation::ValidationError;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
pub enum ReferenceIssueType {
BrokenImport {
symbol: String,
},
UndefinedReference {
name: String,
},
CyclicDependency {
cycle: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub struct ReferenceIssue {
pub issue_type: ReferenceIssueType,
pub file_path: PathBuf,
pub location: Location,
pub description: String,
}
impl ReferenceIssue {
pub fn new(
issue_type: ReferenceIssueType,
file_path: PathBuf,
location: Location,
description: String,
) -> Self {
Self {
issue_type,
file_path,
location,
description,
}
}
pub fn broken_import(symbol: String, file_path: PathBuf, location: Location) -> Self {
Self {
issue_type: ReferenceIssueType::BrokenImport {
symbol: symbol.clone(),
},
file_path,
location,
description: format!("Import '{}' not found in project", symbol),
}
}
pub fn undefined_reference(name: String, file_path: PathBuf, location: Location) -> Self {
Self {
issue_type: ReferenceIssueType::UndefinedReference { name: name.clone() },
file_path,
location,
description: format!("Undefined reference '{}'", name),
}
}
pub fn cyclic_dependency(cycle: Vec<String>, file_path: PathBuf) -> Self {
Self {
issue_type: ReferenceIssueType::CyclicDependency {
cycle: cycle.clone(),
},
file_path,
location: Location { line: 1, column: 1 },
description: format!("Cyclic dependency detected: {}", cycle.join(" -> ")),
}
}
}
#[derive(Clone)]
pub struct ReferenceChecker {
pdg: Arc<ProgramDependenceGraph>,
}
impl ReferenceChecker {
pub fn new(pdg: Arc<ProgramDependenceGraph>) -> Self {
Self { pdg }
}
pub fn check_references(
&self,
changes: &[ResolvedEditChange],
) -> Result<Vec<ReferenceIssue>, ValidationError> {
let mut issues = Vec::new();
for change in changes {
let imports = self.extract_imports(change);
for import in imports {
if !self.import_exists_in_pdg(&import) {
issues.push(ReferenceIssue::broken_import(
import,
change.file_path.clone(),
Location { line: 1, column: 1 },
));
}
}
let undefined = self.find_undefined_references(change);
issues.extend(undefined);
}
issues.extend(self.check_for_cycles(changes)?);
Ok(issues)
}
fn extract_imports(&self, change: &ResolvedEditChange) -> Vec<String> {
let mut imports = Vec::new();
let lang = change.infer_language();
match lang {
"python" => {
for line in change.new_content.lines() {
let line = line.trim();
if line.starts_with("import ") || line.starts_with("from ") {
if let Some(rest) = line.strip_prefix("import ") {
let import_path = rest.split(" as ").next().unwrap_or(rest).trim();
imports.push(import_path.to_string());
} else if let Some(rest) = line.strip_prefix("from ") {
let import_path = rest.split(" import ").next().unwrap_or(rest).trim();
imports.push(import_path.to_string());
}
}
}
}
"javascript" | "typescript" => {
for line in change.new_content.lines() {
let line = line.trim();
if line.contains("import ") && line.contains("from ") {
if let Some(start) = line.find("from ") {
let rest = &line[start + 5..];
let quote = rest.chars().next();
if let Some('"') | Some('\'') = quote {
if let Some(end) = rest[1..].find(quote.unwrap()) {
imports.push(rest[1..end + 1].to_string());
}
}
}
} else if line.contains("require(") {
if let Some(start) = line.find("require(") {
let rest = &line[start + 8..]; if let Some(end) = rest.find(')') {
let inner = &rest[..end];
let inner = inner.trim();
if inner.starts_with('"') || inner.starts_with('\'') {
imports.push(inner[1..inner.len() - 1].to_string());
}
}
}
}
}
}
"rust" => {
for line in change.new_content.lines() {
let line = line.trim();
if line.starts_with("use ") {
let import_path = line
.trim_start_matches("use ")
.trim_end_matches(';')
.trim()
.to_string();
imports.push(import_path);
} else if line.starts_with("mod ") {
let mod_name = line
.trim_start_matches("mod ")
.trim_end_matches(';')
.trim()
.to_string();
imports.push(mod_name);
}
}
}
"go" => {
for line in change.new_content.lines() {
let line = line.trim();
if line.starts_with("\"") && line.contains("\"") {
let import_path = line.trim_matches('"');
imports.push(import_path.to_string());
}
}
}
_ => {
}
}
imports
}
fn import_exists_in_pdg(&self, import: &str) -> bool {
let import_lower = import.to_lowercase();
for node_id in self.pdg.node_indices() {
if let Some(node) = self.pdg.get_node(node_id) {
let node_name_lower = node.name.to_lowercase();
if node_name_lower.contains(&import_lower)
|| import_lower.contains(&node_name_lower)
{
return true;
}
}
}
for node_id in self.pdg.node_indices() {
if let Some(node) = self.pdg.get_node(node_id) {
let file_path_lower = node.file_path.to_lowercase();
if file_path_lower.contains(&import_lower) {
return true;
}
}
}
false
}
fn find_undefined_references(&self, change: &ResolvedEditChange) -> Vec<ReferenceIssue> {
let mut issues = Vec::new();
let lang = change.infer_language();
match lang {
"python" => {
for (line_num, line) in change.new_content.lines().enumerate() {
let calls = self.extract_python_function_calls(line);
for call in calls {
if !self.symbol_exists_in_pdg(&call) {
issues.push(ReferenceIssue::undefined_reference(
call,
change.file_path.clone(),
Location {
line: line_num + 1,
column: 1,
},
));
}
}
}
}
_ => {
}
}
issues
}
fn extract_python_function_calls(&self, line: &str) -> Vec<String> {
let mut calls = Vec::new();
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i].is_alphabetic() || chars[i] == '_' {
let start = i;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
let name: String = chars[start..i].iter().collect();
if i < chars.len() && chars[i] == '(' {
if !self.is_python_builtin(&name) {
calls.push(name);
}
}
} else {
i += 1;
}
}
calls
}
fn is_python_builtin(&self, name: &str) -> bool {
const BUILTINS: &[&str] = &[
"print",
"len",
"str",
"int",
"float",
"list",
"dict",
"set",
"tuple",
"range",
"enumerate",
"zip",
"map",
"filter",
"sorted",
"reversed",
"sum",
"min",
"max",
"abs",
"all",
"any",
"bool",
"type",
"isinstance",
"open",
"input",
"exit",
"quit",
"help",
"dir",
"vars",
"id",
"super",
"self",
"cls",
"None",
"True",
"False",
"await",
"async",
"if",
"else",
"elif",
"for",
"while",
"def",
"class",
"return",
"yield",
"import",
"from",
"as",
"with",
"try",
"except",
"finally",
"raise",
"assert",
"pass",
"break",
"continue",
"and",
"or",
"not",
"in",
"is",
"lambda",
"global",
"nonlocal",
"del",
];
BUILTINS.contains(&name)
}
fn symbol_exists_in_pdg(&self, symbol: &str) -> bool {
self.pdg.find_by_symbol(symbol).is_some()
}
fn check_for_cycles(
&self,
changes: &[ResolvedEditChange],
) -> Result<Vec<ReferenceIssue>, ValidationError> {
let mut issues = Vec::new();
let mut deps: HashMap<String, Vec<String>> = HashMap::new();
let mut visited: HashSet<String> = HashSet::new();
let mut rec_stack: HashSet<String> = HashSet::new();
for change in changes {
let file_name = change.file_path.to_string_lossy().to_string();
let imports = self.extract_imports(change);
deps.insert(file_name.clone(), imports);
}
for node in deps.keys() {
if !visited.contains(node) {
if let Some(cycle) = self.detect_cycle(node, &deps, &mut visited, &mut rec_stack) {
issues.push(ReferenceIssue::cyclic_dependency(
cycle,
PathBuf::from(node),
));
}
}
}
Ok(issues)
}
fn detect_cycle(
&self,
node: &str,
deps: &HashMap<String, Vec<String>>,
visited: &mut HashSet<String>,
rec_stack: &mut HashSet<String>,
) -> Option<Vec<String>> {
visited.insert(node.to_string());
rec_stack.insert(node.to_string());
if let Some(neighbors) = deps.get(node) {
for neighbor in neighbors {
if !visited.contains(neighbor) {
if let Some(cycle) = self.detect_cycle(neighbor, deps, visited, rec_stack) {
let mut result = vec![node.to_string()];
result.extend(cycle);
return Some(result);
}
} else if rec_stack.contains(neighbor) {
return Some(vec![neighbor.to_string(), node.to_string()]);
}
}
}
rec_stack.remove(node);
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[test]
fn test_reference_issue_broken_import() {
let issue = ReferenceIssue::broken_import(
"missing_module".to_string(),
PathBuf::from("test.py"),
Location { line: 1, column: 1 },
);
assert!(matches!(
issue.issue_type,
ReferenceIssueType::BrokenImport { .. }
));
assert_eq!(issue.file_path, PathBuf::from("test.py"));
}
#[test]
fn test_reference_issue_undefined_reference() {
let issue = ReferenceIssue::undefined_reference(
"undefined_func".to_string(),
PathBuf::from("test.py"),
Location {
line: 5,
column: 10,
},
);
assert!(matches!(
issue.issue_type,
ReferenceIssueType::UndefinedReference { .. }
));
}
#[test]
fn test_reference_issue_cyclic_dependency() {
let cycle = vec!["a".to_string(), "b".to_string(), "a".to_string()];
let issue = ReferenceIssue::cyclic_dependency(cycle, PathBuf::from("test.py"));
assert!(matches!(
issue.issue_type,
ReferenceIssueType::CyclicDependency { .. }
));
assert!(issue.description.contains("a -> b -> a"));
}
#[test]
fn test_reference_checker_new() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let _checker = ReferenceChecker::new(pdg);
assert!(true);
}
#[test]
fn test_extract_imports_python() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let checker = ReferenceChecker::new(pdg);
let change = ResolvedEditChange::new(
PathBuf::from("test.py"),
String::new(),
"import os\nimport sys\nfrom collections import defaultdict\n".to_string(),
);
let imports = checker.extract_imports(&change);
assert_eq!(imports, vec!["os", "sys", "collections"]);
}
#[test]
fn test_extract_imports_javascript() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let checker = ReferenceChecker::new(pdg);
let change = ResolvedEditChange::new(
PathBuf::from("test.js"),
String::new(),
"import { foo } from 'bar';\nconst baz = require('qux');\n".to_string(),
);
let imports = checker.extract_imports(&change);
assert_eq!(imports.len(), 2);
}
#[test]
fn test_extract_imports_rust() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let checker = ReferenceChecker::new(pdg);
let change = ResolvedEditChange::new(
PathBuf::from("test.rs"),
String::new(),
"use std::collections::HashMap;\nmod my_module;\n".to_string(),
);
let imports = checker.extract_imports(&change);
assert_eq!(imports, vec!["std::collections::HashMap", "my_module"]);
}
#[test]
fn test_is_python_builtin() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let checker = ReferenceChecker::new(pdg);
assert!(checker.is_python_builtin("print"));
assert!(checker.is_python_builtin("len"));
assert!(checker.is_python_builtin("if"));
assert!(!checker.is_python_builtin("my_function"));
}
#[test]
fn test_extract_python_function_calls() {
let pdg = Arc::new(ProgramDependenceGraph::new());
let checker = ReferenceChecker::new(pdg);
let calls = checker.extract_python_function_calls("x = foo() + bar()");
assert_eq!(calls, vec!["foo", "bar"]);
let calls = checker.extract_python_function_calls("print('hello')");
assert!(calls.is_empty());
let calls = checker.extract_python_function_calls("my_func()");
assert_eq!(calls, vec!["my_func"]);
}
#[test]
fn test_reference_issue_type_equality() {
assert_eq!(
ReferenceIssueType::BrokenImport {
symbol: "foo".to_string()
},
ReferenceIssueType::BrokenImport {
symbol: "foo".to_string()
}
);
assert_ne!(
ReferenceIssueType::BrokenImport {
symbol: "foo".to_string()
},
ReferenceIssueType::UndefinedReference {
name: "foo".to_string()
}
);
}
}