From eb354608d2d9b2016ec9e0227190c9dcde66180b Mon Sep 17 00:00:00 2001 From: GF <66461774+Guillaume-Fgt@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:15:13 +0000 Subject: [PATCH] [ty] Add LSP debug information command (#20379) Co-authored-by: Micha Reiser --- crates/ty_python_semantic/src/lint.rs | 31 ++- crates/ty_server/src/capabilities.rs | 41 +++ crates/ty_server/src/server/api.rs | 1 + crates/ty_server/src/server/api/requests.rs | 2 + .../server/api/requests/execute_command.rs | 76 ++++++ crates/ty_server/tests/e2e/commands.rs | 55 ++++ crates/ty_server/tests/e2e/main.rs | 1 + .../e2e__commands__debug_command.snap | 238 ++++++++++++++++++ .../e2e__initialize__initialization.snap | 7 + ...ialize__initialization_with_workspace.snap | 7 + 10 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 crates/ty_server/src/server/api/requests/execute_command.rs create mode 100644 crates/ty_server/tests/e2e/commands.rs create mode 100644 crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap diff --git a/crates/ty_python_semantic/src/lint.rs b/crates/ty_python_semantic/src/lint.rs index 0800ee5983..6432d56956 100644 --- a/crates/ty_python_semantic/src/lint.rs +++ b/crates/ty_python_semantic/src/lint.rs @@ -463,7 +463,7 @@ impl From<&'static LintMetadata> for LintEntry { } } -#[derive(Debug, Clone, Default, PartialEq, Eq, get_size2::GetSize)] +#[derive(Clone, Default, PartialEq, Eq, get_size2::GetSize)] pub struct RuleSelection { /// Map with the severity for each enabled lint rule. /// @@ -541,6 +541,35 @@ impl RuleSelection { } } +// The default `LintId` debug implementation prints the entire lint metadata. +// This is way too verbose. +impl fmt::Debug for RuleSelection { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let lints = self.lints.iter().sorted_by_key(|(lint, _)| lint.name); + + if f.alternate() { + let mut f = f.debug_map(); + + for (lint, (severity, source)) in lints { + f.entry( + &lint.name().as_str(), + &format_args!("{severity:?} ({source:?})"), + ); + } + + f.finish() + } else { + let mut f = f.debug_set(); + + for (lint, _) in lints { + f.entry(&lint.name()); + } + + f.finish() + } + } +} + #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, get_size2::GetSize)] pub enum LintSource { /// The user didn't enable the rule explicitly, instead it's enabled by default. diff --git a/crates/ty_server/src/capabilities.rs b/crates/ty_server/src/capabilities.rs index f0b77a9533..fed7ac811e 100644 --- a/crates/ty_server/src/capabilities.rs +++ b/crates/ty_server/src/capabilities.rs @@ -10,6 +10,8 @@ use lsp_types::{ use crate::PositionEncoding; use crate::session::GlobalSettings; +use lsp_types as types; +use std::str::FromStr; bitflags::bitflags! { /// Represents the resolved client capabilities for the language server. @@ -37,6 +39,36 @@ bitflags::bitflags! { } } +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum SupportedCommand { + Debug, +} + +impl SupportedCommand { + /// Returns the identifier of the command. + const fn identifier(self) -> &'static str { + match self { + SupportedCommand::Debug => "ty.printDebugInformation", + } + } + + /// Returns all the commands that the server currently supports. + const fn all() -> [SupportedCommand; 1] { + [SupportedCommand::Debug] + } +} + +impl FromStr for SupportedCommand { + type Err = anyhow::Error; + + fn from_str(name: &str) -> anyhow::Result { + Ok(match name { + "ty.printDebugInformation" => Self::Debug, + _ => return Err(anyhow::anyhow!("Invalid command `{name}`")), + }) + } +} + impl ResolvedClientCapabilities { /// Returns `true` if the client supports workspace diagnostic refresh. pub(crate) const fn supports_workspace_diagnostic_refresh(self) -> bool { @@ -319,6 +351,15 @@ pub(crate) fn server_capabilities( ServerCapabilities { position_encoding: Some(position_encoding.into()), + execute_command_provider: Some(types::ExecuteCommandOptions { + commands: SupportedCommand::all() + .map(|command| command.identifier().to_string()) + .to_vec(), + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: Some(false), + }, + }), + diagnostic_provider, text_document_sync: Some(TextDocumentSyncCapability::Options( TextDocumentSyncOptions { diff --git a/crates/ty_server/src/server/api.rs b/crates/ty_server/src/server/api.rs index af963853df..6fd1cde43a 100644 --- a/crates/ty_server/src/server/api.rs +++ b/crates/ty_server/src/server/api.rs @@ -32,6 +32,7 @@ pub(super) fn request(req: server::Request) -> Task { let id = req.id.clone(); match req.method.as_str() { + requests::ExecuteCommand::METHOD => sync_request_task::(req), requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::< requests::DocumentDiagnosticRequestHandler, >( diff --git a/crates/ty_server/src/server/api/requests.rs b/crates/ty_server/src/server/api/requests.rs index 0be690a38e..3d76a55c00 100644 --- a/crates/ty_server/src/server/api/requests.rs +++ b/crates/ty_server/src/server/api/requests.rs @@ -2,6 +2,7 @@ mod completion; mod diagnostic; mod doc_highlights; mod document_symbols; +mod execute_command; mod goto_declaration; mod goto_definition; mod goto_references; @@ -22,6 +23,7 @@ pub(super) use completion::CompletionRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub(super) use doc_highlights::DocumentHighlightRequestHandler; pub(super) use document_symbols::DocumentSymbolRequestHandler; +pub(super) use execute_command::ExecuteCommand; pub(super) use goto_declaration::GotoDeclarationRequestHandler; pub(super) use goto_definition::GotoDefinitionRequestHandler; pub(super) use goto_references::ReferencesRequestHandler; diff --git a/crates/ty_server/src/server/api/requests/execute_command.rs b/crates/ty_server/src/server/api/requests/execute_command.rs new file mode 100644 index 0000000000..8a2fc52fd1 --- /dev/null +++ b/crates/ty_server/src/server/api/requests/execute_command.rs @@ -0,0 +1,76 @@ +use crate::capabilities::SupportedCommand; +use crate::server; +use crate::server::api::LSPResult; +use crate::server::api::RequestHandler; +use crate::server::api::traits::SyncRequestHandler; +use crate::session::Session; +use crate::session::client::Client; +use lsp_server::ErrorCode; +use lsp_types::{self as types, request as req}; +use std::fmt::Write; +use std::str::FromStr; +use ty_project::Db; + +pub(crate) struct ExecuteCommand; + +impl RequestHandler for ExecuteCommand { + type RequestType = req::ExecuteCommand; +} + +impl SyncRequestHandler for ExecuteCommand { + fn run( + session: &mut Session, + _client: &Client, + params: types::ExecuteCommandParams, + ) -> server::Result> { + let command = SupportedCommand::from_str(¶ms.command) + .with_failure_code(ErrorCode::InvalidParams)?; + + match command { + SupportedCommand::Debug => Ok(Some(serde_json::Value::String( + debug_information(session).with_failure_code(ErrorCode::InternalError)?, + ))), + } + } +} + +/// Returns a string with detailed memory usage. +fn debug_information(session: &Session) -> crate::Result { + let mut buffer = String::new(); + + writeln!( + buffer, + "Client capabilities: {:#?}", + session.client_capabilities() + )?; + writeln!( + buffer, + "Position encoding: {:#?}", + session.position_encoding() + )?; + writeln!(buffer, "Global settings: {:#?}", session.global_settings())?; + writeln!( + buffer, + "Open text documents: {}", + session.text_document_keys().count() + )?; + writeln!(buffer)?; + + for (root, workspace) in session.workspaces() { + writeln!(buffer, "Workspace {root} ({})", workspace.url())?; + writeln!(buffer, "Settings: {:#?}", workspace.settings())?; + writeln!(buffer)?; + } + + for db in session.project_dbs() { + writeln!(buffer, "Project at {}", db.project().root(db))?; + writeln!(buffer, "Settings: {:#?}", db.project().settings(db))?; + writeln!(buffer)?; + writeln!( + buffer, + "Memory report:\n{}", + db.salsa_memory_dump().display_full() + )?; + } + Ok(buffer) +} diff --git a/crates/ty_server/tests/e2e/commands.rs b/crates/ty_server/tests/e2e/commands.rs new file mode 100644 index 0000000000..4b4686fe86 --- /dev/null +++ b/crates/ty_server/tests/e2e/commands.rs @@ -0,0 +1,55 @@ +use anyhow::Result; +use lsp_types::{ExecuteCommandParams, WorkDoneProgressParams, request::ExecuteCommand}; +use ruff_db::system::SystemPath; + +use crate::{TestServer, TestServerBuilder}; + +// Sends an executeCommand request to the TestServer +fn execute_command( + server: &mut TestServer, + command: String, + arguments: Vec, +) -> anyhow::Result> { + let params = ExecuteCommandParams { + command, + arguments, + work_done_progress_params: WorkDoneProgressParams::default(), + }; + let id = server.send_request::(params); + server.await_response::(&id) +} + +#[test] +fn debug_command() -> Result<()> { + let workspace_root = SystemPath::new("src"); + let foo = SystemPath::new("src/foo.py"); + let foo_content = "\ +def foo() -> str: +return 42 +"; + + let mut server = TestServerBuilder::new()? + .with_workspace(workspace_root, None)? + .with_file(foo, foo_content)? + .enable_pull_diagnostics(false) + .build()? + .wait_until_workspaces_are_initialized()?; + + let response = execute_command(&mut server, "ty.printDebugInformation".to_string(), vec![])?; + let response = response.expect("expect server response"); + + let response = response + .as_str() + .expect("debug command to return a string response"); + + insta::with_settings!({ + filters =>vec![ + (r"\b[0-9]+.[0-9]+MB\b","[X.XXMB]"), + (r"Workspace .+\)","Workspace XXX"), + (r"Project at .+","Project at XXX"), + ]}, { + insta::assert_snapshot!(response); + }); + + Ok(()) +} diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index 5963755f72..14054f4b48 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -27,6 +27,7 @@ //! [`await_request`]: TestServer::await_request //! [`await_notification`]: TestServer::await_notification +mod commands; mod initialize; mod inlay_hints; mod publish_diagnostics; diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap new file mode 100644 index 0000000000..cc435c14c9 --- /dev/null +++ b/crates/ty_server/tests/e2e/snapshots/e2e__commands__debug_command.snap @@ -0,0 +1,238 @@ +--- +source: crates/ty_server/tests/e2e/commands.rs +expression: response +--- +Client capabilities: ResolvedClientCapabilities( + WORKSPACE_CONFIGURATION, +) +Position encoding: UTF16 +Global settings: GlobalSettings { + diagnostic_mode: OpenFilesOnly, + experimental: ExperimentalSettings { + rename: false, + auto_import: false, + }, +} +Open text documents: 0 + +Workspace XXX +Settings: WorkspaceSettings { + disable_language_services: false, + inlay_hints: InlayHintSettings { + variable_types: true, + call_argument_names: true, + }, + overrides: None, +} + +Project at XXX +Settings: Settings { + rules: { + "ambiguous-protocol-member": Warning (Default), + "byte-string-type-annotation": Error (Default), + "call-non-callable": Error (Default), + "conflicting-argument-forms": Error (Default), + "conflicting-declarations": Error (Default), + "conflicting-metaclass": Error (Default), + "cyclic-class-definition": Error (Default), + "deprecated": Warning (Default), + "duplicate-base": Error (Default), + "duplicate-kw-only": Error (Default), + "escape-character-in-forward-annotation": Error (Default), + "fstring-type-annotation": Error (Default), + "implicit-concatenated-string-type-annotation": Error (Default), + "inconsistent-mro": Error (Default), + "index-out-of-bounds": Error (Default), + "instance-layout-conflict": Error (Default), + "invalid-argument-type": Error (Default), + "invalid-assignment": Error (Default), + "invalid-attribute-access": Error (Default), + "invalid-await": Error (Default), + "invalid-base": Error (Default), + "invalid-context-manager": Error (Default), + "invalid-declaration": Error (Default), + "invalid-exception-caught": Error (Default), + "invalid-generic-class": Error (Default), + "invalid-ignore-comment": Warning (Default), + "invalid-key": Error (Default), + "invalid-legacy-type-variable": Error (Default), + "invalid-metaclass": Error (Default), + "invalid-named-tuple": Error (Default), + "invalid-overload": Error (Default), + "invalid-parameter-default": Error (Default), + "invalid-protocol": Error (Default), + "invalid-raise": Error (Default), + "invalid-return-type": Error (Default), + "invalid-super-argument": Error (Default), + "invalid-syntax-in-forward-annotation": Error (Default), + "invalid-type-alias-type": Error (Default), + "invalid-type-checking-constant": Error (Default), + "invalid-type-form": Error (Default), + "invalid-type-guard-call": Error (Default), + "invalid-type-guard-definition": Error (Default), + "invalid-type-variable-constraints": Error (Default), + "missing-argument": Error (Default), + "missing-typed-dict-key": Error (Default), + "no-matching-overload": Error (Default), + "non-subscriptable": Error (Default), + "not-iterable": Error (Default), + "parameter-already-assigned": Error (Default), + "possibly-unbound-attribute": Warning (Default), + "possibly-unbound-implicit-call": Warning (Default), + "possibly-unbound-import": Warning (Default), + "raw-string-type-annotation": Error (Default), + "redundant-cast": Warning (Default), + "static-assert-error": Error (Default), + "subclass-of-final-class": Error (Default), + "too-many-positional-arguments": Error (Default), + "type-assertion-failure": Error (Default), + "unavailable-implicit-super-arguments": Error (Default), + "undefined-reveal": Warning (Default), + "unknown-argument": Error (Default), + "unknown-rule": Warning (Default), + "unresolved-attribute": Error (Default), + "unresolved-global": Warning (Default), + "unresolved-import": Error (Default), + "unresolved-reference": Error (Default), + "unsupported-base": Warning (Default), + "unsupported-bool-conversion": Error (Default), + "unsupported-operator": Error (Default), + "zero-stepsize-in-slice": Error (Default), + }, + terminal: TerminalSettings { + output_format: Full, + error_on_warning: false, + }, + src: SrcSettings { + respect_ignore_files: true, + files: IncludeExcludeFilter { + include: IncludeFilter( + [ + "**", + ], + .. + ), + exclude: ExcludeFilter { + ignore: Gitignore( + [ + IgnoreGlob { + original: "**/.bzr/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.direnv/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.eggs/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.git/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.git-rewrite/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.hg/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.mypy_cache/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.nox/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.pants.d/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.pytype/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.ruff_cache/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.svn/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.tox/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/.venv/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/__pypackages__/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/_build/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/buck-out/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/dist/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/node_modules/", + is_allow: false, + is_only_dir: true, + }, + IgnoreGlob { + original: "**/venv/", + is_allow: false, + is_only_dir: true, + }, + ], + .. + ), + }, + }, + }, + overrides: [], +} + +Memory report: +=======SALSA STRUCTS======= +`Program` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`Project` metadata=[X.XXMB] fields=[X.XXMB] count=1 +`FileRoot` metadata=[X.XXMB] fields=[X.XXMB] count=1 +=======SALSA QUERIES======= +=======SALSA SUMMARY======= +TOTAL MEMORY USAGE: [X.XXMB] + struct metadata = [X.XXMB] + struct fields = [X.XXMB] + memo metadata = [X.XXMB] + memo fields = [X.XXMB] diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap index 0c00d640c8..075d9dd805 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization.snap @@ -1,5 +1,6 @@ --- source: crates/ty_server/tests/e2e/initialize.rs +assertion_line: 17 expression: initialization_result --- { @@ -32,6 +33,12 @@ expression: initialization_result "documentSymbolProvider": true, "workspaceSymbolProvider": true, "declarationProvider": true, + "executeCommandProvider": { + "commands": [ + "ty.printDebugInformation" + ], + "workDoneProgress": false + }, "semanticTokensProvider": { "legend": { "tokenTypes": [ diff --git a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap index 0c00d640c8..3ea91f66be 100644 --- a/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap +++ b/crates/ty_server/tests/e2e/snapshots/e2e__initialize__initialization_with_workspace.snap @@ -1,5 +1,6 @@ --- source: crates/ty_server/tests/e2e/initialize.rs +assertion_line: 32 expression: initialization_result --- { @@ -32,6 +33,12 @@ expression: initialization_result "documentSymbolProvider": true, "workspaceSymbolProvider": true, "declarationProvider": true, + "executeCommandProvider": { + "commands": [ + "ty.printDebugInformation" + ], + "workDoneProgress": false + }, "semanticTokensProvider": { "legend": { "tokenTypes": [