redactable-derive 0.10.1

Derive macros for the redactable crate
Documentation
use proc_macro2::{TokenStream, TokenTree};
use quote::quote;

use super::*;

fn collect_paths(stream: TokenStream, paths: &mut Vec<Vec<String>>) {
    let tokens: Vec<_> = stream.into_iter().collect();
    for token in &tokens {
        if let TokenTree::Group(group) = token {
            collect_paths(group.stream(), paths);
        }
    }
    for start in 0..tokens.len() {
        if start >= 3
            && matches!(tokens[start - 1], TokenTree::Punct(ref punct) if punct.as_char() == ':')
            && matches!(tokens[start - 2], TokenTree::Punct(ref punct) if punct.as_char() == ':')
            && matches!(tokens[start - 3], TokenTree::Ident(_))
        {
            continue;
        }
        let TokenTree::Ident(first) = &tokens[start] else {
            continue;
        };
        let mut path = vec![first.to_string()];
        let mut index = start + 1;
        while index + 2 < tokens.len() {
            let (TokenTree::Punct(a), TokenTree::Punct(b), TokenTree::Ident(segment)) =
                (&tokens[index], &tokens[index + 1], &tokens[index + 2])
            else {
                break;
            };
            if a.as_char() != ':' || b.as_char() != ':' {
                break;
            }
            path.push(segment.to_string());
            index += 3;
        }
        if path.len() > 1 {
            paths.push(path);
        }
    }
}

fn assert_private_dependencies(tokens: TokenStream, requires_serde: bool) {
    let mut paths = Vec::new();
    collect_paths(tokens, &mut paths);
    assert!(
        paths
            .iter()
            .all(|path| !matches!(path.first().map(String::as_str), Some("slog" | "serde"))),
        "generated dependencies must not resolve from the consumer root: {paths:?}"
    );
    for item in ["Value", "Record", "Key", "Serializer", "Result"] {
        assert!(
            paths.iter().any(|path| {
                path.windows(2)
                    .any(|segments| segments == ["__private", "slog"])
                    && path.last().is_some_and(|segment| segment == item)
            }),
            "missing private slog::{item} path: {paths:?}"
        );
    }
    assert_eq!(
        paths.iter().any(|path| {
            path.windows(2)
                .any(|segments| segments == ["__private", "serde"])
                && path.last().is_some_and(|segment| segment == "Serialize")
        }),
        requires_serde,
        "private serde::Serialize path mismatch: {paths:?}"
    );
}

pub(super) fn run_structural_generated_dependency_roots() {
    let not_sensitive: DeriveInput = syn::parse2(quote! {
        struct PublicEvent { value: String }
    })
    .unwrap();
    assert_private_dependencies(expand_not_sensitive(not_sensitive).unwrap(), true);

    let not_sensitive_display: DeriveInput = syn::parse2(quote! {
        struct PublicDisplayEvent;
    })
    .unwrap();
    assert_private_dependencies(
        expand_not_sensitive_display(not_sensitive_display).unwrap(),
        false,
    );

    let sensitive: DeriveInput = syn::parse2(quote! {
        struct SecretEvent { #[sensitive(redactable::Secret)] value: String }
    })
    .unwrap();
    assert_private_dependencies(expand(sensitive, DeriveKind::Sensitive).unwrap(), true);

    let sensitive_display: DeriveInput = syn::parse2(quote! {
        #[error("{value}")]
        struct SecretDisplayEvent { #[sensitive(redactable::Secret)] value: String }
    })
    .unwrap();
    assert_private_dependencies(
        expand(sensitive_display, DeriveKind::SensitiveDisplay).unwrap(),
        false,
    );
}