[ty] Add LSP debug information command (#20379)

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
GF 2025-09-20 11:15:13 +00:00 committed by GitHub
parent 12086dfa69
commit eb354608d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 458 additions and 1 deletions

View file

@ -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 { pub struct RuleSelection {
/// Map with the severity for each enabled lint rule. /// 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)] #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, get_size2::GetSize)]
pub enum LintSource { pub enum LintSource {
/// The user didn't enable the rule explicitly, instead it's enabled by default. /// The user didn't enable the rule explicitly, instead it's enabled by default.

View file

@ -10,6 +10,8 @@ use lsp_types::{
use crate::PositionEncoding; use crate::PositionEncoding;
use crate::session::GlobalSettings; use crate::session::GlobalSettings;
use lsp_types as types;
use std::str::FromStr;
bitflags::bitflags! { bitflags::bitflags! {
/// Represents the resolved client capabilities for the language server. /// 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<Self, Self::Err> {
Ok(match name {
"ty.printDebugInformation" => Self::Debug,
_ => return Err(anyhow::anyhow!("Invalid command `{name}`")),
})
}
}
impl ResolvedClientCapabilities { impl ResolvedClientCapabilities {
/// Returns `true` if the client supports workspace diagnostic refresh. /// Returns `true` if the client supports workspace diagnostic refresh.
pub(crate) const fn supports_workspace_diagnostic_refresh(self) -> bool { pub(crate) const fn supports_workspace_diagnostic_refresh(self) -> bool {
@ -319,6 +351,15 @@ pub(crate) fn server_capabilities(
ServerCapabilities { ServerCapabilities {
position_encoding: Some(position_encoding.into()), 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, diagnostic_provider,
text_document_sync: Some(TextDocumentSyncCapability::Options( text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions { TextDocumentSyncOptions {

View file

@ -32,6 +32,7 @@ pub(super) fn request(req: server::Request) -> Task {
let id = req.id.clone(); let id = req.id.clone();
match req.method.as_str() { match req.method.as_str() {
requests::ExecuteCommand::METHOD => sync_request_task::<requests::ExecuteCommand>(req),
requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::< requests::DocumentDiagnosticRequestHandler::METHOD => background_document_request_task::<
requests::DocumentDiagnosticRequestHandler, requests::DocumentDiagnosticRequestHandler,
>( >(

View file

@ -2,6 +2,7 @@ mod completion;
mod diagnostic; mod diagnostic;
mod doc_highlights; mod doc_highlights;
mod document_symbols; mod document_symbols;
mod execute_command;
mod goto_declaration; mod goto_declaration;
mod goto_definition; mod goto_definition;
mod goto_references; mod goto_references;
@ -22,6 +23,7 @@ pub(super) use completion::CompletionRequestHandler;
pub(super) use diagnostic::DocumentDiagnosticRequestHandler; pub(super) use diagnostic::DocumentDiagnosticRequestHandler;
pub(super) use doc_highlights::DocumentHighlightRequestHandler; pub(super) use doc_highlights::DocumentHighlightRequestHandler;
pub(super) use document_symbols::DocumentSymbolRequestHandler; pub(super) use document_symbols::DocumentSymbolRequestHandler;
pub(super) use execute_command::ExecuteCommand;
pub(super) use goto_declaration::GotoDeclarationRequestHandler; pub(super) use goto_declaration::GotoDeclarationRequestHandler;
pub(super) use goto_definition::GotoDefinitionRequestHandler; pub(super) use goto_definition::GotoDefinitionRequestHandler;
pub(super) use goto_references::ReferencesRequestHandler; pub(super) use goto_references::ReferencesRequestHandler;

View file

@ -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<Option<serde_json::Value>> {
let command = SupportedCommand::from_str(&params.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<String> {
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)
}

View file

@ -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<serde_json::Value>,
) -> anyhow::Result<Option<serde_json::Value>> {
let params = ExecuteCommandParams {
command,
arguments,
work_done_progress_params: WorkDoneProgressParams::default(),
};
let id = server.send_request::<ExecuteCommand>(params);
server.await_response::<ExecuteCommand>(&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(())
}

View file

@ -27,6 +27,7 @@
//! [`await_request`]: TestServer::await_request //! [`await_request`]: TestServer::await_request
//! [`await_notification`]: TestServer::await_notification //! [`await_notification`]: TestServer::await_notification
mod commands;
mod initialize; mod initialize;
mod inlay_hints; mod inlay_hints;
mod publish_diagnostics; mod publish_diagnostics;

View file

@ -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]

View file

@ -1,5 +1,6 @@
--- ---
source: crates/ty_server/tests/e2e/initialize.rs source: crates/ty_server/tests/e2e/initialize.rs
assertion_line: 17
expression: initialization_result expression: initialization_result
--- ---
{ {
@ -32,6 +33,12 @@ expression: initialization_result
"documentSymbolProvider": true, "documentSymbolProvider": true,
"workspaceSymbolProvider": true, "workspaceSymbolProvider": true,
"declarationProvider": true, "declarationProvider": true,
"executeCommandProvider": {
"commands": [
"ty.printDebugInformation"
],
"workDoneProgress": false
},
"semanticTokensProvider": { "semanticTokensProvider": {
"legend": { "legend": {
"tokenTypes": [ "tokenTypes": [

View file

@ -1,5 +1,6 @@
--- ---
source: crates/ty_server/tests/e2e/initialize.rs source: crates/ty_server/tests/e2e/initialize.rs
assertion_line: 32
expression: initialization_result expression: initialization_result
--- ---
{ {
@ -32,6 +33,12 @@ expression: initialization_result
"documentSymbolProvider": true, "documentSymbolProvider": true,
"workspaceSymbolProvider": true, "workspaceSymbolProvider": true,
"declarationProvider": true, "declarationProvider": true,
"executeCommandProvider": {
"commands": [
"ty.printDebugInformation"
],
"workDoneProgress": false
},
"semanticTokensProvider": { "semanticTokensProvider": {
"legend": { "legend": {
"tokenTypes": [ "tokenTypes": [