Skip to content

Commit abd7fd7

Browse files
Add cycle detection for message references
Implements cycle detection using depth-first search to find cyclic references between messages and terms. - detect_cycles: Analyzes all messages/terms in a resource to find cycles - has_cycle: DFS algorithm that tracks visited nodes and current path Prevents infinite loops at runtime by detecting them at compile time.
1 parent 8934a2f commit abd7fd7

File tree

1 file changed

+80
-0
lines changed

1 file changed

+80
-0
lines changed

src/lib.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,86 @@ fn check_expression_references(
524524
}
525525
}
526526

527+
/// Helper function to detect cycles in message references
528+
fn detect_cycles(resource: &FluentResource) -> Vec<ValidationError> {
529+
use fluent_syntax::ast;
530+
use std::collections::{HashMap, HashSet};
531+
532+
let mut errors = Vec::new();
533+
534+
// Build a map of message IDs to their referenced IDs
535+
let mut message_refs: HashMap<String, Vec<String>> = HashMap::new();
536+
537+
for entry in resource.entries() {
538+
match entry {
539+
ast::Entry::Message(msg) => {
540+
let msg_id = msg.id.name.to_string();
541+
let mut refs = Vec::new();
542+
collect_references(&msg.value, &mut refs);
543+
for attr in &msg.attributes {
544+
collect_references(&Some(attr.value.clone()), &mut refs);
545+
}
546+
message_refs.insert(msg_id, refs);
547+
}
548+
ast::Entry::Term(term) => {
549+
let term_id = format!("-{}", term.id.name);
550+
let mut refs = Vec::new();
551+
collect_references(&Some(term.value.clone()), &mut refs);
552+
for attr in &term.attributes {
553+
collect_references(&Some(attr.value.clone()), &mut refs);
554+
}
555+
message_refs.insert(term_id, refs);
556+
}
557+
_ => {}
558+
}
559+
}
560+
561+
// Check each message for cycles using DFS
562+
for (msg_id, _) in &message_refs {
563+
let mut visited = HashSet::new();
564+
let mut path = Vec::new();
565+
if has_cycle(msg_id, &message_refs, &mut visited, &mut path) {
566+
errors.push(ValidationError {
567+
error_type: "CyclicReference".to_string(),
568+
message: format!("Cyclic reference detected: {}", path.join(" -> ")),
569+
message_id: Some(msg_id.clone()),
570+
reference: None,
571+
});
572+
}
573+
}
574+
575+
errors
576+
}
577+
578+
fn has_cycle(
579+
msg_id: &str,
580+
message_refs: &HashMap<String, Vec<String>>,
581+
visited: &mut HashSet<String>,
582+
path: &mut Vec<String>,
583+
) -> bool {
584+
if visited.contains(msg_id) {
585+
// Found a cycle - add the current message to show where cycle completes
586+
path.push(msg_id.to_string());
587+
return true;
588+
}
589+
590+
visited.insert(msg_id.to_string());
591+
path.push(msg_id.to_string());
592+
593+
// Check all referenced messages
594+
if let Some(refs) = message_refs.get(msg_id) {
595+
for ref_id in refs {
596+
if has_cycle(ref_id, message_refs, visited, path) {
597+
return true;
598+
}
599+
}
600+
}
601+
602+
path.pop();
603+
visited.remove(msg_id);
604+
false
605+
}
606+
527607
#[pymodule]
528608
mod rustfluent {
529609
use super::*;

0 commit comments

Comments
 (0)