Warn when the used toolchain looks too old for rust-analyzer

This commit is contained in:
Lukas Wirth 2025-02-27 17:40:10 +01:00
parent 505b52da5f
commit 7ab7633e91
5 changed files with 140 additions and 114 deletions

View file

@ -32,13 +32,13 @@ use triomphe::Arc;
use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath}; use vfs::{AbsPath, AbsPathBuf, FileId, VfsPath};
use crate::{ use crate::{
completion_item_hash,
config::{Config, RustfmtConfig, WorkspaceSymbolConfig}, config::{Config, RustfmtConfig, WorkspaceSymbolConfig},
diagnostics::convert_diagnostic, diagnostics::convert_diagnostic,
global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot}, global_state::{FetchWorkspaceRequest, GlobalState, GlobalStateSnapshot},
hack_recover_crate_name, hack_recover_crate_name,
line_index::LineEndings, line_index::LineEndings,
lsp::{ lsp::{
completion_item_hash,
ext::{ ext::{
InternalTestingFetchConfigOption, InternalTestingFetchConfigParams, InternalTestingFetchConfigOption, InternalTestingFetchConfigParams,
InternalTestingFetchConfigResponse, InternalTestingFetchConfigResponse,

View file

@ -9,6 +9,15 @@
//! The `cli` submodule implements some batch-processing analysis, primarily as //! The `cli` submodule implements some batch-processing analysis, primarily as
//! a debugging aid. //! a debugging aid.
/// Any toolchain less than this version will likely not work with rust-analyzer built from this revision.
pub const MINIMUM_SUPPORTED_TOOLCHAIN_VERSION: semver::Version = semver::Version {
major: 1,
minor: 78,
patch: 0,
pre: semver::Prerelease::EMPTY,
build: semver::BuildMetadata::EMPTY,
};
pub mod cli; pub mod cli;
mod command; mod command;
@ -47,10 +56,7 @@ use self::lsp::ext as lsp_ext;
#[cfg(test)] #[cfg(test)]
mod integrated_benchmarks; mod integrated_benchmarks;
use hir::Mutability;
use ide::{CompletionItem, CompletionItemRefMode, CompletionRelevance};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use tenthash::TentHash;
pub use crate::{ pub use crate::{
lsp::capabilities::server_capabilities, main_loop::main_loop, reload::ws_to_crate_graph, lsp::capabilities::server_capabilities, main_loop::main_loop, reload::ws_to_crate_graph,
@ -65,115 +71,6 @@ pub fn from_json<T: DeserializeOwned>(
.map_err(|e| anyhow::format_err!("Failed to deserialize {what}: {e}; {json}")) .map_err(|e| anyhow::format_err!("Failed to deserialize {what}: {e}; {json}"))
} }
fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; 20] {
fn hash_completion_relevance(hasher: &mut TentHash, relevance: &CompletionRelevance) {
use ide_completion::{
CompletionRelevancePostfixMatch, CompletionRelevanceReturnType,
CompletionRelevanceTypeMatch,
};
hasher.update([
u8::from(relevance.exact_name_match),
u8::from(relevance.is_local),
u8::from(relevance.is_name_already_imported),
u8::from(relevance.requires_import),
u8::from(relevance.is_private_editable),
]);
match relevance.type_match {
None => hasher.update([0u8]),
Some(CompletionRelevanceTypeMatch::CouldUnify) => hasher.update([1u8]),
Some(CompletionRelevanceTypeMatch::Exact) => hasher.update([2u8]),
}
hasher.update([u8::from(relevance.trait_.is_some())]);
if let Some(trait_) = &relevance.trait_ {
hasher.update([u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]);
}
match relevance.postfix_match {
None => hasher.update([0u8]),
Some(CompletionRelevancePostfixMatch::NonExact) => hasher.update([1u8]),
Some(CompletionRelevancePostfixMatch::Exact) => hasher.update([2u8]),
}
hasher.update([u8::from(relevance.function.is_some())]);
if let Some(function) = &relevance.function {
hasher.update([u8::from(function.has_params), u8::from(function.has_self_param)]);
let discriminant: u8 = match function.return_type {
CompletionRelevanceReturnType::Other => 0,
CompletionRelevanceReturnType::DirectConstructor => 1,
CompletionRelevanceReturnType::Constructor => 2,
CompletionRelevanceReturnType::Builder => 3,
};
hasher.update([discriminant]);
}
}
let mut hasher = TentHash::new();
hasher.update([
u8::from(is_ref_completion),
u8::from(item.is_snippet),
u8::from(item.deprecated),
u8::from(item.trigger_call_info),
]);
hasher.update(item.label.primary.len().to_ne_bytes());
hasher.update(&item.label.primary);
hasher.update([u8::from(item.label.detail_left.is_some())]);
if let Some(label_detail) = &item.label.detail_left {
hasher.update(label_detail.len().to_ne_bytes());
hasher.update(label_detail);
}
hasher.update([u8::from(item.label.detail_right.is_some())]);
if let Some(label_detail) = &item.label.detail_right {
hasher.update(label_detail.len().to_ne_bytes());
hasher.update(label_detail);
}
// NB: do not hash edits or source range, as those may change between the time the client sends the resolve request
// and the time it receives it: some editors do allow changing the buffer between that, leading to ranges being different.
//
// Documentation hashing is skipped too, as it's a large blob to process,
// while not really making completion properties more unique as they are already.
let kind_tag = item.kind.tag();
hasher.update(kind_tag.len().to_ne_bytes());
hasher.update(kind_tag);
hasher.update(item.lookup.len().to_ne_bytes());
hasher.update(&item.lookup);
hasher.update([u8::from(item.detail.is_some())]);
if let Some(detail) = &item.detail {
hasher.update(detail.len().to_ne_bytes());
hasher.update(detail);
}
hash_completion_relevance(&mut hasher, &item.relevance);
hasher.update([u8::from(item.ref_match.is_some())]);
if let Some((ref_mode, text_size)) = &item.ref_match {
let discriminant = match ref_mode {
CompletionItemRefMode::Reference(Mutability::Shared) => 0u8,
CompletionItemRefMode::Reference(Mutability::Mut) => 1u8,
CompletionItemRefMode::Dereference => 2u8,
};
hasher.update([discriminant]);
hasher.update(u32::from(*text_size).to_ne_bytes());
}
hasher.update(item.import_to_add.len().to_ne_bytes());
for import_path in &item.import_to_add {
hasher.update(import_path.len().to_ne_bytes());
hasher.update(import_path);
}
hasher.finalize()
}
#[doc(hidden)] #[doc(hidden)]
macro_rules! try_default_ { macro_rules! try_default_ {
($it:expr $(,)?) => { ($it:expr $(,)?) => {

View file

@ -2,6 +2,10 @@
use core::fmt; use core::fmt;
use hir::Mutability;
use ide::{CompletionItem, CompletionItemRefMode, CompletionRelevance};
use tenthash::TentHash;
pub mod ext; pub mod ext;
pub(crate) mod capabilities; pub(crate) mod capabilities;
@ -29,3 +33,112 @@ impl fmt::Display for LspError {
} }
impl std::error::Error for LspError {} impl std::error::Error for LspError {}
pub(crate) fn completion_item_hash(item: &CompletionItem, is_ref_completion: bool) -> [u8; 20] {
fn hash_completion_relevance(hasher: &mut TentHash, relevance: &CompletionRelevance) {
use ide_completion::{
CompletionRelevancePostfixMatch, CompletionRelevanceReturnType,
CompletionRelevanceTypeMatch,
};
hasher.update([
u8::from(relevance.exact_name_match),
u8::from(relevance.is_local),
u8::from(relevance.is_name_already_imported),
u8::from(relevance.requires_import),
u8::from(relevance.is_private_editable),
]);
match relevance.type_match {
None => hasher.update([0u8]),
Some(CompletionRelevanceTypeMatch::CouldUnify) => hasher.update([1u8]),
Some(CompletionRelevanceTypeMatch::Exact) => hasher.update([2u8]),
}
hasher.update([u8::from(relevance.trait_.is_some())]);
if let Some(trait_) = &relevance.trait_ {
hasher.update([u8::from(trait_.is_op_method), u8::from(trait_.notable_trait)]);
}
match relevance.postfix_match {
None => hasher.update([0u8]),
Some(CompletionRelevancePostfixMatch::NonExact) => hasher.update([1u8]),
Some(CompletionRelevancePostfixMatch::Exact) => hasher.update([2u8]),
}
hasher.update([u8::from(relevance.function.is_some())]);
if let Some(function) = &relevance.function {
hasher.update([u8::from(function.has_params), u8::from(function.has_self_param)]);
let discriminant: u8 = match function.return_type {
CompletionRelevanceReturnType::Other => 0,
CompletionRelevanceReturnType::DirectConstructor => 1,
CompletionRelevanceReturnType::Constructor => 2,
CompletionRelevanceReturnType::Builder => 3,
};
hasher.update([discriminant]);
}
}
let mut hasher = TentHash::new();
hasher.update([
u8::from(is_ref_completion),
u8::from(item.is_snippet),
u8::from(item.deprecated),
u8::from(item.trigger_call_info),
]);
hasher.update(item.label.primary.len().to_ne_bytes());
hasher.update(&item.label.primary);
hasher.update([u8::from(item.label.detail_left.is_some())]);
if let Some(label_detail) = &item.label.detail_left {
hasher.update(label_detail.len().to_ne_bytes());
hasher.update(label_detail);
}
hasher.update([u8::from(item.label.detail_right.is_some())]);
if let Some(label_detail) = &item.label.detail_right {
hasher.update(label_detail.len().to_ne_bytes());
hasher.update(label_detail);
}
// NB: do not hash edits or source range, as those may change between the time the client sends the resolve request
// and the time it receives it: some editors do allow changing the buffer between that, leading to ranges being different.
//
// Documentation hashing is skipped too, as it's a large blob to process,
// while not really making completion properties more unique as they are already.
let kind_tag = item.kind.tag();
hasher.update(kind_tag.len().to_ne_bytes());
hasher.update(kind_tag);
hasher.update(item.lookup.len().to_ne_bytes());
hasher.update(&item.lookup);
hasher.update([u8::from(item.detail.is_some())]);
if let Some(detail) = &item.detail {
hasher.update(detail.len().to_ne_bytes());
hasher.update(detail);
}
hash_completion_relevance(&mut hasher, &item.relevance);
hasher.update([u8::from(item.ref_match.is_some())]);
if let Some((ref_mode, text_size)) = &item.ref_match {
let discriminant = match ref_mode {
CompletionItemRefMode::Reference(Mutability::Shared) => 0u8,
CompletionItemRefMode::Reference(Mutability::Mut) => 1u8,
CompletionItemRefMode::Dereference => 2u8,
};
hasher.update([discriminant]);
hasher.update(u32::from(*text_size).to_ne_bytes());
}
hasher.update(item.import_to_add.len().to_ne_bytes());
for import_path in &item.import_to_add {
hasher.update(import_path.len().to_ne_bytes());
hasher.update(import_path);
}
hasher.finalize()
}

View file

@ -24,11 +24,11 @@ use serde_json::to_value;
use vfs::AbsPath; use vfs::AbsPath;
use crate::{ use crate::{
completion_item_hash,
config::{CallInfoConfig, Config}, config::{CallInfoConfig, Config},
global_state::GlobalStateSnapshot, global_state::GlobalStateSnapshot,
line_index::{LineEndings, LineIndex, PositionEncoding}, line_index::{LineEndings, LineIndex, PositionEncoding},
lsp::{ lsp::{
completion_item_hash,
ext::ShellRunnableArgs, ext::ShellRunnableArgs,
semantic_tokens::{self, standard_fallback_type}, semantic_tokens::{self, standard_fallback_type},
utils::invalid_params_error, utils::invalid_params_error,

View file

@ -182,6 +182,22 @@ impl GlobalState {
self.proc_macro_clients.iter().map(Some).chain(iter::repeat_with(|| None)); self.proc_macro_clients.iter().map(Some).chain(iter::repeat_with(|| None));
for (ws, proc_macro_client) in self.workspaces.iter().zip(proc_macro_clients) { for (ws, proc_macro_client) in self.workspaces.iter().zip(proc_macro_clients) {
if let Some(toolchain) = &ws.toolchain {
if *toolchain < crate::MINIMUM_SUPPORTED_TOOLCHAIN_VERSION {
status.health |= lsp_ext::Health::Warning;
format_to!(
message,
"Workspace `{}` is using an outdated toolchain version `{}` but \
rust-analyzer only supports `{}` and higher.\n\
Consider using the rust-analyzer rustup component for your toolchain or
upgrade your toolchain to a supported version.\n\n",
ws.manifest_or_root(),
toolchain,
crate::MINIMUM_SUPPORTED_TOOLCHAIN_VERSION,
);
}
}
if let ProjectWorkspaceKind::Cargo { error: Some(error), .. } if let ProjectWorkspaceKind::Cargo { error: Some(error), .. }
| ProjectWorkspaceKind::DetachedFile { | ProjectWorkspaceKind::DetachedFile {
cargo: Some((_, _, Some(error))), .. cargo: Some((_, _, Some(error))), ..