From 82cdbc69cb5240b969b72b6bbe8ea2b566c38b16 Mon Sep 17 00:00:00 2001 From: Rintaro Itokawa Date: Mon, 26 Jan 2026 01:01:21 +0900 Subject: [PATCH 01/10] perf(js-plugins): replace ESLint step classes with plain objects This is Part 1 of CFG walker optimization series addressing TODO comments in cfg.ts. Changes: - Remove `@eslint/plugin-kit` dependency (no longer needed) - Replace `VisitNodeStep` and `CallMethodStep` classes with plain objects - Merge `kind` and `phase` into a single `type` property: - 0 = enter visit - 1 = exit visit - 2 = call method (CFG event) This reduces object creation overhead and improves memory efficiency by using simpler data structures. Co-Authored-By: Claude Opus 4.5 --- apps/oxlint/package.json | 1 - apps/oxlint/src-js/plugins/cfg.ts | 156 ++++++++++++++++-------------- pnpm-lock.yaml | 20 ---- 3 files changed, 81 insertions(+), 96 deletions(-) diff --git a/apps/oxlint/package.json b/apps/oxlint/package.json index 0d3df297fdd2f..35e2b941e50b4 100644 --- a/apps/oxlint/package.json +++ b/apps/oxlint/package.json @@ -24,7 +24,6 @@ }, "devDependencies": { "@arethetypeswrong/core": "catalog:", - "@eslint/plugin-kit": "^0.5.0", "@napi-rs/cli": "catalog:", "@types/esquery": "^1.5.4", "@types/estree": "^1.0.8", diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 0a7f0b9a8d468..4469f2b934bfa 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -9,7 +9,6 @@ import CodePathAnalyzer from "../../node_modules/eslint/lib/linter/code-path-ana // @ts-expect-error - internal module of ESLint with no types import Traverser from "../../node_modules/eslint/lib/shared/traverser.js"; -import { VisitNodeStep, CallMethodStep } from "@eslint/plugin-kit"; import visitorKeys from "../generated/keys.ts"; import { LEAF_NODE_TYPES_COUNT, NODE_TYPE_IDS_MAP } from "../generated/type_ids.ts"; import { ancestors } from "../generated/walk.js"; @@ -20,14 +19,27 @@ import type { Node, Program } from "../generated/types.d.ts"; import type { CompiledVisitors } from "../generated/walk.js"; /** - * Step to walk AST. + * Step type constants (merged kind + phase into single type). */ -type Step = VisitNodeStep | CallMethodStep; +const STEP_TYPE_ENTER = 0; +const STEP_TYPE_EXIT = 1; +const STEP_TYPE_CALL = 2; -const STEP_KIND_VISIT = 1; +/** + * Step to walk AST - using plain objects instead of ESLint's class instances. + */ +type VisitStep = { + type: typeof STEP_TYPE_ENTER | typeof STEP_TYPE_EXIT; + target: Node; +}; -const STEP_PHASE_ENTER = 1; -const STEP_PHASE_EXIT = 2; +type CallStep = { + type: typeof STEP_TYPE_CALL; + target: string; + args: unknown[]; +}; + +type Step = VisitStep | CallStep; // Array of steps to walk AST. // Singleton array which is re-used for each walk, and emptied after each walk. @@ -59,8 +71,10 @@ export function resetCfgWalk(): void { * 2. Visit AST with provided visitor. * Run through the steps, in order, calling visit functions for each step. * - * TODO: This is copied from ESLint and is not very efficient. We could improve its performance in many ways. - * See TODO comments in the code below for some ideas for optimization. + * TODO: Further optimizations possible: + * - Reduce object creation by storing steps as 2 arrays (struct of arrays pattern). + * - Avoid repeated conversions from `type` (string) to `typeId` (number) when iterating through steps. + * - Use a faster walker instead of ESLint's Traverser. * * @param ast - AST * @param visitors - Visitors array @@ -75,47 +89,50 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo for (let i = 0; i < stepsLen; i++) { const step = steps[i]; - if (step.kind === STEP_KIND_VISIT) { + const stepType = step.type; + + if (stepType === STEP_TYPE_ENTER) { + // Enter node - can be leaf or non-leaf node const node = step.target as Node; + const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; + const visit = visitors[typeId]; - if (step.phase === STEP_PHASE_ENTER) { - // Enter node - can be leaf or non-leaf node - const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; - const visit = visitors[typeId]; - if (typeId < LEAF_NODE_TYPES_COUNT) { - // Leaf node - if (visit !== null) { - typeAssertIs(visit); - visit(node); - } - // Don't add node to `ancestors`, because we don't visit them on exit - } else { - // Non-leaf node - if (visit !== null) { - typeAssertIs(visit); - const { enter } = visit; - if (enter !== null) enter(node); - } - - ancestors.unshift(node); + if (typeId < LEAF_NODE_TYPES_COUNT) { + // Leaf node + if (visit != null) { + typeAssertIs(visit); + visit(node); } + // Don't add node to `ancestors`, because we don't visit them on exit } else { - // Exit non-leaf node - ancestors.shift(); - - const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; - const enterExit = visitors[typeId]; - if (enterExit !== null) { - typeAssertIs(enterExit); - const { exit } = enterExit; - if (exit !== null) exit(node); + // Non-leaf node + if (visit != null) { + typeAssertIs(visit); + const { enter } = visit; + if (enter != null) enter(node); } + + ancestors.unshift(node); + } + } else if (stepType === STEP_TYPE_EXIT) { + // Exit non-leaf node + const node = step.target as Node; + ancestors.shift(); + + const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; + const enterExit = visitors[typeId]; + if (enterExit != null) { + typeAssertIs(enterExit); + const { exit } = enterExit; + if (exit != null) exit(node); } } else { - const eventId = NODE_TYPE_IDS_MAP.get(step.target)!; + // Call method (CFG event) + const callStep = step as CallStep; + const eventId = NODE_TYPE_IDS_MAP.get(callStep.target)!; const visit = visitors[eventId]; - if (visit !== null) { - (visit as any).apply(undefined, step.args); + if (visit != null) { + (visit as any).apply(undefined, callStep.args); } } } @@ -137,35 +154,25 @@ function prepareSteps(ast: Program) { let stepsLenAfterEnter = 0; // Create `CodePathAnalyzer`. - // It stores steps to walk AST. + // It stores steps to walk AST using plain objects instead of ESLint's class instances. // - // This is really inefficient code. - // We could improve it in several ways (in ascending order of complexity): + // Further optimizations possible (in ascending order of complexity): // - // * Get rid of the bloated `VisitNodeStep` and `CallMethodStep` classes. Just use plain objects. - // * Combine `step.kind` and `step.phase` into a single `step.type` property. // * Reduce object creation by storing steps as 2 arrays (struct of arrays pattern): // * Array 1: Step type (number). // * Array 2: Step data - AST node object for enter/exit node steps, args for CFG events. - // * Alternatively, use a single array containing step objects as now, but recycle the objects - // (SoA option is probably better). // * Avoid repeated conversions from `type` (string) to `typeId` (number) when iterating through steps. - // * Generate separate `enterNode` / `exitNode` functions for each node type. - // * Set them on `analyzer.original` before calling `analyzer.enterNode` / `analyzer.exitNode`. - // * These functions would know the type ID of the node already, and then could store type ID in steps. + // * Store type ID in steps during preparation phase. // * When iterating through steps, use that type ID instead of converting `node.type` to `typeId` every time. - // * Copy `CodePathAnalyzer` code into this repo and rewrite it to work entirely with type IDs instead of strings. + // * Use a faster walker instead of ESLint's Traverser. // - // TODO: Apply these optimizations (or at least some of them). + // TODO: Apply these optimizations. const analyzer = new CodePathAnalyzer({ enterNode(node: Node) { - steps.push( - new VisitNodeStep({ - target: node, - phase: STEP_PHASE_ENTER, - args: [node], - }), - ); + steps.push({ + type: STEP_TYPE_ENTER, + target: node, + }); if (DEBUG) stepsLenAfterEnter = steps.length; }, @@ -175,13 +182,10 @@ function prepareSteps(ast: Program) { if (typeId >= LEAF_NODE_TYPES_COUNT) { // Non-leaf node - steps.push( - new VisitNodeStep({ - target: node, - phase: STEP_PHASE_EXIT, - args: [node], - }), - ); + steps.push({ + type: STEP_TYPE_EXIT, + target: node, + }); } else { // Leaf node. // Don't add a step. @@ -194,7 +198,10 @@ function prepareSteps(ast: Program) { // visit functions are called in would be wrong. // `exit` visit fn would be called before the CFG event handlers, instead of after. if (DEBUG && steps.length !== stepsLenAfterEnter) { - const eventNames = steps.slice(stepsLenAfterEnter).map((step) => step.target) as string[]; + const eventNames = steps + .slice(stepsLenAfterEnter) + .filter((step): step is CallStep => step.type === STEP_TYPE_CALL) + .map((step) => step.target); throw new Error( `CFG events emitted during visiting leaf node \`${node.type}\`: ${eventNames.join(", ")}`, ); @@ -202,13 +209,12 @@ function prepareSteps(ast: Program) { } }, - emit(eventName: string, args: any[]) { - steps.push( - new CallMethodStep({ - target: eventName, - args, - }), - ); + emit(eventName: string, args: unknown[]) { + steps.push({ + type: STEP_TYPE_CALL, + target: eventName, + args, + }); }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42d3bca23e4d7..4df0825684929 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -107,9 +107,6 @@ importers: '@arethetypeswrong/core': specifier: 'catalog:' version: 0.18.2 - '@eslint/plugin-kit': - specifier: ^0.5.0 - version: 0.5.0 '@napi-rs/cli': specifier: 'catalog:' version: 3.5.1(@emnapi/runtime@1.7.1)(@types/node@24.1.0) @@ -1044,10 +1041,6 @@ packages: resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@1.0.0': - resolution: {integrity: sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/eslintrc@3.3.3': resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1064,10 +1057,6 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.5.0': - resolution: {integrity: sha512-rSXBsAcmx80jI9OUevyNBU0f5pZRQJkNmk4bLX6hCbm1qKe5Z/TcU7vwXc2nR8814mhRlgbZIHL1+HSiYS0VkQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -6269,10 +6258,6 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@1.0.0': - dependencies: - '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 @@ -6296,11 +6281,6 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 - '@eslint/plugin-kit@0.5.0': - dependencies: - '@eslint/core': 1.0.0 - levn: 0.4.1 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': From 7e96746791184901b0d9cfd15de9ba177be4dc95 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:00:02 +0000 Subject: [PATCH 02/10] use strict `!== null` checks --- apps/oxlint/src-js/plugins/cfg.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 4469f2b934bfa..5c1a878c58ea5 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -99,17 +99,17 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo if (typeId < LEAF_NODE_TYPES_COUNT) { // Leaf node - if (visit != null) { + if (visit !== null) { typeAssertIs(visit); visit(node); } // Don't add node to `ancestors`, because we don't visit them on exit } else { // Non-leaf node - if (visit != null) { + if (visit !== null) { typeAssertIs(visit); const { enter } = visit; - if (enter != null) enter(node); + if (enter !== null) enter(node); } ancestors.unshift(node); @@ -121,17 +121,17 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; const enterExit = visitors[typeId]; - if (enterExit != null) { + if (enterExit !== null) { typeAssertIs(enterExit); const { exit } = enterExit; - if (exit != null) exit(node); + if (exit !== null) exit(node); } } else { // Call method (CFG event) const callStep = step as CallStep; const eventId = NODE_TYPE_IDS_MAP.get(callStep.target)!; const visit = visitors[eventId]; - if (visit != null) { + if (visit !== null) { (visit as any).apply(undefined, callStep.args); } } From 2b11ba3fb16ac2cfccd2c5599eb039a2f480f85c Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:02:12 +0000 Subject: [PATCH 03/10] rename `CallStep::target` to `eventName` --- apps/oxlint/src-js/plugins/cfg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 5c1a878c58ea5..6f35937ca6452 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -35,7 +35,7 @@ type VisitStep = { type CallStep = { type: typeof STEP_TYPE_CALL; - target: string; + eventName: string; args: unknown[]; }; @@ -129,7 +129,7 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo } else { // Call method (CFG event) const callStep = step as CallStep; - const eventId = NODE_TYPE_IDS_MAP.get(callStep.target)!; + const eventId = NODE_TYPE_IDS_MAP.get(callStep.eventName)!; const visit = visitors[eventId]; if (visit !== null) { (visit as any).apply(undefined, callStep.args); @@ -201,7 +201,7 @@ function prepareSteps(ast: Program) { const eventNames = steps .slice(stepsLenAfterEnter) .filter((step): step is CallStep => step.type === STEP_TYPE_CALL) - .map((step) => step.target); + .map((step) => step.eventName); throw new Error( `CFG events emitted during visiting leaf node \`${node.type}\`: ${eventNames.join(", ")}`, ); @@ -212,7 +212,7 @@ function prepareSteps(ast: Program) { emit(eventName: string, args: unknown[]) { steps.push({ type: STEP_TYPE_CALL, - target: eventName, + eventName, args, }); }, From 17c1d931e691ad3629a8aa88e3f788f85c042ad7 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:06:32 +0000 Subject: [PATCH 04/10] fix debugging events emitted for leaf nodes --- apps/oxlint/src-js/plugins/cfg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 6f35937ca6452..7824b905482fd 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -198,10 +198,10 @@ function prepareSteps(ast: Program) { // visit functions are called in would be wrong. // `exit` visit fn would be called before the CFG event handlers, instead of after. if (DEBUG && steps.length !== stepsLenAfterEnter) { - const eventNames = steps - .slice(stepsLenAfterEnter) - .filter((step): step is CallStep => step.type === STEP_TYPE_CALL) - .map((step) => step.eventName); + const eventNames = steps.slice(stepsLenAfterEnter).map((step) => { + if (step.type === STEP_TYPE_CALL) return step.eventName; + return `${step.type === STEP_TYPE_ENTER ? "enter" : "exit"} ${node.type}`; + }); throw new Error( `CFG events emitted during visiting leaf node \`${node.type}\`: ${eventNames.join(", ")}`, ); From 76e786dc10e916b021631baece19cd7126785245 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:20:40 +0000 Subject: [PATCH 05/10] replace `type` with `interface` --- apps/oxlint/src-js/plugins/cfg.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 7824b905482fd..f48e3a0558d0a 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -28,16 +28,16 @@ const STEP_TYPE_CALL = 2; /** * Step to walk AST - using plain objects instead of ESLint's class instances. */ -type VisitStep = { +interface VisitStep { type: typeof STEP_TYPE_ENTER | typeof STEP_TYPE_EXIT; target: Node; -}; +} -type CallStep = { +interface CallStep { type: typeof STEP_TYPE_CALL; eventName: string; args: unknown[]; -}; +} type Step = VisitStep | CallStep; From e8ef16e4a08932d6fcb2798caea6b233c4e8fc58 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:13:45 +0000 Subject: [PATCH 06/10] remove unnecessary type assertions --- apps/oxlint/src-js/plugins/cfg.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index f48e3a0558d0a..29e75001b14f5 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -93,7 +93,7 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo if (stepType === STEP_TYPE_ENTER) { // Enter node - can be leaf or non-leaf node - const node = step.target as Node; + const node = step.target; const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; const visit = visitors[typeId]; @@ -116,7 +116,7 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo } } else if (stepType === STEP_TYPE_EXIT) { // Exit non-leaf node - const node = step.target as Node; + const node = step.target; ancestors.shift(); const typeId = NODE_TYPE_IDS_MAP.get(node.type)!; From c2a3da01a139f19f0e2e898391e43aef16e5197b Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:14:21 +0000 Subject: [PATCH 07/10] separate step types Helps TS infer `step.type` in if/else below, so can remove an `as` type assertion. --- apps/oxlint/src-js/plugins/cfg.ts | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 29e75001b14f5..c8ff89b6a2b01 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -26,21 +26,35 @@ const STEP_TYPE_EXIT = 1; const STEP_TYPE_CALL = 2; /** - * Step to walk AST - using plain objects instead of ESLint's class instances. + * Step to walk AST. */ -interface VisitStep { - type: typeof STEP_TYPE_ENTER | typeof STEP_TYPE_EXIT; +type Step = EnterStep | ExitStep | CallStep; + +/** + * Step for entering AST node. + */ +interface EnterStep { + type: typeof STEP_TYPE_ENTER; target: Node; } +/** + * Step for exiting AST node. + */ +interface ExitStep { + type: typeof STEP_TYPE_EXIT; + target: Node; +} + +/** + * Step for calling a CFG event handler. + */ interface CallStep { type: typeof STEP_TYPE_CALL; eventName: string; args: unknown[]; } -type Step = VisitStep | CallStep; - // Array of steps to walk AST. // Singleton array which is re-used for each walk, and emptied after each walk. const steps: Step[] = []; @@ -128,7 +142,7 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo } } else { // Call method (CFG event) - const callStep = step as CallStep; + const callStep = step; const eventId = NODE_TYPE_IDS_MAP.get(callStep.eventName)!; const visit = visitors[eventId]; if (visit !== null) { From bec88384c76e198ca594cdff93c9a29156f66663 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:27:56 +0000 Subject: [PATCH 08/10] remove temp var --- apps/oxlint/src-js/plugins/cfg.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index c8ff89b6a2b01..c4fb7215c95f5 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -142,11 +142,10 @@ export function walkProgramWithCfg(ast: Program, visitors: CompiledVisitors): vo } } else { // Call method (CFG event) - const callStep = step; - const eventId = NODE_TYPE_IDS_MAP.get(callStep.eventName)!; + const eventId = NODE_TYPE_IDS_MAP.get(step.eventName)!; const visit = visitors[eventId]; if (visit !== null) { - (visit as any).apply(undefined, callStep.args); + (visit as any).apply(undefined, step.args); } } } From bcd6f85d393a075b8effcd33313a64103c11bd9a Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:47:01 +0000 Subject: [PATCH 09/10] revert changes to comments --- apps/oxlint/src-js/plugins/cfg.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index c4fb7215c95f5..5f447474637a3 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -85,10 +85,9 @@ export function resetCfgWalk(): void { * 2. Visit AST with provided visitor. * Run through the steps, in order, calling visit functions for each step. * - * TODO: Further optimizations possible: - * - Reduce object creation by storing steps as 2 arrays (struct of arrays pattern). - * - Avoid repeated conversions from `type` (string) to `typeId` (number) when iterating through steps. - * - Use a faster walker instead of ESLint's Traverser. + * TODO: This is was originally copied from ESLint, and has been adapted for better performance. + * But we could further improve its performance in many ways. + * See TODO comments in the code below for some ideas for optimization. * * @param ast - AST * @param visitors - Visitors array @@ -167,19 +166,23 @@ function prepareSteps(ast: Program) { let stepsLenAfterEnter = 0; // Create `CodePathAnalyzer`. - // It stores steps to walk AST using plain objects instead of ESLint's class instances. + // It stores steps to walk AST. // - // Further optimizations possible (in ascending order of complexity): + // We could improve performance in several ways (in ascending order of complexity): // // * Reduce object creation by storing steps as 2 arrays (struct of arrays pattern): // * Array 1: Step type (number). // * Array 2: Step data - AST node object for enter/exit node steps, args for CFG events. + // * Alternatively, use a single array containing step objects as now, but recycle the objects + // (SoA option is probably better). // * Avoid repeated conversions from `type` (string) to `typeId` (number) when iterating through steps. - // * Store type ID in steps during preparation phase. + // * Generate separate `enterNode` / `exitNode` functions for each node type. + // * Set them on `analyzer.original` before calling `analyzer.enterNode` / `analyzer.exitNode`. + // * These functions would know the type ID of the node already, and then could store type ID in steps. // * When iterating through steps, use that type ID instead of converting `node.type` to `typeId` every time. - // * Use a faster walker instead of ESLint's Traverser. + // * Copy `CodePathAnalyzer` code into this repo and rewrite it to work entirely with type IDs instead of strings. // - // TODO: Apply these optimizations. + // TODO: Apply these optimizations (or at least some of them). const analyzer = new CodePathAnalyzer({ enterNode(node: Node) { steps.push({ From bfb1277842fbe04e12af05189f711fed14a35a17 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Mon, 26 Jan 2026 12:47:09 +0000 Subject: [PATCH 10/10] update comment --- apps/oxlint/src-js/plugins/cfg.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/oxlint/src-js/plugins/cfg.ts b/apps/oxlint/src-js/plugins/cfg.ts index 5f447474637a3..664926fb5a58a 100644 --- a/apps/oxlint/src-js/plugins/cfg.ts +++ b/apps/oxlint/src-js/plugins/cfg.ts @@ -18,9 +18,8 @@ import type { EnterExit, VisitFn } from "./visitor.ts"; import type { Node, Program } from "../generated/types.d.ts"; import type { CompiledVisitors } from "../generated/walk.js"; -/** - * Step type constants (merged kind + phase into single type). - */ +// Step type constants. +// Equivalent to an enum, but minifies better. const STEP_TYPE_ENTER = 0; const STEP_TYPE_EXIT = 1; const STEP_TYPE_CALL = 2;