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:
morlutoandShitK committed 2026-10-16 05:43:14 +08:00
1 parent c0d550b231
commit 0ef938f4c5
8 files changed
+421 -58

No files matched your search

@@ -6,7 +6,6 @@ import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
propertyName,
runtimeFunctionParameters,
semanticStaticPropertyKey,
} from "./javascriptAstValues.js";
@@ -78,6 +77,8 @@ export const collectJavaScriptExports = (
const semanticTokens = (node: t.Node): string[] => {
const tokens = [`node:${node.type}`];
if (t.isStringLiteral(node)) tokens.push(`string:${node.value}`);
else if (t.isBigIntLiteral(node)) tokens.push(`bigint:${String(node.value)}`);
else if (t.isDirectiveLiteral(node)) tokens.push(`directive:${node.value}`);
else if (t.isNumericLiteral(node))
tokens.push(`number:${String(node.value)}`);
else if (t.isBooleanLiteral(node))
@@ -86,21 +87,51 @@ const semanticTokens = (node: t.Node): string[] => {
tokens.push(`regexp:${node.pattern}/${node.flags}`);
else if (t.isTemplateElement(node))
tokens.push(`template:${JSON.stringify(node.value)}`);
else if (t.isBinaryExpression(node) || t.isLogicalExpression(node))
tokens.push(`operator:${node.operator}`);
else if (t.isUnaryExpression(node) || t.isUpdateExpression(node))
else if (
t.isBinaryExpression(node) ||
t.isLogicalExpression(node) ||
t.isUnaryExpression(node) ||
t.isUpdateExpression(node) ||
t.isAssignmentExpression(node)
)
tokens.push(`operator:${node.operator}`);
if (t.isUpdateExpression(node)) tokens.push(`prefix:${String(node.prefix)}`);
if (t.isVariableDeclaration(node)) tokens.push(`kind:${node.kind}`);
if (t.isFunction(node)) {
tokens.push(`async:${String(node.async)}`);
tokens.push(`generator:${String(node.generator)}`);
}
if (t.isObjectMethod(node)) tokens.push(`kind:${node.kind}`);
if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
tokens.push(`kind:${node.kind}`);
tokens.push(`static:${String(node.static)}`);
}
if (
t.isClassProperty(node) ||
t.isClassPrivateProperty(node) ||
t.isClassAccessorProperty(node)
)
tokens.push(`static:${String(node.static)}`);
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
// A computed identifier names a local binding, not a fixed member.
tokens.push(`computed:${String(node.computed)}`);
if (!node.computed) {
const property = propertyName(node.property);
if (property !== "") tokens.push(`property:${property}`);
const property = semanticStaticPropertyKey(node.property, false);
if (property !== null) tokens.push(`property:${property}`);
}
}
if ((t.isObjectProperty(node) || t.isObjectMethod(node)) && !node.computed) {
const key = propertyName(node.key);
if (key !== "") tokens.push(`key:${key}`);
if (
t.isObjectProperty(node) ||
t.isObjectMethod(node) ||
t.isClassMethod(node) ||
t.isClassProperty(node) ||
t.isClassAccessorProperty(node)
) {
tokens.push(`computed:${String(node.computed)}`);
if (!node.computed) {
const key = semanticStaticPropertyKey(node.key, false);
if (key !== null) tokens.push(`key:${key}`);
}
}
return tokens;
};