Skip to content

Commit 25d4e77

Browse files
authored
fix(macros): resolve quick start source info from item spans
Fixes #17. - prefer annotated item spans over anonymous proc-macro call sites when resolving source and module information - fall back for validator line numbers when impl span location data is unavailable - add a Quick Start regression fixture and repo-relative docs links for CI
1 parent bb04073 commit 25d4e77

10 files changed

Lines changed: 173 additions & 48 deletions

File tree

docs/inspector-plan.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Current shipping surface:
3636
## Dependency
3737

3838
The exact static substrate is tracked separately in
39-
[exact-static-relations-plan.md](/home/eran/code/statum/docs/exact-static-relations-plan.md).
39+
[exact-static-relations-plan.md](exact-static-relations-plan.md).
4040

4141
That plan is the protocol-truth dependency for this one.
4242

@@ -205,7 +205,7 @@ Heuristic lane:
205205

206206
Source of truth:
207207

208-
- [exact-static-relations-plan.md](/home/eran/code/statum/docs/exact-static-relations-plan.md)
208+
- [exact-static-relations-plan.md](exact-static-relations-plan.md)
209209

210210
Status:
211211

macro_registry/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
path = "../module_path_extractor"
33
version = "0.6.9"
44

5+
[dependencies.proc-macro2]
6+
features = ["span-locations"]
7+
version = "1.0"
8+
59
[dependencies.syn]
610
features = ["full"]
711
version = "2.0"

macro_registry/src/callsite.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use module_path_extractor::{find_module_path, get_pseudo_module_path, get_source_info};
2+
use proc_macro2::Span;
23
use std::path::Path;
34

45
fn normalize_file_path(file_path: &str) -> String {
@@ -13,9 +14,39 @@ fn normalize_file_path(file_path: &str) -> String {
1314
}
1415
}
1516

17+
fn source_file_exists(file_path: &str) -> bool {
18+
Path::new(file_path).is_file()
19+
}
20+
1621
/// Returns `(file_path, line_number)` for the current macro call-site when available.
1722
pub fn current_source_info() -> Option<(String, usize)> {
18-
get_source_info().map(|(file_path, line_number)| (normalize_file_path(&file_path), line_number))
23+
get_source_info()
24+
.map(|(file_path, line_number)| (normalize_file_path(&file_path), line_number))
25+
.filter(|(file_path, _)| source_file_exists(file_path))
26+
}
27+
28+
/// Returns `(file_path, line_number)` for an explicit span when available.
29+
pub fn source_info_for_span(span: Span) -> Option<(String, usize)> {
30+
let file_path = span
31+
.local_file()
32+
.map(|path| path.to_string_lossy().into_owned())
33+
.or_else(|| {
34+
let file_path = span.file();
35+
(!file_path.is_empty()).then_some(file_path)
36+
})?;
37+
38+
let file_path = normalize_file_path(&file_path);
39+
source_file_exists(&file_path).then_some((file_path, span.start().line))
40+
}
41+
42+
/// Returns `(file_path, line_number)` for an explicit span, falling back to the
43+
/// current macro call-site when the span does not carry usable source info.
44+
pub fn source_info_for_span_or_callsite(span: Span) -> Option<(String, usize)> {
45+
match source_info_for_span(span) {
46+
Some((file_path, line_number)) if line_number > 0 => Some((file_path, line_number)),
47+
Some((file_path, _)) => current_source_info().or(Some((file_path, 0))),
48+
None => current_source_info(),
49+
}
1950
}
2051

2152
/// Returns the best-effort source file for the current macro call-site.
@@ -35,6 +66,16 @@ pub fn current_module_path_at_line(line_number: usize) -> Option<String> {
3566
module_path_for_line(&file_path, line_number)
3667
}
3768

69+
/// Returns the best-effort module path for an explicit span or, if needed, the
70+
/// current macro call-site.
71+
pub fn module_path_for_span(span: Span) -> Option<String> {
72+
let (file_path, line_number) = source_info_for_span_or_callsite(span)?;
73+
if line_number == 0 {
74+
return None;
75+
}
76+
module_path_for_line(&file_path, line_number)
77+
}
78+
3879
/// Returns the best-effort module path for the current macro call-site.
3980
pub fn current_module_path() -> String {
4081
current_module_path_opt().unwrap_or_else(get_pseudo_module_path)

statum-macros/src/lib.rs

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub(crate) use syntax::{
4242
extract_derives, source_file_fingerprint,
4343
};
4444

45-
use macro_registry::callsite::{current_module_path_at_line, current_module_path_opt};
45+
use macro_registry::callsite::{module_path_for_span, source_info_for_span_or_callsite};
4646
use proc_macro::TokenStream;
4747
use proc_macro2::Span;
4848
use syn::{Item, ItemImpl, parse_macro_input};
@@ -138,7 +138,7 @@ pub fn transition(
138138
item: proc_macro::TokenStream,
139139
) -> proc_macro::TokenStream {
140140
let input = parse_macro_input!(item as ItemImpl);
141-
let module_path = match resolved_current_module_path(Span::call_site(), "#[transition]") {
141+
let module_path = match resolved_current_module_path(input.impl_token.span, "#[transition]") {
142142
Ok(path) => path,
143143
Err(err) => return err,
144144
};
@@ -172,12 +172,21 @@ pub fn transition(
172172
/// `.build_reports()`.
173173
#[proc_macro_attribute]
174174
pub fn validators(attr: TokenStream, item: TokenStream) -> TokenStream {
175-
let line_number = Span::call_site().start().line;
176-
let module_path = match resolved_current_module_path(Span::call_site(), "#[validators]") {
175+
let item_impl = parse_macro_input!(item as ItemImpl);
176+
let span = item_impl.impl_token.span;
177+
let line_number = source_info_for_span_or_callsite(span)
178+
.map(|(_, line_number)| line_number)
179+
.filter(|line_number| *line_number > 0)
180+
.or_else(|| {
181+
let raw_line_number = span.start().line;
182+
(raw_line_number > 0).then_some(raw_line_number)
183+
})
184+
.unwrap_or_default();
185+
let module_path = match resolved_current_module_path(span, "#[validators]") {
177186
Ok(path) => path,
178187
Err(err) => return err,
179188
};
180-
parse_validators(attr, item, &module_path, line_number)
189+
parse_validators(attr, item_impl, &module_path, line_number)
181190
}
182191

183192
#[doc(hidden)]
@@ -190,12 +199,7 @@ pub(crate) fn resolved_current_module_path(
190199
span: Span,
191200
macro_name: &str,
192201
) -> Result<String, TokenStream> {
193-
let line_number = span.start().line;
194-
let resolved = if line_number == 0 {
195-
current_module_path_opt()
196-
} else {
197-
current_module_path_at_line(line_number).or_else(current_module_path_opt)
198-
};
202+
let resolved = module_path_for_span(span);
199203

200204
resolved.ok_or_else(|| {
201205
let message = format!(

statum-macros/src/machine/metadata.rs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use macro_registry::callsite::{current_source_file, current_source_info, module_path_for_line};
1+
use macro_registry::callsite::{
2+
current_source_info, module_path_for_span, source_info_for_span_or_callsite,
3+
};
24
use macro_registry::query;
35
use proc_macro2::{Span, TokenStream};
46
use quote::{format_ident, quote, ToTokens};
@@ -8,8 +10,7 @@ use crate::{
810
EnumInfo, LoadedStateLookupFailure, ModulePath, SourceFingerprint, StateModulePath,
911
crate_root_for_file, extract_derives, format_loaded_state_candidates,
1012
lookup_loaded_state_enum, lookup_loaded_state_enum_by_name, source_file_fingerprint,
11-
parse_present_attrs, parse_presentation_types_attr, PresentationAttr,
12-
PresentationTypesAttr,
13+
parse_present_attrs, parse_presentation_types_attr, PresentationAttr, PresentationTypesAttr,
1314
};
1415
use super::extra_type_arguments_tokens;
1516

@@ -77,19 +78,19 @@ impl MachineInfo {
7778
}
7879

7980
pub fn from_item_struct(item: &ItemStruct) -> syn::Result<Self> {
80-
let Some(file_path) = current_source_file() else {
81+
let span = item.ident.span();
82+
let Some((file_path, line_number)) = source_info_for_span_or_callsite(span) else {
8183
return Err(syn::Error::new(
82-
item.ident.span(),
84+
span,
8385
format!(
8486
"Internal error: could not read source information for `#[machine]` struct `{}`.",
8587
item.ident
8688
),
8789
));
8890
};
89-
let line_number = item.ident.span().start().line;
90-
let Some(module_path) = module_path_for_line(&file_path, line_number) else {
91+
let Some(module_path) = module_path_for_span(span) else {
9192
return Err(syn::Error::new(
92-
item.ident.span(),
93+
span,
9394
format!(
9495
"Internal error: could not resolve the module path for `#[machine]` struct `{}`.",
9596
item.ident
@@ -129,8 +130,9 @@ impl MachineInfo {
129130
return None;
130131
}
131132

132-
let line_number = item.ident.span().start().line;
133-
let file_path = current_source_file();
133+
let (file_path, line_number) = source_info_for_span_or_callsite(item.ident.span())
134+
.map(|(file_path, line_number)| (Some(file_path), line_number))
135+
.unwrap_or((None, item.ident.span().start().line));
134136
let presentation = parse_present_attrs(&item.attrs).ok()?;
135137
let presentation_types = parse_presentation_types_attr(&item.attrs).ok()?;
136138
Some(Self {

statum-macros/src/state.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use macro_registry::callsite::{current_source_file, module_path_for_line};
1+
use macro_registry::callsite::{module_path_for_span, source_info_for_span_or_callsite};
22
use proc_macro2::TokenStream;
33
use quote::{format_ident, quote, ToTokens};
44
use std::sync::{OnceLock, RwLock};
@@ -336,19 +336,19 @@ pub fn invalid_state_target_error(item: &Item) -> TokenStream {
336336

337337
impl EnumInfo {
338338
pub fn from_item_enum(item: &ItemEnum) -> syn::Result<Self> {
339-
let Some(file_path) = current_source_file() else {
339+
let span = item.ident.span();
340+
let Some((file_path, line_number)) = source_info_for_span_or_callsite(span) else {
340341
return Err(syn::Error::new(
341-
item.ident.span(),
342+
span,
342343
format!(
343344
"Internal error: could not read source information for `#[state]` enum `{}`.",
344345
item.ident
345346
),
346347
));
347348
};
348-
let line_number = item.ident.span().start().line;
349-
let Some(module_path) = module_path_for_line(&file_path, line_number) else {
349+
let Some(module_path) = module_path_for_span(span) else {
350350
return Err(syn::Error::new(
351-
item.ident.span(),
351+
span,
352352
format!(
353353
"Internal error: could not resolve the module path for `#[state]` enum `{}`.",
354354
item.ident
@@ -368,8 +368,9 @@ impl EnumInfo {
368368
item: &ItemEnum,
369369
module_path: StateModulePath,
370370
) -> syn::Result<Self> {
371-
let file_path = current_source_file();
372-
let line_number = item.ident.span().start().line;
371+
let (file_path, line_number) = source_info_for_span_or_callsite(item.ident.span())
372+
.map(|(file_path, line_number)| (Some(file_path), line_number))
373+
.unwrap_or((None, item.ident.span().start().line));
373374
Self::from_item_enum_with_module_and_file(item, module_path, file_path, line_number)
374375
}
375376

statum-macros/src/validators.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,11 @@ struct CollectValidatorContext<'a> {
3434

3535
pub fn parse_validators(
3636
attr: TokenStream,
37-
item: TokenStream,
37+
item_impl: ItemImpl,
3838
module_path: &str,
3939
line_number: usize,
4040
) -> TokenStream {
4141
let machine_ident = parse_macro_input!(attr as Ident);
42-
let item_impl = parse_macro_input!(item as ItemImpl);
4342
let struct_ident = &item_impl.self_ty;
4443
let persisted_type_display = struct_ident.to_token_stream().to_string();
4544
let machine_name = machine_ident.to_string();

statum-macros/src/validators/resolution.rs

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use proc_macro2::TokenStream;
22
use quote::quote;
33
use syn::Ident;
44

5-
use macro_registry::callsite::current_source_info;
5+
use macro_registry::callsite::{current_source_info, source_info_for_span_or_callsite};
66
use macro_registry::query;
77

88
use crate::{
@@ -14,12 +14,14 @@ pub(super) fn resolve_machine_metadata(
1414
module_path: &str,
1515
machine_ident: &Ident,
1616
) -> Result<MachineInfo, TokenStream> {
17+
let source_info = source_info_for_span_or_callsite(machine_ident.span());
1718
let module_path_key: MachinePath = module_path.into();
1819
let machine_name = machine_ident.to_string();
1920
lookup_loaded_machine_in_module(&module_path_key, &machine_name).map_err(|failure| {
20-
let current_line = current_source_info().map(|(_, line)| line).unwrap_or_default();
21-
let available = available_machine_candidates_in_module(module_path);
22-
let same_named_elsewhere = same_named_machine_candidates_elsewhere(&machine_name, module_path);
21+
let current_line = source_info.as_ref().map(|(_, line)| *line).unwrap_or_default();
22+
let available = available_machine_candidates_in_module(source_info.as_ref(), module_path);
23+
let same_named_elsewhere =
24+
same_named_machine_candidates_elsewhere(source_info.as_ref(), &machine_name, module_path);
2325
let loaded_same_named_elsewhere =
2426
same_named_loaded_machines_elsewhere(&module_path_key, &machine_name);
2527
let suggested_machine_name = available
@@ -72,11 +74,14 @@ pub(super) fn resolve_machine_metadata(
7274
} else {
7375
String::new()
7476
};
75-
let missing_attr_line = plain_struct_line_in_module(module_path, &machine_name).map(|line| {
76-
format!(
77-
"A struct named `{machine_name}` exists on line {line}, but it is not annotated with `#[machine]`."
78-
)
79-
});
77+
let missing_attr_line =
78+
plain_struct_line_in_module(source_info.as_ref(), module_path, &machine_name).map(
79+
|line| {
80+
format!(
81+
"A struct named `{machine_name}` exists on line {line}, but it is not annotated with `#[machine]`."
82+
)
83+
},
84+
);
8085
let authority_line = match failure {
8186
LoadedMachineLookupFailure::NotFound => {
8287
"Statum only resolves `#[machine]` items that have already expanded before this `#[validators]` impl.".to_string()
@@ -96,18 +101,28 @@ pub(super) fn resolve_machine_metadata(
96101
})
97102
}
98103

99-
fn available_machine_candidates_in_module(module_path: &str) -> Vec<query::ItemCandidate> {
100-
let Some((file_path, _)) = current_source_info() else {
104+
fn source_file_from_info(source_info: Option<&(String, usize)>) -> Option<String> {
105+
source_info
106+
.map(|(file_path, _)| file_path.clone())
107+
.or_else(|| current_source_info().map(|(file_path, _)| file_path))
108+
}
109+
110+
fn available_machine_candidates_in_module(
111+
source_info: Option<&(String, usize)>,
112+
module_path: &str,
113+
) -> Vec<query::ItemCandidate> {
114+
let Some(file_path) = source_file_from_info(source_info) else {
101115
return Vec::new();
102116
};
103117
query::candidates_in_module(&file_path, module_path, query::ItemKind::Struct, Some("machine"))
104118
}
105119

106120
fn same_named_machine_candidates_elsewhere(
121+
source_info: Option<&(String, usize)>,
107122
machine_name: &str,
108123
module_path: &str,
109124
) -> Option<Vec<query::ItemCandidate>> {
110-
let (file_path, _) = current_source_info()?;
125+
let file_path = source_file_from_info(source_info)?;
111126
let candidates = query::same_named_candidates_elsewhere(
112127
&file_path,
113128
module_path,
@@ -118,8 +133,12 @@ fn same_named_machine_candidates_elsewhere(
118133
(!candidates.is_empty()).then_some(candidates)
119134
}
120135

121-
fn plain_struct_line_in_module(module_path: &str, struct_name: &str) -> Option<usize> {
122-
let (file_path, _) = current_source_info()?;
136+
fn plain_struct_line_in_module(
137+
source_info: Option<&(String, usize)>,
138+
module_path: &str,
139+
struct_name: &str,
140+
) -> Option<usize> {
141+
let file_path = source_file_from_info(source_info)?;
123142
query::plain_item_line_in_module(
124143
&file_path,
125144
module_path,

statum-macros/tests/macro_errors.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ fn test_valid_macro_usage() {
153153
t.pass("tests/ui/valid_same_names_different_modules.rs");
154154
t.pass("tests/ui/valid_transition_nested_wrappers.rs");
155155
t.pass("tests/ui/valid_transition_branch.rs");
156+
t.pass("tests/ui/valid_quick_start.rs");
156157
t.pass("tests/ui/valid_transition_include.rs");
157158
t.pass("tests/ui/valid_into_machines_by.rs");
158159
t.pass("tests/ui/valid_transition_map.rs");

0 commit comments

Comments
 (0)