use crate::detectors::base::{Detector, DetectorConfig};
use crate::graph::GraphQueryExt;
use crate::models::{Finding, Severity};
use anyhow::Result;
use regex::Regex;
use std::collections::HashSet;
use std::path::PathBuf;
use std::sync::LazyLock;
use tracing::info;
static INFINITE_WHILE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(while\s*\(\s*true\s*\)|while\s+True\s*:|while\s*\(\s*1\s*\)|for\s*\(\s*;\s*;\s*\)|\bloop\s*\{)").expect("valid regex")
});
static BREAK_RETURN: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"\b(break|return|raise|throw|exit|panic!|std::process::exit)\b")
.expect("valid regex")
});
pub struct InfiniteLoopDetector {
config: DetectorConfig,
#[allow(dead_code)] repository_path: PathBuf,
max_findings: usize,
}
impl InfiniteLoopDetector {
pub fn new() -> Self {
Self {
config: DetectorConfig::new(),
repository_path: PathBuf::from("."),
max_findings: 50,
}
}
pub fn with_path(repository_path: impl Into<PathBuf>) -> Self {
Self {
config: DetectorConfig::new(),
repository_path: repository_path.into(),
max_findings: 50,
}
}
fn has_exit_in_body(lines: &[&str], loop_start: usize, indent: usize) -> bool {
for line in lines.iter().skip(loop_start + 1) {
let current_indent = line.chars().take_while(|c| c.is_whitespace()).count();
if !line.trim().is_empty() && current_indent <= indent {
break;
}
if BREAK_RETURN.is_match(line) {
return true;
}
}
false
}
fn is_intentional_loop(lines: &[&str], loop_start: usize, path: &str) -> bool {
let path_lower = path.to_lowercase();
if path_lower.contains("server")
|| path_lower.contains("main")
|| path_lower.contains("daemon")
|| path_lower.contains("worker")
|| path_lower.contains("event")
|| path_lower.contains("run")
|| path_lower.contains("loop")
|| path_lower.contains("poll")
|| path_lower.contains("listen")
|| path_lower.contains("serve")
|| path_lower.contains("dispatch")
|| path_lower.contains("scheduler")
|| path_lower.contains("executor")
|| path_lower.contains("runtime")
|| path_lower.contains("pier")
|| path_lower.contains("king")
|| path_lower.contains("lord")
|| path_lower.contains("serf")
|| path_lower.contains("vere")
{
return true;
}
let start = loop_start.saturating_sub(5);
for line in lines.get(start..loop_start).unwrap_or(&[]) {
let lower = line.to_lowercase();
if lower.contains("server")
|| lower.contains("main loop")
|| lower.contains("event loop")
|| lower.contains("forever")
|| lower.contains("daemon")
{
return true;
}
}
for line in lines.iter().skip(loop_start).take(30) {
let lower = line.to_lowercase();
if lower.contains("accept(")
|| lower.contains("recv(")
|| lower.contains("listen")
|| lower.contains("await")
|| lower.contains("poll(")
|| lower.contains("select(")
|| lower.contains("epoll")
|| lower.contains("kqueue")
|| lower.contains("read(")
|| lower.contains("write(")
|| lower.contains("getchar")
|| lower.contains("fgets(")
|| lower.contains("scanf")
{
return true;
}
if lower.contains("sleep(")
|| lower.contains("usleep")
|| lower.contains("nanosleep")
|| lower.contains("wait(")
|| lower.contains("waitpid")
|| lower.contains("pthread_cond_wait")
|| lower.contains("sem_wait")
|| lower.contains("mutex_lock")
|| lower.contains("condition_variable")
{
return true;
}
if lower.contains("event")
|| lower.contains("message")
|| lower.contains("signal")
|| lower.contains("dispatch")
|| lower.contains("handler")
|| lower.contains("callback")
|| lower.contains("queue")
{
return true;
}
if lower.contains("u3_pier")
|| lower.contains("_pier_work")
|| lower.contains("_king_")
|| lower.contains("_lord_")
|| lower.contains("_serf_")
{
return true;
}
}
false
}
fn find_called_functions(lines: &[&str], loop_start: usize, indent: usize) -> Vec<String> {
let call_re = Regex::new(r"\b([a-zA-Z_][a-zA-Z0-9_]*)\s*\(").expect("valid regex");
let mut calls = Vec::new();
for line in lines.iter().skip(loop_start + 1) {
let current_indent = line.chars().take_while(|c| c.is_whitespace()).count();
if !line.trim().is_empty() && current_indent <= indent {
break;
}
for cap in call_re.captures_iter(line) {
if let Some(m) = cap.get(1) {
let name = m.as_str();
if !["if", "while", "for", "print", "len"].contains(&name) {
calls.push(name.to_string());
}
}
}
}
calls
}
fn calls_exit_function(
calls: &[String],
func_by_name: &std::collections::HashMap<String, &crate::graph::CodeNode>,
) -> Vec<String> {
let i = crate::graph::interner::global_interner();
let mut exit_funcs = Vec::new();
for call in calls {
if let Some(func) = func_by_name.get(call) {
if let Ok(content) = std::fs::read_to_string(func.path(i)) {
let lines: Vec<&str> = content.lines().collect();
let start = func.line_start.saturating_sub(1) as usize;
let end = (func.line_end as usize).min(lines.len());
for line in lines.get(start..end).unwrap_or(&[]) {
if line.contains("raise")
|| line.contains("return")
|| line.contains("exit")
|| line.contains("sys.exit")
{
exit_funcs.push(call.clone());
break;
}
}
}
}
}
exit_funcs
}
}
impl Default for InfiniteLoopDetector {
fn default() -> Self {
Self::new()
}
}
impl Detector for InfiniteLoopDetector {
fn name(&self) -> &'static str {
"InfiniteLoopDetector"
}
fn description(&self) -> &'static str {
"Detects potential infinite loops (while True without break)"
}
fn requires_graph(&self) -> bool {
true
}
fn config(&self) -> Option<&DetectorConfig> {
Some(&self.config)
}
fn file_extensions(&self) -> &'static [&'static str] {
&[
"py", "js", "ts", "jsx", "tsx", "java", "go", "rs", "c", "cpp", "cs",
]
}
fn detect(
&self,
ctx: &crate::detectors::analysis_context::AnalysisContext,
) -> Result<Vec<Finding>> {
let graph = ctx.graph;
let files = &ctx.as_file_provider();
let mut findings = vec![];
let all_functions = graph.get_functions_shared();
let mut func_by_name: Option<std::collections::HashMap<String, &crate::graph::CodeNode>> =
None;
for path in
files.files_with_extensions(&["py", "js", "ts", "java", "go", "rs", "rb", "c", "cpp"])
{
if findings.len() >= self.max_findings {
break;
}
let path_str = path.to_string_lossy().to_string();
if crate::detectors::base::is_test_path(&path_str) {
continue;
}
if path_str.contains("/detectors/") {
continue;
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let raw = match files.content(path) {
Some(c) => c,
None => continue,
};
if !raw.contains("while True")
&& !raw.contains("while(true")
&& !raw.contains("while (true")
&& !raw.contains("while(1)")
&& !raw.contains("while (1)")
&& !raw.contains("for(;;")
&& !raw.contains("for (;;")
&& !raw.contains("loop {")
{
continue;
}
if let Some(content) = files.masked_content(path) {
let lines: Vec<&str> = content.lines().collect();
for (i, line) in lines.iter().enumerate() {
let prev_line = if i > 0 { Some(lines[i - 1]) } else { None };
if crate::detectors::is_line_suppressed(line, prev_line) {
continue;
}
if INFINITE_WHILE.is_match(line) {
let indent = line.chars().take_while(|c| c.is_whitespace()).count();
let has_direct_exit = Self::has_exit_in_body(&lines, i, indent);
let is_intentional = Self::is_intentional_loop(&lines, i, &path_str);
if is_intentional {
continue;
}
if matches!(ext, "c" | "cpp" | "h" | "hpp") {
let trimmed = line.trim();
if trimmed.starts_with("for") && trimmed.contains(";;") {
let body_lines = lines
.get(i..std::cmp::min(i + 20, lines.len()))
.unwrap_or(&[]);
let has_comparison = body_lines.iter().any(|l| {
l.contains("< ")
|| l.contains("> ")
|| l.contains("<= ")
|| l.contains(">= ")
|| l.contains("!= ")
});
if has_comparison {
continue; }
}
}
let calls = Self::find_called_functions(&lines, i, indent);
let map = func_by_name.get_or_insert_with(|| {
let gi = crate::graph::interner::global_interner();
all_functions
.iter()
.map(|f| (f.node_name(gi).to_string(), f))
.collect()
});
let exit_funcs = Self::calls_exit_function(&calls, map);
let has_exit = has_direct_exit || !exit_funcs.is_empty();
if has_exit {
continue;
}
let mut notes = Vec::new();
if !calls.is_empty() {
let call_list: Vec<_> = calls.iter().take(5).cloned().collect();
notes.push(format!("📞 Calls: {}", call_list.join(", ")));
}
notes.push("⚠️ No break/return found in loop body".to_string());
let context_notes = format!("\n\n**Analysis:**\n{}", notes.join("\n"));
let suggested_fix = match ext {
"rs" => "Options:\n\
1. Add a break condition\n\
2. Add a return statement\n\
3. If intentional, add a comment: // Intentional infinite loop\n\n\
Example:\n\
```rust\n\
loop {\n\
let data = get_data();\n\
if data.is_none() {\n\
break; // Exit condition\n\
}\n\
process(data.unwrap());\n\
}\n\
```".to_string(),
"c" | "cpp" => "Options:\n\
1. Add a break condition\n\
2. Add a return statement\n\
3. If intentional, add a comment: /* Intentional infinite loop */\n\n\
Example:\n\
```c\n\
while (1) {\n\
data = get_data();\n\
if (data == NULL) {\n\
break; /* Exit condition */\n\
}\n\
process(data);\n\
}\n\
```".to_string(),
"java" | "go" => "Options:\n\
1. Add a break condition\n\
2. Add a return statement\n\
3. If intentional, add a comment\n\n\
Example:\n\
```\n\
while (true) {\n\
data = getData();\n\
if (data == null) {\n\
break; // Exit condition\n\
}\n\
process(data);\n\
}\n\
```".to_string(),
"js" | "ts" => "Options:\n\
1. Add a break condition\n\
2. Add a return statement\n\
3. If intentional, add a comment: // Intentional infinite loop\n\n\
Example:\n\
```javascript\n\
while (true) {\n\
const data = getData();\n\
if (!data) break; // Exit condition\n\
process(data);\n\
}\n\
```".to_string(),
_ => "Options:\n\
1. Add a break condition\n\
2. Add a return statement\n\
3. If intentional, add a comment: # Intentional infinite loop\n\n\
Example:\n\
```python\n\
while True:\n\
data = get_data()\n\
if data is None:\n\
break # Exit condition\n\
process(data)\n\
```".to_string(),
};
findings.push(Finding {
id: String::new(),
detector: "InfiniteLoopDetector".to_string(),
severity: Severity::High,
title: "Potential infinite loop".to_string(),
description: format!(
"Loop with no apparent exit condition detected.{}",
context_notes
),
affected_files: vec![path.to_path_buf()],
line_start: Some((i + 1) as u32),
line_end: Some((i + 1) as u32),
suggested_fix: Some(suggested_fix),
estimated_effort: Some("10 minutes".to_string()),
category: Some("bug-risk".to_string()),
cwe_id: Some("CWE-835".to_string()),
why_it_matters: Some(
"Infinite loops without exit conditions will hang the program \
and consume 100% CPU. Even intentional infinite loops (servers) \
should have shutdown mechanisms."
.to_string(),
),
..Default::default()
});
}
}
}
}
info!(
"InfiniteLoopDetector found {} findings (graph-aware)",
findings.len()
);
Ok(findings)
}
}
impl crate::detectors::RegisteredDetector for InfiniteLoopDetector {
fn create(init: &crate::detectors::DetectorInit) -> std::sync::Arc<dyn Detector> {
std::sync::Arc::new(Self::with_path(init.repo_path))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::builder::GraphBuilder;
#[test]
fn test_detects_while_true_without_break() {
let store = GraphBuilder::new().freeze();
let detector = InfiniteLoopDetector::with_path("/mock/repo");
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(
&store,
vec![(
"processor.py",
"\ndef process():\n while True:\n do_something()\n",
)],
);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
!findings.is_empty(),
"Should detect while True without break"
);
assert!(findings.iter().any(|f| f.title.contains("infinite loop")));
}
#[test]
fn test_no_finding_for_while_true_with_break() {
let store = GraphBuilder::new().freeze();
let detector = InfiniteLoopDetector::with_path("/mock/repo");
let ctx = crate::detectors::analysis_context::AnalysisContext::test_with_mock_files(&store, vec![
("processor.py", "\ndef process():\n while True:\n data = get_data()\n if data is None:\n break\n handle(data)\n"),
]);
let findings = detector.detect(&ctx).expect("detection should succeed");
assert!(
findings.is_empty(),
"Should not flag while True that has a break, but got: {:?}",
findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
}
}