oxc-project / oxc

⚓ A collection of JavaScript tools written in Rust.

Home Page:https://oxc.rs

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[request] Move `Visit` and `VisitMut` default implementations to another method

milesj opened this issue · comments

The Visit and VisitMut traits are great but there's 1 giant usability issue, and that is that each method defines its own implementation.

Now why is this a problem? Well, if I implement my own visitor and override a method, I lost the default implementation, and there's no easy way (besides dynamic dispatch below) to reuse the implementation, so I end up copying and pasting a bunch of code. For example, say I have my own visitor that needs to do something with Program, I would do this:

pub struct Example<'a>;

impl<'a> Visit<'a> for Example<'a> {
    fn visit_program(&mut self, program: &Program<'a>) {
        for stmt in &program.body {
            // Do something here
        }
    }
}

But I still want the visitor to work correctly, so then I have to copy the default code like so:

pub struct Example<'a>;

impl<'a> Visit<'a> for Example<'a> {
    fn visit_program(&mut self, program: &Program<'a>) {
        for stmt in &program.body {
            // Do something here
        }

        // Default
        let kind = AstKind::Program(self.alloc(program));
        self.enter_scope({
            let mut flags = ScopeFlags::Top;
            if program.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);
        for directive in &program.directives {
            self.visit_directive(directive);
        }
        self.visit_statements(&program.body);
        self.leave_node(kind);
        self.leave_scope();
    }
}

You can sort of work around this be using dynamic dispatch, like the following.

pub struct Example<'a>;

impl<'a> Visit<'a> for Example<'a> {
    fn visit_program(&mut self, program: &Program<'a>) {
        for stmt in &program.body {
            // Do something here
        }

        // Default
        Visit::visit_program(self, program);
    }
}

This works for some methods, but causes warnings for others. A warning I see often is "function cannot return without recursing". I'm not sure if this actually causes problems though.

Regardless, I have a suggestion here. Instead of defining a default implementation for methods, why not define another method like do_visit_program that has the implementation, and then update visit_program to call do_visit_program. That way, when the method is overridden, we can still call it normally.

For example:

// Before
pub trait Visit<'a>: Sized {
    fn visit_program(&mut self, program: &Program<'a>) {
        let kind = AstKind::Program(self.alloc(program));
        self.enter_scope({
            let mut flags = ScopeFlags::Top;
            if program.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);
        for directive in &program.directives {
            self.visit_directive(directive);
        }
        self.visit_statements(&program.body);
        self.leave_node(kind);
        self.leave_scope();
    }
}

// After
pub trait Visit<'a>: Sized {
    fn do_visit_program(&mut self, program: &Program<'a>) {
        let kind = AstKind::Program(self.alloc(program));
        self.enter_scope({
            let mut flags = ScopeFlags::Top;
            if program.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);
        for directive in &program.directives {
            self.visit_directive(directive);
        }
        self.visit_statements(&program.body);
        self.leave_node(kind);
        self.leave_scope();
    }

    fn visit_program(&mut self, program: &Program<'a>) {
        self.do_visit_program(program);
    }
}

I had a similar idea before, I need an enter_program and a leave_program method in the transformer

The implementation looks like this

pub trait VisitMut<'a>: Sized {
    fn enter_program(&mut self, program: &mut Program<'a>) {
        // do something
    }
    fn leave_program(&mut self, program: &mut Program<'a>) {
       // After the program body is visited, we may need to modify the program body.
    }

    fn visit_program(&mut self, program: &mut Program<'a>) {
        let kind = AstKind::Program(self.alloc(program));
        self.enter_scope({
            let mut flags = ScopeFlags::Top;
            if program.is_strict() {
                flags |= ScopeFlags::StrictMode;
            }
            flags
        });
        self.enter_node(kind);

        // enter here
        self.enter_program(program);

        for directive in &program.directives {
            self.visit_directive(directive);
        }
        self.visit_statements(&mut program.body);
       
        // leave here
        self.leave_program(program);

        self.leave_node(kind);
        self.leave_scope();
    }
}

This is similar to { visitor: { program() {}, "program:exit"() {} } in babel

Been tinkering around with this more, and the dynamic dispatch approach seems to always trigger a stack overflow.

Try:

This requires a lot of manual work, and given we have some workarounds, I'm going to close this as not planned for now.