mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(javascript): honor effective symbolic descriptor values (#7222)
Resolve ordered descriptor values at the shared JavaScript fact owner and preserve uncertain intrinsic writes instead of stale exports. Normalize ordinary method values and reuse canonical primitive, prototype, ambient identity and ordering facts across consumers. 384 selected tests, format/static/docs checks, all final CI jobs and reviews pass. Actual Node controls cover 35 bundles; one possibly repeated factory explicitly stays unknown. Shipped Obsidian selected-source complete Evidence matches base/fixed CLI and MCP, with coverage and resource measurements in the PR. Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
This commit is contained in:
1 parent
d54f5b07b4
commit
d4c511ae6e
12 files changed
+1117
-102
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import * as t from "@babel/types";
|
||||
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
|
||||
|
||||
/** Type-only class fields neither initialize own slots nor shadow inherited members. */
|
||||
export const isRuntimeJavaScriptClassProperty = (
|
||||
@@ -160,25 +161,109 @@ export const isNullishJavaScriptSyntax = (node: t.Node): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
export type ObjectDataPropertyValue =
|
||||
| t.ObjectProperty["value"]
|
||||
| t.ObjectMethod;
|
||||
|
||||
/** Preserve original data-value nodes; accessor reads remain unresolved. */
|
||||
export const objectDataPropertyValue = (
|
||||
property: t.ObjectProperty | t.ObjectMethod,
|
||||
): ObjectDataPropertyValue | undefined =>
|
||||
t.isObjectProperty(property)
|
||||
? property.value
|
||||
: property.kind === "method"
|
||||
? property
|
||||
: undefined;
|
||||
|
||||
const PROPERTYLESS_UNARY = new Set(["+", "-", "~", "!", "void", "delete"]);
|
||||
const PROPERTYLESS_BINARY = new Set([
|
||||
"-",
|
||||
"*",
|
||||
"/",
|
||||
"%",
|
||||
"**",
|
||||
"<<",
|
||||
">>",
|
||||
">>>",
|
||||
"|",
|
||||
"^",
|
||||
"&",
|
||||
"==",
|
||||
"!=",
|
||||
"===",
|
||||
"!==",
|
||||
"<",
|
||||
"<=",
|
||||
">",
|
||||
">=",
|
||||
"in",
|
||||
"instanceof",
|
||||
]);
|
||||
|
||||
/** Operator results are primitive even when their value is not statically known. */
|
||||
const primitiveOwnKeys = (
|
||||
node: t.Node,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): "indices" | "none" | null => {
|
||||
if (
|
||||
t.isStringLiteral(node) ||
|
||||
t.isTemplateLiteral(node) ||
|
||||
(t.isUnaryExpression(node) && node.operator === "typeof") ||
|
||||
(t.isBinaryExpression(node) && node.operator === "+")
|
||||
)
|
||||
return "indices";
|
||||
return t.isNumericLiteral(node) ||
|
||||
t.isBooleanLiteral(node) ||
|
||||
t.isBigIntLiteral(node) ||
|
||||
t.isNullLiteral(node) ||
|
||||
(t.isUnaryExpression(node) && PROPERTYLESS_UNARY.has(node.operator)) ||
|
||||
t.isUpdateExpression(node) ||
|
||||
(t.isBinaryExpression(node) && PROPERTYLESS_BINARY.has(node.operator)) ||
|
||||
isNullish?.(node) === true
|
||||
? "none"
|
||||
: null;
|
||||
};
|
||||
|
||||
type StaticObjectProperty =
|
||||
| { readonly status: "missing" | "dynamic" }
|
||||
| { readonly status: "explicit"; readonly property: t.ObjectProperty };
|
||||
| {
|
||||
readonly status: "explicit";
|
||||
readonly property: t.ObjectProperty | t.ObjectMethod;
|
||||
readonly value: ObjectDataPropertyValue;
|
||||
};
|
||||
|
||||
type StaticObjectMember =
|
||||
| { readonly kind: "unknown" }
|
||||
| { readonly kind: "prototype-setter" }
|
||||
| {
|
||||
readonly kind: "prototype-setter";
|
||||
readonly prototype: "null" | "ignored" | "unknown";
|
||||
}
|
||||
| { readonly kind: "primitive-indices"; readonly length: number | null }
|
||||
| {
|
||||
readonly kind: "array-indices";
|
||||
readonly elements: t.ArrayExpression["elements"];
|
||||
readonly firstSpread: number;
|
||||
}
|
||||
| {
|
||||
readonly kind: "property";
|
||||
readonly property: t.ObjectProperty | t.ObjectMethod;
|
||||
readonly key: string | null;
|
||||
};
|
||||
|
||||
const UNKNOWN_STATIC_OBJECT_MEMBER: StaticObjectMember = { kind: "unknown" };
|
||||
|
||||
/** Object-literal prototype setters are never ordinary own-property writes. */
|
||||
export const isObjectPrototypeSetter = (property: t.ObjectProperty): boolean =>
|
||||
export const isObjectPrototypeSetter = (
|
||||
property: t.ObjectProperty | t.ObjectMethod,
|
||||
key: string | null = semanticStaticPropertyKey(
|
||||
property.key,
|
||||
property.computed,
|
||||
),
|
||||
): boolean =>
|
||||
t.isObjectProperty(property) &&
|
||||
!property.computed &&
|
||||
!property.shorthand &&
|
||||
semanticStaticPropertyKey(property.key, false) === "__proto__";
|
||||
key === "__proto__";
|
||||
|
||||
/** Expand only bounded literal spreads, preserving unknown writes in order. */
|
||||
const appendStaticObjectMembers = (
|
||||
@@ -189,13 +274,28 @@ const appendStaticObjectMembers = (
|
||||
): void => {
|
||||
for (const property of object.properties) {
|
||||
if (!t.isSpreadElement(property)) {
|
||||
if (t.isObjectProperty(property) && isObjectPrototypeSetter(property)) {
|
||||
const key = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (
|
||||
t.isObjectProperty(property) &&
|
||||
isObjectPrototypeSetter(property, key)
|
||||
) {
|
||||
// A top-level setter changes lookup inheritance but is never an own
|
||||
// key; one copied through object spread has no effect on the target.
|
||||
if (depth === 0) members.push({ kind: "prototype-setter" });
|
||||
if (depth === 0) {
|
||||
const prototype = unwrapJavaScriptExpression(property.value).node;
|
||||
const primitive = primitiveOwnKeys(prototype, isNullish);
|
||||
members.push({
|
||||
kind: "prototype-setter",
|
||||
prototype: t.isNullLiteral(prototype)
|
||||
? "null"
|
||||
: primitive !== null
|
||||
? "ignored"
|
||||
: "unknown",
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
members.push({ kind: "property", property });
|
||||
members.push({ kind: "property", property, key });
|
||||
continue;
|
||||
}
|
||||
const { node: spread, depth: wrappers } = unwrapJavaScriptExpression(
|
||||
@@ -216,12 +316,44 @@ const appendStaticObjectMembers = (
|
||||
appendStaticObjectMembers(members, spread, nextDepth, isNullish);
|
||||
continue;
|
||||
}
|
||||
// These primitive boxes have no own keys; strings can own index keys.
|
||||
if (t.isArrayExpression(spread)) {
|
||||
let firstSpread = -1;
|
||||
let hasElements = false;
|
||||
for (let index = 0; index < spread.elements.length; index++) {
|
||||
const element = spread.elements[index];
|
||||
if (element == null) continue;
|
||||
hasElements = true;
|
||||
if (t.isSpreadElement(element)) {
|
||||
firstSpread = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Literal arrays own only present indices. A spread can shift later
|
||||
// indices, but never adds named keys to the array under construction.
|
||||
if (hasElements)
|
||||
members.push({
|
||||
kind: "array-indices",
|
||||
elements: spread.elements,
|
||||
firstSpread,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const primitive = primitiveOwnKeys(spread, isNullish);
|
||||
if (primitive === "indices") {
|
||||
const literal = readExactJavaScriptLiteral(spread);
|
||||
const length =
|
||||
literal.found && typeof literal.value === "string"
|
||||
? literal.value.length
|
||||
: null;
|
||||
// Retain indexed writes without expanding strings or synthesizing properties.
|
||||
if (length !== 0) members.push({ kind: "primitive-indices", length });
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
t.isBooleanLiteral(spread) ||
|
||||
t.isNumericLiteral(spread) ||
|
||||
t.isBigIntLiteral(spread) ||
|
||||
isNullish?.(property.argument) === true
|
||||
primitive === "none" ||
|
||||
t.isFunctionExpression(spread) ||
|
||||
t.isArrowFunctionExpression(spread) ||
|
||||
t.isRegExpLiteral(spread)
|
||||
)
|
||||
continue;
|
||||
members.push(UNKNOWN_STATIC_OBJECT_MEMBER);
|
||||
@@ -238,31 +370,77 @@ const staticObjectMembers = (
|
||||
return members;
|
||||
};
|
||||
|
||||
type StaticObjectPropertySource =
|
||||
| readonly StaticObjectMember[]
|
||||
| "missing"
|
||||
| "dynamic";
|
||||
|
||||
const staticObjectPropertySource = (
|
||||
object: t.Node | undefined,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): StaticObjectPropertySource =>
|
||||
object === undefined
|
||||
? "missing"
|
||||
: t.isObjectExpression(object)
|
||||
? staticObjectMembers(object, 0, isNullish)
|
||||
: "dynamic";
|
||||
|
||||
const readStaticObjectProperty = (
|
||||
source: StaticObjectPropertySource,
|
||||
name: string,
|
||||
): StaticObjectProperty => {
|
||||
if (typeof source === "string") return { status: source };
|
||||
let prototypeMaySupply = false;
|
||||
for (let index = source.length - 1; index >= 0; index--) {
|
||||
const member = source[index]!;
|
||||
if (member.kind === "prototype-setter") {
|
||||
prototypeMaySupply ||= member.prototype === "unknown";
|
||||
continue;
|
||||
}
|
||||
if (member.kind === "unknown") return { status: "dynamic" };
|
||||
if (member.kind === "primitive-indices") {
|
||||
const index = semanticArrayIndex(name);
|
||||
if (index !== null && (member.length === null || index < member.length))
|
||||
return { status: "dynamic" };
|
||||
continue;
|
||||
}
|
||||
if (member.kind === "array-indices") {
|
||||
const index = semanticArrayIndex(name);
|
||||
if (
|
||||
index !== null &&
|
||||
((member.firstSpread >= 0 && index >= member.firstSpread) ||
|
||||
(index < member.elements.length && member.elements[index] != null))
|
||||
)
|
||||
return { status: "dynamic" };
|
||||
continue;
|
||||
}
|
||||
const { property, key } = member;
|
||||
if (key === null) return { status: "dynamic" };
|
||||
if (key !== name) continue;
|
||||
const value = objectDataPropertyValue(property);
|
||||
return value === undefined
|
||||
? { status: "dynamic" }
|
||||
: { status: "explicit", property, value };
|
||||
}
|
||||
return prototypeMaySupply ? { status: "dynamic" } : { status: "missing" };
|
||||
};
|
||||
|
||||
/** Parse ordered members once for related property queries within one boundary. */
|
||||
export const staticObjectPropertyReader = (
|
||||
object: t.Node | undefined,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): ((name: string) => StaticObjectProperty) => {
|
||||
const source = staticObjectPropertySource(object, isNullish);
|
||||
return (name) => readStaticObjectProperty(source, name);
|
||||
};
|
||||
|
||||
/** Find the effective explicit property, preserving unresolved overrides. */
|
||||
export const staticObjectProperty = (
|
||||
object: t.Node | undefined,
|
||||
name: string,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): StaticObjectProperty => {
|
||||
if (object === undefined) return { status: "missing" };
|
||||
if (!t.isObjectExpression(object)) return { status: "dynamic" };
|
||||
let prototypeMaySupply = false;
|
||||
for (const member of staticObjectMembers(object, 0, isNullish).toReversed()) {
|
||||
if (member.kind === "prototype-setter") {
|
||||
prototypeMaySupply = true;
|
||||
continue;
|
||||
}
|
||||
if (member.kind === "unknown") return { status: "dynamic" };
|
||||
const { property } = member;
|
||||
const key = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (key === null) return { status: "dynamic" };
|
||||
if (key !== name) continue;
|
||||
return t.isObjectProperty(property)
|
||||
? { status: "explicit", property }
|
||||
: { status: "dynamic" };
|
||||
}
|
||||
return prototypeMaySupply ? { status: "dynamic" } : { status: "missing" };
|
||||
};
|
||||
): StaticObjectProperty =>
|
||||
readStaticObjectProperty(staticObjectPropertySource(object, isNullish), name);
|
||||
|
||||
export type StaticObjectPropertyNames =
|
||||
| { readonly status: "missing" | "dynamic" }
|
||||
@@ -280,14 +458,17 @@ export const staticObjectPropertyNames = (
|
||||
let complete = true;
|
||||
for (const member of staticObjectMembers(object, 0, isNullish)) {
|
||||
if (member.kind === "prototype-setter") continue;
|
||||
if (member.kind === "unknown") {
|
||||
if (
|
||||
member.kind === "unknown" ||
|
||||
member.kind === "primitive-indices" ||
|
||||
member.kind === "array-indices"
|
||||
) {
|
||||
// Indexed spread keys remain unenumerated; never claim a complete name
|
||||
// inventory or expand the input into a potentially much larger key set.
|
||||
complete = false;
|
||||
continue;
|
||||
}
|
||||
const key = semanticStaticPropertyKey(
|
||||
member.property.key,
|
||||
member.property.computed,
|
||||
);
|
||||
const { key } = member;
|
||||
if (key === null) {
|
||||
complete = false;
|
||||
continue;
|
||||
@@ -305,7 +486,7 @@ export const semanticFinalObjectProperty = (
|
||||
key: string,
|
||||
): t.Node | undefined => {
|
||||
const property = staticObjectProperty(node, key);
|
||||
return property.status === "explicit" ? property.property.value : undefined;
|
||||
return property.status === "explicit" ? property.value : undefined;
|
||||
};
|
||||
|
||||
export const semanticFinalObjectStringProperty = (
|
||||
|
||||
Reference in new issue
Block a user