From 4ee94a3308c6cb4645389e0e01d216ab3eecb987 Mon Sep 17 00:00:00 2001 From: overlookmotel <557937+overlookmotel@users.noreply.github.com> Date: Mon, 22 Sep 2025 11:08:46 +0000 Subject: [PATCH] feat(napi/parser): export visitor keys (#13927) Add visitor keys export to `oxc-parser` NPM package. This is useful in itself, but also the first step towards an ESTree walker. Visitor keys are based on `@typescript-eslint/visitor-keys`. It'd be ideal to generate visitor keys direct from Oxc's own types, but that's proving difficult due to all the custom serializers we use to translate to ESTree. So using the shortcut of borrowing from TS-ESLint for now. Oxc's AST is slightly different from TS-ESTree (notably Oxc's AST adds `ParenthesizedExpression`). But apart from those few differences, TS-ESLint's AST and Oxc's are identical, and I made a PR to TS-ESLint earlier this year (https://github.com/typescript-eslint/typescript-eslint/pull/11279) to ensure visitor key order is correct, and matches Oxc. To avoid adding a runtime dependency on `@typescript-eslint/visitor-keys`, extract the data from TS-ESLint in `oxc_ast_tools`, amend for Oxc/TS-ESTree differences, and re-output it as a simple object literal. This approach will be faster at runtime, as the object's shape is consistent, and also avoids a dependency. --- .github/generated/ast_changes_watch_list.yml | 1 + .github/workflows/ci.yml | 3 + Cargo.lock | 1 + napi/parser/README.md | 6 + napi/parser/generated/visit/keys.mjs | 172 ++++++++++++++++++ napi/parser/package.json | 2 + napi/parser/scripts/visitor-keys.mjs | 4 + napi/parser/src-js/index.mjs | 2 + napi/parser/test/parse.test.ts | 10 +- pnpm-lock.yaml | 24 +++ tasks/ast_tools/Cargo.toml | 5 +- .../ast_tools/src/generators/estree_visit.rs | 146 +++++++++++++++ tasks/ast_tools/src/generators/mod.rs | 2 + tasks/ast_tools/src/main.rs | 1 + tasks/ast_tools/src/utils.rs | 10 + 15 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 napi/parser/generated/visit/keys.mjs create mode 100644 napi/parser/scripts/visitor-keys.mjs create mode 100644 tasks/ast_tools/src/generators/estree_visit.rs diff --git a/.github/generated/ast_changes_watch_list.yml b/.github/generated/ast_changes_watch_list.yml index 42604240c1c21..d629ebcbff853 100644 --- a/.github/generated/ast_changes_watch_list.yml +++ b/.github/generated/ast_changes_watch_list.yml @@ -72,6 +72,7 @@ src: - 'napi/parser/generated/lazy/constructors.mjs' - 'napi/parser/generated/lazy/types.mjs' - 'napi/parser/generated/lazy/walk.mjs' + - 'napi/parser/generated/visit/keys.mjs' - 'napi/parser/src/generated/assert_layouts.rs' - 'napi/parser/src/generated/derive_estree.rs' - 'napi/parser/src/generated/raw_transfer_constants.rs' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21ad1fffeae02..65df2dec2e368 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -357,6 +357,9 @@ jobs: cache-key: ast_changes save-cache: ${{ github.ref_name == 'main' }} + - uses: oxc-project/setup-node@fdbf0dfd334c4e6d56ceeb77d91c76339c2a0885 # v1.0.4 + if: steps.filter.outputs.src == 'true' + - name: Restore dprint plugin cache if: steps.filter.outputs.src == 'true' uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 diff --git a/Cargo.lock b/Cargo.lock index 8fea6cb66b9cc..07415cc9ca23b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1791,6 +1791,7 @@ dependencies = [ "rayon", "rustc-hash", "serde", + "serde_json", "syn", ] diff --git a/napi/parser/README.md b/napi/parser/README.md index 9d79df978583c..a704dccb6d4a3 100644 --- a/napi/parser/README.md +++ b/napi/parser/README.md @@ -41,6 +41,12 @@ import { Statement } from '@oxc-project/types'; [oxc-walker](https://www.npmjs.com/package/oxc-walker) or [estree-walker](https://www.npmjs.com/package/estree-walker) can be used. +This package exports visitor keys which can be used with any ESTree walker. + +```js +import { visitorKeys } from 'oxc-parser'; +``` + ### Fast Mode By default, Oxc parser does not produce semantic errors where symbols and scopes are needed. diff --git a/napi/parser/generated/visit/keys.mjs b/napi/parser/generated/visit/keys.mjs new file mode 100644 index 0000000000000..3c073895b20b7 --- /dev/null +++ b/napi/parser/generated/visit/keys.mjs @@ -0,0 +1,172 @@ +// Auto-generated code, DO NOT EDIT DIRECTLY! +// To edit this generated file you have to edit `tasks/ast_tools/src/generators/estree_visit.rs`. + +export default { + // Leaf nodes + DebuggerStatement: [], + EmptyStatement: [], + Literal: [], + PrivateIdentifier: [], + Super: [], + TemplateElement: [], + ThisExpression: [], + JSXClosingFragment: [], + JSXEmptyExpression: [], + JSXIdentifier: [], + JSXOpeningFragment: [], + JSXText: [], + TSAnyKeyword: [], + TSBigIntKeyword: [], + TSBooleanKeyword: [], + TSIntrinsicKeyword: [], + TSJSDocUnknownType: [], + TSNeverKeyword: [], + TSNullKeyword: [], + TSNumberKeyword: [], + TSObjectKeyword: [], + TSStringKeyword: [], + TSSymbolKeyword: [], + TSThisType: [], + TSUndefinedKeyword: [], + TSUnknownKeyword: [], + TSVoidKeyword: [], + // Non-leaf nodes + AccessorProperty: ['decorators', 'key', 'typeAnnotation', 'value'], + ArrayExpression: ['elements'], + ArrayPattern: ['decorators', 'elements', 'typeAnnotation'], + ArrowFunctionExpression: ['typeParameters', 'params', 'returnType', 'body'], + AssignmentExpression: ['left', 'right'], + AssignmentPattern: ['decorators', 'left', 'right', 'typeAnnotation'], + AwaitExpression: ['argument'], + BinaryExpression: ['left', 'right'], + BlockStatement: ['body'], + BreakStatement: ['label'], + CallExpression: ['callee', 'typeArguments', 'arguments'], + CatchClause: ['param', 'body'], + ChainExpression: ['expression'], + ClassBody: ['body'], + ClassDeclaration: ['decorators', 'id', 'typeParameters', 'superClass', 'superTypeArguments', 'implements', 'body'], + ClassExpression: ['decorators', 'id', 'typeParameters', 'superClass', 'superTypeArguments', 'implements', 'body'], + ConditionalExpression: ['test', 'consequent', 'alternate'], + ContinueStatement: ['label'], + Decorator: ['expression'], + DoWhileStatement: ['body', 'test'], + ExportAllDeclaration: ['exported', 'source', 'attributes'], + ExportDefaultDeclaration: ['declaration'], + ExportNamedDeclaration: ['declaration', 'specifiers', 'source', 'attributes'], + ExportSpecifier: ['local', 'exported'], + ExpressionStatement: ['expression'], + ForInStatement: ['left', 'right', 'body'], + ForOfStatement: ['left', 'right', 'body'], + ForStatement: ['init', 'test', 'update', 'body'], + FunctionDeclaration: ['id', 'typeParameters', 'params', 'returnType', 'body'], + FunctionExpression: ['id', 'typeParameters', 'params', 'returnType', 'body'], + Identifier: ['decorators', 'typeAnnotation'], + IfStatement: ['test', 'consequent', 'alternate'], + ImportAttribute: ['key', 'value'], + ImportDeclaration: ['specifiers', 'source', 'attributes'], + ImportDefaultSpecifier: ['local'], + ImportExpression: ['source', 'options'], + ImportNamespaceSpecifier: ['local'], + ImportSpecifier: ['imported', 'local'], + LabeledStatement: ['label', 'body'], + LogicalExpression: ['left', 'right'], + MemberExpression: ['object', 'property'], + MetaProperty: ['meta', 'property'], + MethodDefinition: ['decorators', 'key', 'value'], + NewExpression: ['callee', 'typeArguments', 'arguments'], + ObjectExpression: ['properties'], + ObjectPattern: ['decorators', 'properties', 'typeAnnotation'], + ParenthesizedExpression: ['expression'], + Program: ['body'], + Property: ['key', 'value'], + PropertyDefinition: ['decorators', 'key', 'typeAnnotation', 'value'], + RestElement: ['decorators', 'argument', 'typeAnnotation'], + ReturnStatement: ['argument'], + SequenceExpression: ['expressions'], + SpreadElement: ['argument'], + StaticBlock: ['body'], + SwitchCase: ['test', 'consequent'], + SwitchStatement: ['discriminant', 'cases'], + TaggedTemplateExpression: ['tag', 'typeArguments', 'quasi'], + TemplateLiteral: ['quasis', 'expressions'], + ThrowStatement: ['argument'], + TryStatement: ['block', 'handler', 'finalizer'], + UnaryExpression: ['argument'], + UpdateExpression: ['argument'], + V8IntrinsicExpression: ['name', 'arguments'], + VariableDeclaration: ['declarations'], + VariableDeclarator: ['id', 'init'], + WhileStatement: ['test', 'body'], + WithStatement: ['object', 'body'], + YieldExpression: ['argument'], + JSXAttribute: ['name', 'value'], + JSXClosingElement: ['name'], + JSXElement: ['openingElement', 'children', 'closingElement'], + JSXExpressionContainer: ['expression'], + JSXFragment: ['openingFragment', 'children', 'closingFragment'], + JSXMemberExpression: ['object', 'property'], + JSXNamespacedName: ['namespace', 'name'], + JSXOpeningElement: ['name', 'typeArguments', 'attributes'], + JSXSpreadAttribute: ['argument'], + JSXSpreadChild: ['expression'], + TSAbstractAccessorProperty: ['decorators', 'key', 'typeAnnotation'], + TSAbstractMethodDefinition: ['key', 'value'], + TSAbstractPropertyDefinition: ['decorators', 'key', 'typeAnnotation'], + TSArrayType: ['elementType'], + TSAsExpression: ['expression', 'typeAnnotation'], + TSCallSignatureDeclaration: ['typeParameters', 'params', 'returnType'], + TSClassImplements: ['expression', 'typeArguments'], + TSConditionalType: ['checkType', 'extendsType', 'trueType', 'falseType'], + TSConstructSignatureDeclaration: ['typeParameters', 'params', 'returnType'], + TSConstructorType: ['typeParameters', 'params', 'returnType'], + TSDeclareFunction: ['id', 'typeParameters', 'params', 'returnType', 'body'], + TSEmptyBodyFunctionExpression: ['id', 'typeParameters', 'params', 'returnType'], + TSEnumBody: ['members'], + TSEnumDeclaration: ['id', 'body'], + TSEnumMember: ['id', 'initializer'], + TSExportAssignment: ['expression'], + TSExternalModuleReference: ['expression'], + TSFunctionType: ['typeParameters', 'params', 'returnType'], + TSImportEqualsDeclaration: ['id', 'moduleReference'], + TSImportType: ['argument', 'options', 'qualifier', 'typeArguments'], + TSIndexSignature: ['parameters', 'typeAnnotation'], + TSIndexedAccessType: ['objectType', 'indexType'], + TSInferType: ['typeParameter'], + TSInstantiationExpression: ['expression', 'typeArguments'], + TSInterfaceBody: ['body'], + TSInterfaceDeclaration: ['id', 'typeParameters', 'extends', 'body'], + TSInterfaceHeritage: ['expression', 'typeArguments'], + TSIntersectionType: ['types'], + TSJSDocNonNullableType: ['typeAnnotation'], + TSJSDocNullableType: ['typeAnnotation'], + TSLiteralType: ['literal'], + TSMappedType: ['key', 'constraint', 'nameType', 'typeAnnotation'], + TSMethodSignature: ['key', 'typeParameters', 'params', 'returnType'], + TSModuleBlock: ['body'], + TSModuleDeclaration: ['id', 'body'], + TSNamedTupleMember: ['label', 'elementType'], + TSNamespaceExportDeclaration: ['id'], + TSNonNullExpression: ['expression'], + TSOptionalType: ['typeAnnotation'], + TSParameterProperty: ['decorators', 'parameter'], + TSParenthesizedType: ['typeAnnotation'], + TSPropertySignature: ['key', 'typeAnnotation'], + TSQualifiedName: ['left', 'right'], + TSRestType: ['typeAnnotation'], + TSSatisfiesExpression: ['expression', 'typeAnnotation'], + TSTemplateLiteralType: ['quasis', 'types'], + TSTupleType: ['elementTypes'], + TSTypeAliasDeclaration: ['id', 'typeParameters', 'typeAnnotation'], + TSTypeAnnotation: ['typeAnnotation'], + TSTypeAssertion: ['typeAnnotation', 'expression'], + TSTypeLiteral: ['members'], + TSTypeOperator: ['typeAnnotation'], + TSTypeParameter: ['name', 'constraint', 'default'], + TSTypeParameterDeclaration: ['params'], + TSTypeParameterInstantiation: ['params'], + TSTypePredicate: ['parameterName', 'typeAnnotation'], + TSTypeQuery: ['exprName', 'typeArguments'], + TSTypeReference: ['typeName', 'typeArguments'], + TSUnionType: ['types'], +}; diff --git a/napi/parser/package.json b/napi/parser/package.json index 63bb1f6c34575..7129a2d50149b 100644 --- a/napi/parser/package.json +++ b/napi/parser/package.json @@ -44,6 +44,7 @@ "generated/lazy/constructors.mjs", "generated/lazy/types.mjs", "generated/lazy/walk.mjs", + "generated/visit/keys.mjs", "src-js/bindings.mjs", "src-js/index.d.ts", "src-js/index.mjs", @@ -68,6 +69,7 @@ "devDependencies": { "@codspeed/vitest-plugin": "^4.0.0", "@napi-rs/wasm-runtime": "catalog:", + "@typescript-eslint/visitor-keys": "^8.44.0", "@vitest/browser": "3.2.4", "esbuild": "^0.25.0", "playwright": "^1.51.0", diff --git a/napi/parser/scripts/visitor-keys.mjs b/napi/parser/scripts/visitor-keys.mjs new file mode 100644 index 0000000000000..fde0ed9fcf9ff --- /dev/null +++ b/napi/parser/scripts/visitor-keys.mjs @@ -0,0 +1,4 @@ +import { visitorKeys } from '@typescript-eslint/visitor-keys'; + +const keys = Object.entries(visitorKeys).map(([name, keys]) => ({ name, keys })); +console.log(JSON.stringify(keys)); diff --git a/napi/parser/src-js/index.mjs b/napi/parser/src-js/index.mjs index 0a9106383854a..5b956cca538ea 100644 --- a/napi/parser/src-js/index.mjs +++ b/napi/parser/src-js/index.mjs @@ -2,6 +2,8 @@ import { createRequire } from 'node:module'; import { parseAsync as parseAsyncBinding, parseSync as parseSyncBinding } from './bindings.mjs'; import { wrap } from './wrap.mjs'; +export { default as visitorKeys } from '../generated/visit/keys.mjs'; + export { ExportExportNameKind, ExportImportNameKind, diff --git a/napi/parser/test/parse.test.ts b/napi/parser/test/parse.test.ts index 3882a8ebdb41c..94765001b6ac4 100644 --- a/napi/parser/test/parse.test.ts +++ b/napi/parser/test/parse.test.ts @@ -1,7 +1,7 @@ import { Worker } from 'node:worker_threads'; import { describe, expect, it, test } from 'vitest'; -import { parseAsync, parseSync } from '../src-js/index.mjs'; +import { parseAsync, parseSync, visitorKeys } from '../src-js/index.mjs'; import type { ExpressionStatement, ParserOptions, @@ -874,6 +874,14 @@ describe('error', () => { }); }); +it('visitor keys', () => { + expect(visitorKeys.Literal).toEqual([]); + expect(visitorKeys.VariableDeclaration).toEqual(['declarations']); + expect(visitorKeys.ObjectPattern).toEqual(['decorators', 'properties', 'typeAnnotation']); + expect(visitorKeys.ParenthesizedExpression).toEqual(['expression']); + expect(visitorKeys.V8IntrinsicExpression).toEqual(['name', 'arguments']); +}); + describe('worker', () => { it('should run', async () => { const code = await new Promise((resolve, reject) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0ebac4b657d3..e5bc5649a1c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -118,6 +118,9 @@ importers: '@napi-rs/wasm-runtime': specifier: 'catalog:' version: 1.0.5 + '@typescript-eslint/visitor-keys': + specifier: ^8.44.0 + version: 8.44.0 '@vitest/browser': specifier: 3.2.4 version: 3.2.4(playwright@1.55.0)(vite@7.1.6(@types/node@24.5.2)(jiti@2.5.1))(vitest@3.2.4) @@ -1771,6 +1774,14 @@ packages: '@types/vscode@1.93.0': resolution: {integrity: sha512-kUK6jAHSR5zY8ps42xuW89NLcBpw1kOabah7yv38J8MyiYuOHxLQBi0e7zeXbQgVefDy/mZZetqEFC+Fl5eIEQ==} + '@typescript-eslint/types@8.44.0': + resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.44.0': + resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.1': resolution: {integrity: sha512-SnbaqayTVFEA6/tYumdF0UmybY0KHyKwGPBXnyckFlrrKdhWFrL3a2HIPXHjht5ZOElKGcXfD2D63P36btb+ww==} engines: {node: '>=20.0.0'} @@ -2392,6 +2403,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} @@ -5380,6 +5395,13 @@ snapshots: '@types/vscode@1.93.0': {} + '@typescript-eslint/types@8.44.0': {} + + '@typescript-eslint/visitor-keys@8.44.0': + dependencies: + '@typescript-eslint/types': 8.44.0 + eslint-visitor-keys: 4.2.1 + '@typespec/ts-http-runtime@0.3.1': dependencies: http-proxy-agent: 7.0.2 @@ -6061,6 +6083,8 @@ snapshots: escape-string-regexp@4.0.0: {} + eslint-visitor-keys@4.2.1: {} + esprima@4.0.1: {} estree-walker@3.0.3: diff --git a/tasks/ast_tools/Cargo.toml b/tasks/ast_tools/Cargo.toml index c8bbd1766773e..95db63fe66a10 100644 --- a/tasks/ast_tools/Cargo.toml +++ b/tasks/ast_tools/Cargo.toml @@ -23,7 +23,7 @@ cow-utils = { workspace = true } indexmap = { workspace = true } itertools = { workspace = true } lazy-regex = { workspace = true } -oxc_index = { workspace = true } +oxc_index = { workspace = true, features = ["serde"] } phf = { workspace = true, features = ["macros"] } phf_codegen = { workspace = true } prettyplease = { workspace = true } @@ -31,5 +31,6 @@ proc-macro2 = { workspace = true } quote = { workspace = true } rayon = { workspace = true } rustc-hash = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } syn = { workspace = true, features = ["clone-impls", "derive", "extra-traits", "full", "parsing", "printing", "proc-macro"] } diff --git a/tasks/ast_tools/src/generators/estree_visit.rs b/tasks/ast_tools/src/generators/estree_visit.rs new file mode 100644 index 0000000000000..fe17147495b8f --- /dev/null +++ b/tasks/ast_tools/src/generators/estree_visit.rs @@ -0,0 +1,146 @@ +//! Generator of ESTree visitor keys. + +use std::{ + cmp::Ordering, + fmt::{self, Display}, + process::{Command, Stdio}, +}; + +use serde::Deserialize; + +use oxc_index::{IndexVec, define_index_type}; + +use crate::{ + Codegen, Generator, NAPI_PARSER_PACKAGE_PATH, + output::Output, + schema::Schema, + utils::{string, write_it}, +}; + +use super::define_generator; + +define_index_type! { + pub struct NodeId = u32; +} + +impl Display for NodeId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.raw().fmt(f) + } +} + +pub struct ESTreeVisitGenerator; + +define_generator!(ESTreeVisitGenerator); + +impl Generator for ESTreeVisitGenerator { + fn generate_many(&self, _schema: &Schema, codegen: &Codegen) -> Vec { + let visitor_keys = generate(codegen); + + vec![Output::Javascript { + path: format!("{NAPI_PARSER_PACKAGE_PATH}/generated/visit/keys.mjs"), + code: visitor_keys, + }] + } +} + +/// Details of a node's name and visitor keys. +#[derive(Deserialize, Debug)] +struct NodeKeys { + name: String, + keys: Vec, +} + +/// Generate visitor keys. +fn generate(codegen: &Codegen) -> String { + // Run `napi/parser/scripts/visitor-keys.mjs` to get visitor keys from TS-ESLint + let script_path = codegen.root_path().join("napi/parser/scripts/visitor-keys.mjs"); + + let output = Command::new("node") + .arg(script_path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .unwrap(); + assert!(output.status.success() && output.stderr.is_empty()); + let json = String::from_utf8(output.stdout).unwrap(); + let mut nodes: IndexVec = serde_json::from_str(&json).unwrap(); + + // Remove types which do not exist in Oxc AST + // TODO: Why don't they exist? + let remove = [ + "TSAbstractKeyword", + "TSAsyncKeyword", + "TSDeclareKeyword", + "TSExportKeyword", + "TSPrivateKeyword", + "TSProtectedKeyword", + "TSPublicKeyword", + "TSReadonlyKeyword", + "TSStaticKeyword", + "ExperimentalRestProperty", + "ExperimentalSpreadProperty", + ]; + nodes.retain(|node| !remove.contains(&node.name.as_str())); + + // Add types which don't exist in TS-ESTree AST + let extra: &[(&str, &[&str])] = &[ + ("ParenthesizedExpression", &["expression"]), + ("V8IntrinsicExpression", &["name", "arguments"]), + ("TSParenthesizedType", &["typeAnnotation"]), + ("TSJSDocNonNullableType", &["typeAnnotation"]), + ("TSJSDocNullableType", &["typeAnnotation"]), + ("TSJSDocUnknownType", &[]), + ]; + nodes.extend(extra.iter().map(|&(name, keys)| NodeKeys { + name: name.to_string(), + keys: keys.iter().map(|&key| key.to_string()).collect(), + })); + + // Sort by: + // * Leaf nodes before non-leaf nodes. + // * JS first, then JSX, then TS. + // * Alphabetical order. + nodes.sort_by(|v1, v2| match (v1.keys.is_empty(), v2.keys.is_empty()) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => { + let name1 = v1.name.as_str(); + let name2 = v2.name.as_str(); + let is_jsx1 = name1.starts_with("JSX"); + let is_ts1 = name1.starts_with("TS"); + let is_jsx2 = name2.starts_with("JSX"); + let is_ts2 = name2.starts_with("TS"); + + match (is_jsx1 || is_ts1, is_jsx2 || is_ts2) { + (false, true) => Ordering::Less, + (true, false) => Ordering::Greater, + (true, true) if is_ts1 != is_ts2 => is_ts1.cmp(&is_ts2), + _ => name1.cmp(name2), + } + } + }); + + #[rustfmt::skip] + let mut visitor_keys = string!(" + export default { + // Leaf nodes + "); + + let mut leaf_nodes_count = None; + for (node_id, node) in nodes.iter_enumerated() { + let is_leaf = node.keys.is_empty(); + if leaf_nodes_count.is_none() && !is_leaf { + leaf_nodes_count = Some(node_id.raw()); + visitor_keys.push_str("// Non-leaf nodes\n"); + } + + let node_name = node.name.as_str(); + let keys = &node.keys; + write_it!(visitor_keys, "{node_name}: {keys:?},\n"); + } + + visitor_keys.push_str("};"); + + visitor_keys +} diff --git a/tasks/ast_tools/src/generators/mod.rs b/tasks/ast_tools/src/generators/mod.rs index 3ab80fa270879..9165d6e09a2c1 100644 --- a/tasks/ast_tools/src/generators/mod.rs +++ b/tasks/ast_tools/src/generators/mod.rs @@ -7,6 +7,7 @@ use crate::{ mod assert_layouts; mod ast_builder; mod ast_kind; +mod estree_visit; mod formatter; mod get_id; mod raw_transfer; @@ -19,6 +20,7 @@ mod visit; pub use assert_layouts::AssertLayouts; pub use ast_builder::AstBuilderGenerator; pub use ast_kind::AstKindGenerator; +pub use estree_visit::ESTreeVisitGenerator; pub use formatter::{FormatterAstNodesGenerator, FormatterFormatGenerator}; pub use get_id::GetIdGenerator; pub use raw_transfer::RawTransferGenerator; diff --git a/tasks/ast_tools/src/main.rs b/tasks/ast_tools/src/main.rs index f77c98d7be231..54dd4d1efb976 100644 --- a/tasks/ast_tools/src/main.rs +++ b/tasks/ast_tools/src/main.rs @@ -283,6 +283,7 @@ const GENERATORS: &[&(dyn Generator + Sync)] = &[ &generators::VisitGenerator, &generators::ScopesCollectorGenerator, &generators::Utf8ToUtf16ConverterGenerator, + &generators::ESTreeVisitGenerator, &generators::RawTransferGenerator, &generators::RawTransferLazyGenerator, &generators::TypescriptGenerator, diff --git a/tasks/ast_tools/src/utils.rs b/tasks/ast_tools/src/utils.rs index 70db2f3c827f9..10e654c54a06e 100644 --- a/tasks/ast_tools/src/utils.rs +++ b/tasks/ast_tools/src/utils.rs @@ -120,3 +120,13 @@ macro_rules! write_it { }} } pub(crate) use write_it; + +/// Macro to create a `String`. +/// +/// e.g. `string!("hello")` -> "hello".to_string(). +macro_rules! string { + ($s:literal) => { + $s.to_string() + }; +} +pub(crate) use string;