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 ProgramParts 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 Identifers, 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 Idents 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.