@@ -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,9 @@ 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) {
407+ let Some ( doc_test_info ) = runnable_doc_test_info ( sema. db , & attrs) else {
408408 return None ;
409- }
409+ } ;
410410 let cfg = attrs. cfgs ( sema. db ) . cloned ( ) ;
411411 let nav = def. try_to_nav ( sema) ?. call_site ( ) ;
412412 let ty = def. self_ty ( sema. db ) ;
@@ -431,7 +431,7 @@ pub(crate) fn runnable_impl(
431431 Some ( Runnable {
432432 use_name_in_title : false ,
433433 nav,
434- kind : RunnableKind :: DocTest { test_id } ,
434+ kind : RunnableKind :: DocTest { test_id, has_compile_fail : doc_test_info . has_compile_fail } ,
435435 cfg,
436436 update_test,
437437 } )
@@ -510,9 +510,9 @@ fn module_def_doctest(sema: &Semantics<'_, RootDatabase>, def: Definition) -> Op
510510 let display_target = krate
511511 . unwrap_or_else ( || ( * db. all_crates ( ) . last ( ) . expect ( "no crate graph present" ) ) . into ( ) )
512512 . to_display_target ( db) ;
513- if ! has_runnable_doc_test ( db, & attrs) {
513+ let Some ( doc_test_info ) = runnable_doc_test_info ( db, & attrs) else {
514514 return None ;
515- }
515+ } ;
516516 let def_name = def. name ( db) ?;
517517 let path = ( || {
518518 let mut path = String :: new ( ) ;
@@ -555,7 +555,7 @@ fn module_def_doctest(sema: &Semantics<'_, RootDatabase>, def: Definition) -> Op
555555 let res = Runnable {
556556 use_name_in_title : false ,
557557 nav,
558- kind : RunnableKind :: DocTest { test_id } ,
558+ kind : RunnableKind :: DocTest { test_id, has_compile_fail : doc_test_info . has_compile_fail } ,
559559 cfg : attrs. cfgs ( db) . cloned ( ) ,
560560 update_test : UpdateTest :: default ( ) ,
561561 } ;
@@ -573,32 +573,64 @@ impl TestAttr {
573573 }
574574}
575575
576- fn has_runnable_doc_test ( db : & RootDatabase , attrs : & hir:: AttrsWithOwner ) -> bool {
576+ #[ derive( Default , Clone , Copy ) ]
577+ struct RunnableDocTestInfo {
578+ has_compile_fail : bool ,
579+ }
580+
581+ fn runnable_doc_test_info (
582+ db : & RootDatabase ,
583+ attrs : & hir:: AttrsWithOwner ,
584+ ) -> Option < RunnableDocTestInfo > {
577585 const RUSTDOC_FENCES : [ & str ; 2 ] = [ "```" , "~~~" ] ;
578586 const RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE : & [ & str ] =
579- & [ "" , "rust" , "should_panic" , "edition2015" , "edition2018" , "edition2021" ] ;
587+ & [ "" , "rust" , "should_panic" , "edition2015" , "edition2018" , "edition2021" , "edition2024" ] ;
588+
589+ let doc = attrs. hir_docs ( db) ?;
590+ let mut info = RunnableDocTestInfo :: default ( ) ;
591+ let mut in_code_block = false ;
592+ let mut runnable_found = false ;
593+
594+ for line in doc. docs ( ) . lines ( ) {
595+ let trimmed_line = line. trim_start ( ) ;
596+ if let Some ( header) =
597+ RUSTDOC_FENCES . into_iter ( ) . find_map ( |fence| trimmed_line. strip_prefix ( fence) )
598+ {
599+ if in_code_block {
600+ in_code_block = false ;
601+ continue ;
602+ }
580603
581- attrs. hir_docs ( db) . is_some_and ( |doc| {
582- let mut in_code_block = false ;
604+ in_code_block = true ;
605+ let mut block_has_compile_fail = false ;
606+ let mut block_runnable = true ;
583607
584- for line in doc. docs ( ) . lines ( ) {
585- if let Some ( header) =
586- RUSTDOC_FENCES . into_iter ( ) . find_map ( |fence| line. strip_prefix ( fence) )
608+ for attr in header
609+ . split ( ',' )
610+ . flat_map ( |segment| segment. split_ascii_whitespace ( ) )
611+ . map ( str:: trim)
612+ . filter ( |attr| !attr. is_empty ( ) )
587613 {
588- in_code_block = !in_code_block;
614+ if attr. eq_ignore_ascii_case ( "compile_fail" ) {
615+ block_has_compile_fail = true ;
616+ block_runnable = false ;
617+ continue ;
618+ }
589619
590- if in_code_block
591- && header
592- . split ( ',' )
593- . all ( |sub| RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE . contains ( & sub. trim ( ) ) )
620+ if !RUSTDOC_CODE_BLOCK_ATTRIBUTES_RUNNABLE
621+ . iter ( )
622+ . any ( |allowed| allowed. eq_ignore_ascii_case ( attr) )
594623 {
595- return true ;
624+ block_runnable = false ;
596625 }
597626 }
627+
628+ info. has_compile_fail |= block_has_compile_fail;
629+ runnable_found |= block_runnable;
598630 }
631+ }
599632
600- false
601- } )
633+ runnable_found. then_some ( info)
602634}
603635
604636// We could create runnables for modules with number_of_test_submodules > 0,
@@ -756,6 +788,7 @@ impl UpdateTest {
756788mod tests {
757789 use expect_test:: { Expect , expect} ;
758790
791+ use super :: RunnableKind ;
759792 use crate :: fixture;
760793
761794 fn check ( #[ rust_analyzer:: rust_fixture] ra_fixture : & str , expect : Expect ) {
@@ -944,6 +977,39 @@ impl Test for StructWithRunnable {}
944977 ) ;
945978 }
946979
980+ #[ test]
981+ fn doc_test_runnable_tracks_compile_fail_blocks ( ) {
982+ let ( analysis, position) = fixture:: position (
983+ r#"
984+ //- /lib.rs
985+ $0
986+ /// ```compile_fail
987+ /// let x = 5;
988+ /// x += 1;
989+ /// ```
990+ ///
991+ /// ```
992+ /// let x = 5;
993+ /// x + 1;
994+ /// ```
995+ fn add(left: u64, right: u64) -> u64 {
996+ left + right
997+ }
998+ "# ,
999+ ) ;
1000+
1001+ let runnables = analysis. runnables ( position. file_id ) . unwrap ( ) ;
1002+ let doc_test = runnables
1003+ . into_iter ( )
1004+ . find ( |runnable| matches ! ( runnable. kind, RunnableKind :: DocTest { .. } ) )
1005+ . expect ( "expected doctest runnable" ) ;
1006+
1007+ match doc_test. kind {
1008+ RunnableKind :: DocTest { has_compile_fail, .. } => assert ! ( has_compile_fail) ,
1009+ _ => panic ! ( "expected doctest runnable" ) ,
1010+ }
1011+ }
1012+
9471013 #[ test]
9481014 fn test_runnables_doc_test_in_impl ( ) {
9491015 check (
0 commit comments