Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #21309 from jackh726/failed_obligations
Add an lsp extension to get failed obligations for a given function
Chayim Refael Friedman 4 months ago
parent 765b7bd · parent f870ae8 · commit 4849b44
-rw-r--r--Cargo.lock2
-rw-r--r--crates/hir-ty/Cargo.toml1
-rw-r--r--crates/hir-ty/src/infer.rs14
-rw-r--r--crates/hir-ty/src/lib.rs2
-rw-r--r--crates/hir-ty/src/next_solver.rs1
-rw-r--r--crates/hir-ty/src/next_solver/format_proof_tree.rs93
-rw-r--r--crates/hir-ty/src/next_solver/fulfill.rs3
-rw-r--r--crates/hir-ty/src/next_solver/infer/at.rs2
-rw-r--r--crates/hir-ty/src/next_solver/infer/mod.rs31
-rw-r--r--crates/hir-ty/src/next_solver/infer/traits.rs14
-rw-r--r--crates/hir-ty/src/next_solver/inspect.rs9
-rw-r--r--crates/hir-ty/src/traits.rs25
-rw-r--r--crates/hir/Cargo.toml1
-rw-r--r--crates/hir/src/semantics.rs39
-rw-r--r--crates/ide/src/lib.rs14
-rw-r--r--crates/rust-analyzer/src/handlers/request.rs16
-rw-r--r--crates/rust-analyzer/src/lsp/ext.rs15
-rw-r--r--crates/rust-analyzer/src/main_loop.rs1
-rw-r--r--crates/rust-analyzer/tests/slow-tests/main.rs39
-rw-r--r--docs/book/src/contributing/lsp-extensions.md13
-rw-r--r--editors/code/package.json9
-rw-r--r--editors/code/src/commands.ts12
-rw-r--r--editors/code/src/lsp_ext.ts3
-rw-r--r--editors/code/src/main.ts1
24 files changed, 344 insertions, 16 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 7ada91d2b6..92a6c3e976 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -788,6 +788,7 @@ dependencies = [
"itertools 0.14.0",
"ra-ap-rustc_type_ir",
"rustc-hash 2.1.1",
+ "serde_json",
"smallvec",
"span",
"stdx",
@@ -901,6 +902,7 @@ dependencies = [
"rustc_apfloat",
"salsa",
"salsa-macros",
+ "serde",
"smallvec",
"span",
"stdx",
diff --git a/crates/hir-ty/Cargo.toml b/crates/hir-ty/Cargo.toml
index c60ecef58e..e54a6190b4 100644
--- a/crates/hir-ty/Cargo.toml
+++ b/crates/hir-ty/Cargo.toml
@@ -18,6 +18,7 @@ itertools.workspace = true
arrayvec.workspace = true
smallvec.workspace = true
ena = "0.14.3"
+serde.workspace = true
either.workspace = true
oorandom = "11.1.5"
tracing = { workspace = true, features = ["attributes"] }
diff --git a/crates/hir-ty/src/infer.rs b/crates/hir-ty/src/infer.rs
index 4f739dc8ee..d527a4ae29 100644
--- a/crates/hir-ty/src/infer.rs
+++ b/crates/hir-ty/src/infer.rs
@@ -75,7 +75,7 @@ use crate::{
AliasTy, Const, DbInterner, ErrorGuaranteed, GenericArg, GenericArgs, Region,
StoredGenericArgs, StoredTy, StoredTys, Ty, TyKind, Tys,
abi::Safety,
- infer::{InferCtxt, traits::ObligationCause},
+ infer::{InferCtxt, ObligationInspector, traits::ObligationCause},
},
traits::FnTrait,
utils::TargetFeatureIsSafeInTarget,
@@ -94,11 +94,23 @@ pub(crate) use closure::analysis::{CaptureKind, CapturedItem, CapturedItemWithou
/// The entry point of type inference.
fn infer_query(db: &dyn HirDatabase, def: DefWithBodyId) -> InferenceResult {
+ infer_query_with_inspect(db, def, None)
+}
+
+pub fn infer_query_with_inspect<'db>(
+ db: &'db dyn HirDatabase,
+ def: DefWithBodyId,
+ inspect: Option<ObligationInspector<'db>>,
+) -> InferenceResult {
let _p = tracing::info_span!("infer_query").entered();
let resolver = def.resolver(db);
let body = db.body(def);
let mut ctx = InferenceContext::new(db, def, &body, resolver);
+ if let Some(inspect) = inspect {
+ ctx.table.infer_ctxt.attach_obligation_inspector(inspect);
+ }
+
match def {
DefWithBodyId::FunctionId(f) => {
ctx.collect_fn(f);
diff --git a/crates/hir-ty/src/lib.rs b/crates/hir-ty/src/lib.rs
index 6081196bac..1674771413 100644
--- a/crates/hir-ty/src/lib.rs
+++ b/crates/hir-ty/src/lib.rs
@@ -88,7 +88,7 @@ pub use infer::{
InferenceTyDiagnosticSource, OverloadedDeref, PointerCast,
cast::CastError,
closure::analysis::{CaptureKind, CapturedItem},
- could_coerce, could_unify, could_unify_deeply,
+ could_coerce, could_unify, could_unify_deeply, infer_query_with_inspect,
};
pub use lower::{
GenericPredicates, ImplTraits, LifetimeElisionKind, TyDefId, TyLoweringContext, ValueTyDefId,
diff --git a/crates/hir-ty/src/next_solver.rs b/crates/hir-ty/src/next_solver.rs
index 298af7d124..605e31404c 100644
--- a/crates/hir-ty/src/next_solver.rs
+++ b/crates/hir-ty/src/next_solver.rs
@@ -9,6 +9,7 @@ mod binder;
mod consts;
mod def_id;
pub mod fold;
+pub mod format_proof_tree;
pub mod fulfill;
mod generic_arg;
pub mod generics;
diff --git a/crates/hir-ty/src/next_solver/format_proof_tree.rs b/crates/hir-ty/src/next_solver/format_proof_tree.rs
new file mode 100644
index 0000000000..fa09cda234
--- /dev/null
+++ b/crates/hir-ty/src/next_solver/format_proof_tree.rs
@@ -0,0 +1,93 @@
+use rustc_type_ir::{solve::GoalSource, solve::inspect::GoalEvaluation};
+use serde::{Deserialize, Serialize};
+
+use crate::next_solver::infer::InferCtxt;
+use crate::next_solver::inspect::{InspectCandidate, InspectGoal};
+use crate::next_solver::{DbInterner, Span};
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ProofTreeData {
+ pub goal: String,
+ pub result: String,
+ pub depth: usize,
+ pub candidates: Vec<CandidateData>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct CandidateData {
+ pub kind: String,
+ pub result: String,
+ pub impl_header: Option<String>,
+ pub nested_goals: Vec<ProofTreeData>,
+}
+
+pub fn dump_proof_tree_structured<'db>(
+ proof_tree: GoalEvaluation<DbInterner<'db>>,
+ _span: Span,
+ infcx: &InferCtxt<'db>,
+) -> ProofTreeData {
+ let goal_eval = InspectGoal::new(infcx, 0, proof_tree, None, GoalSource::Misc);
+ let mut serializer = ProofTreeSerializer::new(infcx);
+ serializer.serialize_goal(&goal_eval)
+}
+
+struct ProofTreeSerializer<'a, 'db> {
+ infcx: &'a InferCtxt<'db>,
+}
+
+impl<'a, 'db> ProofTreeSerializer<'a, 'db> {
+ fn new(infcx: &'a InferCtxt<'db>) -> Self {
+ Self { infcx }
+ }
+
+ fn serialize_goal(&mut self, goal: &InspectGoal<'_, 'db>) -> ProofTreeData {
+ let candidates = goal.candidates();
+ let candidates_data: Vec<CandidateData> =
+ candidates.iter().map(|c| self.serialize_candidate(c)).collect();
+
+ ProofTreeData {
+ goal: format!("{:?}", goal.goal()),
+ result: format!("{:?}", goal.result()),
+ depth: goal.depth(),
+ candidates: candidates_data,
+ }
+ }
+
+ fn serialize_candidate(&mut self, candidate: &InspectCandidate<'_, 'db>) -> CandidateData {
+ let kind = candidate.kind();
+ let impl_header = self.get_impl_header(candidate);
+
+ let mut nested = Vec::new();
+ self.infcx.probe(|_| {
+ for nested_goal in candidate.instantiate_nested_goals() {
+ nested.push(self.serialize_goal(&nested_goal));
+ }
+ });
+
+ CandidateData {
+ kind: format!("{:?}", kind),
+ result: format!("{:?}", candidate.result()),
+ impl_header,
+ nested_goals: nested,
+ }
+ }
+
+ fn get_impl_header(&self, candidate: &InspectCandidate<'_, 'db>) -> Option<String> {
+ use rustc_type_ir::solve::inspect::ProbeKind;
+ match candidate.kind() {
+ ProbeKind::TraitCandidate { source, .. } => {
+ use rustc_type_ir::solve::CandidateSource;
+ match source {
+ CandidateSource::Impl(impl_def_id) => {
+ use hir_def::{Lookup, src::HasSource};
+ let db = self.infcx.interner.db;
+ let impl_src = impl_def_id.0.lookup(db).source(db);
+ Some(impl_src.value.to_string())
+ }
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+ }
+}
diff --git a/crates/hir-ty/src/next_solver/fulfill.rs b/crates/hir-ty/src/next_solver/fulfill.rs
index 4a19e30c64..0fe0732972 100644
--- a/crates/hir-ty/src/next_solver/fulfill.rs
+++ b/crates/hir-ty/src/next_solver/fulfill.rs
@@ -187,6 +187,9 @@ impl<'db> FulfillmentCtxt<'db> {
}
let result = delegate.evaluate_root_goal(goal, Span::dummy(), stalled_on);
+ infcx.inspect_evaluated_obligation(&obligation, &result, || {
+ Some(delegate.evaluate_root_goal_for_proof_tree(goal, Span::dummy()).1)
+ });
let GoalEvaluation { goal: _, certainty, has_changed, stalled_on } = match result {
Ok(result) => result,
Err(NoSolution) => {
diff --git a/crates/hir-ty/src/next_solver/infer/at.rs b/crates/hir-ty/src/next_solver/infer/at.rs
index 6ba1aae2d4..dc0b584084 100644
--- a/crates/hir-ty/src/next_solver/infer/at.rs
+++ b/crates/hir-ty/src/next_solver/infer/at.rs
@@ -68,6 +68,7 @@ impl<'db> InferCtxt<'db> {
inner: self.inner.clone(),
tainted_by_errors: self.tainted_by_errors.clone(),
universe: self.universe.clone(),
+ obligation_inspector: self.obligation_inspector.clone(),
}
}
@@ -84,6 +85,7 @@ impl<'db> InferCtxt<'db> {
inner: self.inner.clone(),
tainted_by_errors: self.tainted_by_errors.clone(),
universe: self.universe.clone(),
+ obligation_inspector: self.obligation_inspector.clone(),
}
}
}
diff --git a/crates/hir-ty/src/next_solver/infer/mod.rs b/crates/hir-ty/src/next_solver/infer/mod.rs
index 14b8a61088..2926dc30de 100644
--- a/crates/hir-ty/src/next_solver/infer/mod.rs
+++ b/crates/hir-ty/src/next_solver/infer/mod.rs
@@ -10,8 +10,9 @@ use ena::unify as ut;
use hir_def::GenericParamId;
use opaque_types::{OpaqueHiddenType, OpaqueTypeStorage};
use region_constraints::{RegionConstraintCollector, RegionConstraintStorage};
-use rustc_next_trait_solver::solve::SolverDelegateEvalExt;
+use rustc_next_trait_solver::solve::{GoalEvaluation, SolverDelegateEvalExt};
use rustc_pattern_analysis::Captures;
+use rustc_type_ir::solve::{NoSolution, inspect};
use rustc_type_ir::{
ClosureKind, ConstVid, FloatVarValue, FloatVid, GenericArgKind, InferConst, InferTy,
IntVarValue, IntVid, OutlivesPredicate, RegionVid, TermKind, TyVid, TypeFoldable, TypeFolder,
@@ -27,6 +28,7 @@ use traits::{ObligationCause, PredicateObligations};
use type_variable::TypeVariableOrigin;
use unify_key::{ConstVariableOrigin, ConstVariableValue, ConstVidKey};
+pub use crate::next_solver::infer::traits::ObligationInspector;
use crate::next_solver::{
ArgOutlivesPredicate, BoundConst, BoundRegion, BoundTy, BoundVarKind, Goal, Predicate,
SolverContext,
@@ -250,6 +252,8 @@ pub struct InferCtxt<'db> {
/// when we enter into a higher-ranked (`for<..>`) type or trait
/// bound.
universe: Cell<UniverseIndex>,
+
+ obligation_inspector: Cell<Option<ObligationInspector<'db>>>,
}
/// See the `error_reporting` module for more details.
@@ -375,6 +379,7 @@ impl<'db> InferCtxtBuilder<'db> {
inner: RefCell::new(InferCtxtInner::new()),
tainted_by_errors: Cell::new(None),
universe: Cell::new(UniverseIndex::ROOT),
+ obligation_inspector: Cell::new(None),
}
}
}
@@ -1223,6 +1228,30 @@ impl<'db> InferCtxt<'db> {
fn sub_unify_ty_vids_raw(&self, a: rustc_type_ir::TyVid, b: rustc_type_ir::TyVid) {
self.inner.borrow_mut().type_variables().sub_unify(a, b);
}
+
+ /// Attach a callback to be invoked on each root obligation evaluated in the new trait solver.
+ pub fn attach_obligation_inspector(&self, inspector: ObligationInspector<'db>) {
+ debug_assert!(
+ self.obligation_inspector.get().is_none(),
+ "shouldn't override a set obligation inspector"
+ );
+ self.obligation_inspector.set(Some(inspector));
+ }
+
+ pub fn inspect_evaluated_obligation(
+ &self,
+ obligation: &PredicateObligation<'db>,
+ result: &Result<GoalEvaluation<DbInterner<'db>>, NoSolution>,
+ get_proof_tree: impl FnOnce() -> Option<inspect::GoalEvaluation<DbInterner<'db>>>,
+ ) {
+ if let Some(inspector) = self.obligation_inspector.get() {
+ let result = match result {
+ Ok(GoalEvaluation { certainty, .. }) => Ok(*certainty),
+ Err(_) => Err(NoSolution),
+ };
+ (inspector)(self, obligation, result, get_proof_tree());
+ }
+ }
}
/// Helper for [InferCtxt::ty_or_const_infer_var_changed] (see comment on that), currently
diff --git a/crates/hir-ty/src/next_solver/infer/traits.rs b/crates/hir-ty/src/next_solver/infer/traits.rs
index 3409de17a1..14df42dc2a 100644
--- a/crates/hir-ty/src/next_solver/infer/traits.rs
+++ b/crates/hir-ty/src/next_solver/infer/traits.rs
@@ -9,8 +9,11 @@ use std::{
use hir_def::TraitId;
use macros::{TypeFoldable, TypeVisitable};
-use rustc_type_ir::Upcast;
use rustc_type_ir::elaborate::Elaboratable;
+use rustc_type_ir::{
+ Upcast,
+ solve::{Certainty, NoSolution, inspect},
+};
use tracing::debug;
use crate::next_solver::{
@@ -79,6 +82,15 @@ pub struct Obligation<'db, T> {
pub recursion_depth: usize,
}
+/// A callback that can be provided to `inspect_typeck`. Invoked on evaluation
+/// of root obligations.
+pub type ObligationInspector<'db> = fn(
+ &InferCtxt<'db>,
+ &PredicateObligation<'db>,
+ Result<Certainty, NoSolution>,
+ Option<inspect::GoalEvaluation<DbInterner<'db>>>,
+);
+
/// For [`Obligation`], a sub-obligation is combined with the current obligation's
/// param-env and cause code.
impl<'db> Elaboratable<DbInterner<'db>> for PredicateObligation<'db> {
diff --git a/crates/hir-ty/src/next_solver/inspect.rs b/crates/hir-ty/src/next_solver/inspect.rs
index d66aa9f277..5286977549 100644
--- a/crates/hir-ty/src/next_solver/inspect.rs
+++ b/crates/hir-ty/src/next_solver/inspect.rs
@@ -74,7 +74,7 @@ impl<'a, 'db> std::fmt::Debug for InspectCandidate<'a, 'db> {
/// treat `NormalizesTo` goals as if they apply the expected
/// type at the end of each candidate.
#[derive(Debug, Copy, Clone)]
-struct NormalizesToTermHack<'db> {
+pub(crate) struct NormalizesToTermHack<'db> {
term: Term<'db>,
unconstrained_term: Term<'db>,
}
@@ -311,10 +311,7 @@ impl<'a, 'db> InspectCandidate<'a, 'db> {
/// Visit all nested goals of this candidate, rolling back
/// all inference constraints.
#[expect(dead_code, reason = "used in rustc")]
- pub(crate) fn visit_nested_in_probe<V: ProofTreeVisitor<'db>>(
- &self,
- visitor: &mut V,
- ) -> V::Result {
+ fn visit_nested_in_probe<V: ProofTreeVisitor<'db>>(&self, visitor: &mut V) -> V::Result {
self.goal.infcx.probe(|_| self.visit_nested_no_probe(visitor))
}
}
@@ -430,7 +427,7 @@ impl<'a, 'db> InspectGoal<'a, 'db> {
candidates.pop().filter(|_| candidates.is_empty())
}
- fn new(
+ pub(crate) fn new(
infcx: &'a InferCtxt<'db>,
depth: usize,
root: inspect::GoalEvaluation<DbInterner<'db>>,
diff --git a/crates/hir-ty/src/traits.rs b/crates/hir-ty/src/traits.rs
index ad8de293d5..fb598fe5ac 100644
--- a/crates/hir-ty/src/traits.rs
+++ b/crates/hir-ty/src/traits.rs
@@ -23,7 +23,10 @@ use crate::{
next_solver::{
Canonical, DbInterner, GenericArgs, Goal, ParamEnv, Predicate, SolverContext, Span,
StoredClauses, Ty, TyKind,
- infer::{DbInternerInferExt, InferCtxt, traits::ObligationCause},
+ infer::{
+ DbInternerInferExt, InferCtxt,
+ traits::{Obligation, ObligationCause},
+ },
obligation_ctxt::ObligationCtxt,
},
};
@@ -107,6 +110,16 @@ pub fn next_trait_solve_canonical_in_ctxt<'db>(
let res = context.evaluate_root_goal(goal, Span::dummy(), None);
+ let obligation = Obligation {
+ cause: ObligationCause::dummy(),
+ param_env: goal.param_env,
+ recursion_depth: 0,
+ predicate: goal.predicate,
+ };
+ infer_ctxt.inspect_evaluated_obligation(&obligation, &res, || {
+ Some(context.evaluate_root_goal_for_proof_tree(goal, Span::dummy()).1)
+ });
+
let res = res.map(|r| (r.has_changed, r.certainty));
tracing::debug!("solve_nextsolver({:?}) => {:?}", goal, res);
@@ -130,6 +143,16 @@ pub fn next_trait_solve_in_ctxt<'db, 'a>(
let res = context.evaluate_root_goal(goal, Span::dummy(), None);
+ let obligation = Obligation {
+ cause: ObligationCause::dummy(),
+ param_env: goal.param_env,
+ recursion_depth: 0,
+ predicate: goal.predicate,
+ };
+ infer_ctxt.inspect_evaluated_obligation(&obligation, &res, || {
+ Some(context.evaluate_root_goal_for_proof_tree(goal, Span::dummy()).1)
+ });
+
let res = res.map(|r| (r.has_changed, r.certainty));
tracing::debug!("solve_nextsolver({:?}) => {:?}", goal, res);
diff --git a/crates/hir/Cargo.toml b/crates/hir/Cargo.toml
index 24b2bd9150..d20ee1546f 100644
--- a/crates/hir/Cargo.toml
+++ b/crates/hir/Cargo.toml
@@ -17,6 +17,7 @@ rustc-hash.workspace = true
either.workspace = true
arrayvec.workspace = true
itertools.workspace = true
+serde_json.workspace = true
smallvec.workspace = true
tracing = { workspace = true, features = ["attributes"] }
triomphe.workspace = true
diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs
index fd08528e86..fcb97ab34e 100644
--- a/crates/hir/src/semantics.rs
+++ b/crates/hir/src/semantics.rs
@@ -32,11 +32,16 @@ use hir_expand::{
use hir_ty::{
InferenceResult,
diagnostics::{unsafe_operations, unsafe_operations_for_body},
- next_solver::DbInterner,
+ infer_query_with_inspect,
+ next_solver::{
+ DbInterner, Span,
+ format_proof_tree::{ProofTreeData, dump_proof_tree_structured},
+ },
};
use intern::{Interned, Symbol, sym};
use itertools::Itertools;
use rustc_hash::{FxHashMap, FxHashSet};
+use rustc_type_ir::inherent::Span as _;
use smallvec::{SmallVec, smallvec};
use span::{FileId, SyntaxContext};
use stdx::{TupleExt, always};
@@ -2310,6 +2315,38 @@ impl<'db> SemanticsImpl<'db> {
Some(locals)
}
+
+ pub fn get_failed_obligations(&self, token: SyntaxToken) -> Option<String> {
+ let node = token.parent()?;
+ let node = self.find_file(&node);
+
+ let container = self.with_ctx(|ctx| ctx.find_container(node))?;
+
+ match container {
+ ChildContainer::DefWithBodyId(def) => {
+ thread_local! {
+ static RESULT: RefCell<Vec<ProofTreeData>> = const { RefCell::new(Vec::new()) };
+ }
+ infer_query_with_inspect(
+ self.db,
+ def,
+ Some(|infer_ctxt, _obligation, result, proof_tree| {
+ if result.is_err()
+ && let Some(tree) = proof_tree
+ {
+ let data = dump_proof_tree_structured(tree, Span::dummy(), infer_ctxt);
+ RESULT.with(|ctx| ctx.borrow_mut().push(data));
+ }
+ }),
+ );
+ let data: Vec<ProofTreeData> =
+ RESULT.with(|data| data.borrow_mut().drain(..).collect());
+ let data = serde_json::to_string_pretty(&data).unwrap_or_else(|_| "[]".to_owned());
+ Some(data)
+ }
+ _ => None,
+ }
+ }
}
// FIXME This can't be the best way to do this
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 0066ceed21..5e4d930393 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -73,7 +73,7 @@ use ide_db::{
};
use ide_db::{MiniCore, ra_fixture::RaFixtureAnalysis};
use macros::UpmapFromRaFixture;
-use syntax::{SourceFile, ast};
+use syntax::{AstNode, SourceFile, ast};
use triomphe::Arc;
use view_memory_layout::{RecursiveMemoryLayout, view_memory_layout};
@@ -905,6 +905,18 @@ impl Analysis {
self.with_db(|db| view_memory_layout(db, position))
}
+ pub fn get_failed_obligations(&self, offset: TextSize, file_id: FileId) -> Cancellable<String> {
+ self.with_db(|db| {
+ let sema = Semantics::new(db);
+ let source_file = sema.parse_guess_edition(file_id);
+
+ let Some(token) = source_file.syntax().token_at_offset(offset).next() else {
+ return String::new();
+ };
+ sema.get_failed_obligations(token).unwrap_or_default()
+ })
+ }
+
pub fn editioned_file_id_to_vfs(&self, file_id: hir::EditionedFileId) -> FileId {
file_id.file_id(&self.db)
}
diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs
index d15b519d69..4d97505768 100644
--- a/crates/rust-analyzer/src/handlers/request.rs
+++ b/crates/rust-analyzer/src/handlers/request.rs
@@ -42,8 +42,8 @@ use crate::{
lsp::{
LspError, completion_item_hash,
ext::{
- InternalTestingFetchConfigOption, InternalTestingFetchConfigParams,
- InternalTestingFetchConfigResponse,
+ GetFailedObligationsParams, InternalTestingFetchConfigOption,
+ InternalTestingFetchConfigParams, InternalTestingFetchConfigResponse,
},
from_proto, to_proto,
utils::{all_edits_are_disjoint, invalid_params_error},
@@ -2575,6 +2575,18 @@ pub(crate) fn internal_testing_fetch_config(
}))
}
+pub(crate) fn get_failed_obligations(
+ snap: GlobalStateSnapshot,
+ params: GetFailedObligationsParams,
+) -> anyhow::Result<String> {
+ let _p = tracing::info_span!("get_failed_obligations").entered();
+ let file_id = try_default!(from_proto::file_id(&snap, &params.text_document.uri)?);
+ let line_index = snap.file_line_index(file_id)?;
+ let offset = from_proto::offset(&line_index, params.position)?;
+
+ Ok(snap.analysis.get_failed_obligations(offset, file_id)?)
+}
+
/// Searches for the directory of a Rust crate given this crate's root file path.
///
/// # Arguments
diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs
index b132323bec..e6493eefef 100644
--- a/crates/rust-analyzer/src/lsp/ext.rs
+++ b/crates/rust-analyzer/src/lsp/ext.rs
@@ -864,3 +864,18 @@ pub struct CompletionImport {
pub struct ClientCommandOptions {
pub commands: Vec<String>,
}
+
+pub enum GetFailedObligations {}
+
+#[derive(Deserialize, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct GetFailedObligationsParams {
+ pub text_document: TextDocumentIdentifier,
+ pub position: Position,
+}
+
+impl Request for GetFailedObligations {
+ type Params = GetFailedObligationsParams;
+ type Result = String;
+ const METHOD: &'static str = "rust-analyzer/getFailedObligations";
+}
diff --git a/crates/rust-analyzer/src/main_loop.rs b/crates/rust-analyzer/src/main_loop.rs
index 1a1c0182f8..6e08b7bb88 100644
--- a/crates/rust-analyzer/src/main_loop.rs
+++ b/crates/rust-analyzer/src/main_loop.rs
@@ -1328,6 +1328,7 @@ impl GlobalState {
.on::<NO_RETRY, lsp_ext::MoveItem>(handlers::handle_move_item)
//
.on::<NO_RETRY, lsp_ext::InternalTestingFetchConfig>(handlers::internal_testing_fetch_config)
+ .on::<RETRY, lsp_ext::GetFailedObligations>(handlers::get_failed_obligations)
.finish();
}
diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs
index 48433342d5..eb1b8c5dd0 100644
--- a/crates/rust-analyzer/tests/slow-tests/main.rs
+++ b/crates/rust-analyzer/tests/slow-tests/main.rs
@@ -1488,3 +1488,42 @@ version = "0.0.0"
server.request::<WorkspaceSymbolRequest>(Default::default(), json!([]));
}
+
+#[test]
+fn test_get_failed_obligations() {
+ use expect_test::expect;
+ if skip_slow_tests() {
+ return;
+ }
+
+ let server = Project::with_fixture(
+ r#"
+//- /Cargo.toml
+[package]
+name = "foo"
+version = "0.0.0"
+
+//- /src/lib.rs
+trait Trait {}
+fn requires_trait<T: Trait>(x: T) {}
+
+fn test() {
+ requires_trait(0usize);
+}
+"#,
+ )
+ .server()
+ .wait_until_workspace_is_loaded();
+
+ let res = server.send_request::<rust_analyzer::lsp::ext::GetFailedObligations>(
+ rust_analyzer::lsp::ext::GetFailedObligationsParams {
+ text_document: server.doc_id("src/lib.rs"),
+ position: Position::new(4, 19),
+ },
+ );
+
+ let res: serde_json::Value = serde_json::from_str(res.as_str().unwrap()).unwrap();
+ let arr = res.as_array().unwrap();
+ assert_eq!(arr.len(), 2);
+ expect![[r#"{"goal":"Goal { param_env: ParamEnv { clauses: [] }, predicate: Binder { value: TraitPredicate(usize: Trait, polarity:Positive), bound_vars: [] } }","result":"Err(NoSolution)","depth":0,"candidates":[]}"#]].assert_eq(&arr[0].to_string());
+}
diff --git a/docs/book/src/contributing/lsp-extensions.md b/docs/book/src/contributing/lsp-extensions.md
index 5922f0b551..5d21c37806 100644
--- a/docs/book/src/contributing/lsp-extensions.md
+++ b/docs/book/src/contributing/lsp-extensions.md
@@ -1,5 +1,5 @@
<!---
-lsp/ext.rs hash: 78e87a78de8f288e
+lsp/ext.rs hash: 235f56089da3dbb5
If you need to change the above hash to make the test pass, please check if you
need to adjust this doc as well and ping this issue:
@@ -731,6 +731,17 @@ For debugging or when working on rust-analyzer itself.
Returns a textual representation of the MIR of the function containing the cursor.
For debugging or when working on rust-analyzer itself.
+## Get Failed Obligations
+
+**Method:** `rust-analyzer/getFailedObligations`
+
+**Request:** `TextDocumentPositionParams`
+
+**Response:** `string`
+
+Returns information about failed trait obligations at the given position.
+For debugging or when working on rust-analyzer itself.
+
## Interpret Function
**Method:** `rust-analyzer/interpretFunction`
diff --git a/editors/code/package.json b/editors/code/package.json
index 97db1322dd..d0410c70da 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -339,6 +339,11 @@
"command": "rust-analyzer.openWalkthrough",
"title": "Open Walkthrough",
"category": "rust-analyzer"
+ },
+ {
+ "command": "rust-analyzer.getFailedObligations",
+ "title": "Get Failed Obligations",
+ "category": "rust-analyzer (debug command)"
}
],
"keybindings": [
@@ -3700,6 +3705,10 @@
{
"command": "rust-analyzer.syntaxTreeShowWhitespace",
"when": "false"
+ },
+ {
+ "command": "rust-analyzer.getFailedObligations",
+ "when": "inRustProject"
}
],
"editor/context": [
diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 16fc586d5d..c1b6f31030 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -599,6 +599,18 @@ export function viewMir(ctx: CtxInit): Cmd {
return viewHirOrMir(ctx, "mir");
}
+export function getFailedObligations(ctx: CtxInit): Cmd {
+ const uri = `rust-analyzer-failed-obligations://getFailedObligations/failedObligations.rs`;
+ const scheme = `rust-analyzer-failed-obligations`;
+ return viewFileUsingTextDocumentContentProvider(
+ ctx,
+ ra.getFailedObligations,
+ uri,
+ scheme,
+ true,
+ );
+}
+
// Opens the virtual file that will show the MIR of the function containing the cursor position
//
// The contents of the file come from the `TextDocumentContentProvider`
diff --git a/editors/code/src/lsp_ext.ts b/editors/code/src/lsp_ext.ts
index 643f61b25d..9712bd4b7b 100644
--- a/editors/code/src/lsp_ext.ts
+++ b/editors/code/src/lsp_ext.ts
@@ -67,6 +67,9 @@ export const interpretFunction = new lc.RequestType<lc.TextDocumentPositionParam
export const viewItemTree = new lc.RequestType<ViewItemTreeParams, string, void>(
"rust-analyzer/viewItemTree",
);
+export const getFailedObligations = new lc.RequestType<lc.TextDocumentPositionParams, string, void>(
+ "rust-analyzer/getFailedObligations",
+);
export type DiscoverTestParams = { testId?: string | undefined };
export type RunTestParams = {
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 190f5866d0..7d91286552 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -215,6 +215,7 @@ function createCommands(): Record<string, CommandFactory> {
syntaxTreeShowWhitespace: {
enabled: commands.syntaxTreeShowWhitespace,
},
+ getFailedObligations: { enabled: commands.getFailedObligations },
};
}