mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
fix(javascript): preserve exact native export facts (#10059)
fix(javascript): preserve exact native export names and unknown identity
This commit is contained in:
1 parent
4073cfc932
commit
ba4fb6a806
5 files changed
+157
-42
No files matched your search
@@ -255,7 +255,11 @@ const buildNativeRuntimeNodes = (): FixtureNode[] => [
|
||||
key: "openProject",
|
||||
},
|
||||
"openProject",
|
||||
{ calling_convention: "napi" },
|
||||
{
|
||||
calling_convention: "napi",
|
||||
specifier: "native/synthetic.node",
|
||||
members: ["openProject"],
|
||||
},
|
||||
artifactEvidence(
|
||||
APPLICATION_GRAPH_DIGESTS.nativeAddon,
|
||||
"native/synthetic.node",
|
||||
|
||||
@@ -144,7 +144,7 @@ const nativeHandoffSchema = z.strictObject({
|
||||
artifact_sha256: digestSchema,
|
||||
artifact_path: z.string().min(1).nullable(),
|
||||
export_node_ids: z.array(nodeIdSchema),
|
||||
requested_exports: z.array(boundedTextSchema),
|
||||
requested_exports: z.array(z.string()),
|
||||
status: z.enum(["evidence-linked", "requires-provider-analysis"]),
|
||||
providers: z.array(providerSchema),
|
||||
evidence_ids: z.array(evidenceIdSchema),
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createJavaScriptApplicationNode } from "./javascriptApplicationGraph.js";
|
||||
import { artifactEvidence } from "./javascriptApplicationGraph.fixture.js";
|
||||
import {
|
||||
createJavaScriptApplicationEdge,
|
||||
createJavaScriptApplicationGraph,
|
||||
createJavaScriptApplicationNode,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import {
|
||||
artifactEvidence,
|
||||
completeCoverage,
|
||||
} from "./javascriptApplicationGraph.fixture.js";
|
||||
import { analyzeCommonJsStaticFixture } from "../../../tests/fixtures/javascriptCommonJsAnalysis.js";
|
||||
import { traceApplicationFeature } from "./javascriptFeatureTrace.js";
|
||||
import type { ApplicationFeatureSeed } from "./javascriptFeatureTraceSchemas.js";
|
||||
import {
|
||||
nativeExportCorrespondenceKey,
|
||||
nativeExportObservationFacts,
|
||||
@@ -28,8 +38,8 @@ const node = (
|
||||
})),
|
||||
});
|
||||
|
||||
it("preserves legacy labels and members while distinguishing omitted and empty requests", () => {
|
||||
const legacy = node([
|
||||
it("preserves structured members while distinguishing omitted and empty requests", () => {
|
||||
const membersOnly = node([
|
||||
{
|
||||
label: "open",
|
||||
properties: { specifier: "./addon.node", members: ["open"] },
|
||||
@@ -41,17 +51,17 @@ it("preserves legacy labels and members while distinguishing omitted and empty r
|
||||
properties: { specifier: "./addon.node", requested_members: [] },
|
||||
},
|
||||
]);
|
||||
expect(nativeExportObservationFacts(legacy.observations[0]!)).toMatchObject({
|
||||
expect(
|
||||
nativeExportObservationFacts(membersOnly.observations[0]!),
|
||||
).toMatchObject({
|
||||
requestedPresent: false,
|
||||
members: ["open"],
|
||||
label: "open",
|
||||
});
|
||||
expect(nativeExportObservationFacts(empty.observations[0]!)).toMatchObject({
|
||||
requestedPresent: true,
|
||||
requested: [],
|
||||
label: null,
|
||||
});
|
||||
expect(nativeExportCorrespondenceKey(legacy)).not.toEqual(
|
||||
expect(nativeExportCorrespondenceKey(membersOnly)).not.toEqual(
|
||||
nativeExportCorrespondenceKey(empty),
|
||||
);
|
||||
});
|
||||
@@ -67,24 +77,29 @@ it("considers later conflicting observations and retains malformed values withou
|
||||
};
|
||||
expect(nativeExportCorrespondenceKey(node([first]))).not.toBeNull();
|
||||
expect(nativeExportCorrespondenceKey(node([first, second]))).toBeNull();
|
||||
const malformed = node([
|
||||
{
|
||||
label: "legacy",
|
||||
properties: {
|
||||
specifier: "./addon.node",
|
||||
requested_members: ["open", 42],
|
||||
for (const requested of [["open", 42], null, "invalid", {}]) {
|
||||
const malformed = node([
|
||||
{
|
||||
label: "display summary",
|
||||
properties: {
|
||||
specifier: "./addon.node",
|
||||
requested_members: requested,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
const before = JSON.stringify(malformed);
|
||||
expect(
|
||||
nativeExportObservationFacts(malformed.observations[0]!),
|
||||
).toMatchObject({ valid: false, requested: ["open"], label: null });
|
||||
expect(nativeExportCorrespondenceKey(malformed)).toBeNull();
|
||||
expect(JSON.stringify(malformed)).toBe(before);
|
||||
]);
|
||||
const before = JSON.stringify(malformed);
|
||||
expect(
|
||||
nativeExportObservationFacts(malformed.observations[0]!),
|
||||
).toMatchObject({
|
||||
valid: false,
|
||||
requested: Array.isArray(requested) ? ["open"] : [],
|
||||
});
|
||||
expect(nativeExportCorrespondenceKey(malformed)).toBeNull();
|
||||
expect(JSON.stringify(malformed)).toBe(before);
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves label-only fallback without treating display labels as structured members", () => {
|
||||
it("leaves missing structured identity unknown regardless of display labels", () => {
|
||||
for (const requested_members of [["open"], ["close"], []])
|
||||
expect(
|
||||
nativeExportCorrespondenceKey(
|
||||
@@ -93,7 +108,7 @@ it("preserves label-only fallback without treating display labels as structured
|
||||
).toBeNull();
|
||||
expect(
|
||||
nativeExportCorrespondenceKey(node([{ label: "open", properties: {} }])),
|
||||
).not.toBeNull();
|
||||
).toBeNull();
|
||||
const first = node([
|
||||
{
|
||||
label: "summary one",
|
||||
@@ -110,3 +125,112 @@ it("preserves label-only fallback without treating display labels as structured
|
||||
nativeExportCorrespondenceKey(second),
|
||||
);
|
||||
});
|
||||
|
||||
const traceRequestedExport = (
|
||||
requested: ReturnType<typeof node>,
|
||||
seed: ApplicationFeatureSeed,
|
||||
) => {
|
||||
const addon = createJavaScriptApplicationNode({
|
||||
kind: "native-addon",
|
||||
identity: {
|
||||
strategy: "content-digest",
|
||||
stability: "global-exact",
|
||||
sha256: "b".repeat(64),
|
||||
},
|
||||
observations: [
|
||||
{
|
||||
label: "addon.node",
|
||||
properties: {},
|
||||
evidence: artifactEvidence("b".repeat(64), "addon.node"),
|
||||
},
|
||||
],
|
||||
});
|
||||
return traceApplicationFeature({
|
||||
sourceEvidenceId: `ev_${"c".repeat(64)}`,
|
||||
graph: createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
nodes: [addon, requested],
|
||||
root_node_ids: [addon.node_id],
|
||||
edges: [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: addon.node_id,
|
||||
target_node_id: requested.node_id,
|
||||
relation: "exposes",
|
||||
properties: {},
|
||||
evidence: artifactEvidence("b".repeat(64), "addon.node"),
|
||||
}),
|
||||
],
|
||||
coverage: completeCoverage,
|
||||
limitations: [],
|
||||
}),
|
||||
nativeEvidence: [],
|
||||
seed,
|
||||
direction: "both",
|
||||
});
|
||||
};
|
||||
|
||||
it("retains an empty JavaScript-requested native member in a feature handoff", () => {
|
||||
const analyzed = analyzeCommonJsStaticFixture(
|
||||
'const empty = require("./addon.node")[""]; empty();',
|
||||
);
|
||||
const binding = analyzed.electron.native_addon_bindings[0];
|
||||
if (binding === undefined) throw new Error("Expected native addon binding");
|
||||
expect(binding.members).toEqual([""]);
|
||||
const requested = node([
|
||||
{
|
||||
label: "requested member summary",
|
||||
properties: {
|
||||
specifier: binding.specifier,
|
||||
requested_members: [...binding.members],
|
||||
},
|
||||
},
|
||||
]);
|
||||
const trace = traceRequestedExport(requested, {
|
||||
kind: "native-export",
|
||||
value: "",
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(trace.seed_matches).toMatchObject([{ node_id: requested.node_id }]);
|
||||
expect(trace.native_handoffs).toMatchObject([
|
||||
{
|
||||
artifact_sha256: "b".repeat(64),
|
||||
requested_exports: [""],
|
||||
status: "requires-provider-analysis",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
const handoffCases: {
|
||||
properties: Record<string, JsonValue>;
|
||||
expected: string[];
|
||||
}[] = [
|
||||
{ properties: {}, expected: [] },
|
||||
{ properties: { requested_members: null }, expected: [] },
|
||||
{ properties: { requested_members: "invalid" }, expected: [] },
|
||||
{ properties: { requested_members: {} }, expected: [] },
|
||||
{ properties: { requested_members: ["open", 42] }, expected: ["open"] },
|
||||
];
|
||||
|
||||
it.each(handoffCases)(
|
||||
"uses only structured native names in a handoff: $properties",
|
||||
({ properties, expected }) => {
|
||||
const requested = node([
|
||||
{
|
||||
label: "display summary, never an export name",
|
||||
properties: { specifier: "./addon.node", ...properties },
|
||||
},
|
||||
]);
|
||||
const before = JSON.stringify(requested);
|
||||
const trace = traceRequestedExport(requested, {
|
||||
kind: "node-id",
|
||||
value: requested.node_id,
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
});
|
||||
expect(trace.native_handoffs).toMatchObject([
|
||||
{ requested_exports: expected, status: "requires-provider-analysis" },
|
||||
]);
|
||||
expect(JSON.stringify(requested)).toBe(before);
|
||||
},
|
||||
);
|
||||
@@ -6,7 +6,7 @@ import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
|
||||
|
||||
type Observation = ApplicationNode["observations"][number];
|
||||
|
||||
/** Read member facts without changing historical observations or their identifiers. */
|
||||
/** Read source-owned member facts without interpreting display labels. */
|
||||
export const nativeExportObservationFacts = (observation: Observation) => {
|
||||
const requested = observation.properties.requested_members;
|
||||
const members = observation.properties.members;
|
||||
@@ -29,9 +29,6 @@ export const nativeExportObservationFacts = (observation: Observation) => {
|
||||
(!membersPresent || validList(members)),
|
||||
requested: strings(requested),
|
||||
members: strings(members),
|
||||
// Labels were the historical fallback, but current labels summarize names
|
||||
// or namespace access and must not become requested native symbols.
|
||||
label: Array.isArray(requested) ? null : observation.label,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -43,12 +40,7 @@ export const nativeExportCorrespondenceKey = (
|
||||
const facts = nativeExportObservationFacts(observation);
|
||||
if (!facts.valid) return null;
|
||||
const specifier = observation.properties.specifier;
|
||||
if (typeof specifier !== "string" || specifier === "")
|
||||
return observation.label === null ||
|
||||
facts.requestedPresent ||
|
||||
facts.membersPresent
|
||||
? null
|
||||
: compositeKey(["label", observation.label]);
|
||||
if (typeof specifier !== "string" || specifier === "") return null;
|
||||
const flag = (name: string) => {
|
||||
const value = observation.properties[name];
|
||||
return typeof value === "boolean" ? value : null;
|
||||
@@ -61,7 +53,6 @@ export const nativeExportCorrespondenceKey = (
|
||||
facts.membersPresent
|
||||
? ["members", [...facts.members].sort(compareUnicodeCodePoints)]
|
||||
: ["members-unknown"],
|
||||
facts.label,
|
||||
flag("namespace_access"),
|
||||
flag("dynamic_member_access"),
|
||||
]);
|
||||
|
||||
@@ -106,11 +106,7 @@ const requestedExports = (nodes: readonly ApplicationNode[]): string[] => {
|
||||
const values = nodes.flatMap((node) =>
|
||||
node.observations.flatMap((observation) => {
|
||||
const facts = nativeExportObservationFacts(observation);
|
||||
return [
|
||||
...(facts.label === null ? [] : [facts.label]),
|
||||
...facts.requested,
|
||||
...facts.members,
|
||||
];
|
||||
return [...facts.requested, ...facts.members];
|
||||
}),
|
||||
);
|
||||
return [...new Set(values)].sort(compareUnicodeCodePoints);
|
||||
|
||||
Reference in new issue
Block a user