@@ -57,7 +57,7 @@ pub enum RunnableKind {
5757 TestMod { path : String } ,
5858 Test { test_id : TestId , attr : TestAttr } ,
5959 Bench { test_id : TestId } ,
60- DocTest { test_id : TestId } ,
60+ DocTest { test_id : TestId , has_compile_fail : bool } ,
6161 Bin ,
6262}
6363
@@ -404,9 +404,7 @@ pub(crate) fn runnable_impl(
404404 let display_target = def. module ( sema. db ) . krate ( sema. db ) . to_display_target ( sema. db ) ;
405405 let edition = display_target. edition ;
406406 let attrs = def. attrs ( sema. db ) ;
407- if !has_runnable_doc_test ( sema. db , & attrs) {
408- return None ;
409- }
407+ let doc_test_info = runnable_doc_test_info ( sema. db , & attrs) ?;
410408 let cfg = attrs. cfgs ( sema. db ) . cloned ( ) ;
411409 let nav = def. try_to_nav ( sema) ?. call_site ( ) ;
412410 let ty = def. self_ty ( sema. db ) ;
@@ -429,7 +427,7 @@ pub(crate) fn runnable_impl(
429427 Some ( Runnable {
430428 use_name_in_title : false ,
431429 nav,
432- kind : RunnableKind :: DocTest { test_id } ,
430+ kind : RunnableKind :: DocTest { test_id, has_compile_fail : doc_test_info . has_compile_fail } ,
433431 cfg,
434432 update_test,
435433 } )
@@ -508,9 +506,7 @@ fn module_def_doctest(sema: &Semantics<'_, RootDatabase>, def: Definition) -> Op
508506 let display_target = krate
509507 . unwrap_or_else ( || ( * db. all_crates ( ) . last ( ) . expect ( "no crate graph present" ) ) . into ( ) )
510508 . to_display_target ( db) ;
511- if !has_runnable_doc_test ( db, & attrs) {
512- return None ;
513- }
509+ let doc_test_info = runnable_doc_test_info ( db, & attrs) ?;
514510 let def_name = def. name ( db) ?;
515511 let path = ( || {
516512 let mut path = String :: new ( ) ;
@@ -551,7 +547,7 @@ fn module_def_doctest(sema: &Semantics<'_, RootDatabase>, def: Definition) -> Op
551547 let res = Runnable {
552548 use_name_in_title : false ,
553549 nav,
554- kind : RunnableKind :: DocTest { test_id } ,
550+ kind : RunnableKind :: DocTest { test_id, has_compile_fail : doc_test_info . has_compile_fail } ,
555551 cfg : attrs. cfgs ( db) . cloned ( ) ,
556552 update_test : UpdateTest :: default ( ) ,
557553 } ;
@@ -569,32 +565,64 @@ impl TestAttr {
569565 }
570566}
571567
572- fn has_runnable_doc_test ( db : & RootDatabase , attrs : & hir:: AttrsWithOwner ) -> bool {
568+ #[ derive( Default , Clone , Copy ) ]
569+ struct RunnableDocTestInfo {
570+ has_compile_fail : bool ,
571+ }
572+
573+ fn runnable_doc_test_info (
574+ db : & RootDatabase ,
575+ attrs : & hir:: AttrsWithOwner ,
576+ ) -> Option < RunnableDocTestInfo > {
573577 const RUSTDOC_FENCES : [ & str ; 2 ] = [ "```" , "~~~" ] ;
574578 const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE : & [ & str ] =
575- & [ "" , "rust" , "should_panic" , "edition2015" , "edition2018" , "edition2021" ] ;
579+ & [ "" , "rust" , "should_panic" , "edition2015" , "edition2018" , "edition2021" , "edition2024" ] ;
580+
581+ let doc = attrs. hir_docs ( db) ?;
582+ let mut info = RunnableDocTestInfo :: default ( ) ;
583+ let mut in_code_block = false ;
584+ let mut runnable_found = false ;
585+
586+ for line in doc. docs ( ) . lines ( ) {
587+ let trimmed_line = line. trim_start ( ) ;
588+ if let Some ( header) =
589+ RUSTDOC_FENCES . into_iter ( ) . find_map ( |fence| trimmed_line. strip_prefix ( fence) )
590+ {
591+ if in_code_block {
592+ in_code_block = false ;
593+ continue ;
594+ }
576595
577- attrs. hir_docs ( db) . is_some_and ( |doc| {
578- let mut in_code_block = false ;
596+ in_code_block = true ;
597+ let mut block_has_compile_fail = false ;
598+ let mut block_runnable = true ;
579599
580- for line in doc. docs ( ) . lines ( ) {
581- if let Some ( header) =
582- RUSTDOC_FENCES . into_iter ( ) . find_map ( |fence| line. strip_prefix ( fence) )
600+ for attr in header
601+ . split ( ',' )
602+ . flat_map ( |segment| segment. split_ascii_whitespace ( ) )
603+ . map ( str:: trim)
604+ . filter ( |attr| !attr. is_empty ( ) )
583605 {
584- in_code_block = !in_code_block;
606+ if attr. eq_ignore_ascii_case ( "compile_fail" ) {
607+ block_has_compile_fail = true ;
608+ block_runnable = false ;
609+ continue ;
610+ }
585611
586- if in_code_block
587- && header
588- . split ( ',' )
589- . all ( |sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE . contains ( & sub. trim ( ) ) )
612+ if !RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE
613+ . iter ( )
614+ . any ( |allowed| allowed. eq_ignore_ascii_case ( attr) )
590615 {
591- return true ;
616+ block_runnable = false ;
592617 }
593618 }
619+
620+ info. has_compile_fail |= block_has_compile_fail;
621+ runnable_found |= block_runnable;
594622 }
623+ }
595624
596- false
597- } )
625+ runnable_found. then_some ( info)
598626}
599627
600628// We could create runnables for modules with number_of_test_submodules > 0,
@@ -752,6 +780,7 @@ impl UpdateTest {
752780mod tests {
753781 use expect_test:: { Expect , expect} ;
754782
783+ use super :: RunnableKind ;
755784 use crate :: fixture;
756785
757786 fn check ( #[ rust_analyzer:: rust_fixture] ra_fixture : & str , expect : Expect ) {
@@ -940,6 +969,39 @@ impl Test for StructWithRunnable {}
940969 ) ;
941970 }
942971
972+ #[ test]
973+ fn doc_test_runnable_tracks_compile_fail_blocks ( ) {
974+ let ( analysis, position) = fixture:: position (
975+ r#"
976+ //- /lib.rs
977+ $0
978+ /// ```compile_fail
979+ /// let x = 5;
980+ /// x += 1;
981+ /// ```
982+ ///
983+ /// ```
984+ /// let x = 5;
985+ /// x + 1;
986+ /// ```
987+ fn add(left: u64, right: u64) -> u64 {
988+ left + right
989+ }
990+ "# ,
991+ ) ;
992+
993+ let runnables = analysis. runnables ( position. file_id ) . unwrap ( ) ;
994+ let doc_test = runnables
995+ . into_iter ( )
996+ . find ( |runnable| matches ! ( runnable. kind, RunnableKind :: DocTest { .. } ) )
997+ . expect ( "expected doctest runnable" ) ;
998+
999+ match doc_test. kind {
1000+ RunnableKind :: DocTest { has_compile_fail, .. } => assert ! ( has_compile_fail) ,
1001+ _ => panic ! ( "expected doctest runnable" ) ,
1002+ }
1003+ }
1004+
9431005 #[ test]
9441006 fn test_runnables_doc_test_in_impl ( ) {
9451007 check (
0 commit comments