about summary refs log tree commit diff
path: root/src/tools
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-04-15 08:02:23 +0000
committerbors <bors@rust-lang.org>2025-04-15 08:02:23 +0000
commitf433fa46b0fd27d35219357ad75f54d294081bc4 (patch)
tree856536d645c04538bda01df760fe645d77529dd0 /src/tools
parent58c2dd9a54a325f4ce96f70332ceb07a3b58f0e5 (diff)
parent783b08156ebf2e8f2ee766bb4bfc2f8025c38243 (diff)
downloadrust-f433fa46b0fd27d35219357ad75f54d294081bc4.tar.gz
rust-f433fa46b0fd27d35219357ad75f54d294081bc4.zip
Auto merge of #139845 - Zalathar:rollup-u5u5y1v, r=Zalathar
Rollup of 17 pull requests

Successful merges:

 - #138374 (Enable contracts for const functions)
 - #138380 (ci: add runners for vanilla LLVM 20)
 - #138393 (Allow const patterns of matches to contain pattern types)
 - #139517 (std: sys: process: uefi: Use NULL stdin by default)
 - #139554 (std: add Output::exit_ok)
 - #139660 (compiletest: Add an experimental new executor to replace libtest)
 - #139669 (Overhaul `AssocItem`)
 - #139671 (Proc macro span API redesign: Replace proc_macro::SourceFile by Span::{file, local_file})
 - #139750 (std/thread: Use default stack size from menuconfig for NuttX)
 - #139772 (Remove `hir::Map`)
 - #139785 (Let CStrings be either 1 or 2 byte aligned.)
 - #139789 (do not unnecessarily leak auto traits in item bounds)
 - #139791 (drop global where-bounds before merging candidates)
 - #139798 (normalize: prefer `ParamEnv` over `AliasBound` candidates)
 - #139822 (Fix: Map EOPNOTSUPP to ErrorKind::Unsupported on Unix)
 - #139833 (Fix some HIR pretty-printing problems)
 - #139836 (Basic tests of MPMC receiver cloning)

r? `@ghost`
`@rustbot` modify labels: rollup
Diffstat (limited to 'src/tools')
-rw-r--r--src/tools/clippy/clippy_lints/src/assigning_clones.rs4
-rw-r--r--src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/format_args.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/len_zero.rs13
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/double_ended_iterator_last.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/needless_collect.rs4
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs6
-rw-r--r--src/tools/clippy/clippy_lints/src/missing_trait_methods.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs2
-rw-r--r--src/tools/clippy/clippy_lints/src/same_name_method.rs5
-rw-r--r--src/tools/clippy/clippy_lints/src/unconditional_recursion.rs4
-rw-r--r--src/tools/clippy/clippy_lints/src/unused_self.rs2
-rw-r--r--src/tools/clippy/clippy_utils/src/ty/mod.rs8
-rw-r--r--src/tools/compiletest/src/common.rs5
-rw-r--r--src/tools/compiletest/src/executor.rs345
-rw-r--r--src/tools/compiletest/src/executor/deadline.rs78
-rw-r--r--src/tools/compiletest/src/executor/json.rs111
-rw-r--r--src/tools/compiletest/src/executor/libtest.rs111
-rw-r--r--src/tools/compiletest/src/lib.rs20
-rw-r--r--src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs29
-rw-r--r--src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/token_id.rs22
-rw-r--r--src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/mod.rs2
26 files changed, 606 insertions, 181 deletions
diff --git a/src/tools/clippy/clippy_lints/src/assigning_clones.rs b/src/tools/clippy/clippy_lints/src/assigning_clones.rs
index ab34af7c317..e5439a6d401 100644
--- a/src/tools/clippy/clippy_lints/src/assigning_clones.rs
+++ b/src/tools/clippy/clippy_lints/src/assigning_clones.rs
@@ -111,8 +111,8 @@ impl<'tcx> LateLintPass<'tcx> for AssigningClones {
             // Only suggest if `clone_from`/`clone_into` is explicitly implemented
             && resolved_assoc_items.in_definition_order().any(|assoc|
                 match which_trait {
-                    CloneTrait::Clone => assoc.name == sym::clone_from,
-                    CloneTrait::ToOwned => assoc.name.as_str() == "clone_into",
+                    CloneTrait::Clone => assoc.name() == sym::clone_from,
+                    CloneTrait::ToOwned => assoc.name().as_str() == "clone_into",
                 }
             )
             && !clone_source_borrows_from_dest(cx, lhs, rhs.span)
diff --git a/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs b/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs
index ad18c7039ee..4a876b85416 100644
--- a/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs
+++ b/src/tools/clippy/clippy_lints/src/bool_assert_comparison.rs
@@ -56,7 +56,7 @@ fn is_impl_not_trait_with_bool_out<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -
             cx.tcx.associated_items(trait_id).find_by_ident_and_kind(
                 cx.tcx,
                 Ident::from_str("Output"),
-                ty::AssocKind::Type,
+                ty::AssocTag::Type,
                 trait_id,
             )
         })
diff --git a/src/tools/clippy/clippy_lints/src/format_args.rs b/src/tools/clippy/clippy_lints/src/format_args.rs
index 3862ff7921d..06224f57c5c 100644
--- a/src/tools/clippy/clippy_lints/src/format_args.rs
+++ b/src/tools/clippy/clippy_lints/src/format_args.rs
@@ -550,7 +550,7 @@ impl<'tcx> FormatArgsExpr<'_, 'tcx> {
         // a `Target` that is in `self.ty_msrv_map`.
         if let Some(deref_trait_id) = self.cx.tcx.lang_items().deref_trait()
             && implements_trait(self.cx, ty, deref_trait_id, &[])
-            && let Some(target_ty) = self.cx.get_associated_type(ty, deref_trait_id, "Target")
+            && let Some(target_ty) = self.cx.get_associated_type(ty, deref_trait_id, sym::Target)
             && let Some(msrv) = self.ty_msrv_map.get(&target_ty)
             && msrv.is_none_or(|msrv| self.msrv.meets(self.cx, msrv))
         {
diff --git a/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs b/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs
index d02d9b2102b..430f2411183 100644
--- a/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs
+++ b/src/tools/clippy/clippy_lints/src/implied_bounds_in_impls.rs
@@ -315,7 +315,7 @@ fn check<'tcx>(cx: &LateContext<'tcx>, bounds: GenericBounds<'tcx>) {
                 assocs
                     .filter_by_name_unhygienic(constraint.ident.name)
                     .next()
-                    .is_some_and(|assoc| assoc.kind == ty::AssocKind::Type)
+                    .is_some_and(|assoc| assoc.is_type())
                 })
         {
             emit_lint(cx, poly_trait, bounds, index, implied_constraints, bound);
diff --git a/src/tools/clippy/clippy_lints/src/len_zero.rs b/src/tools/clippy/clippy_lints/src/len_zero.rs
index 72e22ae59d8..77085d52a32 100644
--- a/src/tools/clippy/clippy_lints/src/len_zero.rs
+++ b/src/tools/clippy/clippy_lints/src/len_zero.rs
@@ -13,7 +13,7 @@ use rustc_hir::{
     QPath, TraitItemRef, TyKind,
 };
 use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::ty::{self, AssocKind, FnSig, Ty};
+use rustc_middle::ty::{self, FnSig, Ty};
 use rustc_session::declare_lint_pass;
 use rustc_span::source_map::Spanned;
 use rustc_span::symbol::sym;
@@ -288,8 +288,7 @@ fn check_trait_items(cx: &LateContext<'_>, visited_trait: &Item<'_>, ident: Iden
             .items()
             .flat_map(|&i| cx.tcx.associated_items(i).filter_by_name_unhygienic(is_empty))
             .any(|i| {
-                i.kind == AssocKind::Fn
-                    && i.fn_has_self_parameter
+                i.is_method()
                     && cx.tcx.fn_sig(i.def_id).skip_binder().inputs().skip_binder().len() == 1
             });
 
@@ -466,7 +465,7 @@ fn check_for_is_empty(
         .inherent_impls(impl_ty)
         .iter()
         .flat_map(|&id| cx.tcx.associated_items(id).filter_by_name_unhygienic(is_empty))
-        .find(|item| item.kind == AssocKind::Fn);
+        .find(|item| item.is_fn());
 
     let (msg, is_empty_span, self_kind) = match is_empty {
         None => (
@@ -486,7 +485,7 @@ fn check_for_is_empty(
             None,
         ),
         Some(is_empty)
-            if !(is_empty.fn_has_self_parameter
+            if !(is_empty.is_method()
                 && check_is_empty_sig(
                     cx,
                     cx.tcx.fn_sig(is_empty.def_id).instantiate_identity().skip_binder(),
@@ -608,7 +607,7 @@ fn is_empty_array(expr: &Expr<'_>) -> bool {
 fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
     /// Gets an `AssocItem` and return true if it matches `is_empty(self)`.
     fn is_is_empty(cx: &LateContext<'_>, item: &ty::AssocItem) -> bool {
-        if item.kind == AssocKind::Fn {
+        if item.is_fn() {
             let sig = cx.tcx.fn_sig(item.def_id).skip_binder();
             let ty = sig.skip_binder();
             ty.inputs().len() == 1
@@ -644,7 +643,7 @@ fn has_is_empty(cx: &LateContext<'_>, expr: &Expr<'_>) -> bool {
                         && cx.tcx.get_diagnostic_item(sym::Deref).is_some_and(|deref_id| {
                             implements_trait(cx, ty, deref_id, &[])
                                 && cx
-                                    .get_associated_type(ty, deref_id, "Target")
+                                    .get_associated_type(ty, deref_id, sym::Target)
                                     .is_some_and(|deref_ty| ty_has_is_empty(cx, deref_ty, depth + 1))
                         }))
             },
diff --git a/src/tools/clippy/clippy_lints/src/methods/double_ended_iterator_last.rs b/src/tools/clippy/clippy_lints/src/methods/double_ended_iterator_last.rs
index e82211bbf3e..b5adc69e9a7 100644
--- a/src/tools/clippy/clippy_lints/src/methods/double_ended_iterator_last.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/double_ended_iterator_last.rs
@@ -24,7 +24,7 @@ pub(super) fn check(cx: &LateContext<'_>, expr: &'_ Expr<'_>, self_expr: &'_ Exp
         && let Ok(Some(fn_def)) = Instance::try_resolve(cx.tcx, cx.typing_env(), id, args)
         // find the provided definition of Iterator::last
         && let Some(item) = cx.tcx.get_diagnostic_item(sym::Iterator)
-        && let Some(last_def) = cx.tcx.provided_trait_methods(item).find(|m| m.name.as_str() == "last")
+        && let Some(last_def) = cx.tcx.provided_trait_methods(item).find(|m| m.name().as_str() == "last")
         // if the resolved method is the same as the provided definition
         && fn_def.def_id() == last_def.def_id
     {
diff --git a/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs b/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs
index f51bdc78f8a..7bb625222ec 100644
--- a/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/iter_overeager_cloned.rs
@@ -48,7 +48,7 @@ pub(super) fn check<'tcx>(
         && let Some(method_id) = typeck.type_dependent_def_id(cloned_call.hir_id)
         && cx.tcx.trait_of_item(method_id) == Some(iter_id)
         && let cloned_recv_ty = typeck.expr_ty_adjusted(cloned_recv)
-        && let Some(iter_assoc_ty) = cx.get_associated_type(cloned_recv_ty, iter_id, "Item")
+        && let Some(iter_assoc_ty) = cx.get_associated_type(cloned_recv_ty, iter_id, sym::Item)
         && matches!(*iter_assoc_ty.kind(), ty::Ref(_, ty, _) if !is_copy(cx, ty))
     {
         if needs_into_iter
diff --git a/src/tools/clippy/clippy_lints/src/methods/needless_collect.rs b/src/tools/clippy/clippy_lints/src/methods/needless_collect.rs
index 239ee6c729f..e4a29b6560e 100644
--- a/src/tools/clippy/clippy_lints/src/methods/needless_collect.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/needless_collect.rs
@@ -17,7 +17,7 @@ use rustc_hir::{
 };
 use rustc_lint::LateContext;
 use rustc_middle::hir::nested_filter;
-use rustc_middle::ty::{self, AssocKind, ClauseKind, EarlyBinder, GenericArg, GenericArgKind, Ty};
+use rustc_middle::ty::{self, AssocTag, ClauseKind, EarlyBinder, GenericArg, GenericArgKind, Ty};
 use rustc_span::symbol::Ident;
 use rustc_span::{Span, sym};
 
@@ -241,7 +241,7 @@ fn is_contains_sig(cx: &LateContext<'_>, call_id: HirId, iter_expr: &Expr<'_>) -
         && let Some(iter_item) = cx.tcx.associated_items(iter_trait).find_by_ident_and_kind(
             cx.tcx,
             Ident::with_dummy_span(sym::Item),
-            AssocKind::Type,
+            AssocTag::Type,
             iter_trait,
         )
         && let args = cx.tcx.mk_args(&[GenericArg::from(typeck.expr_ty_adjusted(iter_expr))])
diff --git a/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs
index c03420a5143..6eeeea5d77c 100644
--- a/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/or_fun_call.rs
@@ -78,7 +78,7 @@ pub(super) fn check<'tcx>(
                 .iter()
                 .flat_map(|impl_id| cx.tcx.associated_items(impl_id).filter_by_name_unhygienic(sugg))
                 .find_map(|assoc| {
-                    if assoc.fn_has_self_parameter
+                    if assoc.is_method()
                         && cx.tcx.fn_sig(assoc.def_id).skip_binder().inputs().skip_binder().len() == 1
                     {
                         Some(assoc.def_id)
diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs
index c0e01568588..20cf35363d1 100644
--- a/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_iter_cloned.rs
@@ -99,7 +99,7 @@ pub fn check_for_loop_iter(
             && let Some(into_iterator_trait_id) = cx.tcx.get_diagnostic_item(sym::IntoIterator)
             && let collection_ty = cx.typeck_results().expr_ty(collection)
             && implements_trait(cx, collection_ty, into_iterator_trait_id, &[])
-            && let Some(into_iter_item_ty) = cx.get_associated_type(collection_ty, into_iterator_trait_id, "Item")
+            && let Some(into_iter_item_ty) = cx.get_associated_type(collection_ty, into_iterator_trait_id, sym::Item)
             && iter_item_ty == into_iter_item_ty
             && let Some(collection_snippet) = collection.span.get_source_text(cx)
         {
diff --git a/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs b/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs
index 62ba3012643..206b0a8ae3c 100644
--- a/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs
+++ b/src/tools/clippy/clippy_lints/src/methods/unnecessary_to_owned.rs
@@ -153,7 +153,7 @@ fn check_addr_of_expr(
         }
         if let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref)
             && implements_trait(cx, receiver_ty, deref_trait_id, &[])
-            && cx.get_associated_type(receiver_ty, deref_trait_id, "Target") == Some(target_ty)
+            && cx.get_associated_type(receiver_ty, deref_trait_id, sym::Target) == Some(target_ty)
             // Make sure that it's actually calling the right `.to_string()`, (#10033)
             // *or* this is a `Cow::into_owned()` call (which would be the wrong into_owned receiver (str != Cow)
             // but that's ok for Cow::into_owned specifically)
@@ -322,7 +322,7 @@ fn check_split_call_arg(cx: &LateContext<'_>, expr: &Expr<'_>, method_name: Symb
         // add `.as_ref()` to the suggestion.
         let as_ref = if is_type_lang_item(cx, cx.typeck_results().expr_ty(expr), LangItem::String)
             && let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref)
-            && cx.get_associated_type(cx.typeck_results().expr_ty(receiver), deref_trait_id, "Target")
+            && cx.get_associated_type(cx.typeck_results().expr_ty(receiver), deref_trait_id, sym::Target)
                 != Some(cx.tcx.types.str_)
         {
             ".as_ref()"
@@ -648,7 +648,7 @@ fn is_to_string_on_string_like<'a>(
         && let GenericArgKind::Type(ty) = generic_arg.unpack()
         && let Some(deref_trait_id) = cx.tcx.get_diagnostic_item(sym::Deref)
         && let Some(as_ref_trait_id) = cx.tcx.get_diagnostic_item(sym::AsRef)
-        && (cx.get_associated_type(ty, deref_trait_id, "Target") == Some(cx.tcx.types.str_)
+        && (cx.get_associated_type(ty, deref_trait_id, sym::Target) == Some(cx.tcx.types.str_)
             || implements_trait(cx, ty, as_ref_trait_id, &[cx.tcx.types.str_.into()]))
     {
         true
diff --git a/src/tools/clippy/clippy_lints/src/missing_trait_methods.rs b/src/tools/clippy/clippy_lints/src/missing_trait_methods.rs
index 7ee746365d1..e266c36b6e7 100644
--- a/src/tools/clippy/clippy_lints/src/missing_trait_methods.rs
+++ b/src/tools/clippy/clippy_lints/src/missing_trait_methods.rs
@@ -81,7 +81,7 @@ impl<'tcx> LateLintPass<'tcx> for MissingTraitMethods {
                     cx,
                     MISSING_TRAIT_METHODS,
                     cx.tcx.def_span(item.owner_id),
-                    format!("missing trait method provided by default: `{}`", assoc.name),
+                    format!("missing trait method provided by default: `{}`", assoc.name()),
                     |diag| {
                         diag.span_help(cx.tcx.def_span(assoc.def_id), "implement the method");
                     },
diff --git a/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs b/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs
index f686cc912dd..e579dd5947d 100644
--- a/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs
+++ b/src/tools/clippy/clippy_lints/src/needless_borrows_for_generic_args.rs
@@ -299,7 +299,7 @@ fn has_ref_mut_self_method(cx: &LateContext<'_>, trait_def_id: DefId) -> bool {
         .associated_items(trait_def_id)
         .in_definition_order()
         .any(|assoc_item| {
-            if assoc_item.fn_has_self_parameter {
+            if assoc_item.is_method() {
                 let self_ty = cx
                     .tcx
                     .fn_sig(assoc_item.def_id)
diff --git a/src/tools/clippy/clippy_lints/src/same_name_method.rs b/src/tools/clippy/clippy_lints/src/same_name_method.rs
index 552135b15fd..33815cc3bac 100644
--- a/src/tools/clippy/clippy_lints/src/same_name_method.rs
+++ b/src/tools/clippy/clippy_lints/src/same_name_method.rs
@@ -3,7 +3,6 @@ use rustc_data_structures::fx::FxHashMap;
 use rustc_hir::def::{DefKind, Res};
 use rustc_hir::{HirId, Impl, ItemKind, Node, Path, QPath, TraitRef, TyKind};
 use rustc_lint::{LateContext, LateLintPass};
-use rustc_middle::ty::AssocKind;
 use rustc_session::declare_lint_pass;
 use rustc_span::Span;
 use rustc_span::symbol::Symbol;
@@ -85,8 +84,8 @@ impl<'tcx> LateLintPass<'tcx> for SameNameMethod {
                             cx.tcx
                                 .associated_items(did)
                                 .in_definition_order()
-                                .filter(|assoc_item| matches!(assoc_item.kind, AssocKind::Fn))
-                                .map(|assoc_item| assoc_item.name)
+                                .filter(|assoc_item| assoc_item.is_fn())
+                                .map(|assoc_item| assoc_item.name())
                                 .collect()
                         } else {
                             BTreeSet::new()
diff --git a/src/tools/clippy/clippy_lints/src/unconditional_recursion.rs b/src/tools/clippy/clippy_lints/src/unconditional_recursion.rs
index 51c7d6fce31..0c17cc5d8f6 100644
--- a/src/tools/clippy/clippy_lints/src/unconditional_recursion.rs
+++ b/src/tools/clippy/clippy_lints/src/unconditional_recursion.rs
@@ -10,7 +10,7 @@ use rustc_hir::{Body, Expr, ExprKind, FnDecl, HirId, Item, ItemKind, Node, QPath
 use rustc_hir_analysis::lower_ty;
 use rustc_lint::{LateContext, LateLintPass};
 use rustc_middle::hir::nested_filter;
-use rustc_middle::ty::{self, AssocKind, Ty, TyCtxt};
+use rustc_middle::ty::{self, Ty, TyCtxt};
 use rustc_session::impl_lint_pass;
 use rustc_span::symbol::{Ident, kw};
 use rustc_span::{Span, sym};
@@ -322,7 +322,7 @@ impl UnconditionalRecursion {
                             .in_definition_order()
                             // We're not interested in foreign implementations of the `Default` trait.
                             .find(|item| {
-                                item.kind == AssocKind::Fn && item.def_id.is_local() && item.name == kw::Default
+                                item.is_fn() && item.def_id.is_local() && item.name() == kw::Default
                             })
                         && let Some(body_node) = cx.tcx.hir_get_if_local(assoc_item.def_id)
                         && let Some(body_id) = body_node.body_id()
diff --git a/src/tools/clippy/clippy_lints/src/unused_self.rs b/src/tools/clippy/clippy_lints/src/unused_self.rs
index 582aa6e6001..d0067b1a65e 100644
--- a/src/tools/clippy/clippy_lints/src/unused_self.rs
+++ b/src/tools/clippy/clippy_lints/src/unused_self.rs
@@ -74,7 +74,7 @@ impl<'tcx> LateLintPass<'tcx> for UnusedSelf {
             .is_some()
         };
         if let ItemKind::Impl(Impl { of_trait: None, .. }) = parent_item.kind
-            && assoc_item.fn_has_self_parameter
+            && assoc_item.is_method()
             && let ImplItemKind::Fn(.., body_id) = &impl_item.kind
             && (!cx.effective_visibilities.is_exported(impl_item.owner_id.def_id) || !self.avoid_breaking_exported_api)
             && let body = cx.tcx.hir_body(*body_id)
diff --git a/src/tools/clippy/clippy_utils/src/ty/mod.rs b/src/tools/clippy/clippy_utils/src/ty/mod.rs
index 29cbf62c3d4..d33e59342a5 100644
--- a/src/tools/clippy/clippy_utils/src/ty/mod.rs
+++ b/src/tools/clippy/clippy_utils/src/ty/mod.rs
@@ -19,7 +19,7 @@ use rustc_middle::mir::interpret::Scalar;
 use rustc_middle::traits::EvaluationResult;
 use rustc_middle::ty::layout::ValidityRequirement;
 use rustc_middle::ty::{
-    self, AdtDef, AliasTy, AssocItem, AssocKind, Binder, BoundRegion, FnSig, GenericArg, GenericArgKind,
+    self, AdtDef, AliasTy, AssocItem, AssocTag, Binder, BoundRegion, FnSig, GenericArg, GenericArgKind,
     GenericArgsRef, GenericParamDefKind, IntTy, ParamEnv, Region, RegionKind, TraitRef, Ty, TyCtxt, TypeSuperVisitable,
     TypeVisitable, TypeVisitableExt, TypeVisitor, UintTy, Upcast, VariantDef, VariantDiscr,
 };
@@ -156,7 +156,7 @@ pub fn contains_ty_adt_constructor_opaque<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'
 pub fn get_iterator_item_ty<'tcx>(cx: &LateContext<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
     cx.tcx
         .get_diagnostic_item(sym::Iterator)
-        .and_then(|iter_did| cx.get_associated_type(ty, iter_did, "Item"))
+        .and_then(|iter_did| cx.get_associated_type(ty, iter_did, sym::Item))
 }
 
 /// Get the diagnostic name of a type, e.g. `sym::HashMap`. To check if a type
@@ -1112,7 +1112,7 @@ pub fn make_projection<'tcx>(
         let Some(assoc_item) = tcx.associated_items(container_id).find_by_ident_and_kind(
             tcx,
             Ident::with_dummy_span(assoc_ty),
-            AssocKind::Type,
+            AssocTag::Type,
             container_id,
         ) else {
             debug_assert!(false, "type `{assoc_ty}` not found in `{container_id:?}`");
@@ -1345,7 +1345,7 @@ pub fn get_adt_inherent_method<'a>(cx: &'a LateContext<'_>, ty: Ty<'_>, method_n
                 .associated_items(did)
                 .filter_by_name_unhygienic(method_name)
                 .next()
-                .filter(|item| item.kind == AssocKind::Fn)
+                .filter(|item| item.as_tag() == AssocTag::Fn)
         })
     } else {
         None
diff --git a/src/tools/compiletest/src/common.rs b/src/tools/compiletest/src/common.rs
index 604c5fcbddf..31c696ed41f 100644
--- a/src/tools/compiletest/src/common.rs
+++ b/src/tools/compiletest/src/common.rs
@@ -414,6 +414,11 @@ pub struct Config {
     /// cross-compilation scenarios that do not otherwise want/need to `-Zbuild-std`. Used in e.g.
     /// ABI tests.
     pub minicore_path: Utf8PathBuf,
+
+    /// If true, run tests with the "new" executor that was written to replace
+    /// compiletest's dependency on libtest. Eventually this will become the
+    /// default, and the libtest dependency will be removed.
+    pub new_executor: bool,
 }
 
 impl Config {
diff --git a/src/tools/compiletest/src/executor.rs b/src/tools/compiletest/src/executor.rs
index 527d6b8a36e..0c173d476af 100644
--- a/src/tools/compiletest/src/executor.rs
+++ b/src/tools/compiletest/src/executor.rs
@@ -1,22 +1,251 @@
-//! This module encapsulates all of the code that interacts directly with
-//! libtest, to execute the collected tests.
-//!
-//! This will hopefully make it easier to migrate away from libtest someday.
+//! This module contains a reimplementation of the subset of libtest
+//! functionality needed by compiletest.
 
 use std::borrow::Cow;
-use std::io;
-use std::sync::Arc;
+use std::collections::HashMap;
+use std::hash::{BuildHasherDefault, DefaultHasher};
+use std::num::NonZero;
+use std::sync::{Arc, Mutex, mpsc};
+use std::{env, hint, io, mem, panic, thread};
 
 use crate::common::{Config, TestPaths};
 
-/// Delegates to libtest to run the list of collected tests.
+mod deadline;
+mod json;
+pub(crate) mod libtest;
+
+pub(crate) fn run_tests(config: &Config, tests: Vec<CollectedTest>) -> bool {
+    let tests_len = tests.len();
+    let filtered = filter_tests(config, tests);
+    // Iterator yielding tests that haven't been started yet.
+    let mut fresh_tests = (0..).map(TestId).zip(&filtered);
+
+    let concurrency = get_concurrency();
+    assert!(concurrency > 0);
+    let concurrent_capacity = concurrency.min(filtered.len());
+
+    let mut listener = json::Listener::new();
+    let mut running_tests = HashMap::with_capacity_and_hasher(
+        concurrent_capacity,
+        BuildHasherDefault::<DefaultHasher>::new(),
+    );
+    let mut deadline_queue = deadline::DeadlineQueue::with_capacity(concurrent_capacity);
+
+    let num_filtered_out = tests_len - filtered.len();
+    listener.suite_started(filtered.len(), num_filtered_out);
+
+    // Channel used by test threads to report the test outcome when done.
+    let (completion_tx, completion_rx) = mpsc::channel::<TestCompletion>();
+
+    // Unlike libtest, we don't have a separate code path for concurrency=1.
+    // In that case, the tests will effectively be run serially anyway.
+    loop {
+        // Spawn new test threads, up to the concurrency limit.
+        // FIXME(let_chains): Use a let-chain here when stable in bootstrap.
+        'spawn: while running_tests.len() < concurrency {
+            let Some((id, test)) = fresh_tests.next() else { break 'spawn };
+            listener.test_started(test);
+            deadline_queue.push(id, test);
+            let join_handle = spawn_test_thread(id, test, completion_tx.clone());
+            running_tests.insert(id, RunningTest { test, join_handle });
+        }
+
+        // If all running tests have finished, and there weren't any unstarted
+        // tests to spawn, then we're done.
+        if running_tests.is_empty() {
+            break;
+        }
+
+        let completion = deadline_queue
+            .read_channel_while_checking_deadlines(&completion_rx, |_id, test| {
+                listener.test_timed_out(test);
+            })
+            .expect("receive channel should never be closed early");
+
+        let RunningTest { test, join_handle } = running_tests.remove(&completion.id).unwrap();
+        if let Some(join_handle) = join_handle {
+            join_handle.join().unwrap_or_else(|_| {
+                panic!("thread for `{}` panicked after reporting completion", test.desc.name)
+            });
+        }
+
+        listener.test_finished(test, &completion);
+
+        if completion.outcome.is_failed() && config.fail_fast {
+            // Prevent any other in-flight threads from panicking when they
+            // write to the completion channel.
+            mem::forget(completion_rx);
+            break;
+        }
+    }
+
+    let suite_passed = listener.suite_finished();
+    suite_passed
+}
+
+/// Spawns a thread to run a single test, and returns the thread's join handle.
 ///
-/// Returns `Ok(true)` if all tests passed, or `Ok(false)` if one or more tests failed.
-pub(crate) fn execute_tests(config: &Config, tests: Vec<CollectedTest>) -> io::Result<bool> {
-    let opts = test_opts(config);
-    let tests = tests.into_iter().map(|t| t.into_libtest()).collect::<Vec<_>>();
+/// Returns `None` if the test was ignored, so no thread was spawned.
+fn spawn_test_thread(
+    id: TestId,
+    test: &CollectedTest,
+    completion_tx: mpsc::Sender<TestCompletion>,
+) -> Option<thread::JoinHandle<()>> {
+    if test.desc.ignore && !test.config.run_ignored {
+        completion_tx
+            .send(TestCompletion { id, outcome: TestOutcome::Ignored, stdout: None })
+            .unwrap();
+        return None;
+    }
+
+    let runnable_test = RunnableTest::new(test);
+    let should_panic = test.desc.should_panic;
+    let run_test = move || run_test_inner(id, should_panic, runnable_test, completion_tx);
+
+    let thread_builder = thread::Builder::new().name(test.desc.name.clone());
+    let join_handle = thread_builder.spawn(run_test).unwrap();
+    Some(join_handle)
+}
+
+/// Runs a single test, within the dedicated thread spawned by the caller.
+fn run_test_inner(
+    id: TestId,
+    should_panic: ShouldPanic,
+    runnable_test: RunnableTest,
+    completion_sender: mpsc::Sender<TestCompletion>,
+) {
+    let is_capture = !runnable_test.config.nocapture;
+    let capture_buf = is_capture.then(|| Arc::new(Mutex::new(vec![])));
+
+    if let Some(capture_buf) = &capture_buf {
+        io::set_output_capture(Some(Arc::clone(capture_buf)));
+    }
+
+    let panic_payload = panic::catch_unwind(move || runnable_test.run()).err();
+
+    if is_capture {
+        io::set_output_capture(None);
+    }
+
+    let outcome = match (should_panic, panic_payload) {
+        (ShouldPanic::No, None) | (ShouldPanic::Yes, Some(_)) => TestOutcome::Succeeded,
+        (ShouldPanic::No, Some(_)) => TestOutcome::Failed { message: None },
+        (ShouldPanic::Yes, None) => {
+            TestOutcome::Failed { message: Some("test did not panic as expected") }
+        }
+    };
+    let stdout = capture_buf.map(|mutex| mutex.lock().unwrap_or_else(|e| e.into_inner()).to_vec());
+
+    completion_sender.send(TestCompletion { id, outcome, stdout }).unwrap();
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+struct TestId(usize);
+
+struct RunnableTest {
+    config: Arc<Config>,
+    testpaths: TestPaths,
+    revision: Option<String>,
+}
+
+impl RunnableTest {
+    fn new(test: &CollectedTest) -> Self {
+        let config = Arc::clone(&test.config);
+        let testpaths = test.testpaths.clone();
+        let revision = test.revision.clone();
+        Self { config, testpaths, revision }
+    }
+
+    fn run(&self) {
+        __rust_begin_short_backtrace(|| {
+            crate::runtest::run(
+                Arc::clone(&self.config),
+                &self.testpaths,
+                self.revision.as_deref(),
+            );
+        });
+    }
+}
+
+/// Fixed frame used to clean the backtrace with `RUST_BACKTRACE=1`.
+#[inline(never)]
+fn __rust_begin_short_backtrace<T, F: FnOnce() -> T>(f: F) -> T {
+    let result = f();
+
+    // prevent this frame from being tail-call optimised away
+    hint::black_box(result)
+}
 
-    test::run_tests_console(&opts, tests)
+struct RunningTest<'a> {
+    test: &'a CollectedTest,
+    join_handle: Option<thread::JoinHandle<()>>,
+}
+
+/// Test completion message sent by individual test threads when their test
+/// finishes (successfully or unsuccessfully).
+struct TestCompletion {
+    id: TestId,
+    outcome: TestOutcome,
+    stdout: Option<Vec<u8>>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+enum TestOutcome {
+    Succeeded,
+    Failed { message: Option<&'static str> },
+    Ignored,
+}
+
+impl TestOutcome {
+    fn is_failed(&self) -> bool {
+        matches!(self, Self::Failed { .. })
+    }
+}
+
+/// Applies command-line arguments for filtering/skipping tests by name.
+///
+/// Adapted from `filter_tests` in libtest.
+///
+/// FIXME(#139660): After the libtest dependency is removed, redesign the whole
+/// filtering system to do a better job of understanding and filtering _paths_,
+/// instead of being tied to libtest's substring/exact matching behaviour.
+fn filter_tests(opts: &Config, tests: Vec<CollectedTest>) -> Vec<CollectedTest> {
+    let mut filtered = tests;
+
+    let matches_filter = |test: &CollectedTest, filter_str: &str| {
+        let test_name = &test.desc.name;
+        if opts.filter_exact { test_name == filter_str } else { test_name.contains(filter_str) }
+    };
+
+    // Remove tests that don't match the test filter
+    if !opts.filters.is_empty() {
+        filtered.retain(|test| opts.filters.iter().any(|filter| matches_filter(test, filter)));
+    }
+
+    // Skip tests that match any of the skip filters
+    if !opts.skip.is_empty() {
+        filtered.retain(|test| !opts.skip.iter().any(|sf| matches_filter(test, sf)));
+    }
+
+    filtered
+}
+
+/// Determines the number of tests to run concurrently.
+///
+/// Copied from `get_concurrency` in libtest.
+///
+/// FIXME(#139660): After the libtest dependency is removed, consider making
+/// bootstrap specify the number of threads on the command-line, instead of
+/// propagating the `RUST_TEST_THREADS` environment variable.
+fn get_concurrency() -> usize {
+    if let Ok(value) = env::var("RUST_TEST_THREADS") {
+        match value.parse::<NonZero<usize>>().ok() {
+            Some(n) => n.get(),
+            _ => panic!("RUST_TEST_THREADS is `{value}`, should be a positive integer."),
+        }
+    } else {
+        thread::available_parallelism().map(|n| n.get()).unwrap_or(1)
+    }
 }
 
 /// Information needed to create a `test::TestDescAndFn`.
@@ -35,45 +264,6 @@ pub(crate) struct CollectedTestDesc {
     pub(crate) should_panic: ShouldPanic,
 }
 
-impl CollectedTest {
-    fn into_libtest(self) -> test::TestDescAndFn {
-        let Self { desc, config, testpaths, revision } = self;
-        let CollectedTestDesc { name, ignore, ignore_message, should_panic } = desc;
-
-        // Libtest requires the ignore message to be a &'static str, so we might
-        // have to leak memory to create it. This is fine, as we only do so once
-        // per test, so the leak won't grow indefinitely.
-        let ignore_message = ignore_message.map(|msg| match msg {
-            Cow::Borrowed(s) => s,
-            Cow::Owned(s) => &*String::leak(s),
-        });
-
-        let desc = test::TestDesc {
-            name: test::DynTestName(name),
-            ignore,
-            ignore_message,
-            source_file: "",
-            start_line: 0,
-            start_col: 0,
-            end_line: 0,
-            end_col: 0,
-            should_panic: should_panic.to_libtest(),
-            compile_fail: false,
-            no_run: false,
-            test_type: test::TestType::Unknown,
-        };
-
-        // This closure is invoked when libtest returns control to compiletest
-        // to execute the test.
-        let testfn = test::DynTestFn(Box::new(move || {
-            crate::runtest::run(config, &testpaths, revision.as_deref());
-            Ok(())
-        }));
-
-        test::TestDescAndFn { desc, testfn }
-    }
-}
-
 /// Whether console output should be colored or not.
 #[derive(Copy, Clone, Default, Debug)]
 pub enum ColorConfig {
@@ -83,16 +273,6 @@ pub enum ColorConfig {
     NeverColor,
 }
 
-impl ColorConfig {
-    fn to_libtest(self) -> test::ColorConfig {
-        match self {
-            Self::AutoColor => test::ColorConfig::AutoColor,
-            Self::AlwaysColor => test::ColorConfig::AlwaysColor,
-            Self::NeverColor => test::ColorConfig::NeverColor,
-        }
-    }
-}
-
 /// Format of the test results output.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
 pub enum OutputFormat {
@@ -105,52 +285,9 @@ pub enum OutputFormat {
     Json,
 }
 
-impl OutputFormat {
-    fn to_libtest(self) -> test::OutputFormat {
-        match self {
-            Self::Pretty => test::OutputFormat::Pretty,
-            Self::Terse => test::OutputFormat::Terse,
-            Self::Json => test::OutputFormat::Json,
-        }
-    }
-}
-
 /// Whether test is expected to panic or not.
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub(crate) enum ShouldPanic {
     No,
     Yes,
 }
-
-impl ShouldPanic {
-    fn to_libtest(self) -> test::ShouldPanic {
-        match self {
-            Self::No => test::ShouldPanic::No,
-            Self::Yes => test::ShouldPanic::Yes,
-        }
-    }
-}
-
-fn test_opts(config: &Config) -> test::TestOpts {
-    test::TestOpts {
-        exclude_should_panic: false,
-        filters: config.filters.clone(),
-        filter_exact: config.filter_exact,
-        run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
-        format: config.format.to_libtest(),
-        logfile: None,
-        run_tests: true,
-        bench_benchmarks: true,
-        nocapture: config.nocapture,
-        color: config.color.to_libtest(),
-        shuffle: false,
-        shuffle_seed: None,
-        test_threads: None,
-        skip: config.skip.clone(),
-        list: false,
-        options: test::Options::new(),
-        time_options: None,
-        force_run_in_process: false,
-        fail_fast: config.fail_fast,
-    }
-}
diff --git a/src/tools/compiletest/src/executor/deadline.rs b/src/tools/compiletest/src/executor/deadline.rs
new file mode 100644
index 00000000000..83b8591a416
--- /dev/null
+++ b/src/tools/compiletest/src/executor/deadline.rs
@@ -0,0 +1,78 @@
+use std::collections::VecDeque;
+use std::sync::mpsc::{self, RecvError, RecvTimeoutError};
+use std::time::{Duration, Instant};
+
+use crate::executor::{CollectedTest, TestId};
+
+const TEST_WARN_TIMEOUT_S: u64 = 60;
+
+struct DeadlineEntry<'a> {
+    id: TestId,
+    test: &'a CollectedTest,
+    deadline: Instant,
+}
+
+pub(crate) struct DeadlineQueue<'a> {
+    queue: VecDeque<DeadlineEntry<'a>>,
+}
+
+impl<'a> DeadlineQueue<'a> {
+    pub(crate) fn with_capacity(capacity: usize) -> Self {
+        Self { queue: VecDeque::with_capacity(capacity) }
+    }
+
+    pub(crate) fn push(&mut self, id: TestId, test: &'a CollectedTest) {
+        let deadline = Instant::now() + Duration::from_secs(TEST_WARN_TIMEOUT_S);
+        self.queue.push_back(DeadlineEntry { id, test, deadline });
+    }
+
+    /// Equivalent to `rx.read()`, except that if any test exceeds its deadline
+    /// during the wait, the given callback will also be called for that test.
+    pub(crate) fn read_channel_while_checking_deadlines<T>(
+        &mut self,
+        rx: &mpsc::Receiver<T>,
+        mut on_deadline_passed: impl FnMut(TestId, &CollectedTest),
+    ) -> Result<T, RecvError> {
+        loop {
+            let Some(next_deadline) = self.next_deadline() else {
+                // All currently-running tests have already exceeded their
+                // deadline, so do a normal receive.
+                return rx.recv();
+            };
+            let wait_duration = next_deadline.saturating_duration_since(Instant::now());
+
+            let recv_result = rx.recv_timeout(wait_duration);
+            match recv_result {
+                Ok(value) => return Ok(value),
+                Err(RecvTimeoutError::Timeout) => {
+                    // Notify the callback of tests that have exceeded their
+                    // deadline, then loop and do annother channel read.
+                    for DeadlineEntry { id, test, .. } in self.remove_tests_past_deadline() {
+                        on_deadline_passed(id, test);
+                    }
+                }
+                Err(RecvTimeoutError::Disconnected) => return Err(RecvError),
+            }
+        }
+    }
+
+    fn next_deadline(&self) -> Option<Instant> {
+        Some(self.queue.front()?.deadline)
+    }
+
+    fn remove_tests_past_deadline(&mut self) -> Vec<DeadlineEntry<'a>> {
+        let now = Instant::now();
+        let mut timed_out = vec![];
+        while let Some(deadline_entry) = pop_front_if(&mut self.queue, |entry| now < entry.deadline)
+        {
+            timed_out.push(deadline_entry);
+        }
+        timed_out
+    }
+}
+
+/// FIXME(vec_deque_pop_if): Use `VecDeque::pop_front_if` when it is stable in bootstrap.
+fn pop_front_if<T>(queue: &mut VecDeque<T>, predicate: impl FnOnce(&T) -> bool) -> Option<T> {
+    let first = queue.front()?;
+    if predicate(first) { queue.pop_front() } else { None }
+}
diff --git a/src/tools/compiletest/src/executor/json.rs b/src/tools/compiletest/src/executor/json.rs
new file mode 100644
index 00000000000..c74ed81a36b
--- /dev/null
+++ b/src/tools/compiletest/src/executor/json.rs
@@ -0,0 +1,111 @@
+//! Collects statistics and emits suite/test events as JSON messages, using
+//! the same JSON format as libtest's JSON formatter.
+//!
+//! These messages are then parsed by bootstrap, which replaces them with
+//! user-friendly terminal output.
+
+use std::time::Instant;
+
+use serde_json::json;
+
+use crate::executor::{CollectedTest, TestCompletion, TestOutcome};
+
+pub(crate) struct Listener {
+    suite_start: Option<Instant>,
+    passed: usize,
+    failed: usize,
+    ignored: usize,
+    filtered_out: usize,
+}
+
+impl Listener {
+    pub(crate) fn new() -> Self {
+        Self { suite_start: None, passed: 0, failed: 0, ignored: 0, filtered_out: 0 }
+    }
+
+    fn print_message(&self, message: &serde_json::Value) {
+        println!("{message}");
+    }
+
+    fn now(&self) -> Instant {
+        Instant::now()
+    }
+
+    pub(crate) fn suite_started(&mut self, test_count: usize, filtered_out: usize) {
+        self.suite_start = Some(self.now());
+        self.filtered_out = filtered_out;
+        let message = json!({ "type": "suite", "event": "started", "test_count": test_count });
+        self.print_message(&message);
+    }
+
+    pub(crate) fn test_started(&mut self, test: &CollectedTest) {
+        let name = test.desc.name.as_str();
+        let message = json!({ "type": "test", "event": "started", "name": name });
+        self.print_message(&message);
+    }
+
+    pub(crate) fn test_timed_out(&mut self, test: &CollectedTest) {
+        let name = test.desc.name.as_str();
+        let message = json!({ "type": "test", "event": "timeout", "name": name });
+        self.print_message(&message);
+    }
+
+    pub(crate) fn test_finished(&mut self, test: &CollectedTest, completion: &TestCompletion) {
+        let event;
+        let name = test.desc.name.as_str();
+        let mut maybe_message = None;
+        let maybe_stdout = completion.stdout.as_deref().map(String::from_utf8_lossy);
+
+        match completion.outcome {
+            TestOutcome::Succeeded => {
+                self.passed += 1;
+                event = "ok";
+            }
+            TestOutcome::Failed { message } => {
+                self.failed += 1;
+                maybe_message = message;
+                event = "failed";
+            }
+            TestOutcome::Ignored => {
+                self.ignored += 1;
+                maybe_message = test.desc.ignore_message.as_deref();
+                event = "ignored";
+            }
+        };
+
+        // This emits optional fields as `null`, instead of omitting them
+        // completely as libtest does, but bootstrap can parse the result
+        // either way.
+        let json = json!({
+            "type": "test",
+            "event": event,
+            "name": name,
+            "message": maybe_message,
+            "stdout": maybe_stdout,
+        });
+
+        self.print_message(&json);
+    }
+
+    pub(crate) fn suite_finished(&mut self) -> bool {
+        let exec_time = self.suite_start.map(|start| (self.now() - start).as_secs_f64());
+        let suite_passed = self.failed == 0;
+
+        let event = if suite_passed { "ok" } else { "failed" };
+        let message = json!({
+            "type": "suite",
+            "event": event,
+            "passed": self.passed,
+            "failed": self.failed,
+            "ignored": self.ignored,
+            // Compiletest doesn't run any benchmarks, but we still need to set this
+            // field to 0 so that bootstrap's JSON parser can read our message.
+            "measured": 0,
+            "filtered_out": self.filtered_out,
+            "exec_time": exec_time,
+        });
+
+        self.print_message(&message);
+        suite_passed
+    }
+}
diff --git a/src/tools/compiletest/src/executor/libtest.rs b/src/tools/compiletest/src/executor/libtest.rs
new file mode 100644
index 00000000000..032b3f4fa9a
--- /dev/null
+++ b/src/tools/compiletest/src/executor/libtest.rs
@@ -0,0 +1,111 @@
+//! This submodule encapsulates all of the code that actually interacts with
+//! libtest, so that it can be easily removed after the new executor becomes
+//! the default.
+
+use std::borrow::Cow;
+use std::io;
+
+use crate::common::Config;
+use crate::executor::{CollectedTest, CollectedTestDesc, ColorConfig, OutputFormat, ShouldPanic};
+
+/// Delegates to libtest to run the list of collected tests.
+///
+/// Returns `Ok(true)` if all tests passed, or `Ok(false)` if one or more tests failed.
+pub(crate) fn execute_tests(config: &Config, tests: Vec<CollectedTest>) -> io::Result<bool> {
+    let opts = test_opts(config);
+    let tests = tests.into_iter().map(|t| t.into_libtest()).collect::<Vec<_>>();
+
+    test::run_tests_console(&opts, tests)
+}
+
+impl CollectedTest {
+    fn into_libtest(self) -> test::TestDescAndFn {
+        let Self { desc, config, testpaths, revision } = self;
+        let CollectedTestDesc { name, ignore, ignore_message, should_panic } = desc;
+
+        // Libtest requires the ignore message to be a &'static str, so we might
+        // have to leak memory to create it. This is fine, as we only do so once
+        // per test, so the leak won't grow indefinitely.
+        let ignore_message = ignore_message.map(|msg| match msg {
+            Cow::Borrowed(s) => s,
+            Cow::Owned(s) => &*String::leak(s),
+        });
+
+        let desc = test::TestDesc {
+            name: test::DynTestName(name),
+            ignore,
+            ignore_message,
+            source_file: "",
+            start_line: 0,
+            start_col: 0,
+            end_line: 0,
+            end_col: 0,
+            should_panic: should_panic.to_libtest(),
+            compile_fail: false,
+            no_run: false,
+            test_type: test::TestType::Unknown,
+        };
+
+        // This closure is invoked when libtest returns control to compiletest
+        // to execute the test.
+        let testfn = test::DynTestFn(Box::new(move || {
+            crate::runtest::run(config, &testpaths, revision.as_deref());
+            Ok(())
+        }));
+
+        test::TestDescAndFn { desc, testfn }
+    }
+}
+
+impl ColorConfig {
+    fn to_libtest(self) -> test::ColorConfig {
+        match self {
+            Self::AutoColor => test::ColorConfig::AutoColor,
+            Self::AlwaysColor => test::ColorConfig::AlwaysColor,
+            Self::NeverColor => test::ColorConfig::NeverColor,
+        }
+    }
+}
+
+impl OutputFormat {
+    fn to_libtest(self) -> test::OutputFormat {
+        match self {
+            Self::Pretty => test::OutputFormat::Pretty,
+            Self::Terse => test::OutputFormat::Terse,
+            Self::Json => test::OutputFormat::Json,
+        }
+    }
+}
+
+impl ShouldPanic {
+    fn to_libtest(self) -> test::ShouldPanic {
+        match self {
+            Self::No => test::ShouldPanic::No,
+            Self::Yes => test::ShouldPanic::Yes,
+        }
+    }
+}
+
+fn test_opts(config: &Config) -> test::TestOpts {
+    test::TestOpts {
+        exclude_should_panic: false,
+        filters: config.filters.clone(),
+        filter_exact: config.filter_exact,
+        run_ignored: if config.run_ignored { test::RunIgnored::Yes } else { test::RunIgnored::No },
+        format: config.format.to_libtest(),
+        logfile: None,
+        run_tests: true,
+        bench_benchmarks: true,
+        nocapture: config.nocapture,
+        color: config.color.to_libtest(),
+        shuffle: false,
+        shuffle_seed: None,
+        test_threads: None,
+        skip: config.skip.clone(),
+        list: false,
+        options: test::Options::new(),
+        time_options: None,
+        force_run_in_process: false,
+        fail_fast: config.fail_fast,
+    }
+}
diff --git a/src/tools/compiletest/src/lib.rs b/src/tools/compiletest/src/lib.rs
index b969b22750b..4bbd4ab4790 100644
--- a/src/tools/compiletest/src/lib.rs
+++ b/src/tools/compiletest/src/lib.rs
@@ -1,7 +1,8 @@
 #![crate_name = "compiletest"]
-// The `test` crate is the only unstable feature
-// allowed here, just to share similar code.
+// Needed by the libtest-based test executor.
 #![feature(test)]
+// Needed by the "new" test executor that does not depend on libtest.
+#![feature(internal_output_capture)]
 
 extern crate test;
 
@@ -202,6 +203,7 @@ pub fn parse_config(args: Vec<String>) -> Config {
             "COMMAND",
         )
         .reqopt("", "minicore-path", "path to minicore aux library", "PATH")
+        .optflag("n", "new-executor", "enables the new test executor instead of using libtest")
         .optopt(
             "",
             "debugger",
@@ -447,6 +449,8 @@ pub fn parse_config(args: Vec<String>) -> Config {
         diff_command: matches.opt_str("compiletest-diff-tool"),
 
         minicore_path: opt_path(matches, "minicore-path"),
+
+        new_executor: matches.opt_present("new-executor"),
     }
 }
 
@@ -570,10 +574,14 @@ pub fn run_tests(config: Arc<Config>) {
 
     tests.sort_by(|a, b| Ord::cmp(&a.desc.name, &b.desc.name));
 
-    // Delegate to libtest to filter and run the big list of structures created
-    // during test discovery. When libtest decides to run a test, it will
-    // return control to compiletest by invoking a closure.
-    let res = crate::executor::execute_tests(&config, tests);
+    // Delegate to the executor to filter and run the big list of test structures
+    // created during test discovery. When the executor decides to run a test,
+    // it will return control to the rest of compiletest by calling `runtest::run`.
+    let res = if config.new_executor {
+        Ok(executor::run_tests(&config, tests))
+    } else {
+        crate::executor::libtest::execute_tests(&config, tests)
+    };
 
     // Check the outcome reported by libtest.
     match res {
diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs
index 59293ee3f96..80f6d85a3d0 100644
--- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs
+++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/rust_analyzer_span.rs
@@ -11,7 +11,7 @@ use std::{
 
 use intern::Symbol;
 use proc_macro::bridge::{self, server};
-use span::{FileId, Span, FIXUP_ERASED_FILE_AST_ID_MARKER};
+use span::{Span, FIXUP_ERASED_FILE_AST_ID_MARKER};
 use tt::{TextRange, TextSize};
 
 use crate::server_impl::{literal_kind_to_internal, token_stream::TokenStreamBuilder, TopSubtree};
@@ -27,10 +27,6 @@ mod tt {
 
 type TokenStream = crate::server_impl::TokenStream<Span>;
 
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub struct SourceFile {
-    file_id: FileId,
-}
 pub struct FreeFunctions;
 
 pub struct RaSpanServer {
@@ -46,7 +42,6 @@ pub struct RaSpanServer {
 impl server::Types for RaSpanServer {
     type FreeFunctions = FreeFunctions;
     type TokenStream = TokenStream;
-    type SourceFile = SourceFile;
     type Span = Span;
     type Symbol = Symbol;
 }
@@ -245,25 +240,17 @@ impl server::TokenStream for RaSpanServer {
     }
 }
 
-impl server::SourceFile for RaSpanServer {
-    fn eq(&mut self, file1: &Self::SourceFile, file2: &Self::SourceFile) -> bool {
-        file1 == file2
-    }
-    fn path(&mut self, _file: &Self::SourceFile) -> String {
-        // FIXME
-        String::new()
-    }
-    fn is_real(&mut self, _file: &Self::SourceFile) -> bool {
-        true
-    }
-}
-
 impl server::Span for RaSpanServer {
     fn debug(&mut self, span: Self::Span) -> String {
         format!("{:?}", span)
     }
-    fn source_file(&mut self, span: Self::Span) -> Self::SourceFile {
-        SourceFile { file_id: span.anchor.file_id.file_id() }
+    fn file(&mut self, _: Self::Span) -> String {
+        // FIXME
+        String::new()
+    }
+    fn local_file(&mut self, _: Self::Span) -> Option<String> {
+        // FIXME
+        None
     }
     fn save_span(&mut self, _span: Self::Span) -> usize {
         // FIXME, quote is incompatible with third-party tools
diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/token_id.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/token_id.rs
index 409cf3cc781..4d7c7c46766 100644
--- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/token_id.rs
+++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/server_impl/token_id.rs
@@ -24,8 +24,6 @@ type Literal = tt::Literal;
 type Span = tt::TokenId;
 type TokenStream = crate::server_impl::TokenStream<Span>;
 
-#[derive(Clone)]
-pub struct SourceFile;
 pub struct FreeFunctions;
 
 pub struct TokenIdServer {
@@ -37,7 +35,6 @@ pub struct TokenIdServer {
 impl server::Types for TokenIdServer {
     type FreeFunctions = FreeFunctions;
     type TokenStream = TokenStream;
-    type SourceFile = SourceFile;
     type Span = Span;
     type Symbol = Symbol;
 }
@@ -223,24 +220,15 @@ impl server::TokenStream for TokenIdServer {
     }
 }
 
-impl server::SourceFile for TokenIdServer {
-    fn eq(&mut self, _file1: &Self::SourceFile, _file2: &Self::SourceFile) -> bool {
-        true
-    }
-    fn path(&mut self, _file: &Self::SourceFile) -> String {
-        String::new()
-    }
-    fn is_real(&mut self, _file: &Self::SourceFile) -> bool {
-        true
-    }
-}
-
 impl server::Span for TokenIdServer {
     fn debug(&mut self, span: Self::Span) -> String {
         format!("{:?}", span.0)
     }
-    fn source_file(&mut self, _span: Self::Span) -> Self::SourceFile {
-        SourceFile {}
+    fn file(&mut self, _span: Self::Span) -> String {
+        String::new()
+    }
+    fn local_file(&mut self, _span: Self::Span) -> Option<String> {
+        None
     }
     fn save_span(&mut self, _span: Self::Span) -> usize {
         0
diff --git a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/mod.rs b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/mod.rs
index 15de88ea656..4bd365be7ca 100644
--- a/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/mod.rs
+++ b/src/tools/rust-analyzer/crates/proc-macro-srv/src/tests/mod.rs
@@ -97,6 +97,7 @@ fn test_fn_like_macro_clone_raw_ident() {
 }
 
 #[test]
+#[cfg(not(bootstrap))]
 fn test_fn_like_fn_like_span_join() {
     assert_expand(
         "fn_like_span_join",
@@ -111,6 +112,7 @@ fn test_fn_like_fn_like_span_join() {
 }
 
 #[test]
+#[cfg(not(bootstrap))]
 fn test_fn_like_fn_like_span_ops() {
     assert_expand(
         "fn_like_span_ops",