cargo-twin4rust 0.1.0

Cargo subcommand that fails the build when a Rust source file has no mirrored test file beside it
Documentation
// Copyright 2025 Umberto Gotti <umberto.gotti@umbertogotti.dev>
// Licensed under the MIT License
// SPDX-License-Identifier: MIT

use anyhow::{Context, Result};
use quote::ToTokens;
use syn::{Attribute, File, Item, ItemImpl, ItemMod, parse_file};

use crate::trivial_constructor_detector::TrivialConstructorDetector;

#[derive(Default)]
pub struct DefinitionAnalyzer;

impl DefinitionAnalyzer {
    pub fn new() -> Self {
        Self
    }

    pub fn is_definition_only_source(&self, source: &str) -> Result<bool> {
        let syntax = parse_file(source).context("failed to parse Rust source")?;
        Ok(self.file_is_definition_only(&syntax))
    }

    pub fn mod_file_is_import_only(&self, source: &str) -> Result<bool> {
        let syntax = parse_file(source).context("failed to parse Rust source")?;
        let items = self.top_level_non_test_items(&syntax.items);
        Ok(items.iter().all(|item| {
            matches!(
                item,
                Item::Use(_) | Item::ExternCrate(_) | Item::Mod(ItemMod { content: None, .. })
            )
        }))
    }

    fn file_is_definition_only(&self, syntax: &File) -> bool {
        let items = self.top_level_non_test_items(&syntax.items);

        if items.is_empty() {
            return false;
        }

        if self.all_definition_only(&items) {
            let type_items = self.count_type_items(&items);
            return type_items > 0;
        }

        let detector = TrivialConstructorDetector::new();
        detector.file_has_trivial_constructor(&items)
    }

    fn all_definition_only(&self, items: &[&Item]) -> bool {
        items.iter().all(|item| {
            matches!(
                item,
                Item::Use(_)
                    | Item::ExternCrate(_)
                    | Item::Mod(ItemMod { content: None, .. })
                    | Item::Const(_)
                    | Item::Static(_)
                    | Item::Macro(_)
                    | Item::Verbatim(_)
                    | Item::Struct(_)
                    | Item::Enum(_)
                    | Item::Type(_)
                    | Item::Trait(_)
            ) || Self::is_behaviourless_impl(item)
        })
    }

    // `impl Marker for T {}` and `impl<T> Alias for T where ...` introduce no
    // executable behaviour, so a mirrored test could only restate the compiler.
    // Only an impl that carries a method is worth a test.
    fn is_behaviourless_impl(item: &Item) -> bool {
        match item {
            Item::Impl(item) => {
                item.trait_.is_some()
                    && !item
                        .items
                        .iter()
                        .any(|member| matches!(member, syn::ImplItem::Fn(_)))
            }
            _ => false,
        }
    }

    fn count_type_items(&self, items: &[&Item]) -> usize {
        items
            .iter()
            .filter(|item| {
                matches!(
                    item,
                    Item::Struct(_) | Item::Enum(_) | Item::Type(_) | Item::Trait(_)
                )
            })
            .count()
    }

    fn top_level_non_test_items<'a>(&self, items: &'a [Item]) -> Vec<&'a Item> {
        items
            .iter()
            .filter(|item| !Self::has_test_attrs(item.attrs()))
            .collect()
    }

    pub fn has_test_attrs(attrs: &[Attribute]) -> bool {
        attrs.iter().any(|attr| {
            let path = attr.path();
            path.is_ident("cfg") && attr.meta.to_token_stream().to_string().contains("test")
        })
    }
}

trait ItemAttrs {
    fn attrs(&self) -> &[Attribute];
}

impl ItemAttrs for Item {
    fn attrs(&self) -> &[Attribute] {
        match self {
            Item::Const(item) => &item.attrs,
            Item::Enum(item) => &item.attrs,
            Item::ExternCrate(item) => &item.attrs,
            Item::Fn(item) => &item.attrs,
            Item::ForeignMod(item) => &item.attrs,
            Item::Impl(ItemImpl { attrs, .. }) => attrs,
            Item::Macro(item) => &item.attrs,
            Item::Mod(item) => &item.attrs,
            Item::Static(item) => &item.attrs,
            Item::Struct(item) => &item.attrs,
            Item::Trait(item) => &item.attrs,
            Item::TraitAlias(item) => &item.attrs,
            Item::Type(item) => &item.attrs,
            Item::Union(item) => &item.attrs,
            Item::Use(item) => &item.attrs,
            Item::Verbatim(_) => &[],
            _ => &[],
        }
    }
}