use std::collections::HashSet;
const META_MODEL_JSON: &str = include_str!("../../generated/metaModel.json");
pub const IMPLEMENTED_METHODS: &[&str] = &[
"initialize",
"initialized",
"shutdown",
"textDocument/didOpen",
"textDocument/didChange",
"textDocument/willSave",
"textDocument/willSaveWaitUntil",
"textDocument/didSave",
"textDocument/didClose",
"textDocument/declaration",
"textDocument/definition",
"textDocument/typeDefinition",
"textDocument/implementation",
"textDocument/references",
"textDocument/prepareCallHierarchy",
"callHierarchy/incomingCalls",
"callHierarchy/outgoingCalls",
"textDocument/prepareTypeHierarchy",
"typeHierarchy/supertypes",
"typeHierarchy/subtypes",
"textDocument/documentHighlight",
"textDocument/documentLink",
"documentLink/resolve",
"textDocument/hover",
"textDocument/codeLens",
"codeLens/resolve",
"textDocument/foldingRange",
"textDocument/selectionRange",
"textDocument/documentSymbol",
"textDocument/documentColor",
"textDocument/colorPresentation",
"textDocument/linkedEditingRange",
"textDocument/moniker",
"textDocument/completion",
"completionItem/resolve",
"textDocument/signatureHelp",
"textDocument/codeAction",
"codeAction/resolve",
"textDocument/rename",
"textDocument/prepareRename",
"textDocument/formatting",
"textDocument/rangeFormatting",
"textDocument/rangesFormatting",
"textDocument/onTypeFormatting",
"workspace/symbol",
"workspaceSymbol/resolve",
"workspace/executeCommand",
"workspace/didChangeConfiguration",
"workspace/didChangeWatchedFiles",
"workspace/didChangeWorkspaceFolders",
"workspace/willCreateFiles",
"workspace/willRenameFiles",
"workspace/willDeleteFiles",
"workspace/didCreateFiles",
"workspace/didRenameFiles",
"workspace/didDeleteFiles",
"workspace/textDocumentContent",
"textDocument/semanticTokens/full",
"textDocument/semanticTokens/full/delta",
"textDocument/semanticTokens/range",
"textDocument/inlayHint",
"inlayHint/resolve",
"textDocument/inlineValue",
"textDocument/inlineCompletion",
"textDocument/diagnostic",
"workspace/diagnostic",
"notebookDocument/didOpen",
"notebookDocument/didChange",
"notebookDocument/didSave",
"notebookDocument/didClose",
"window/workDoneProgress/cancel",
"$/setTrace",
"$/progress",
];
#[derive(Debug, Clone)]
pub struct CoverageReport {
pub implemented: Vec<String>,
pub unimplemented: Vec<String>,
pub coverage_pct: f64,
}
impl CoverageReport {
pub fn summary(&self) -> String {
format!(
"LSP 3.18 coverage: {:.1}% ({}/{} methods implemented, {} unimplemented)",
self.coverage_pct,
self.implemented.len(),
self.implemented.len() + self.unimplemented.len(),
self.unimplemented.len(),
)
}
}
pub fn lsp_coverage() -> CoverageReport {
let meta: serde_json::Value =
serde_json::from_str(META_MODEL_JSON).expect("metaModel.json is not valid JSON");
let mut spec_methods: HashSet<String> = HashSet::new();
if let Some(requests) = meta.get("requests").and_then(|v| v.as_array()) {
for req in requests {
if let Some(method) = req.get("method").and_then(|v| v.as_str()) {
spec_methods.insert(method.to_string());
}
}
}
if let Some(notifs) = meta.get("notifications").and_then(|v| v.as_array()) {
for notif in notifs {
if let Some(method) = notif.get("method").and_then(|v| v.as_str()) {
spec_methods.insert(method.to_string());
}
}
}
let impl_set: HashSet<&str> = IMPLEMENTED_METHODS
.iter()
.copied()
.filter(|m| !m.starts_with("max/"))
.collect();
let mut implemented: Vec<String> = spec_methods
.iter()
.filter(|m| impl_set.contains(m.as_str()))
.cloned()
.collect();
implemented.sort();
let mut unimplemented: Vec<String> = spec_methods
.iter()
.filter(|m| !impl_set.contains(m.as_str()))
.cloned()
.collect();
unimplemented.sort();
let total = implemented.len() + unimplemented.len();
let coverage_pct = if total == 0 {
0.0
} else {
100.0 * implemented.len() as f64 / total as f64
};
CoverageReport {
implemented,
unimplemented,
coverage_pct,
}
}
#[cfg(test)]
mod tests {
use super::*;
const COVERAGE_WATERMARK: f64 = 75.0;
#[test]
fn lsp_coverage_meets_watermark() {
let report = lsp_coverage();
eprintln!("{}", report.summary());
if !report.unimplemented.is_empty() {
eprintln!("Unimplemented LSP 3.18 methods:");
for m in &report.unimplemented {
eprintln!(" - {}", m);
}
}
assert!(
report.coverage_pct >= COVERAGE_WATERMARK,
"LSP 3.18 coverage {:.1}% is below watermark {:.1}%.\n\
Unimplemented methods:\n{}",
report.coverage_pct,
COVERAGE_WATERMARK,
report.unimplemented.join("\n"),
);
}
#[test]
fn initialize_and_shutdown_are_implemented() {
let report = lsp_coverage();
assert!(
report.implemented.contains(&"initialize".to_string()),
"initialize must be implemented"
);
assert!(
report.implemented.contains(&"shutdown".to_string()),
"shutdown must be implemented"
);
}
#[test]
fn meta_model_parses_to_non_empty_spec() {
let meta: serde_json::Value =
serde_json::from_str(META_MODEL_JSON).expect("metaModel.json parse failed");
let req_count = meta
.get("requests")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
let notif_count = meta
.get("notifications")
.and_then(|v| v.as_array())
.map(|a| a.len())
.unwrap_or(0);
assert!(
req_count > 50,
"expected >50 requests in metaModel.json, got {}",
req_count
);
assert!(
notif_count > 10,
"expected >10 notifications, got {}",
notif_count
);
}
#[test]
fn no_max_methods_in_coverage_denominator() {
let report = lsp_coverage();
for method in &report.implemented {
assert!(
!method.starts_with("max/"),
"max/* method {:?} must not appear in the standard LSP coverage report",
method
);
}
for method in &report.unimplemented {
assert!(
!method.starts_with("max/"),
"max/* method {:?} must not appear in the standard LSP coverage report",
method
);
}
}
}