use std::path::PathBuf;
use anyhow::{Context, anyhow};
use crossbeam::channel;
use lsp_types as types;
use serde::Serialize;
use serde_json::json;
use crate::edit::DocumentVersion;
use crate::session::{ClientOptions, GlobalOptions};
use crate::{Client, PositionEncoding, Session, TextDocument, Workspace, Workspaces};
#[derive(Clone, Debug)]
pub struct RequestSurfaceInput<'a> {
pub source: &'a str,
pub language_id: &'a str,
pub file_name: &'a str,
pub encoding: PositionEncoding,
pub capabilities: types::ClientCapabilities,
pub client_options: ClientOptions,
pub position: types::Position,
pub range: types::Range,
pub new_name: &'a str,
pub workspace_query: &'a str,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TextDocumentState {
pub contents: String,
pub version: DocumentVersion,
}
pub fn apply_text_document_changes(
source: &str,
version: DocumentVersion,
changes: Vec<types::TextDocumentContentChangeEvent>,
new_version: DocumentVersion,
encoding: PositionEncoding,
) -> TextDocumentState {
let mut document =
TextDocument::new(source.to_owned(), version).with_language_id("shellscript");
document.apply_changes(changes, new_version, encoding);
TextDocumentState {
contents: document.contents().to_owned(),
version: document.version(),
}
}
pub fn exercise_request_surface(
input: RequestSurfaceInput<'_>,
) -> anyhow::Result<Vec<serde_json::Value>> {
let (mut session, client, _client_receiver, uri) = build_session(
input.encoding,
input.capabilities,
input.client_options,
input.file_name,
)?;
session.open_text_document(
uri.clone(),
TextDocument::new(input.source.to_owned(), 1).with_language_id(input.language_id),
);
let Some(snapshot) = session.take_snapshot(uri.clone()) else {
return Err(anyhow!("opened fuzz document did not produce a snapshot"));
};
let mut outputs = Vec::new();
let diagnostics = crate::lint::generate_diagnostics(&snapshot);
record_value(&mut outputs, "textDocument/diagnostic", &diagnostics)?;
let code_actions = crate::fix::code_actions(
snapshot.clone(),
&client,
types::CodeActionParams {
text_document: text_document_identifier(uri.clone()),
range: input.range,
context: types::CodeActionContext {
diagnostics: diagnostics.clone(),
only: None,
trigger_kind: None,
},
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
},
);
if let Some(Some(actions)) =
record_server_result(&mut outputs, "textDocument/codeAction", code_actions)?
{
for action in actions.into_iter().take(4) {
let types::CodeActionOrCommand::CodeAction(action) = action else {
continue;
};
let resolved = crate::fix::resolve_code_action(&session, &client, action);
let _ = record_server_result(&mut outputs, "codeAction/resolve", resolved)?;
}
}
let text_position = text_document_position(uri.clone(), input.position);
let hover = crate::resolve::hover(
snapshot.clone(),
&client,
types::HoverParams {
text_document_position_params: text_position.clone(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/hover", hover)?;
let completion = crate::editor_features::completion(
snapshot.clone(),
&client,
types::CompletionParams {
text_document_position: text_position.clone(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
context: None,
},
);
if let Some(Some(completion)) =
record_server_result(&mut outputs, "textDocument/completion", completion)?
{
let items = match completion {
types::CompletionResponse::Array(items) => items,
types::CompletionResponse::List(list) => list.items,
};
for item in items.into_iter().take(4) {
let resolved = crate::editor_features::resolve_completion_item(item);
let _ = record_server_result(&mut outputs, "completionItem/resolve", resolved)?;
}
}
let definition = crate::editor_features::definition(
snapshot.clone(),
&client,
types::GotoDefinitionParams {
text_document_position_params: text_position.clone(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/definition", definition)?;
let references = crate::editor_features::references(
snapshot.clone(),
&client,
types::ReferenceParams {
text_document_position: text_position.clone(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
context: types::ReferenceContext {
include_declaration: true,
},
},
);
let _ = record_server_result(&mut outputs, "textDocument/references", references)?;
let highlights = crate::editor_features::document_highlight(
snapshot.clone(),
&client,
types::DocumentHighlightParams {
text_document_position_params: text_position.clone(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/documentHighlight", highlights)?;
let prepare_rename =
crate::editor_features::prepare_rename(snapshot.clone(), &client, text_position.clone());
let _ = record_server_result(&mut outputs, "textDocument/prepareRename", prepare_rename)?;
let rename = crate::editor_features::rename(
snapshot.clone(),
&client,
types::RenameParams {
text_document_position: text_position,
new_name: input.new_name.to_owned(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/rename", rename)?;
let document_symbols = crate::symbols::document_symbols(
snapshot.clone(),
&client,
types::DocumentSymbolParams {
text_document: text_document_identifier(uri.clone()),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
},
);
let _ = record_server_result(
&mut outputs,
"textDocument/documentSymbol",
document_symbols,
)?;
let workspace_symbols = crate::symbols::workspace_symbols(
session.workspace_symbol_context(),
&client,
types::WorkspaceSymbolParams {
query: input.workspace_query.to_owned(),
work_done_progress_params: types::WorkDoneProgressParams::default(),
partial_result_params: types::PartialResultParams::default(),
},
);
let _ = record_server_result(&mut outputs, "workspace/symbol", workspace_symbols)?;
let format_document = crate::format::format_document(
snapshot.clone(),
&client,
types::DocumentFormattingParams {
text_document: text_document_identifier(uri.clone()),
options: types::FormattingOptions {
tab_size: 4,
insert_spaces: true,
..types::FormattingOptions::default()
},
work_done_progress_params: types::WorkDoneProgressParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/formatting", format_document)?;
let format_range = crate::format::format_range(
snapshot,
&client,
types::DocumentRangeFormattingParams {
text_document: text_document_identifier(uri),
range: input.range,
options: types::FormattingOptions {
tab_size: 4,
insert_spaces: true,
..types::FormattingOptions::default()
},
work_done_progress_params: types::WorkDoneProgressParams::default(),
},
);
let _ = record_server_result(&mut outputs, "textDocument/rangeFormatting", format_range)?;
Ok(outputs)
}
fn build_session(
encoding: PositionEncoding,
capabilities: types::ClientCapabilities,
client_options: ClientOptions,
file_name: &str,
) -> anyhow::Result<(
Session,
Client,
channel::Receiver<lsp_server::Message>,
types::Url,
)> {
let (main_loop_sender, _main_loop_receiver) = channel::unbounded();
let (client_sender, client_receiver) = channel::unbounded();
let client = Client::new(main_loop_sender, client_sender);
let workspace_root = fuzz_workspace_root()?;
let workspace_uri = types::Url::from_file_path(&workspace_root)
.map_err(|()| anyhow!("failed to build fuzz workspace URI"))?;
let workspaces = Workspaces::new(vec![Workspace::default(workspace_uri)]);
let global = GlobalOptions::default().into_settings(client.clone());
let mut session = Session::new(&capabilities, encoding, global, &workspaces, &client)?;
session.update_client_options(client_options);
let uri = types::Url::from_file_path(workspace_root.join(file_name))
.map_err(|()| anyhow!("failed to build fuzz document URI"))?;
Ok((session, client, client_receiver, uri))
}
fn fuzz_workspace_root() -> anyhow::Result<PathBuf> {
let root = std::env::temp_dir().join(format!("shuck-lsp-fuzz-{}", std::process::id()));
std::fs::create_dir_all(&root)
.with_context(|| format!("create fuzz workspace {}", root.display()))?;
Ok(root)
}
fn text_document_identifier(uri: types::Url) -> types::TextDocumentIdentifier {
types::TextDocumentIdentifier { uri }
}
fn text_document_position(
uri: types::Url,
position: types::Position,
) -> types::TextDocumentPositionParams {
types::TextDocumentPositionParams {
text_document: text_document_identifier(uri),
position,
}
}
fn record_server_result<T>(
outputs: &mut Vec<serde_json::Value>,
method: &str,
result: crate::server::Result<T>,
) -> anyhow::Result<Option<T>>
where
T: Serialize,
{
match result {
Ok(value) => {
record_value(outputs, method, &value)?;
Ok(Some(value))
}
Err(error) => {
outputs.push(json!({
"method": method,
"ok": false,
"code": error.code as i32,
"message": error.to_string(),
}));
Ok(None)
}
}
}
fn record_value<T>(
outputs: &mut Vec<serde_json::Value>,
method: &str,
value: &T,
) -> anyhow::Result<()>
where
T: Serialize,
{
outputs.push(json!({
"method": method,
"ok": true,
"result": serde_json::to_value(value)?,
}));
Ok(())
}