use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{Ident, Path, PathArguments};
use crate::diagnostics::{DiagnosticMessage, compact_display, compile_error_at};
use crate::source::{current_source_info, module_path_from_file_with_root, module_root_from_file};
use super::lookup::preferred_machine_attr_suggestion;
pub(crate) struct ValidatorMachineAttr {
pub(crate) machine_path: Path,
pub(crate) machine_ident: Ident,
pub(crate) machine_name: String,
pub(crate) machine_module_path: String,
pub(crate) attr_display: String,
pub(crate) path_kind: ValidatorMachinePathKind,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ValidatorMachinePathKind {
BareCurrentModule,
Anchored,
RelativeMultiSegment,
}
pub(crate) fn resolve_validator_machine_attr(
current_module_path: &str,
machine_path: &Path,
) -> Result<ValidatorMachineAttr, TokenStream> {
validate_validator_machine_path(machine_path)?;
let machine_ident = machine_path
.segments
.last()
.map(|segment| segment.ident.clone())
.expect("validated machine path has a last segment");
let machine_name = machine_ident.to_string();
let attr_display = path_display(machine_path);
let path_kind = validator_machine_path_kind(machine_path);
let machine_module_path =
resolve_validator_machine_module_path(current_module_path, machine_path, &machine_name)?;
Ok(ValidatorMachineAttr {
machine_path: machine_path.clone(),
machine_ident,
machine_name,
machine_module_path,
attr_display,
path_kind,
})
}
fn validate_validator_machine_path(machine_path: &Path) -> Result<(), TokenStream> {
let Some(last_segment) = machine_path.segments.last() else {
let message = DiagnosticMessage::new("`#[validators(...)]` requires a machine path.")
.expected("`Machine` or `crate::flow::Machine`")
.fix("write `#[validators(Machine)]` for the current module or an anchored path like `#[validators(crate::flow::Machine)]`.");
return Err(compile_error_at(proc_macro2::Span::call_site(), &message));
};
if machine_path.leading_colon.is_some() {
let message = DiagnosticMessage::new(
"`#[validators(...)]` does not accept leading-`::` paths.",
)
.found(format!("`{}`", compact_display(machine_path)))
.expected("`Machine`, `self::flow::Machine`, `super::flow::Machine`, or `crate::flow::Machine`")
.fix("drop the leading `::` and anchor the path with `crate::`, `self::`, or `super::` when needed.");
return Err(compile_error_at(proc_macro2::Span::call_site(), &message));
}
if machine_path
.segments
.iter()
.any(|segment| !matches!(segment.arguments, PathArguments::None))
{
let message = DiagnosticMessage::new(
"`#[validators(...)]` expects a machine type path without generic arguments.",
)
.found(format!("`{}`", compact_display(machine_path)))
.expected("`Machine` or `crate::flow::Machine`")
.fix("remove generic arguments from the attribute path. Statum reads the machine type itself, not a concrete instantiation.");
return Err(compile_error_at(proc_macro2::Span::call_site(), &message));
}
let reserved = last_segment.ident.to_string();
if reserved == "crate" || reserved == "self" || reserved == "super" {
let message = DiagnosticMessage::new(
"`#[validators(...)]` must end with the Statum machine type name.",
)
.found(format!("`{}`", compact_display(machine_path)))
.expected("`Machine` or `crate::flow::Machine`")
.fix("end the path with the machine type itself, for example `#[validators(crate::flow::Machine)]`.");
return Err(compile_error_at(proc_macro2::Span::call_site(), &message));
}
Ok(())
}
fn resolve_validator_machine_module_path(
current_module_path: &str,
machine_path: &Path,
machine_name: &str,
) -> Result<String, TokenStream> {
let segments = machine_path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>();
let module_segments = &segments[..segments.len().saturating_sub(1)];
if module_segments.is_empty() {
return Ok(current_module_path.to_owned());
}
let first = module_segments[0].as_str();
let relative_is_ambiguous =
!module_segments.is_empty() && first != "crate" && first != "self" && first != "super";
if crate::strict_introspection_enabled() && relative_is_ambiguous {
let suggestion = preferred_machine_attr_suggestion(
current_module_path,
machine_name,
None,
machine_name,
)
.unwrap_or_else(|| format!("crate::path::{machine_name}"));
let written_path = path_display(machine_path);
let message = DiagnosticMessage::new(format!(
"`#[validators({written_path})]` is not accepted in strict introspection mode."
))
.found(format!("`#[validators({written_path})]`"))
.expected(format!("`#[validators({suggestion})]`"))
.fix("use a direct machine path rooted at `crate::`, `self::`, or `super::`.".to_string())
.reason(format!(
"relative multi-segment paths like `{written_path}` can name either module paths or imported aliases, and strict mode only accepts locally readable machine bindings."
));
return Err(compile_error_at(proc_macro2::Span::call_site(), &message));
}
let mut index = 0usize;
let mut base = match first {
"crate" => {
index = 1;
source_observation_root_module()
}
"self" => {
index = 1;
current_module_path.to_owned()
}
"super" => {
let mut module = current_module_path.to_owned();
while module_segments
.get(index)
.is_some_and(|segment| segment == "super")
{
module = parent_module_path(&module).ok_or_else(|| {
let message = "Error: `#[validators(super::...)]` climbed past the crate root.\nFix: use `crate::...` for an absolute machine path.";
quote! { compile_error!(#message); }
})?;
index += 1;
}
module
}
_ => current_module_path.to_owned(),
};
for segment in &module_segments[index..] {
base = child_module_path(&base, segment);
}
Ok(base)
}
fn validator_machine_path_kind(machine_path: &Path) -> ValidatorMachinePathKind {
let module_segment_count = machine_path.segments.len().saturating_sub(1);
if module_segment_count == 0 {
return ValidatorMachinePathKind::BareCurrentModule;
}
let first = machine_path
.segments
.first()
.map(|segment| segment.ident.to_string())
.expect("validated machine path has a first segment");
match first.as_str() {
"crate" | "self" | "super" => ValidatorMachinePathKind::Anchored,
_ => ValidatorMachinePathKind::RelativeMultiSegment,
}
}
pub(crate) fn unresolved_relative_validator_path_line(
machine_attr: &ValidatorMachineAttr,
suggested_attr: &str,
) -> Option<String> {
if machine_attr.path_kind != ValidatorMachinePathKind::RelativeMultiSegment {
return None;
}
Some(format!(
"Path note: Statum interpreted `{}` as the local child-module path `self::{}`.\nImported aliases and re-exports are not supported in `#[validators(...)]` path resolution.\nIf you meant that local module, declare the `#[machine]` there or spell it `#[validators(self::{})]` for clarity. If you meant a different module, anchor the real path, for example `#[validators({suggested_attr})]`.",
machine_attr.attr_display, machine_attr.attr_display, machine_attr.attr_display,
))
}
pub(crate) fn machine_attr_display_for_module(
current_module_path: &str,
module_path: &str,
machine_name: &str,
) -> String {
if module_path == current_module_path {
machine_name.to_owned()
} else if module_path == "crate" {
format!("crate::{machine_name}")
} else if module_path.starts_with("crate::") {
format!("{module_path}::{machine_name}")
} else {
format!("crate::{module_path}::{machine_name}")
}
}
fn parent_module_path(module_path: &str) -> Option<String> {
if module_path == "crate" {
return None;
}
module_path
.rsplit_once("::")
.map(|(parent, _)| parent.to_owned())
.or_else(|| Some("crate".to_owned()))
}
fn child_module_path(base: &str, child: &str) -> String {
if base == "crate" {
child.to_owned()
} else {
format!("{base}::{child}")
}
}
fn path_display(path: &Path) -> String {
path.to_token_stream().to_string().replace(" :: ", "::")
}
fn source_observation_root_module() -> String {
let Some((file_path, _)) = current_source_info() else {
return "crate".to_owned();
};
if let Some(crate_root) = crate::crate_root_for_file(&file_path) {
let src_root = std::path::PathBuf::from(crate_root).join("src");
if std::path::PathBuf::from(&file_path).starts_with(&src_root) {
return "crate".to_owned();
}
}
let module_root = module_root_from_file(&file_path);
module_path_from_file_with_root(&file_path, &module_root)
}