How to find all structs that Derive known macro in Rust
Tags: rust, tutorial, beginners
Published: 2023-05-24 18:44:26

I've recently embarked on an endeavor to add migration generation to my favorite Rust ORM [Welds](https://github.com/weldsorm/welds).

Rust's powerful macro system allows us to extend the language in ways that can greatly enhance productivity and maintainability. In this article, I'll focus on a specific use case: finding all the structs in a codebase that derive a particular macro, in this case the `WeldsModel` macro.

The solution provided in the question uses the `syn` crate, a parsing library for Rust syntax, and the `walkdir` crate, which provides an iterator over the files in a directory and its subdirectories. Here, we'll walk through this solution and explain how it works.

## The Main Function

First, let's look at the main function:

```rust
fn main() {
    for entry in walkdir::WalkDir::new("welds/examples")
        .into_iter()
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if path.is_file() && path.extension().unwrap() == "rs" {
            let mut file = File::open(path).expect("Unable to open file");
            let mut contents = String::new();
            file.read_to_string(&mut contents)
                .expect("Unable to read file");
            let syntax_tree = syn::parse_file(&contents).unwrap();
            let weldsmodel_structs = find_weldsmodel_structs(syntax_tree);
            for weldsmodel_struct in weldsmodel_structs {
                println!("Found struct: {}", weldsmodel_struct.ident);
            }
        }
    }
}
```

The `main` function here uses the `WalkDir` iterator from the `walkdir` crate to iterate over all the files in the `welds/examples` directory and its subdirectories. It filters out directories and non-Rust files (`path.extension().unwrap() == "rs"`), then reads the contents of each Rust file into a `String`.

The `syn::parse_file` function is then used to parse this string into a syntax tree, which represents the structure of the Rust code in the file. This syntax tree is passed to the `find_weldsmodel_structs` function, which returns a `Vec<ItemStruct>` containing all structs in the file that derive the `WeldsModel` macro. [ItemStruct](https://docs.rs/syn/latest/syn/struct.ItemStruct.html) is a `syn` data model that describes `struct` in a Rust AST, they will be used later in the project as a data source to build schema from.

Finally, the `main` function prints out the name of each struct found.

## The find_weldsmodel_structs Function

Next, let's look at the `find_weldsmodel_structs` function:

```rust
fn find_weldsmodel_structs(syntax_tree: syn::File) -> Vec<ItemStruct> {
    let mut weldsmodel_structs = Vec::new();
    for item in syntax_tree.items {
        if let syn::Item::Struct(item_struct) = item {
            if has_weldsmodel_derive(&item_struct) {
                weldsmodel_structs.push(item_struct);
            }
        }
    }
    weldsmodel_structs
}
```

This function iterates over all items in the syntax tree, checks if each item is a struct, and if so, checks if the struct derives the `WeldsModel` macro using the `has_weldsmodel_derive` function. If both conditions are met, the struct is added to the `weldsmodel_structs` vector.

## The has_weldsmodel_derive Function

Finally, the `has_weldsmodel_derive` function:

```rust
fn has_weldsmodel_derive(item_struct: &ItemStruct) -> bool {
    let attrs = &item_struct.attrs;
    for attr in attrs {
        let meta = &attr.meta;
        if meta.path().is_ident("derive") {
            let nested = attr
                .parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated)
                .unwrap();
            for nested_meta in nested {
                if nested_meta.path().is_ident("WeldsModel") {
                    return true;
                }
            }
        }
    }
    false
}
```

This function checks if a given struct item has the `WeldsModel` derive attribute. It does this by looking at each attribute of the struct, and if the attribute's path is "derive", it parses the arguments of the attribute with `parse_args_with`. This gives a `Punctuated` list of `Meta` items, each representing an argument of the derive attribute. If any of these arguments' path is "WeldsModel", the function returns `true`, otherwise `false`.

## TLDR

That's the overall solution: use `WalkDir` to iterate over all Rust files in a directory, use `syn::parse_file` to parse the contents of each file into a syntax tree, and then check each ATS node attributes and meta to find structures you need.