use super::state::AnalysisSnapshot;
use super::uri::file_uri_for_path;
use super::{
COPY_CONTEXT_COMMAND, COPY_SUGGESTED_ASSERTION_COMMAND, COPY_TARGETED_TEST_BRIEF_COMMAND,
OPEN_RELATED_TEST_COMMAND, REFRESH_COMMAND,
};
use crate::analysis::ClassifiedSeam;
use crate::analysis::test_grip_evidence::{RelatedTestGrip, RelationConfidence};
use crate::domain::OracleStrength;
use crate::output::agent_seam_packets::{
suggested_assertion_for_classified_seam, targeted_test_brief_for_classified_seam,
};
use std::path::PathBuf;
use tower_lsp_server::ls_types::{
CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, Command,
Diagnostic, LSPAny,
};
pub(super) fn code_action_response(
params: &CodeActionParams,
snapshot: Option<&AnalysisSnapshot>,
) -> CodeActionResponse {
let mut actions = Vec::new();
if let Some(context) = seam_action_context(params, snapshot) {
push_seam_actions(&mut actions, params, snapshot, context);
}
if let Some(diagnostic) = params
.context
.diagnostics
.iter()
.find(|d| is_ripr_diagnostic(d) && !is_seam_diagnostic(d))
{
actions.push(copy_context_action(
"Copy ripr context packet",
"Copy ripr context",
copy_context_target(params, diagnostic),
));
}
actions.push(CodeActionOrCommand::CodeAction(CodeAction {
title: "Refresh ripr analysis".to_string(),
kind: Some(CodeActionKind::SOURCE),
command: Some(Command {
title: "Refresh ripr analysis".to_string(),
command: REFRESH_COMMAND.to_string(),
arguments: Some(Vec::new()),
}),
..CodeAction::default()
}));
actions
}
struct SeamActionContext<'a> {
diagnostic: &'a Diagnostic,
seam: &'a ClassifiedSeam,
}
fn seam_action_context<'a>(
params: &'a CodeActionParams,
snapshot: Option<&'a AnalysisSnapshot>,
) -> Option<SeamActionContext<'a>> {
let snapshot = snapshot?;
params
.context
.diagnostics
.iter()
.filter(|d| is_ripr_diagnostic(d) && is_seam_diagnostic(d))
.find_map(|diagnostic| {
snapshot
.classified_seam_for_diagnostic(diagnostic)
.map(|seam| SeamActionContext { diagnostic, seam })
})
}
fn push_seam_actions(
actions: &mut CodeActionResponse,
params: &CodeActionParams,
snapshot: Option<&AnalysisSnapshot>,
context: SeamActionContext<'_>,
) {
actions.push(copy_context_action(
"Copy seam packet",
"Copy seam packet",
copy_seam_packet_target(params, context.diagnostic, context.seam),
));
actions.push(copy_targeted_test_brief_action(
context.seam,
targeted_test_brief_for_classified_seam(context.seam),
));
if let Some(assertion) = suggested_assertion_for_classified_seam(context.seam) {
actions.push(copy_suggested_assertion_action(context.seam, assertion));
}
if let Some(snapshot) = snapshot
&& let Some(related) = best_related_test_for_editor(context.seam)
&& let Some(target) = related_test_target(snapshot, related)
{
actions.push(open_related_test_action(target));
}
}
fn copy_context_action(title: &str, command_title: &str, target: LSPAny) -> CodeActionOrCommand {
CodeActionOrCommand::CodeAction(CodeAction {
title: title.to_string(),
kind: Some(CodeActionKind::QUICKFIX),
command: Some(Command {
title: command_title.to_string(),
command: COPY_CONTEXT_COMMAND.to_string(),
arguments: Some(vec![target]),
}),
..CodeAction::default()
})
}
fn copy_targeted_test_brief_action(seam: &ClassifiedSeam, brief: String) -> CodeActionOrCommand {
CodeActionOrCommand::CodeAction(CodeAction {
title: "Copy targeted test brief".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
command: Some(Command {
title: "Copy targeted test brief".to_string(),
command: COPY_TARGETED_TEST_BRIEF_COMMAND.to_string(),
arguments: Some(vec![serde_json::json!({
"seam_id": seam.seam.id().as_str(),
"brief": brief,
})]),
}),
..CodeAction::default()
})
}
fn copy_suggested_assertion_action(
seam: &ClassifiedSeam,
assertion: String,
) -> CodeActionOrCommand {
CodeActionOrCommand::CodeAction(CodeAction {
title: "Copy suggested assertion".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
command: Some(Command {
title: "Copy suggested assertion".to_string(),
command: COPY_SUGGESTED_ASSERTION_COMMAND.to_string(),
arguments: Some(vec![serde_json::json!({
"seam_id": seam.seam.id().as_str(),
"assertion": assertion,
})]),
}),
..CodeAction::default()
})
}
fn open_related_test_action(target: LSPAny) -> CodeActionOrCommand {
CodeActionOrCommand::CodeAction(CodeAction {
title: "Open best related test".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
command: Some(Command {
title: "Open best related test".to_string(),
command: OPEN_RELATED_TEST_COMMAND.to_string(),
arguments: Some(vec![target]),
}),
..CodeAction::default()
})
}
fn is_ripr_diagnostic(diagnostic: &Diagnostic) -> bool {
diagnostic.source.as_deref() == Some("ripr")
}
fn is_seam_diagnostic(diagnostic: &Diagnostic) -> bool {
diagnostic
.data
.as_ref()
.and_then(|data| data.get("seam_id"))
.and_then(|value| value.as_str())
.is_some()
}
fn copy_context_target(params: &CodeActionParams, diagnostic: &Diagnostic) -> LSPAny {
let mut target = serde_json::Map::new();
target.insert(
"uri".to_string(),
serde_json::Value::String(params.text_document.uri.as_str().to_string()),
);
target.insert(
"line".to_string(),
serde_json::Value::Number(serde_json::Number::from(
params.range.start.line.saturating_add(1),
)),
);
if let Some(data) = &diagnostic.data
&& let Some(obj) = data.as_object()
{
if let Some(finding_id) = obj.get("finding_id").and_then(|v| v.as_str()) {
target.insert(
"finding_id".to_string(),
serde_json::Value::String(finding_id.to_string()),
);
}
if let Some(probe_id) = obj.get("probe_id").and_then(|v| v.as_str()) {
target.insert(
"probe_id".to_string(),
serde_json::Value::String(probe_id.to_string()),
);
}
if let Some(seam_id) = obj.get("seam_id").and_then(|v| v.as_str()) {
target.insert(
"seam_id".to_string(),
serde_json::Value::String(seam_id.to_string()),
);
}
if let Some(seam_kind) = obj.get("seam_kind").and_then(|v| v.as_str()) {
target.insert(
"seam_kind".to_string(),
serde_json::Value::String(seam_kind.to_string()),
);
}
}
serde_json::Value::Object(target)
}
fn copy_seam_packet_target(
params: &CodeActionParams,
diagnostic: &Diagnostic,
seam: &ClassifiedSeam,
) -> LSPAny {
let mut target = copy_context_target(params, diagnostic);
if let Some(obj) = target.as_object_mut() {
obj.insert(
"line".to_string(),
serde_json::Value::Number(serde_json::Number::from(seam.seam.display_line())),
);
obj.insert(
"seam_id".to_string(),
serde_json::Value::String(seam.seam.id().as_str().to_string()),
);
obj.insert(
"seam_kind".to_string(),
serde_json::Value::String(seam.seam.kind().as_str().to_string()),
);
}
target
}
fn best_related_test_for_editor(seam: &ClassifiedSeam) -> Option<&RelatedTestGrip> {
seam.evidence
.related_tests
.iter()
.find(|test| test.oracle_strength == OracleStrength::Strong)
.or_else(|| {
seam.evidence
.related_tests
.iter()
.min_by_key(|test| relation_confidence_rank(test.relation_confidence))
})
}
fn relation_confidence_rank(confidence: RelationConfidence) -> u8 {
match confidence {
RelationConfidence::High => 0,
RelationConfidence::Medium => 1,
RelationConfidence::Low => 2,
RelationConfidence::Opaque => 3,
}
}
fn related_test_target(snapshot: &AnalysisSnapshot, related: &RelatedTestGrip) -> Option<LSPAny> {
let path = absolute_related_test_path(snapshot, related);
let uri = file_uri_for_path(&path).ok()?;
Some(serde_json::json!({
"uri": uri.as_str(),
"line": related.line,
"test_name": related.test_name.as_str(),
}))
}
fn absolute_related_test_path(snapshot: &AnalysisSnapshot, related: &RelatedTestGrip) -> PathBuf {
if related.file.is_absolute() {
related.file.clone()
} else {
snapshot.root.join(&related.file)
}
}