use std::collections::HashMap;
use std::fs;
use serde_json::{json, Value};
use crate::mcp::context::AnalysisContext;
pub fn execute(
args: &HashMap<String, Value>,
context: Option<&AnalysisContext>,
) -> Result<Value, String> {
let file = args
.get("file")
.and_then(|v| v.as_str())
.ok_or("Missing required argument 'file'")?;
let line = args
.get("line")
.and_then(|v| v.as_u64())
.ok_or("Missing required argument 'line'")? as usize;
let rule_id = args.get("rule_id").and_then(|v| v.as_str());
let source =
fs::read_to_string(file).map_err(|e| format!("Failed to read file '{}': {}", file, e))?;
let lines: Vec<&str> = source.lines().collect();
let total_lines = lines.len();
if line == 0 || line > total_lines {
return Err(format!(
"Line {} is out of range (file has {} lines)",
line, total_lines
));
}
let context_radius = 10;
let start = line.saturating_sub(context_radius).max(1);
let end = (line + context_radius).min(total_lines);
let mut context_lines = Vec::new();
for i in start..=end {
let prefix = if i == line { ">>> " } else { " " };
context_lines.push(format!("{}{:>4} | {}", prefix, i, lines[i - 1]));
}
let code_context = context_lines.join("\n");
let db = crate::rules::RuleDatabase::with_defaults();
let rule_info = if let Some(id) = rule_id {
db.get_rule(id).map(|rule| {
json!({
"id": rule.id,
"title": rule.name,
"description": rule.description,
"severity": format!("{}", rule.severity),
"cwe": rule.cwe,
"owasp": rule.owasp,
"confidence": format!("{}", rule.confidence),
"tags": rule.tags,
})
})
} else {
None
};
let suggested_fix = if let Some(id) = rule_id {
db.get_rule(id).and_then(|r| r.fix_suggestion.clone())
} else {
None
};
let potential_rules: Vec<Value> = if rule_id.is_none() {
db.all_rules()
.iter()
.filter(|r| r.enabled)
.filter(|r| {
let line_content = lines[line - 1];
match r.id.as_str() {
"SEC001" => line_content.contains("execute") || line_content.contains("query"),
"SEC002" => {
line_content.contains("innerHTML")
|| line_content.contains("document.write")
}
"SEC003" => line_content.contains("os.system") || line_content.contains("exec"),
"SEC004" => {
line_content.contains("password")
|| line_content.contains("secret")
|| line_content.contains("api_key")
}
"SEC005" => line_content.contains("open") && line_content.contains("request"),
"SEC006" => {
line_content.contains("pickle") || line_content.contains("yaml.unsafe_load")
}
"SEC007" => line_content.contains("md5") || line_content.contains("sha1"),
_ => false,
}
})
.map(|r| {
json!({
"id": r.id,
"title": r.name,
"severity": format!("{}", r.severity),
"description": r.description,
})
})
.collect()
} else {
Vec::new()
};
let dead_code_info = context.and_then(|ctx| explain_dead_code(ctx, file, line));
let effective_rule = rule_info.or_else(|| {
dead_code_info.as_ref().map(|dc| {
json!({
"id": "DEAD-CODE",
"title": "Unreachable code",
"description": dc.reason,
"severity": "info",
"confidence": dc.confidence,
"tags": ["dead-code", "maintainability"],
})
})
});
let effective_fix =
suggested_fix.or_else(|| dead_code_info.as_ref().map(|dc| dc.suggested_fix.clone()));
let result = json!({
"file": file,
"line": line,
"code_context": code_context,
"rule": effective_rule,
"suggested_fix": effective_fix,
"potential_rules": potential_rules,
"total_file_lines": total_lines,
"dead_code_analysis": dead_code_info.as_ref().map(|dc| json!({
"function_name": dc.function_name,
"is_reachable": dc.is_reachable,
"callers": dc.callers,
"callees": dc.callees,
"attributes": dc.attributes,
"reason": dc.reason,
})),
});
Ok(json!({
"content": [{
"type": "text",
"text": serde_json::to_string_pretty(&result).unwrap_or_default()
}]
}))
}
struct DeadCodeInfo {
function_name: String,
is_reachable: bool,
callers: Vec<String>,
callees: Vec<String>,
attributes: Vec<String>,
reason: String,
suggested_fix: String,
confidence: String,
}
fn explain_dead_code(ctx: &AnalysisContext, file: &str, line: usize) -> Option<DeadCodeInfo> {
let target_node = ctx
.graph
.nodes()
.filter(|(_, node)| {
(node.location.file.ends_with(file)
|| file.ends_with(&node.location.file)
|| node.location.file == file)
&& node.location.line_start <= line
&& node.location.line_end >= line
})
.min_by_key(|(_, node)| node.location.line_end - node.location.line_start);
let found = target_node.or_else(|| {
ctx.graph.nodes().find(|(_, node)| {
(node.location.file.ends_with(file)
|| file.ends_with(&node.location.file)
|| node.location.file == file)
&& node.location.line_start == line
})
});
let (node_idx, node) = match found {
Some(pair) => pair,
None => {
let file_has_nodes = ctx.graph.nodes().any(|(_, n)| {
n.location.file.ends_with(file)
|| file.ends_with(&n.location.file)
|| n.location.file == file
});
let reason = if file_has_nodes {
format!(
"No function/class definition found at line {}. \
The line may be inside a function body, or the code may not have been \
recognized as a definition by the parser.",
line
)
} else {
format!(
"No analysis data for file '{}'. The file may not have been included \
in the analysis scan, or its language may not be supported.",
file
)
};
return Some(DeadCodeInfo {
function_name: String::new(),
is_reachable: false,
callers: Vec::new(),
callees: Vec::new(),
attributes: Vec::new(),
reason,
suggested_fix: "Run a full analysis scan to ensure this file is included."
.to_string(),
confidence: "unknown".to_string(),
});
}
};
if node.name.starts_with("<module:") {
return None;
}
let entry_points: Vec<_> = ctx.graph.entry_points().iter().copied().collect();
let is_reachable = entry_points.iter().any(|&ep| {
let mut visited = std::collections::HashSet::new();
let mut queue = std::collections::VecDeque::new();
queue.push_back(ep);
visited.insert(ep);
while let Some(current) = queue.pop_front() {
if current == node_idx {
return true;
}
for neighbor in ctx.graph.calls_from(current) {
if visited.insert(neighbor) {
queue.push_back(neighbor);
}
}
}
false
});
let callers: Vec<String> = ctx
.graph
.callers_of(node_idx)
.filter_map(|caller_idx| {
ctx.graph.get_node(caller_idx).map(|n| {
format!(
"{}() at {}:{}",
n.name, n.location.file, n.location.line_start
)
})
})
.collect();
let callees: Vec<String> = ctx
.graph
.calls_from(node_idx)
.filter_map(|callee_idx| {
ctx.graph.get_node(callee_idx).map(|n| {
format!(
"{}() at {}:{}",
n.name, n.location.file, n.location.line_start
)
})
})
.collect();
let attributes = node.attributes.clone();
let reason = if is_reachable {
format!(
"{}() is reachable from entry points and is NOT dead code.",
node.name
)
} else if callers.is_empty() {
format!(
"{}() has zero callers and is not reachable from any entry point. \
No other function in the project calls this function.",
node.name
)
} else {
let caller_names: Vec<&str> = callers.iter().map(|s| s.as_str()).collect();
format!(
"{}() has {} caller(s) ({}) but none of them are reachable from entry points. \
The entire call chain is disconnected from the application's execution flow.",
node.name,
callers.len(),
caller_names.join(", ")
)
};
let has_trait_impl = attributes.iter().any(|a| a.starts_with("impl_trait:"));
let has_extends = attributes
.iter()
.any(|a| a.starts_with("extends:") || a.starts_with("implements:"));
let is_test = node.is_test || attributes.iter().any(|a| a == "test" || a == "cfg_test");
let suggested_fix = if is_reachable {
"This function is reachable and not dead code. No action needed.".to_string()
} else if is_test {
format!(
"{}() appears to be a test function. It may be invoked by the test runner \
and not directly from application code. Consider if this is a false positive.",
node.name
)
} else if has_trait_impl {
let trait_name = attributes
.iter()
.find(|a| a.starts_with("impl_trait:"))
.map(|a| &a["impl_trait:".len()..])
.unwrap_or("unknown");
format!(
"{}() implements trait '{}'. It may be called via dynamic dispatch (dyn {}) \
or generic bounds. Verify it's not needed before removing.",
node.name, trait_name, trait_name
)
} else if has_extends {
format!(
"{}() overrides a parent class method. It may be called via polymorphic dispatch. \
Verify it's not needed before removing.",
node.name
)
} else if callers.is_empty() {
format!(
"Remove {}() and its associated code. No callers reference this function.",
node.name
)
} else {
format!(
"The callers of {}() are themselves dead. Consider removing the entire \
dead call chain: {}",
node.name,
callers.join(", ")
)
};
let confidence = if is_reachable {
"not_dead"
} else if has_trait_impl || has_extends || is_test {
"low"
} else if callers.is_empty() {
"high"
} else {
"medium"
};
Some(DeadCodeInfo {
function_name: node.name.clone(),
is_reachable,
callers,
callees,
attributes,
reason,
suggested_fix,
confidence: confidence.to_string(),
})
}