statum-macros 0.9.0

Proc macros for representing legal workflow and protocol states explicitly in Rust
Documentation
use proc_macro2::TokenStream;

use crate::diagnostics::{DiagnosticMessage, compile_error_at};
use crate::source::{SourceModuleQuery, current_source_info, format_candidates};
use crate::{
    EnumInfo, LoadedMachineLookupFailure, LoadedStateLookupFailure, MachineInfo, MachinePath,
    StateModulePath, format_loaded_machine_candidates, format_loaded_state_candidates,
    lookup_loaded_machine_in_module, lookup_loaded_state_enum, lookup_loaded_state_enum_by_name,
    lookup_unique_loaded_machine_by_name,
};

use super::attr::{
    ValidatorMachineAttr, machine_attr_display_for_module, unresolved_relative_validator_path_line,
};

pub(crate) fn resolve_machine_metadata(
    current_module_path: &str,
    machine_attr: &ValidatorMachineAttr,
) -> Result<MachineInfo, TokenStream> {
    let module_path = machine_attr.machine_module_path.as_str();
    let source_query = SourceModuleQuery::current(module_path);
    let module_path_key: MachinePath = module_path.into();
    let machine_name = machine_attr.machine_name.as_str();
    lookup_loaded_machine_in_module(&module_path_key, machine_name).map_err(|failure| {
        let current_line = current_source_info().map(|(_, line)| line).unwrap_or_default();
        let available = source_query.machine_candidates();
        let suggested_machine_name = available
            .first()
            .map(|candidate| candidate.name.as_str())
            .unwrap_or(machine_name);
        let suggested_attr = preferred_machine_attr_suggestion(
            current_module_path,
            machine_name,
            Some(module_path),
            suggested_machine_name,
        )
        .unwrap_or_else(|| format!("crate::path::{suggested_machine_name}"));
        let available_line = if available.is_empty() {
            "No `#[machine]` items were found in this module.".to_string()
        } else {
            format!(
                "Available `#[machine]` items in this module: {}.",
                format_candidates(&available)
            )
        };
        let ordering_line = available
            .iter()
            .find(|candidate| candidate.name == machine_name && candidate.line_number > current_line)
            .map(|candidate| {
                format!(
                    "Source scan found `#[machine]` item `{machine_name}` later in this module on line {}. If that item is active for this build, move it above this `#[validators]` impl because Statum resolves these relationships in expansion order.",
                    candidate.line_number
                )
            })
            .map(|line| format!("{line}\n"))
            .unwrap_or_default();
        let elsewhere_line = source_query.same_named_machine_candidates_elsewhere(machine_name)
            .map(|candidates| {
                format!(
                    "Same-named `#[machine]` items elsewhere in this file: {}.",
                    format_candidates(&candidates)
                )
            })
            .unwrap_or_else(|| "No same-named `#[machine]` items were found in other modules of this file.".to_string());
        let missing_attr_line = source_query.plain_machine_struct_line(machine_name).map(|line| {
            format!(
                "A struct named `{machine_name}` exists on line {line}, but it is not annotated with `#[machine]`."
            )
        });
        let relative_path_line = unresolved_relative_validator_path_line(machine_attr, &suggested_attr)
            .map(|line| format!("{line}\n"))
            .unwrap_or_default();
        let authority_line = match failure {
            LoadedMachineLookupFailure::NotFound => {
                "Statum only resolves `#[machine]` items that have already expanded before this `#[validators]` impl.".to_string()
            }
            LoadedMachineLookupFailure::Ambiguous(candidates) => format!(
                "Loaded `#[machine]` candidates were ambiguous: {}.",
                format_loaded_machine_candidates(&candidates)
            ),
        };
        let message = DiagnosticMessage::new(format!(
            "`#[validators({})]` could not resolve a matching `#[machine]` in module `{module_path}`.",
            machine_attr.attr_display,
        ))
        .found(format!("`#[validators({})]`", machine_attr.attr_display))
        .expected(format!("`#[validators({suggested_attr})]`"))
        .fix("point `#[validators(...)]` at the Statum machine type declared in that module and declare that `#[machine]` item before this validators impl.".to_string())
        .reason(authority_line)
        .assumption(relative_path_line.trim_end().to_string())
        .note(ordering_line.trim_end().to_string())
        .note(
            missing_attr_line
                .unwrap_or_else(|| "No plain struct with that name was found in this module either.".to_string()),
        )
        .candidates(elsewhere_line)
        .candidates(available_line)
        .help(format!(
            "Correct shape: `#[validators({suggested_attr})] impl PersistedRow {{ ... }}`."
        ));
        compile_error_at(proc_macro2::Span::call_site(), &message)
    })
}

pub(crate) fn resolve_state_enum_info(
    machine_metadata: &MachineInfo,
) -> Result<EnumInfo, TokenStream> {
    let module_path = machine_metadata.module_path.as_ref();
    let source_query = SourceModuleQuery::current(module_path);
    let state_path_key: StateModulePath = module_path.into();
    let machine_name = machine_metadata.name.clone();
    let expected_state_name = machine_metadata.state_generic_name.as_deref();
    let state_enum_info = match expected_state_name {
        Some(expected_name) => lookup_loaded_state_enum_by_name(&state_path_key, expected_name),
        None => lookup_loaded_state_enum(&state_path_key),
    };
    state_enum_info.map_err(|failure| {
        let current_line = current_source_info().map(|(_, line)| line).unwrap_or_default();
        let available = source_query.state_candidates();
        let available_line = if available.is_empty() {
            "No `#[state]` enums were found in this module.".to_string()
        } else {
            format!(
                "Available `#[state]` enums in this module: {}.",
                format_candidates(&available)
            )
        };
        let elsewhere_line = expected_state_name
            .and_then(|name| source_query.same_named_state_candidates_elsewhere(name))
            .map(|candidates| {
                format!(
                    "Same-named `#[state]` enums elsewhere in this file: {}.",
                    format_candidates(&candidates)
                )
            })
            .unwrap_or_else(|| "No same-named `#[state]` enums were found in other modules of this file.".to_string());
        let expected_line = expected_state_name
            .map(|name| {
                format!(
                    "Machine `{machine_name}` expects its first generic parameter to name `#[state]` enum `{name}`."
                )
            })
            .unwrap_or_else(|| {
                format!(
                    "Machine `{machine_name}` did not expose a resolvable first generic parameter for its `#[state]` enum."
                )
            });
        let ordering_line = expected_state_name.and_then(|name| {
            available
                .iter()
                .find(|candidate| candidate.name == name && candidate.line_number > current_line)
                .map(|candidate| {
                    format!(
                        "Source scan found `#[state]` enum `{name}` later in this module on line {}. If that item is active for this build, move it above the machine and this `#[validators]` impl because Statum resolves these relationships in expansion order.",
                        candidate.line_number
                    )
                })
        });
        let ordering_line = ordering_line
            .map(|line| format!("{line}\n"))
            .unwrap_or_default();
        let missing_attr_line = expected_state_name.as_ref().and_then(|name| {
            source_query.plain_state_enum_line(name).map(|line| {
                format!("An enum named `{name}` exists on line {line}, but it is not annotated with `#[state]`.")
            })
        });
        let authority_line = match failure {
            LoadedStateLookupFailure::NotFound => {
                "Statum only resolves `#[state]` enums that have already expanded before this `#[validators]` impl.".to_string()
            }
            LoadedStateLookupFailure::Ambiguous(candidates) => format!(
                "Loaded `#[state]` candidates were ambiguous: {}.",
                format_loaded_state_candidates(&candidates)
            ),
        };
        let message = DiagnosticMessage::new(format!(
            "machine `{machine_name}` could not resolve its `#[state]` enum in module `{module_path}` for this `#[validators]` impl."
        ))
        .expected(format!(
            "`struct {machine_name}<ExpectedState> {{ ... }}` where `ExpectedState` is a `#[state]` enum declared in `{module_path}`"
        ))
        .fix("make the machine's first generic name the right local `#[state]` enum and declare that enum before the machine and validators impl.".to_string())
        .reason(expected_line)
        .note(authority_line)
        .note(ordering_line.trim_end().to_string())
        .note(
            missing_attr_line
                .unwrap_or_else(|| "No plain enum with that expected name was found in this module either.".to_string()),
        )
        .candidates(elsewhere_line)
        .candidates(available_line);
        compile_error_at(proc_macro2::Span::call_site(), &message)
    })
}

pub(crate) fn preferred_machine_attr_suggestion(
    current_module_path: &str,
    machine_name: &str,
    fallback_module_path: Option<&str>,
    fallback_machine_name: &str,
) -> Option<String> {
    if let Ok(machine_info) = lookup_unique_loaded_machine_by_name(machine_name) {
        return Some(machine_attr_display_for_module(
            current_module_path,
            machine_info.module_path.as_ref(),
            &machine_info.name,
        ));
    }

    fallback_module_path.map(|module_path| {
        machine_attr_display_for_module(current_module_path, module_path, fallback_machine_name)
    })
}