refactor(traverse): indicate scope entry point with scope(enter_before) attr (#3882)

Improve annotation of AST types for codegen.

Currently:

```rs
#[visited_node(
    scope(ScopeFlags::empty()),
    enter_scope_before(cases),
)]
pub struct SwitchStatement<'a> {
    pub span: Span,
    pub discriminant: Expression<'a>,
    pub cases: Vec<'a, SwitchCase<'a>>,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

After this PR:

```rs
#[visited_node(scope(ScopeFlags::empty()))]
pub struct SwitchStatement<'a> {
    pub span: Span,
    pub discriminant: Expression<'a>,
    #[scope(enter_before)]
    pub cases: Vec<'a, SwitchCase<'a>>,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

I think this is easier to read.

In order to enable use of `#[scope]` attr, this introduces a dummy `VisitedNode` derive macro. Like the `visited_node` macro, `VisitedNode` derive macro is designed to do very minimal work and have no heavy dependencies, so it should be almost 0 cost in terms of compile time.
This commit is contained in:
overlookmotel 2024-06-24 14:12:15 +00:00
parent 24979c98b2
commit fcd21a6a75
4 changed files with 37 additions and 9 deletions

View file

@ -1228,7 +1228,7 @@ pub struct WithStatement<'a> {
}
/// Switch Statement
#[visited_node(scope(ScopeFlags::empty()), enter_scope_before(cases))]
#[visited_node(scope(ScopeFlags::empty()))]
#[derive(Debug)]
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]
#[cfg_attr(feature = "serialize", serde(tag = "type"))]
@ -1236,6 +1236,7 @@ pub struct SwitchStatement<'a> {
#[cfg_attr(feature = "serialize", serde(flatten))]
pub span: Span,
pub discriminant: Expression<'a>,
#[scope(enter_before)]
pub cases: Vec<'a, SwitchCase<'a>>,
pub scope_id: Cell<Option<ScopeId>>,
}
@ -1566,7 +1567,7 @@ pub struct YieldExpression<'a> {
}
/// Class Definitions
#[visited_node(scope(ScopeFlags::StrictMode), enter_scope_before(id))]
#[visited_node(scope(ScopeFlags::StrictMode))]
#[derive(Debug)]
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
@ -1575,6 +1576,7 @@ pub struct Class<'a> {
#[cfg_attr(feature = "serialize", serde(flatten))]
pub span: Span,
pub decorators: Vec<'a, Decorator<'a>>,
#[scope(enter_before)]
pub id: Option<BindingIdentifier<'a>>,
pub super_class: Option<Expression<'a>>,
pub body: Box<'a, ClassBody<'a>>,

View file

@ -46,7 +46,7 @@ pub struct TSThisParameter<'a> {
/// Enum Declaration
///
/// `const_opt`enum`BindingIdentifier`{`EnumBody_opt`}
#[visited_node(scope(ScopeFlags::empty()), enter_scope_before(members))]
#[visited_node(scope(ScopeFlags::empty()))]
#[derive(Debug)]
#[cfg_attr(feature = "serialize", derive(Serialize, Tsify))]
#[cfg_attr(feature = "serialize", serde(tag = "type"))]
@ -54,6 +54,7 @@ pub struct TSEnumDeclaration<'a> {
#[cfg_attr(feature = "serialize", serde(flatten))]
pub span: Span,
pub id: BindingIdentifier<'a>,
#[scope(enter_before)]
pub members: Vec<'a, TSEnumMember<'a>>,
pub r#const: bool,
pub declare: bool,
@ -784,7 +785,6 @@ pub enum TSTypePredicateName<'a> {
#[visited_node(
scope(ScopeFlags::TsModuleBlock),
enter_scope_before(body),
strict_if(self.body.as_ref().is_some_and(|body| body.is_strict())),
)]
#[derive(Debug)]
@ -794,6 +794,7 @@ pub struct TSModuleDeclaration<'a> {
#[cfg_attr(feature = "serialize", serde(flatten))]
pub span: Span,
pub id: TSModuleDeclarationName<'a>,
#[scope(enter_before)]
pub body: Option<TSModuleDeclarationBody<'a>>,
/// The keyword used to define this module declaration
/// ```text

View file

@ -1,8 +1,28 @@
use proc_macro::TokenStream;
use std::str::FromStr;
/// Attach to AST node type (struct or enum), to signal to codegen to create visitor for this type.
/// Macro itself does nothing - just passes through the token stream unchanged.
///
/// Macro does not generate any code - it's purely a means to communicate information to the codegen.
///
/// Only thing macro does is add `#[derive(VisitedNode)]` to the item.
/// Deriving `VisitedNode` does nothing, but supports the `#[scope]` attr on struct fields.
/// This is a workaround for Rust not supporting helper attributes for `proc_macro_attribute` macros,
/// so we need to use a derive macro to get that support.
///
/// Use native Rust `TokenStream`, to avoid dependency on slow-compiling crates like `syn` and `quote`.
#[proc_macro_attribute]
#[allow(clippy::missing_panics_doc)]
pub fn visited_node(_args: TokenStream, input: TokenStream) -> TokenStream {
input
let mut stream = TokenStream::from_str("#[derive(::oxc_ast_macros::VisitedNode)]").unwrap();
stream.extend(input);
stream
}
/// Dummy derive macro for a non-existent trait `VisitedNode`.
///
/// Does not generate any code, only purpose is to allow using `#[scope]` attr in the type def.
#[proc_macro_derive(VisitedNode, attributes(scope))]
pub fn visited_node_derive(_item: TokenStream) -> TokenStream {
TokenStream::new()
}

View file

@ -68,8 +68,11 @@ function parseFile(code, filename, types) {
function parseStruct(name, rawName, lines, scopeArgs, filename, startLineIndex) {
const fields = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#[')) {
let line = lines[i];
const isScopeEntry = line === '#[scope(enter_before)]';
if (isScopeEntry) {
line = lines[++i];
} else if (line.startsWith('#[')) {
while (!lines[i].endsWith(']')) {
i++;
}
@ -86,6 +89,8 @@ function parseStruct(name, rawName, lines, scopeArgs, filename, startLineIndex)
{name: innerTypeName, wrappers} = typeAndWrappers(typeName);
fields.push({name, typeName, rawName, rawTypeName, innerTypeName, wrappers});
if (isScopeEntry) scopeArgs.enterScopeBefore = name;
}
return {kind: 'struct', name, rawName, fields, scopeArgs};
}
@ -128,7 +133,7 @@ function parseScopeArgs(argsStr, filename, lineIndex) {
while (true) {
const [key] = matchAndConsume(/^([a-z_]+)\(/);
assert(
['scope', 'scope_if', 'strict_if', 'enter_scope_before'].includes(key),
['scope', 'scope_if', 'strict_if'].includes(key),
`Unexpected visited_node macro arg: ${key}`
);