Skip to content

Commit 6b0901a

Browse files
authored
Merge pull request #4359 from easyops-cn/steve/v3-refine-traverse
Steve/v3-refine-traverse
2 parents ba05ead + e3693c2 commit 6b0901a

File tree

12 files changed

+104
-10
lines changed

12 files changed

+104
-10
lines changed

packages/cook/src/cook.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,10 @@ export function cook(
301301
`Unsupported unicode flag in regular expression: ${node.raw}`
302302
);
303303
}
304+
// Always create a new RegExp, because the AST will be reused.
305+
return NormalCompletion(
306+
new RegExp(node.regex.pattern, node.regex.flags)
307+
);
304308
}
305309
return NormalCompletion(node.value);
306310
}

packages/cook/src/interfaces.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export interface EstreeLiteral {
5050
value: unknown;
5151
raw: string;
5252
regex?: {
53+
pattern: string;
5354
flags: string;
5455
};
5556
loc: SourceLocation;

packages/cook/src/precookFunction.spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { describe, it, jest, expect } from "@jest/globals";
2-
import { precookFunction } from "./precookFunction.js";
2+
import { clearFunctionASTCache, precookFunction } from "./precookFunction.js";
3+
import { cook } from "./cook.js";
34

45
const consoleWarn = jest
56
.spyOn(console, "warn")
67
.mockImplementation(() => void 0);
78

89
describe("precookFunction", () => {
10+
beforeEach(() => {
11+
clearFunctionASTCache();
12+
});
13+
914
it.each<[string, string, string[]]>([
1015
[
1116
"lexical variables in block statement",
@@ -206,7 +211,7 @@ describe("precookFunction", () => {
206211
`,
207212
["c", "b", "d"],
208213
],
209-
])("%s", (desc, source, result) => {
214+
])("%s", (_desc, source, result) => {
210215
expect(Array.from(precookFunction(source).attemptToVisitGlobals)).toEqual(
211216
result
212217
);
@@ -263,6 +268,26 @@ describe("precookFunction", () => {
263268
}
264269
);
265270

271+
it("should isolate regexp", () => {
272+
const source = `function test() {
273+
const r = /\\w/g;
274+
r.exec("abc");
275+
return r.lastIndex;
276+
}`;
277+
const fn = {
278+
name: "test",
279+
source,
280+
};
281+
const attempt1 = precookFunction(source, { cacheKey: fn });
282+
const fn1 = cook(attempt1.function, source) as Function;
283+
expect(fn1()).toBe(1);
284+
285+
const attempt2 = precookFunction(source, { cacheKey: fn });
286+
const fn2 = cook(attempt2.function, source) as Function;
287+
// The second RegExp is using the cached one
288+
expect(fn2()).toBe(1);
289+
});
290+
266291
it("should warn unsupported type", () => {
267292
const { attemptToVisitGlobals } = precookFunction(
268293
"function test() { this }"
Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
import { FunctionDeclaration } from "@babel/types";
1+
import type { FunctionDeclaration } from "@babel/types";
22
import { parseAsEstree } from "./parse.js";
33
import { precook, PrecookOptions } from "./precook.js";
44

5+
let ASTCache = new WeakMap<object, FunctionDeclaration>();
6+
57
export interface PrecookFunctionOptions extends PrecookOptions {
8+
cacheKey?: object;
69
typescript?: boolean;
710
}
811

@@ -13,12 +16,22 @@ export interface PrecookFunctionResult {
1316

1417
export function precookFunction(
1518
source: string,
16-
{ typescript, ...restOptions }: PrecookFunctionOptions = {}
19+
{ typescript, cacheKey, ...restOptions }: PrecookFunctionOptions = {}
1720
): PrecookFunctionResult {
18-
const func = parseAsEstree(source, { typescript });
21+
let func = cacheKey ? ASTCache.get(cacheKey) : undefined;
22+
if (!func) {
23+
func = parseAsEstree(source, { typescript });
24+
if (cacheKey) {
25+
ASTCache.set(cacheKey, func);
26+
}
27+
}
1928
const attemptToVisitGlobals = precook(func, restOptions);
2029
return {
2130
function: func,
2231
attemptToVisitGlobals,
2332
};
2433
}
34+
35+
export function clearFunctionASTCache(): void {
36+
ASTCache = new WeakMap();
37+
}

packages/cook/src/preevaluate.spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { cook } from "./cook.js";
12
import {
23
isEvaluable,
34
preevaluate,
45
shouldAllowRecursiveEvaluations,
56
isTrackAll,
7+
clearExpressionASTCache,
68
} from "./preevaluate.js";
79

810
describe("isEvaluable", () => {
@@ -28,6 +30,10 @@ describe("isEvaluable", () => {
2830
});
2931

3032
describe("preevaluate", () => {
33+
beforeEach(() => {
34+
clearExpressionASTCache();
35+
});
36+
3137
it("should work", () => {
3238
const { attemptToVisitGlobals, ...restResult } = preevaluate(
3339
"<% DATA, EVENT.detail %>"
@@ -52,6 +58,22 @@ describe("preevaluate", () => {
5258
});
5359
});
5460

61+
it("should isolate regexp", () => {
62+
const precooked1 = preevaluate(
63+
"<% ((r) => (r.exec('abc'), r.lastIndex))(/\\w/g) %>",
64+
{ cache: true }
65+
);
66+
const result1 = cook(precooked1.expression, precooked1.source);
67+
expect(result1).toBe(1);
68+
69+
const precooked2 = preevaluate(
70+
"<%= ((r) => (r.exec('abc'), r.lastIndex))(/\\w/g) %>",
71+
{ cache: true }
72+
);
73+
const result2 = cook(precooked2.expression, precooked2.source);
74+
expect(result2).toBe(1);
75+
});
76+
5577
it("should throw SyntaxError", () => {
5678
expect(() => {
5779
preevaluate("<% DATA : EVENT %>");

packages/cook/src/preevaluate.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
import { Expression } from "@babel/types";
1+
import type { Expression } from "@babel/types";
22
import { parseAsEstreeExpression } from "./parse.js";
33
import { precook, PrecookOptions } from "./precook.js";
44

5-
export type PreevaluateOptions = Omit<PrecookOptions, "expressionOnly">;
5+
const ASTCache = new Map<string, Expression>();
6+
7+
export type PreevaluateOptions = Omit<PrecookOptions, "expressionOnly"> & {
8+
cache?: boolean;
9+
};
610

711
export interface PreevaluateResult {
812
expression: Expression;
@@ -15,16 +19,22 @@ export interface PreevaluateResult {
1519
// `raw` should always be asserted by `isEvaluable`.
1620
export function preevaluate(
1721
raw: string,
18-
options?: PreevaluateOptions
22+
{ cache, ...restOptions }: PreevaluateOptions = {}
1923
): PreevaluateResult {
2024
const fixes: string[] = [];
2125
const source = raw.replace(/^\s*<%[~=]?\s|\s%>\s*$/g, (m) => {
2226
fixes.push(m);
2327
return "";
2428
});
25-
const expression = parseAsEstreeExpression(source);
29+
let expression = cache ? ASTCache.get(source) : undefined;
30+
if (!expression) {
31+
expression = parseAsEstreeExpression(source);
32+
if (cache) {
33+
ASTCache.set(source, expression);
34+
}
35+
}
2636
const attemptToVisitGlobals = precook(expression, {
27-
...options,
37+
...restOptions,
2838
expressionOnly: true,
2939
});
3040
return {
@@ -47,3 +57,7 @@ export function shouldAllowRecursiveEvaluations(raw: string): boolean {
4757
export function isTrackAll(raw: string): boolean {
4858
return /^\s*<%=\s/.test(raw) && /\s%>\s*$/.test(raw);
4959
}
60+
61+
export function clearExpressionASTCache(): void {
62+
ASTCache.clear();
63+
}

packages/runtime/src/StoryboardFunctionRegistry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export function StoryboardFunctionRegistryFactory({
148148
hooks: collector && {
149149
beforeVisit: collector.beforeVisit,
150150
},
151+
cacheKey: fn,
151152
});
152153
const globalVariables = supply(
153154
precooked.attemptToVisitGlobals,

packages/runtime/src/internal/Router.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import type {
66
Storyboard,
77
} from "@next-core/types";
88
import { HttpAbortError } from "@next-core/http";
9+
import {
10+
clearExpressionASTCache,
11+
clearFunctionASTCache,
12+
} from "@next-core/cook";
913
import { uniqueId } from "lodash";
1014
import { NextHistoryState, NextLocation, getHistory } from "../history.js";
1115
import {
@@ -290,6 +294,11 @@ export class Router {
290294
? previousApp.id !== currentApp.id
291295
: previousApp !== currentApp;
292296

297+
clearExpressionASTCache();
298+
if (appChanged) {
299+
clearFunctionASTCache();
300+
}
301+
293302
// TODO: handle favicon
294303

295304
// Set `Router::#currentApp` before calling `getFeatureFlags()`

packages/runtime/src/internal/compute/evaluate.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ function lowLevelEvaluate(
151151
let precooked: PreevaluateResult;
152152
try {
153153
precooked = preevaluate(raw, {
154+
cache: true,
154155
withParent: true,
155156
hooks: {
156157
beforeVisitGlobal(node, parent) {

packages/utils/src/storyboard/expressions/track.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function track(
2323
hasNonStaticUsage: false,
2424
};
2525
const { expression } = preevaluate(raw, {
26+
cache: true,
2627
withParent: true,
2728
hooks: {
2829
beforeVisitGlobal: beforeVisitGlobalMember(usage, variableName),
@@ -58,6 +59,7 @@ export function trackAll(raw: string): trackAllResult | false {
5859
hasNonStaticUsage: false,
5960
};
6061
preevaluate(raw, {
62+
cache: true,
6163
withParent: true,
6264
hooks: {
6365
beforeVisitGlobal: beforeVisitGlobalMember(usage, TRACK_NAMES, 1, true),

0 commit comments

Comments
 (0)