Skip to content

Commit e0bb276

Browse files
Owen Phillipsophilli
authored andcommitted
feat: Propagate rustdoc to generated (super)state enums
Problem: `state_machine!()` users could not document their state and superstate handler functions with doc comments that would appear on the generated enum variants. This made the generated code harder to understand, reduced IDE support, and meant users could not use lints like `missing_docs`. Solution: Propagate doc comments through the macro pipeline by extracting them from the state/superstate handler functions during analysis, and then attaching the extracted doc comments as attributes to the generated enum variants. Additionally we added default doc comments to the generated State and Superstate enums. Testing: Added trybuild tests that use `#![deny(missing_docs)]` to validate that the doc comments are propagated.
1 parent ec1db37 commit e0bb276

File tree

7 files changed

+154
-2
lines changed

7 files changed

+154
-2
lines changed

macro/src/analyze.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ pub struct State {
8686
pub context_arg: Option<PatType>,
8787
/// Whether the function is async or not.
8888
pub is_async: bool,
89+
/// Doc comments from the state handler function.
90+
pub doc_comments: Vec<Attribute>,
8991
}
9092

9193
/// Information regarding a superstate.
@@ -113,6 +115,8 @@ pub struct Superstate {
113115
pub context_arg: Option<PatType>,
114116
/// Whether the function is async or not.
115117
pub is_async: bool,
118+
/// Doc comments from the superstate handler function.
119+
pub doc_comments: Vec<Attribute>,
116120
}
117121

118122
/// Information regarding an action.
@@ -540,6 +544,8 @@ pub fn analyze_state(method: &mut ImplItemFn, state_machine: &StateMachine) -> S
540544
}
541545
}
542546

547+
let doc_comments = extract_doc_comments(&method.attrs);
548+
543549
State {
544550
handler_name,
545551
superstate,
@@ -552,6 +558,7 @@ pub fn analyze_state(method: &mut ImplItemFn, state_machine: &StateMachine) -> S
552558
event_arg,
553559
context_arg,
554560
is_async,
561+
doc_comments,
555562
}
556563
}
557564

@@ -665,6 +672,8 @@ pub fn analyze_superstate(method: &ImplItemFn, state_machine: &StateMachine) ->
665672
}
666673
}
667674

675+
let doc_comments = extract_doc_comments(&method.attrs);
676+
668677
Superstate {
669678
handler_name,
670679
superstate,
@@ -677,6 +686,7 @@ pub fn analyze_superstate(method: &ImplItemFn, state_machine: &StateMachine) ->
677686
event_arg,
678687
context_arg,
679688
is_async,
689+
doc_comments,
680690
}
681691
}
682692

@@ -744,6 +754,15 @@ pub fn get_meta(attrs: &[Attribute], name: &str) -> Vec<Meta> {
744754
.collect()
745755
}
746756

757+
/// Extract doc comments from function attributes.
758+
fn extract_doc_comments(attrs: &[Attribute]) -> Vec<Attribute> {
759+
attrs
760+
.iter()
761+
.filter(|attr| attr.path().is_ident("doc"))
762+
.cloned()
763+
.collect()
764+
}
765+
747766
/// Get the ident of the shared storage type.
748767
pub fn get_shared_storage_path(ty: &Type) -> Path {
749768
match ty {
@@ -859,6 +878,7 @@ fn valid_state_analyze() {
859878
}),
860879
context_arg: None,
861880
is_async: false,
881+
doc_comments: vec![],
862882
};
863883

864884
let superstate = Superstate {
@@ -877,6 +897,7 @@ fn valid_state_analyze() {
877897
}),
878898
context_arg: None,
879899
is_async: false,
900+
doc_comments: vec![],
880901
};
881902

882903
let entry_action = Action {

macro/src/codegen.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ fn codegen_state(ir: &Ir) -> Option<ItemEnum> {
206206
match &ir.state_machine.state_ident {
207207
crate::lower::StateIdent::CustomState(_ident) => None,
208208
crate::lower::StateIdent::StatigState(state_ident) => Some(parse_quote!(
209+
/// State enum generated by Statig.
209210
#[derive(#(#state_derives),*)]
210211
# visibility enum #state_ident #state_generics {
211212
#(#variants),*
@@ -382,6 +383,7 @@ fn codegen_superstate(ir: &Ir) -> ItemEnum {
382383
let visibility = &ir.state_machine.visibility;
383384

384385
parse_quote!(
386+
/// Superstate enum generated by Statig.
385387
#[derive(#(#superstate_derives),*)]
386388
#visibility enum #superstate_ident #superstate_generics {
387389
#(#variants),*

macro/src/lower.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,10 @@ pub fn lower_state(state: &analyze::State, state_machine: &analyze::StateMachine
535535

536536
let handler_inputs: Vec<Ident> = state.inputs.iter().map(fn_arg_to_ident).collect();
537537

538-
let variant = parse_quote!(#variant_name { #(#variant_fields),* });
538+
let mut variant: Variant = parse_quote!(#variant_name { #(#variant_fields),* });
539+
// Attach doc comments from the state handler to the enum variant
540+
variant.attrs = state.doc_comments.clone();
541+
539542
let pat = parse_quote!(#state_name::#variant_name { #(#pat_fields),*});
540543
let constructor = parse_quote!(fn #state_handler_name ( #(#constructor_args),* ) -> Self { Self::#variant_name { #(#field_values),*} });
541544

@@ -597,7 +600,10 @@ pub fn lower_superstate(
597600
.collect();
598601
let handler_inputs: Vec<Ident> = superstate.inputs.iter().map(fn_arg_to_ident).collect();
599602

600-
let variant = parse_quote!(#superstate_name { #(#variant_fields),* });
603+
let mut variant: Variant = parse_quote!(#superstate_name { #(#variant_fields),* });
604+
// Attach doc comments from the superstate handler to the enum variant
605+
variant.attrs = superstate.doc_comments.clone();
606+
601607
let pat = parse_quote!(#superstate_type::#superstate_name { #(#pat_fields),*});
602608

603609
let handler_call = match &superstate.is_async {
@@ -865,6 +871,7 @@ fn create_analyze_state() -> analyze::State {
865871
},
866872
],
867873
is_async: false,
874+
doc_comments: vec![],
868875
}
869876
}
870877

@@ -932,6 +939,7 @@ fn create_analyze_superstate() -> analyze::Superstate {
932939
],
933940
is_async: false,
934941
initial_state: None,
942+
doc_comments: vec![],
935943
}
936944
}
937945

statig/tests/ui/doc_comments.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#![deny(missing_docs)]
2+
//! Test doc comment propagation to generated State and Superstate enums
3+
4+
use statig::prelude::*;
5+
6+
/// Machine demonstrating doc comment propagation
7+
#[derive(Default)]
8+
pub struct DocMachine {}
9+
10+
/// Test state machine with various doc comment scenarios
11+
#[state_machine(initial = "State::simple_state()")]
12+
impl DocMachine {
13+
/// Simple state with basic documentation.
14+
#[state(superstate = "documented_superstate")]
15+
fn simple_state(&mut self) -> Outcome<State> {
16+
Transition(State::multi_line_state())
17+
}
18+
19+
/// Multi-line state with comprehensive documentation.
20+
///
21+
/// # Purpose
22+
/// This state demonstrates multi-line doc comments
23+
/// with various formatting including:
24+
///
25+
/// - Bullet points
26+
/// - Headers
27+
/// - Multiple paragraphs
28+
///
29+
/// All of this documentation should be preserved
30+
/// on the generated State::MultiLineState variant.
31+
#[state(superstate = "documented_superstate")]
32+
fn multi_line_state(&mut self) -> Outcome<State> {
33+
Transition(State::standalone_state())
34+
}
35+
36+
/// Standalone state without superstate grouping.
37+
///
38+
/// This tests doc comment propagation for states that
39+
/// are not part of any superstate hierarchy.
40+
#[state]
41+
fn standalone_state(&mut self) -> Outcome<State> {
42+
Transition(State::simple_state())
43+
}
44+
45+
/// Documented superstate containing multiple states.
46+
///
47+
/// This superstate groups together all the documented states
48+
/// and demonstrates that superstate doc comments are also
49+
/// properly propagated to the generated Superstate enum.
50+
#[superstate]
51+
fn documented_superstate(&mut self) -> Outcome<State> {
52+
Super
53+
}
54+
}
55+
56+
/// Main function for the test
57+
fn main() {
58+
let machine = DocMachine::default();
59+
let _state_machine = machine.uninitialized_state_machine().init();
60+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#![deny(missing_docs)]
2+
//! Critical validation test: This MUST fail to prove our doc comment tests work.
3+
//!
4+
//! This test intentionally has missing doc comments on a state handler.
5+
//! The #![deny(missing_docs)] lint should catch this and cause compilation to fail.
6+
//!
7+
//! If this test passes, it means either:
8+
//! 1. Our doc comment implementation is broken, OR
9+
//! 2. The missing_docs lint isn't working
10+
//!
11+
//! Either case would invalidate our positive doc comment tests.
12+
13+
use statig::prelude::*;
14+
15+
/// Test machine for missing docs validation
16+
#[derive(Default)]
17+
pub struct ValidationMachine {}
18+
19+
/// State machine to validate missing docs detection
20+
#[state_machine(initial = "State::documented()")]
21+
impl ValidationMachine {
22+
/// This state has documentation.
23+
#[state]
24+
fn documented(&mut self) -> Outcome<State> {
25+
Transition(State::undocumented())
26+
}
27+
28+
// INTENTIONALLY MISSING DOC COMMENT - this should cause compile failure
29+
#[state]
30+
fn undocumented(&mut self) -> Outcome<State> {
31+
Transition(State::documented())
32+
}
33+
34+
/// This superstate has doc comments.
35+
#[superstate]
36+
fn container(&mut self) -> Outcome<State> {
37+
Super
38+
}
39+
}
40+
41+
/// Main function for the test
42+
fn main() {
43+
let machine = ValidationMachine::default();
44+
let _state_machine = machine.uninitialized_state_machine().init();
45+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
error: missing documentation for a variant
2+
--> tests/ui/doc_comments_missing_lint.rs:20:1
3+
|
4+
20 | #[state_machine(initial = "State::documented()")]
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
note: the lint level is defined here
8+
--> tests/ui/doc_comments_missing_lint.rs:1:9
9+
|
10+
1 | #![deny(missing_docs)]
11+
| ^^^^^^^^^^^^
12+
= note: this error originates in the attribute macro `state_machine` (in Nightly builds, run with -Z macro-backtrace for more info)

statig/tests/ui_tests.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,8 @@ fn ui() {
33
let t = trybuild::TestCases::new();
44
t.pass("tests/ui/custom_state.rs");
55
t.compile_fail("tests/ui/custom_state_derive_error.rs");
6+
7+
// Doc comment propagation tests
8+
t.pass("tests/ui/doc_comments.rs");
9+
t.compile_fail("tests/ui/doc_comments_missing_lint.rs");
610
}

0 commit comments

Comments
 (0)