sc-lint-attributes 0.2.0

Source-level declarative attributes for sc-lint analyzers.
Documentation
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use sc_lint_directives::AttributeInput;
use syn::Result;

#[cfg(test)]
use sc_lint_directives::Directive;

fn expand_sc_lint(args: TokenStream2, item: TokenStream2) -> Result<TokenStream2> {
    let _parsed = syn::parse2::<AttributeInput>(args)?;
    Ok(item)
}

#[proc_macro_attribute]
pub fn sc_lint(args: TokenStream, item: TokenStream) -> TokenStream {
    let item_ts: TokenStream2 = item.clone().into();
    let args_ts: TokenStream2 = args.into();
    match expand_sc_lint(args_ts, item_ts) {
        Ok(expanded) => TokenStream::from(expanded),
        Err(error) => TokenStream::from(error.to_compile_error()),
    }
}

#[cfg(test)]
mod tests {
    use super::AttributeInput;
    use super::Directive;
    use super::expand_sc_lint;
    use quote::quote;

    #[test]
    fn parses_boundary_allow_rule() {
        let parsed: AttributeInput =
            syn::parse2(quote!(boundary.allow("cycle.type_method_self_loop"))).unwrap();
        assert_eq!(
            parsed.directives,
            vec![Directive::Allow(vec![
                "cycle.type_method_self_loop".to_string()
            ])]
        );
    }

    #[test]
    fn parses_boundary_internal_only() {
        let parsed: AttributeInput = syn::parse2(quote!(boundary.internal_only)).unwrap();
        assert_eq!(parsed.directives, vec![Directive::InternalOnly]);
    }

    #[test]
    fn parses_boundary_forbid_external_impls() {
        let parsed: AttributeInput = syn::parse2(quote!(boundary.forbid_external_impls)).unwrap();
        assert_eq!(parsed.directives, vec![Directive::ForbidExternalImpls]);
    }

    #[test]
    fn parses_multiple_directives() {
        let parsed: AttributeInput = syn::parse2(quote!(
            boundary.internal_only,
            boundary.forbid_external_impls,
            boundary.allow("cycle.type_method_self_loop")
        ))
        .unwrap();
        assert_eq!(
            parsed.directives,
            vec![
                Directive::InternalOnly,
                Directive::ForbidExternalImpls,
                Directive::Allow(vec!["cycle.type_method_self_loop".to_string()]),
            ]
        );
    }

    #[test]
    fn rejects_unknown_boundary_directive() {
        let error = syn::parse2::<AttributeInput>(quote!(boundary.unknown)).unwrap_err();
        assert!(error.to_string().contains("unsupported boundary directive"));
    }

    #[test]
    fn expansion_is_noop_for_supported_directives() {
        let expanded = expand_sc_lint(
            quote!(
                boundary.internal_only,
                boundary.forbid_external_impls,
                boundary.allow("cycle.type_method_self_loop")
            ),
            quote!(
                pub struct Example;
            ),
        )
        .unwrap();
        assert_eq!(
            expanded.to_string(),
            quote!(
                pub struct Example;
            )
            .to_string()
        );
    }
}