//! Fault-injectable fake rust-analyzer used by integration tests. use lsp_types as _; use ra_mcp_domain as _; use ra_mcp_engine as _; use serde as _; use serde_json::{Value, json}; #[cfg(test)] use serial_test as _; use std::{ fs, io::{self, BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, time::Duration, }; #[cfg(test)] use tempfile as _; use thiserror as _; use tokio as _; use tracing as _; use url as _; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Mode { Stable, CrashOnFirstHover, } fn main() -> Result<(), Box> { run().map_err(|error| Box::new(error) as Box) } fn run() -> io::Result<()> { let mut mode = Mode::Stable; let mut marker = None::; let mut hover_delay = Duration::ZERO; let mut execute_command_delay = Duration::ZERO; let mut execute_command_log = None::; let mut diagnostic_warmup_count = 0_u8; let mut diagnostic_cancel_count = 0_u8; let mut strict_root_match = false; let mut workspace_root = None::; let mut args = std::env::args().skip(1); loop { let argument = args.next(); let Some(argument) = argument else { break; }; match argument.as_str() { "--mode" => { if let Some(value) = args.next() { mode = parse_mode(&value).unwrap_or(Mode::Stable); } } "--crash-marker" => { if let Some(value) = args.next() { marker = Some(PathBuf::from(value)); } } "--hover-delay-ms" => { if let Some(value) = args.next() { let parsed = value.parse::().ok(); if let Some(delay_ms) = parsed { hover_delay = Duration::from_millis(delay_ms); } } } "--execute-command-delay-ms" => { if let Some(value) = args.next() { let parsed = value.parse::().ok(); if let Some(delay_ms) = parsed { execute_command_delay = Duration::from_millis(delay_ms); } } } "--execute-command-log" => { if let Some(value) = args.next() { execute_command_log = Some(PathBuf::from(value)); } } "--diagnostic-warmup-count" => { if let Some(value) = args.next() { let parsed = value.parse::().ok(); if let Some(count) = parsed { diagnostic_warmup_count = count; } } } "--diagnostic-cancel-count" => { if let Some(value) = args.next() { let parsed = value.parse::().ok(); if let Some(count) = parsed { diagnostic_cancel_count = count; } } } "--strict-root-match" => { strict_root_match = true; } _ => {} } } let stdin = io::stdin(); let stdout = io::stdout(); let mut reader = BufReader::new(stdin.lock()); let mut writer = stdout.lock(); loop { let frame = match read_frame(&mut reader) { Ok(frame) => frame, Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => break, Err(error) => return Err(error), }; let message: Value = serde_json::from_slice(&frame) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string()))?; if let Some(method) = message.get("method").and_then(Value::as_str) { if method == "initialized" { continue; } let request_id = message.get("id").cloned(); let Some(request_id) = request_id else { continue; }; if method == "initialize" { workspace_root = initialized_workspace_root(&message); } if mode == Mode::CrashOnFirstHover && method == "textDocument/hover" && should_crash(&marker)? { std::process::exit(0); } if method == "textDocument/hover" && !hover_delay.is_zero() { std::thread::sleep(hover_delay); } if method == "workspace/executeCommand" { if let Some(path) = execute_command_log.as_ref() { log_execute_command_effect(path, &message)?; } if !execute_command_delay.is_zero() { std::thread::sleep(execute_command_delay); } } let response = if strict_root_match && request_targets_outside_workspace(&message, workspace_root.as_deref()) { strict_root_mismatch_response(method, request_id, &message) } else if method == "textDocument/diagnostic" && diagnostic_cancel_count > 0 { diagnostic_cancel_count = diagnostic_cancel_count.saturating_sub(1); server_cancelled_response(request_id) } else if method == "textDocument/diagnostic" && diagnostic_warmup_count > 0 { diagnostic_warmup_count = diagnostic_warmup_count.saturating_sub(1); warmup_unlinked_diagnostic_response(request_id) } else { make_response(method, request_id, &message) }; write_frame(&mut writer, &response)?; } } Ok(()) } fn parse_mode(raw: &str) -> Option { match raw { "stable" => Some(Mode::Stable), "crash_on_first_hover" => Some(Mode::CrashOnFirstHover), _ => None, } } fn should_crash(marker: &Option) -> io::Result { let Some(marker) = marker else { return Ok(true); }; if marker.exists() { return Ok(false); } fs::write(marker, b"crashed")?; Ok(true) } fn log_execute_command_effect(path: &PathBuf, request: &Value) -> io::Result<()> { let command = request .get("params") .and_then(|params| params.get("command")) .and_then(Value::as_str) .unwrap_or(""); let mut file = fs::OpenOptions::new() .create(true) .append(true) .open(path)?; writeln!(file, "{command}")?; Ok(()) } fn initialized_workspace_root(request: &Value) -> Option { let root_uri = request .get("params") .and_then(|params| params.get("rootUri")) .and_then(Value::as_str)?; let root_url = url::Url::parse(root_uri).ok()?; root_url.to_file_path().ok() } fn request_targets_outside_workspace(request: &Value, workspace_root: Option<&Path>) -> bool { let Some(workspace_root) = workspace_root else { return false; }; let file_path = request_document_path(request); let Some(file_path) = file_path else { return false; }; !file_path.starts_with(workspace_root) } fn request_document_path(request: &Value) -> Option { let uri = request .get("params") .and_then(|params| params.get("textDocument")) .and_then(|doc| doc.get("uri")) .and_then(Value::as_str)?; let url = url::Url::parse(uri).ok()?; url.to_file_path().ok() } fn strict_root_mismatch_response(method: &str, request_id: Value, request: &Value) -> Value { match method { "textDocument/hover" => json!({ "jsonrpc": "2.0", "id": request_id, "result": Value::Null }), "textDocument/definition" => json!({ "jsonrpc": "2.0", "id": request_id, "result": Value::Null }), "textDocument/references" => json!({ "jsonrpc": "2.0", "id": request_id, "result": Value::Null }), "textDocument/rename" => { let uri = request .get("params") .and_then(|params| params.get("textDocument")) .and_then(|doc| doc.get("uri")) .and_then(Value::as_str) .unwrap_or("file:///tmp/fallback.rs") .to_owned(); json!({ "jsonrpc": "2.0", "id": request_id, "result": { "changes": { uri: [] } } }) } "textDocument/diagnostic" => warmup_unlinked_diagnostic_response(request_id), _ => make_response(method, request_id, request), } } fn make_response(method: &str, request_id: Value, request: &Value) -> Value { match method { "initialize" => json!({ "jsonrpc": "2.0", "id": request_id, "result": { "capabilities": {} } }), "textDocument/hover" => json!({ "jsonrpc": "2.0", "id": request_id, "result": { "contents": { "kind": "markdown", "value": "hover::ok" } } }), "textDocument/definition" => { let uri = request .get("params") .and_then(|params| params.get("textDocument")) .and_then(|doc| doc.get("uri")) .cloned() .unwrap_or(Value::String("file:///tmp/fallback.rs".to_owned())); json!({ "jsonrpc": "2.0", "id": request_id, "result": [{ "uri": uri, "range": { "start": { "line": 2, "character": 3 }, "end": { "line": 2, "character": 8 } } }] }) } "textDocument/references" => { let uri = request .get("params") .and_then(|params| params.get("textDocument")) .and_then(|doc| doc.get("uri")) .cloned() .unwrap_or(Value::String("file:///tmp/fallback.rs".to_owned())); json!({ "jsonrpc": "2.0", "id": request_id, "result": [{ "uri": uri, "range": { "start": { "line": 4, "character": 1 }, "end": { "line": 4, "character": 5 } } }] }) } "textDocument/rename" => { let uri = request .get("params") .and_then(|params| params.get("textDocument")) .and_then(|doc| doc.get("uri")) .and_then(Value::as_str) .unwrap_or("file:///tmp/fallback.rs") .to_owned(); json!({ "jsonrpc": "2.0", "id": request_id, "result": { "changes": { uri: [ { "range": { "start": { "line": 1, "character": 1 }, "end": { "line": 1, "character": 4 } }, "newText": "renamed_symbol" } ] } } }) } "textDocument/diagnostic" => json!({ "jsonrpc": "2.0", "id": request_id, "result": { "kind": "full", "items": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 3 } }, "severity": 1, "message": "fake diagnostic" }] } }), "workspace/executeCommand" => { let command = request .get("params") .and_then(|params| params.get("command")) .cloned() .unwrap_or(Value::Null); json!({ "jsonrpc": "2.0", "id": request_id, "result": { "ack": "ok", "command": command } }) } _ => json!({ "jsonrpc": "2.0", "id": request_id, "error": { "code": -32601, "message": format!("method not found: {method}") } }), } } fn warmup_unlinked_diagnostic_response(request_id: Value) -> Value { json!({ "jsonrpc": "2.0", "id": request_id, "result": { "kind": "full", "items": [{ "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 } }, "severity": 2, "code": "unlinked-file", "message": "This file is not part of any crate, so rust-analyzer can't offer IDE services." }] } }) } fn server_cancelled_response(request_id: Value) -> Value { json!({ "jsonrpc": "2.0", "id": request_id, "error": { "code": -32802, "message": "server cancelled request during workspace reload" } }) } fn read_frame(reader: &mut BufReader) -> io::Result> { let mut content_length = None::; loop { let mut line = String::new(); let bytes = reader.read_line(&mut line)?; if bytes == 0 { return Err(io::Error::new( io::ErrorKind::UnexpectedEof, "EOF while reading headers", )); } if line == "\r\n" || line == "\n" { break; } let trimmed = line.trim_end_matches(['\r', '\n']); if let Some(raw_length) = trimmed.strip_prefix("Content-Length:") { let parsed = raw_length.trim().parse::().map_err(|error| { io::Error::new( io::ErrorKind::InvalidData, format!("invalid Content-Length header: {error}"), ) })?; content_length = Some(parsed); } } let length = content_length.ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidData, "missing Content-Length header") })?; let mut payload = vec![0_u8; length]; reader.read_exact(&mut payload)?; Ok(payload) } fn write_frame(writer: &mut impl Write, payload: &Value) -> io::Result<()> { let serialized = serde_json::to_vec(payload) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string()))?; let header = format!("Content-Length: {}\r\n\r\n", serialized.len()); writer.write_all(header.as_bytes())?; writer.write_all(&serialized)?; writer.flush()?; Ok(()) }