Files
rea/src/domain/javascript/javascriptAstFingerprint.test.ts
T
morluto 0ef938f4c5 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>
2026-10-16 05:43:14 +08:00

221 lines
7.1 KiB
TypeScript

import { parse } from "@babel/parser";
import { expect, it } from "vitest";
import { fingerprintJavaScriptAst } from "./javascriptAstFingerprint.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
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) => {
const left = parse(`function read(obj, key) { return ${expression}; }`);
const renamed = expression
.replace("obj", "value")
.replace("key", "selector");
const right = parse(
`function read(value, selector) { return ${renamed}; }`,
);
expect(fingerprintJavaScriptAst(left)).toBe(
fingerprintJavaScriptAst(right),
);
},
);
it("distinguishes computed reads from fixed named members", () => {
expect(fingerprintJavaScriptAst(parse("obj[key]"))).not.toBe(
fingerprintJavaScriptAst(parse("obj.key")),
);
expect(fingerprintJavaScriptAst(parse("obj.key"))).not.toBe(
fingerprintJavaScriptAst(parse("obj.other")),
);
expect(fingerprintJavaScriptAst(parse('obj["key"]'))).not.toBe(
fingerprintJavaScriptAst(parse('obj["other"]')),
);
});
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`]"],
["obj[true]", "obj[false]"],
["obj[null]", "obj[false]"],
['obj["" ]', 'obj["right"]'],
["obj[`prefix-${key}`]", "obj[`changed-${key}`]"],
])("retains computed literal contents in %s", (left, right) => {
expect(fingerprintJavaScriptAst(parse(left))).not.toBe(
fingerprintJavaScriptAst(parse(right)),
);
});
it("keeps renamed computed-key bundle modules structurally matchable", () => {
const source = (name: string) => `globalThis.webpackChunkApp.push([[1], {
17(module, exports) { const ${name} = selectKey(); exports.read = value[${name}]; }
}]);`;
const before = analyzeJavaScriptStaticSource(source("key"))
.bundler_registrations[0]?.modules[0];
const after = analyzeJavaScriptStaticSource(source("selector"))
.bundler_registrations[0]?.modules[0];
expect(before).toBeDefined();
expect(after).toBeDefined();
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(
after?.structural_fingerprint_sha256,
);
});