Building a Debug Helper
Demo
To simplify things, we are just going to lift the technique for getting the JavaScript text from the ress example, so we won't be covering that again.
With that out of the way let's take a look at the Cargo.toml
and use
statements for our program.
[package]
name = "console_logify"
version = "0.1.0"
authors = ["Robert Masen <r@robertmasen.pizza>"]
edition = "2018"
[dependencies]
ressa = "0.5"
atty = "0.2"
resw = "0.2"
resast = "0.2"
# #![allow(unused_variables)] #fn main() { use ressa::{ Parser, }; use std::{ io::Read, env::args, fs::read_to_string, }; use resw::Writer; #}
This will make sure that all of the items we will need from ressa
and resast
are in scope. Now we can start defining our method for inserting the debug logging into any functions that we find. To start we are going to create a function that will generate a new ProgramPart::Stmt
that will represent our call to console.log
which might look like this.
# #![allow(unused_variables)] #fn main() { } } prop } fn console_log(args: Vec<Expr>) -> ProgramPart { ProgramPart::Stmt( Stmt::Expr( Expr::call( Expr::member( Expr::ident("console"), Expr::ident("log"), false, #}
We need to make the arguments configurable so we can insert the context information for each instance of a function but otherwise it is a pretty straight forward. Now that we have that, we need to start digging into the ProgramPart
to identify anything we want to modify. Since Parser
implements Iterator
and its Item
is Result<ProgramPart, Error>
we first need to use filter_map
to extract the ProgramPart
from the result. It would probably be good to handle the error case here but for the sake of simplicity we are going to skip any errors. Now that we have an Iterator
over ProgramPart
s we can use map
to update each part.
fn main() { let js = get_js().expect("Unable to get JavaScript"); let parser = Parser::new(&js).expect("Unable to construct parser"); for part in parser.filter_map(|p| p.ok()).map(map_part) { //FIXME: Write updated program part to somewhere } }
With that in mind the entry point is going to be a function that takes a ProgramPart
and returns a new ProgramPart
. It might look like this
# #![allow(unused_variables)] #fn main() { fn map_part(part: ProgramPart) -> ProgramPart { match part { ProgramPart::Decl(ref decl) => ProgramPart::Decl(map_decl(decl)), ProgramPart::Stmt(ref stmt) => ProgramPart::Stmt(map_stmt(stmt)), ProgramPart::Dir(_) => part, } #}
We are going to match on the part provided and either return that part if it is a Directive
or if it isn't we need to investigate further to discover if it is a function or not. We do that in two places map_decl
and map_stmt
both of which are going to utilize similar method for digging further into the tree.
# #![allow(unused_variables)] #fn main() { fn map_decl(decl: &Decl) -> Decl { match decl { Decl::Function(ref f) => Decl::Function(map_func(f)), Decl::Class(ref class) => Decl::Class(map_class(class)), _ => decl.clone() } } fn map_stmt(stmt: &Stmt) -> Stmt { match stmt { Stmt::Expr(ref expr) => Stmt::Expr(map_expr(expr)), _ => stmt.clone(), } #}
There are two ways for a Decl
to resolve into a function or method and that is with the Function
and Class
variants while a Stmt
can end up there if it is an Expr
. When we include map_expr
we see that there are cases for both Function
and Class
in the Expr
enum. That means once we get past those we will be handling the rest in the exact same way.
# #![allow(unused_variables)] #fn main() { fn map_expr(expr: &Expr) -> Expr { match expr { Expr::Function(ref f) => Expr::Function(map_func(f)), Expr::Class(ref c) => Expr::Class(map_class(c)), _ => expr.clone(), } #}
Finally we are going to start manipulating the AST in map_func
.
# #![allow(unused_variables)] #fn main() { fn map_func(func: &Function) -> Function { let mut f = func.clone(); let mut args = vec![]; if let Some(ref name) = f.id { args.push( Expr::string(&format!("'{}'", name)) ); } for arg in f.params.iter().filter_map(|a| match a { FunctionArg::Expr(e) => match e { Expr::Ident(i) => Some(i), _ => None, }, FunctionArg::Pat(p) => match p { Pat::Identifier(i) => Some(i), _ => None, }, }) { args.push(Expr::ident(arg)); } f.body.insert( 0, console_log(args), ); f.body = f.body.into_iter().map(map_part).collect(); f } #}
The first thing we are going to do is to clone the func
to give us a mutable version. Next we are going to check if the id
is Some
, if it is we can add that name to our console.log
arguments. Now function arguments can be pretty complicated, to try and keep things simple we are going to only worry about the ones that are either Expr::Ident
or Pat::Identifier
. To build something more robust it might be good to include destructured arguments or arguments with default values but for this example we are just going to keep it simple.
First we are going to filter_map
the func.params
to only get the items that ultimately resolve to Identifer
s, at that point we can wrap all of these identifiers in an Expr::Ident
and add them to the console.log
args. Now we can simply insert the result of passing those args to console_log
at the first position of the func.body
. Because functions can appear in the body of other functions we also want to map all of the func.body
program parts. Once that has completed we can return the updated func
to the caller.
The next thing we are going to want to deal with is Class
, we want to insert console.log into the top of each method on a class. This is a bit unique because we also want to provide the name of that class (if it exists) as the first argument to console.log. That might look like this.
# #![allow(unused_variables)] #fn main() { } fn map_class(class: &Class) -> Class { let mut class = class.clone(); let prefix = if let Some(ref id) = class.id { id.clone() } else { String::new() }; class.body = class.body .iter() .map(|prop| map_class_prop(&prefix, prop)) .collect(); class } fn map_class_prop(prefix: &str, prop: &Property) -> Property { let mut prop = prop.clone(); let mut args = match prop.kind { PropertyKind::Ctor => { vec![Expr::string(&format!("'new {}'", prefix))] }, PropertyKind::Get => { vec![ Expr::string(&format!("'{}'", prefix)), Expr::string("get"), ] }, PropertyKind::Set => { vec![ Expr::string(&format!("'{}'", prefix)), Expr::string("set"), ] }, PropertyKind::Method => { vec![ Expr::string(&format!("'{}'", prefix)), ] }, _ => vec![], }; match &prop.key { PropertyKey::Expr(ref e) => { match e { Expr::Ident(i) => if i != "constructor" { args.push(Expr::string(&format!("'{}'", i))); }, _ => (), } }, PropertyKey::Literal(ref l) => { match l { Literal::Boolean(ref b) => { args.push(Expr::string(&format!("'{}'", b))); }, Literal::Null => { args.push(Expr::string("'null'")); }, Literal::Number(ref n) => { args.push(Expr::string(&format!("'{}'", n))); } Literal::RegEx(ref r) => { args.push(Expr::string(&format!("'/{}/{}'", r.pattern, r.flags))); }, Literal::String(ref s) => { if s != "constructor" { args.push(Expr::string(s)); } }, _ => (), } }, PropertyKey::Pat(ref p) => { match p { Pat::Identifier(ref i) => { args.push(Expr::string(&format!("'{}'", i))); }, _ => (), } }, } if let PropertyValue::Expr(ref mut expr) = prop.value { match expr { Expr::Function(ref mut f) => { for ref arg in &f.params { match arg { FunctionArg::Expr(ref expr) => { match expr { Expr::Ident(_) => args.push(expr.clone()), _ => (), } }, FunctionArg::Pat(ref pat) => { match pat { Pat::Identifier(ref ident) => { args.push(Expr::ident(ident)) }, _ => {}, } } } } f.body.insert(0, console_log(args) ) #}
Here we have two functions, the first pulls out the id from the provided class or uses an empty string of it doesn't exist. We then just pass that off to map_class_prop
which will handle all of the different types of properties a class can have. The first thing this does is map the prefix
into the right format, so a call to new Thing()
would print new Thing
, or a get method would print Thing get
before the method name. Next we take a look at the property.key
, this will provide us with the name of our function, but according to the specification a class property key can be an identifier, a literal value, or a pattern, so we need to figure out what the name of this method is by digging into that value. First in the case that it is an ident we want to add it to the args, unless it is the value constructor
because we already put the new
keyword in that one. Next we can pull out the literal values and add those as they appear. Lastly we will only handle the pattern case when it is a Pat::Identifier
otherwise we will just skip it. Now to get the parameter names from the method definition we need to look at the property.value
which should always be an Expr::Function
. Once we match on that we simply repeat the process of map_function
pulling the args out but only when they are Ident
s and then passing that along to console_log
and inserting that Expr
at the top of the function body.
At this point we have successfully updated our AST to include a call to console.log
at the top of each function and method in our code. Now the big question is how do we write that out to a file. This problem is not a small one, in the next section we are going to cover a third crate resw
that we can use to finish this project.