Module ast

Source
Expand description

This module leverages the syn crate and provides an easy mechanism to interact with a rust AST represented as a syn::File with just a few lines of code.

At the heart of this module are the Finder and Mutator structs, which can be thought of a game console:

  • Their purpose is clear: they either find something in an AST or mutate parts of the AST itself (similar to how a game console’s purpose is to run games).
  • They can be customized to find or mutate specific parts of the AST in concrete ways (just as each game has its own unique narrative).

These structs “load” the implementors (the games, following the previous analogy), which specify exactly where and how the structs should act. For example, they may be used to locate a specific item within a particular trait or to mutate a macro invocation, as the following example shows:

use test_builder::TestBuilder;
use rust_writer::ast::{
  mutator::{Mutator, ToMutate},
  finder::{Finder, ToFind},
  implementors::{ItemToTrait, TokenStreamToMacro}
};
use syn::{
  visit::Visit,
  visit_mut::VisitMut,
  parse_quote,
  TraitItem
};

TestBuilder::default()
 .with_trait_ast()
 .with_macro_ast()
 .execute(|mut builder|{
  // Define the ItemToTrait implementor. This implementor means:
  // 1. We're looking inside a trait called `MyTrait`   .
  // 2. We're interested in a type called `Type1` with trait bound `From<String>`.
  let item_to_trait: ItemToTrait =
   ("MyTrait", parse_quote! {type Type1: From<String>;}).into();

  let ast = builder.get_ref_ast_file("trait.rs").expect("This exists; qed;");

  // 'Load' the implementor into a `Finder` using `to_find`.
  let mut finder = Finder::default().to_find(&item_to_trait);

  // Use the new finder variable to check if there's a trait called `MyTrait` in the AST
  // containing a type called `Type1` whose trait bound is `From<String>`.
  assert!(finder.find(ast));

  // Define the TokenStreamToMacro implementor. This implementor means:
  // 1. We're looking inside a macro called `my_macro`.
  // 2. We're interested in a `TokenStream` composed by the token `D`.
  let token_to_macro: TokenStreamToMacro =
    (parse_quote! { my_macro }, None, parse_quote! { D }).into();

  let ast = builder.get_mut_ast_file("macro.rs").expect("This exists; qed;");

  let mut finder = Finder::default().to_find(&token_to_macro);

  // The token `D` isn't in the macro at the beginning.
  assert!(!finder.find(ast));

  // 'Load' the implementor into a 'Mutator' using `to_mutate` and mutate the ast.
  let mut mutator = Mutator::default().to_mutate(&token_to_macro);
  assert!(mutator.mutate(ast).is_ok());

  // We can now find the token `D` inside `my_macro`.
  let mut finder = Finder::default().to_find(&token_to_macro);
  assert!(finder.find(ast));
});

This crate comes with a set of predefined implementors, which can be found in the implementors module. This set may be extended in the future to include additional implementors as the crate evolves. All the included implementors comes with a From implementation that

§Combining implementors

It was so good up to this point, but creating a new Finder/Mutator for each AST operation may feel a bit cumbersome in some situations. The #[finder] and #[mutator] macros come in to combine several implementors into a new one capable of executing all operations simultaneously.

It’s recomended to go through their docs to fully understand what those macros are doing and how to use them. As a teaser, let’s replicate the example of the item added to a trait and the token stream added to a macro using this handy approach.

use test_builder::TestBuilder;
use rust_writer::ast::{
  mutator::{Mutator, ToMutate},
  finder::{Finder, ToFind},
  implementors::{ItemToTrait, TokenStreamToMacro},
  mutator,
  finder
};
use syn::{
  visit::Visit,
  visit_mut::VisitMut,
  parse_quote,
};

#[finder(ItemToTrait<'a>, TokenStreamToMacro)]
#[mutator(ItemToTrait<'a>, TokenStreamToMacro)]
#[impl_from]
struct CombinedImplementor;

TestBuilder::default()
 .with_trait_ast()
 .with_macro_ast()
 .execute(|mut builder|{
  let combined_implementor: CombinedImplementor = (
    ("MyTrait", parse_quote! { type Type1: From<String>; }).into(),
    (parse_quote! { my_macro }, None, parse_quote! { D }).into()
  ).into();

  let mut ast = builder.get_mut_ast_file("trait.rs").expect("This exists; qed;").clone();
  ast.items.extend(builder.get_ref_ast_file("macro.rs").expect("This exists;
  qed;").items.clone());

  let mut finder: CombinedImplementorFinderWrapper = Finder::default().to_find(&combined_implementor).into();

  // The `Finder` fails to find all the elements.
  assert!(!finder.find(&ast, None));

  // But we can still check that the item_to_trait implementor succeeded in its research,
  // thanks to the finder struct and the handy `get_missing_indexes` method that tell us which
  // implementors couldn't find its target.
  let missing_indexes = finder.get_missing_indexes();
  assert_eq!(missing_indexes, Some(vec![1]));

  // And that it was the macro implementor who failed
  assert!(!finder.0.found[1]);

  // Let's mutate and complete the ast
  let mut mutator: CombinedImplementorMutatorWrapper = Mutator::default().to_mutate(&combined_implementor).into();

  // We can mutate just the elements that were not found, so we don't duplicate the rest.
  assert!(mutator.mutate(&mut ast, missing_indexes.as_deref()).is_ok());

  let mut finder: CombinedImplementorFinderWrapper = Finder::default().to_find(&combined_implementor).into();

  // Now the finder can find both elements
  assert!(finder.find(&ast, None));
});

§Defining new implementors

If the set of predefined implementors isn’t enough, defining a new implementor is perfectly possible. However, it’s not directly feasible… Both Finder and Mutator need to implement syn::visit::Visit and syn::visit_mut::VisitMut traits respectively in order to become functional. Implementing a foreign trait in a foreign type is forbidden by the orphan rule, so, what to do in this case? 🤔🤔🤔

There’s basically two approaches:

  1. If the implementor to define isn’t quite linked to the project where it’s needed, open a PR to the rust_writer crate! That way everybody will benefit of the new implementor.
  2. Use the #[local_finder] and #[local_mutator] macros to define a local implementor.

Modules§

finder
This module contains the Finder struct, which is used to search for specific items in an AST in a very targeted way. The Finder struct is completely generic, and thanks to the ToFind trait and the implementors it may be customized to assert if an element is contained inside an AST.
implementors
This module contains a predefined set of implementors that can be used with Finder and Mutator to effectively interact with an AST.
mutator
This module contains the Mutator struct, which is used to mutate an AST in a very targeted way. The Mutator struct is totally generic, and thanks to the ToMutate trait and the implementors, it may be customized to mutate the AST in pretty different ways.

Attribute Macros§

finder
The #[finder] macro is used to define a new implementor which combines other implementors, capable of asserting if different elements are part of an AST with just one instruction, with the same level of precision as if we had used each implementor separately.
local_finder
The #[local_finder] macro is used to create a custom implementor mimicking the behavior of Finder.
local_mutator
The #[local_mutator] macro is used to create a custom implementor mimicking the behavior of a Mutator.
mutator
The #[mutator] macro is used to define a new implementor which combines other implementors, capable of mutating different parts of an AST with just one instruction, with the same level of precision as if we had used each implementor separately.