mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 03:15:54 +00:00
fix(javascript): version syntax-aware structural fingerprints
Retain bigint/directive values, assignment operators, fixed public keys and execution-affecting syntax flags in v3. Preserve local/private binding renames and isolate structural matching by algorithm version. Remove the redundant test grouping and document structural matching as heuristic evidence. Validated AST, worker admission, archived-version matching, reconstruction and public CLI comparisons. Auto-accessors are tested at the AST boundary because the production JavaScript parser does not enable their Babel plugin. Refs #3952, #4020. Co-authored-by: ShitK <1724059440@qq.com>
This commit is contained in:
1 parent
c0d550b231
commit
0ef938f4c5
8 files changed
+421
-58
No files matched your search
@@ -1,21 +1,159 @@
|
||||
import { parse } from "@babel/parser";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { fingerprintJavaScriptAst } from "./javascriptAstFingerprint.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
|
||||
describe("JavaScript AST fingerprints", () => {
|
||||
it("includes complete literal values in the fingerprint", () => {
|
||||
const prefix = "x".repeat(1_024);
|
||||
const left = parse(`const value = ${JSON.stringify(`${prefix}left`)};`);
|
||||
const right = parse(`const value = ${JSON.stringify(`${prefix}right`)};`);
|
||||
|
||||
expect(fingerprintJavaScriptAst(left)).not.toBe(
|
||||
fingerprintJavaScriptAst(right),
|
||||
);
|
||||
});
|
||||
it.each([
|
||||
["bigint values", "const value = 1n;", "const value = 2n;"],
|
||||
[
|
||||
"bigint object keys",
|
||||
"const value = { 1n: 0 };",
|
||||
"const value = { 2n: 0 };",
|
||||
],
|
||||
[
|
||||
"empty fixed object keys",
|
||||
'const value = { "": 0 };',
|
||||
'const value = { "x": 0 };',
|
||||
],
|
||||
["directives", '"use strict";', '"use asm";'],
|
||||
["assignment operators", "value = 1;", "value += 1;"],
|
||||
["public class method names", "class A { foo() {} }", "class A { bar() {} }"],
|
||||
["public class field names", "class A { foo = 1; }", "class A { bar = 1; }"],
|
||||
["update expression prefix", "value++", "++value"],
|
||||
["declaration kind", "var value = 1;", "const value = 1;"],
|
||||
[
|
||||
"function async flag",
|
||||
"function read() { return 1; }",
|
||||
"async function read() { return 1; }",
|
||||
],
|
||||
["function generator flag", "function* read() {}", "function read() {}"],
|
||||
["class method kind", "class A { read() {} }", "class A { get read() {} }"],
|
||||
[
|
||||
"class method static flag",
|
||||
"class A { read() {} }",
|
||||
"class A { static read() {} }",
|
||||
],
|
||||
[
|
||||
"class field static flag",
|
||||
"class A { value = 1; }",
|
||||
"class A { static value = 1; }",
|
||||
],
|
||||
[
|
||||
"private method static flag",
|
||||
"class A { #read() {} }",
|
||||
"class A { static #read() {} }",
|
||||
],
|
||||
[
|
||||
"private field static flag",
|
||||
"class A { #value = 1; }",
|
||||
"class A { static #value = 1; }",
|
||||
],
|
||||
[
|
||||
"object method kind",
|
||||
"const value = { read() {} };",
|
||||
"const value = { get read() {} };",
|
||||
],
|
||||
])("distinguishes %s", (_name, left, right) => {
|
||||
expect(fingerprintJavaScriptAst(parse(left))).not.toBe(
|
||||
fingerprintJavaScriptAst(parse(right)),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes the fixed key of a Babel class auto-accessor", () => {
|
||||
const before = parse("class A { accessor foo = 1; }", {
|
||||
plugins: ["decoratorAutoAccessors"],
|
||||
});
|
||||
const after = parse("class A { accessor bar = 1; }", {
|
||||
plugins: ["decoratorAutoAccessors"],
|
||||
});
|
||||
expect(fingerprintJavaScriptAst(before)).not.toBe(
|
||||
fingerprintJavaScriptAst(after),
|
||||
);
|
||||
});
|
||||
|
||||
it("retains public auto-accessor static and computed flags", () => {
|
||||
const parseAutoAccessor = (source: string) =>
|
||||
parse(source, { plugins: ["decoratorAutoAccessors"] });
|
||||
expect(
|
||||
fingerprintJavaScriptAst(
|
||||
parseAutoAccessor("class A { accessor value = 1; }"),
|
||||
),
|
||||
).not.toBe(
|
||||
fingerprintJavaScriptAst(
|
||||
parseAutoAccessor("class A { static accessor value = 1; }"),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
fingerprintJavaScriptAst(
|
||||
parseAutoAccessor("class A { accessor [key] = 1; }"),
|
||||
),
|
||||
).not.toBe(
|
||||
fingerprintJavaScriptAst(
|
||||
parseAutoAccessor("class A { accessor key = 1; }"),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(["() {}", "= 1;"])(
|
||||
"preserves computed class key semantics for %s",
|
||||
(member) => {
|
||||
const fingerprint = (source: string) =>
|
||||
fingerprintJavaScriptAst(parse(source));
|
||||
expect(
|
||||
fingerprint(`const key = selectKey(); class A { [key]${member} }`),
|
||||
).toBe(
|
||||
fingerprint(
|
||||
`const selector = selectKey(); class B { [selector]${member} }`,
|
||||
),
|
||||
);
|
||||
expect(fingerprint(`class A { ["foo"]${member} }`)).not.toBe(
|
||||
fingerprint(`class A { foo${member} }`),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps computed auto-accessor binding renames compatible", () => {
|
||||
const before = parse(
|
||||
"const key = selectKey(); class A { accessor [key] = 1; }",
|
||||
{ plugins: ["decoratorAutoAccessors"] },
|
||||
);
|
||||
const after = parse(
|
||||
"const selector = selectKey(); class B { accessor [selector] = 1; }",
|
||||
{ plugins: ["decoratorAutoAccessors"] },
|
||||
);
|
||||
expect(fingerprintJavaScriptAst(before)).toBe(
|
||||
fingerprintJavaScriptAst(after),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps private class member renames compatible", () => {
|
||||
const fingerprint = (source: string) =>
|
||||
fingerprintJavaScriptAst(parse(source));
|
||||
expect(fingerprint("class A { #read() {} }")).toBe(
|
||||
fingerprint("class A { #renamed() {} }"),
|
||||
);
|
||||
expect(fingerprint("class A { #value = 1; }")).toBe(
|
||||
fingerprint("class A { #renamed = 1; }"),
|
||||
);
|
||||
const fingerprintAutoAccessor = (source: string) =>
|
||||
fingerprintJavaScriptAst(
|
||||
parse(source, { plugins: ["decoratorAutoAccessors"] }),
|
||||
);
|
||||
expect(fingerprintAutoAccessor("class A { accessor #value = 1; }")).toBe(
|
||||
fingerprintAutoAccessor("class A { accessor #renamed = 1; }"),
|
||||
);
|
||||
});
|
||||
|
||||
it("includes complete literal values in the fingerprint", () => {
|
||||
const prefix = "x".repeat(1_024);
|
||||
const left = parse(`const value = ${JSON.stringify(`${prefix}left`)};`);
|
||||
const right = parse(`const value = ${JSON.stringify(`${prefix}right`)};`);
|
||||
|
||||
expect(fingerprintJavaScriptAst(left)).not.toBe(
|
||||
fingerprintJavaScriptAst(right),
|
||||
);
|
||||
});
|
||||
it.each(["obj[key]", "obj?.[key]"])(
|
||||
"ignores a local computed key binding in %s",
|
||||
(expression) => {
|
||||
@@ -44,6 +182,12 @@ it("distinguishes computed reads from fixed named members", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("distinguishes computed object keys from fixed object keys", () => {
|
||||
expect(fingerprintJavaScriptAst(parse("({ key: value })"))).not.toBe(
|
||||
fingerprintJavaScriptAst(parse("({ [key]: value })")),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["obj[`left`]", "obj[`right`]"],
|
||||
["obj[``]", "obj[`right`]"],
|
||||
@@ -67,7 +211,7 @@ it("keeps renamed computed-key bundle modules structurally matchable", () => {
|
||||
.bundler_registrations[0]?.modules[0];
|
||||
expect(before).toBeDefined();
|
||||
expect(after).toBeDefined();
|
||||
expect(before?.structural_fingerprint_algorithm).toBe("babel-ast-v2");
|
||||
expect(before?.structural_fingerprint_algorithm).toBe("babel-ast-v3");
|
||||
expect(before?.source_sha256).not.toBe(after?.source_sha256);
|
||||
expect(before?.structural_fingerprint_sha256).toMatch(/^[a-f0-9]{64}$/u);
|
||||
expect(before?.structural_fingerprint_sha256).toBe(
|
||||
|
||||
Reference in new issue
Block a user