@@ -34,6 +34,7 @@ struct ParseSourceInfo {
3434 crates : String ,
3535 crate_attrs : String ,
3636 maybe_crate_attrs : String ,
37+ need_stability_attr : bool ,
3738}
3839
3940/// Builder type for `DocTestBuilder`.
@@ -147,6 +148,7 @@ impl<'a> BuildDocTestBuilder<'a> {
147148 crates,
148149 crate_attrs,
149150 maybe_crate_attrs,
151+ need_stability_attr,
150152 } ) ) = result
151153 else {
152154 // If the AST returned an error, we don't want this doctest to be merged with the
@@ -184,6 +186,7 @@ impl<'a> BuildDocTestBuilder<'a> {
184186 test_id,
185187 invalid_ast : false ,
186188 can_be_merged,
189+ need_stability_attr,
187190 }
188191 }
189192}
@@ -204,6 +207,7 @@ pub(crate) struct DocTestBuilder {
204207 pub ( crate ) test_id : Option < String > ,
205208 pub ( crate ) invalid_ast : bool ,
206209 pub ( crate ) can_be_merged : bool ,
210+ need_stability_attr : bool ,
207211}
208212
209213/// Contains needed information for doctest to be correctly generated with expected "wrapping".
@@ -301,6 +305,7 @@ impl DocTestBuilder {
301305 test_id,
302306 invalid_ast : true ,
303307 can_be_merged : false ,
308+ need_stability_attr : false ,
304309 }
305310 }
306311
@@ -379,53 +384,58 @@ impl DocTestBuilder {
379384 }
380385
381386 // FIXME: This code cannot yet handle no_std test cases yet
382- let wrapper = if dont_insert_main
383- || self . has_main_fn
384- || crate_level_code. contains ( "![no_std]" )
385- {
386- None
387- } else {
388- let returns_result = processed_code. ends_with ( "(())" ) ;
389- // Give each doctest main function a unique name.
390- // This is for example needed for the tooling around `-C instrument-coverage`.
391- let inner_fn_name = if let Some ( ref test_id) = self . test_id {
392- format ! ( "_doctest_main_{test_id}" )
387+ let wrapper =
388+ if dont_insert_main || self . has_main_fn || crate_level_code. contains ( "![no_std]" ) {
389+ None
393390 } else {
394- "_inner" . into ( )
395- } ;
396- let inner_attr = if self . test_id . is_some ( ) { "#[allow(non_snake_case)] " } else { "" } ;
397- let ( main_pre, main_post) = if returns_result {
398- (
399- format ! (
400- "pub fn main() {{ {inner_attr}fn {inner_fn_name}() -> core::result::Result<(), impl core::fmt::Debug> {{\n " ,
401- ) ,
402- format ! ( "\n }} {inner_fn_name}().unwrap() }}" ) ,
403- )
404- } else if self . test_id . is_some ( ) {
405- (
406- format ! ( "pub fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n " , ) ,
407- format ! ( "\n }} {inner_fn_name}() }}" ) ,
408- )
409- } else {
410- ( "pub fn main() {\n " . into ( ) , "\n }" . into ( ) )
391+ let extra = if self . need_stability_attr {
392+ "#[stable(feature = \" doctest\" , since = \" 1.0.0\" )]\n "
393+ } else {
394+ ""
395+ } ;
396+ let returns_result = processed_code. ends_with ( "(())" ) ;
397+ // Give each doctest main function a unique name.
398+ // This is for example needed for the tooling around `-C instrument-coverage`.
399+ let inner_fn_name = if let Some ( ref test_id) = self . test_id {
400+ format ! ( "_doctest_main_{test_id}" )
401+ } else {
402+ "_inner" . into ( )
403+ } ;
404+ let inner_attr =
405+ if self . test_id . is_some ( ) { "#[allow(non_snake_case)] " } else { "" } ;
406+ let ( main_pre, main_post) = if returns_result {
407+ (
408+ format ! (
409+ "{extra}pub fn main() {{ {inner_attr}fn {inner_fn_name}() \
410+ -> core::result::Result<(), impl core::fmt::Debug> {{\n ",
411+ ) ,
412+ format ! ( "\n }} {inner_fn_name}().unwrap() }}" ) ,
413+ )
414+ } else if self . test_id . is_some ( ) {
415+ (
416+ format ! ( "{extra}pub fn main() {{ {inner_attr}fn {inner_fn_name}() {{\n " , ) ,
417+ format ! ( "\n }} {inner_fn_name}() }}" ) ,
418+ )
419+ } else {
420+ ( format ! ( "{extra}pub fn main() {{\n " ) , "\n }" . into ( ) )
421+ } ;
422+ // Note on newlines: We insert a line/newline *before*, and *after*
423+ // the doctest and adjust the `line_offset` accordingly.
424+ // In the case of `-C instrument-coverage`, this means that the generated
425+ // inner `main` function spans from the doctest opening codeblock to the
426+ // closing one. For example
427+ // /// ``` <- start of the inner main
428+ // /// <- code under doctest
429+ // /// ``` <- end of the inner main
430+ line_offset += 1 ;
431+
432+ Some ( WrapperInfo {
433+ before : main_pre,
434+ after : main_post,
435+ returns_result,
436+ insert_indent_space : opts. insert_indent_space ,
437+ } )
411438 } ;
412- // Note on newlines: We insert a line/newline *before*, and *after*
413- // the doctest and adjust the `line_offset` accordingly.
414- // In the case of `-C instrument-coverage`, this means that the generated
415- // inner `main` function spans from the doctest opening codeblock to the
416- // closing one. For example
417- // /// ``` <- start of the inner main
418- // /// <- code under doctest
419- // /// ``` <- end of the inner main
420- line_offset += 1 ;
421-
422- Some ( WrapperInfo {
423- before : main_pre,
424- after : main_post,
425- returns_result,
426- insert_indent_space : opts. insert_indent_space ,
427- } )
428- } ;
429439
430440 (
431441 DocTestWrapResult :: Valid {
@@ -575,6 +585,7 @@ fn parse_source(
575585 let mut prev_span_hi = 0 ;
576586 let not_crate_attrs = & [ sym:: forbid, sym:: allow, sym:: warn, sym:: deny, sym:: expect] ;
577587 let parsed = parser. parse_item ( rustc_parse:: parser:: ForceCollect :: No ) ;
588+ let mut need_crate_stability_attr = false ;
578589
579590 let result = match parsed {
580591 Ok ( Some ( ref item) )
@@ -600,58 +611,76 @@ fn parse_source(
600611 ) ;
601612 }
602613 } else {
614+ if !info. need_stability_attr
615+ && attr. has_name ( sym:: feature)
616+ && let Some ( sub_attrs) = attr. meta_item_list ( )
617+ && sub_attrs. iter ( ) . any ( |attr| {
618+ if let ast:: MetaItemInner :: MetaItem ( attr) = attr {
619+ matches ! ( attr. kind, ast:: MetaItemKind :: Word )
620+ && attr. has_name ( sym:: staged_api)
621+ } else {
622+ false
623+ }
624+ } )
625+ {
626+ info. need_stability_attr = true ;
627+ }
628+ if attr. has_any_name ( & [ sym:: stable, sym:: unstable] ) {
629+ need_crate_stability_attr = false ;
630+ }
603631 push_to_s ( & mut info. crate_attrs , source, attr. span , & mut prev_span_hi) ;
604632 }
605633 }
606634 let mut has_non_items = false ;
635+ let mut fn_main_pos = None ;
607636 for stmt in & body. stmts {
608637 let mut is_extern_crate = false ;
609638 match stmt. kind {
610639 StmtKind :: Item ( ref item) => {
611640 let ( found_is_extern_crate, found_main) =
612641 check_item ( item, & mut info, crate_name) ;
613642 is_extern_crate = found_is_extern_crate;
614- if found_main
615- && should_panic
616- && !matches ! ( item. vis. kind, VisibilityKind :: Public )
617- {
618- if matches ! ( item. vis. kind, VisibilityKind :: Inherited ) {
619- push_code (
620- stmt,
621- is_extern_crate,
622- & mut info,
623- source,
624- & mut prev_span_hi,
625- Some ( item. span . lo ( ) ) ,
626- ) ;
627- } else {
628- push_code (
629- stmt,
630- is_extern_crate,
631- & mut info,
643+ if found_main {
644+ fn_main_pos = Some ( info. everything_else . len ( ) ) ;
645+ if !matches ! ( item. vis. kind, VisibilityKind :: Public ) && should_panic {
646+ if matches ! ( item. vis. kind, VisibilityKind :: Inherited ) {
647+ push_code (
648+ stmt,
649+ is_extern_crate,
650+ & mut info,
651+ source,
652+ & mut prev_span_hi,
653+ Some ( item. span . lo ( ) ) ,
654+ ) ;
655+ } else {
656+ push_code (
657+ stmt,
658+ is_extern_crate,
659+ & mut info,
660+ source,
661+ & mut prev_span_hi,
662+ Some ( item. vis . span . lo ( ) ) ,
663+ ) ;
664+ prev_span_hi +=
665+ ( item. vis . span . hi ( ) . 0 - item. vis . span . lo ( ) . 0 ) as usize ;
666+ } ;
667+ if !info
668+ . everything_else
669+ . chars ( )
670+ . last ( )
671+ . is_some_and ( |c| c. is_whitespace ( ) )
672+ {
673+ info. everything_else . push ( ' ' ) ;
674+ }
675+ info. everything_else . push_str ( "pub " ) ;
676+ push_to_s (
677+ & mut info. everything_else ,
632678 source,
679+ item. span ,
633680 & mut prev_span_hi,
634- Some ( item. vis . span . lo ( ) ) ,
635681 ) ;
636- prev_span_hi +=
637- ( item. vis . span . hi ( ) . 0 - item. vis . span . lo ( ) . 0 ) as usize ;
638- } ;
639- if !info
640- . everything_else
641- . chars ( )
642- . last ( )
643- . is_some_and ( |c| c. is_whitespace ( ) )
644- {
645- info. everything_else . push ( ' ' ) ;
682+ continue ;
646683 }
647- info. everything_else . push_str ( "pub " ) ;
648- push_to_s (
649- & mut info. everything_else ,
650- source,
651- item. span ,
652- & mut prev_span_hi,
653- ) ;
654- continue ;
655684 }
656685 }
657686 // We assume that the macro calls will expand to item(s) even though they could
@@ -690,6 +719,19 @@ fn parse_source(
690719 }
691720 push_code ( stmt, is_extern_crate, & mut info, source, & mut prev_span_hi, None ) ;
692721 }
722+ if info. need_stability_attr {
723+ if let Some ( fn_main_pos) = fn_main_pos {
724+ info. everything_else . insert_str (
725+ fn_main_pos,
726+ "#[stable(feature = \" doctest\" , since = \" 1.0.0\" )]\n " ,
727+ ) ;
728+ info. need_stability_attr = false ;
729+ }
730+ if need_crate_stability_attr {
731+ info. crate_attrs
732+ . push_str ( "#![stable(feature = \" doctest\" , since = \" 1.0.0\" )]\n " ) ;
733+ }
734+ }
693735 if has_non_items {
694736 if info. has_main_fn
695737 && let Some ( dcx) = parent_dcx
0 commit comments