feat(javascript): discover semantic call literals in feature traces (#7840)
* feat(javascript): discover semantic call literals in feature traces * fix(javascript): bound retained call literals by transfer capacity * test(javascript): align feature no-match diagnostic
This commit is contained in:
1 parent
685f6db960
commit
f9e7571a3d
20 files changed
+681
-15
No files matched your search
@@ -158,6 +158,10 @@ export const traceApplicationFeatureEvidence = (
|
||||
const result = traceApplicationFeature({
|
||||
sourceEvidenceId: source.evidence.evidence_id,
|
||||
graph: source.graph,
|
||||
semanticGraph: source.semanticGraph,
|
||||
...(input.semantic_page === undefined
|
||||
? {}
|
||||
: { semanticPage: input.semantic_page }),
|
||||
nativeEvidence,
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
@@ -168,6 +172,9 @@ export const traceApplicationFeatureEvidence = (
|
||||
native_evidence_ids: nativeEvidence.map(({ evidence_id: id }) => id),
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
...(input.semantic_page === undefined
|
||||
? {}
|
||||
: { semantic_page: input.semantic_page }),
|
||||
},
|
||||
result,
|
||||
);
|
||||
|
||||
@@ -31,6 +31,99 @@ import { getterExpansionSource } from "../../../tests/fixtures/javascriptGetterE
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
|
||||
it("retains direct call literals and resolves literal queries to their call site", () => {
|
||||
const source =
|
||||
'api.rpc("proc_0", 7, false, null, dynamic, ...values, `fixed`, -3);';
|
||||
const graph = graphFor(source);
|
||||
const call = graph.nodes.find(({ kind }) => kind === "call-site");
|
||||
const argumentsValue = call?.properties.arguments;
|
||||
expect(argumentsValue).toEqual([
|
||||
expect.objectContaining({
|
||||
index: 0,
|
||||
value: "proc_0",
|
||||
source_range: expect.any(Object),
|
||||
}),
|
||||
expect.objectContaining({ index: 1, value: 7 }),
|
||||
expect.objectContaining({ index: 2, value: false }),
|
||||
expect.objectContaining({ index: 3, value: null }),
|
||||
expect.objectContaining({ index: 6, value: "fixed" }),
|
||||
expect.objectContaining({ index: 7, value: -3 }),
|
||||
]);
|
||||
if (call === undefined) throw new Error("Expected call site");
|
||||
const trace = queryJavaScriptSemanticGraph(
|
||||
graph,
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "literal", value: "proc_0" },
|
||||
direction: "forward-influence",
|
||||
}),
|
||||
);
|
||||
expect(trace.seed_node_ids).toContain(call.node_id);
|
||||
});
|
||||
|
||||
it("reports the primitive byte bound when a direct argument is too large", () => {
|
||||
const source = `rpc(${JSON.stringify("x".repeat(175_000))});`;
|
||||
const ir = analyzeJavaScriptSemantics(source);
|
||||
expect(ir.coverage.resourceLimits).toContain("primitive-bytes");
|
||||
const graph = graphFor(source, ir);
|
||||
const call = graph.nodes.find(({ kind }) => kind === "call-site");
|
||||
expect(call?.properties.arguments).toBeUndefined();
|
||||
expect(graph.coverage.status).not.toBe("complete");
|
||||
const unknown = graph.unknowns.find(
|
||||
(entry) =>
|
||||
entry.node_id === call?.node_id && entry.reason === "resource-limit",
|
||||
);
|
||||
expect(unknown?.detail).toMatch(/primitive string-byte budget exceeded/i);
|
||||
expect(
|
||||
graph.evidence_contexts.find(
|
||||
(entry) => entry.context_id === unknown?.evidence.context_id,
|
||||
),
|
||||
).toMatchObject({
|
||||
coverage: {
|
||||
limits: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "javascript_semantic_primitive_bytes",
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a large call's graph within the worker record limit and reports omitted arguments", () => {
|
||||
const value = JSON.stringify("x".repeat(140_000));
|
||||
const source = `rpc(${Array.from({ length: 64 }, () => value).join(",")});`;
|
||||
const graph = graphFor(source);
|
||||
const call = graph.nodes.find(({ kind }) => kind === "call-site");
|
||||
if (call === undefined) throw new Error("Expected call site");
|
||||
const values = call.properties.arguments;
|
||||
expect(Array.isArray(values)).toBe(true);
|
||||
if (!Array.isArray(values)) throw new Error("Expected retained arguments");
|
||||
expect(values.length).toBeGreaterThan(0);
|
||||
expect(values.length).toBeLessThan(64);
|
||||
expect(
|
||||
Buffer.byteLength(`${JSON.stringify({ kind: "node", value: call })}\n`),
|
||||
).toBeLessThanOrEqual(8 * 1024 * 1024);
|
||||
const unknown = graph.unknowns.find(
|
||||
(entry) =>
|
||||
entry.node_id === call.node_id &&
|
||||
entry.detail.includes("worker transfer record ceiling"),
|
||||
);
|
||||
expect(unknown).toMatchObject({ reason: "resource-limit" });
|
||||
expect(
|
||||
graph.evidence_contexts.find(
|
||||
(entry) => entry.context_id === unknown?.evidence.context_id,
|
||||
),
|
||||
).toMatchObject({
|
||||
coverage: {
|
||||
limits: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: "javascript_analysis_transfer_record_bytes",
|
||||
value: 8 * 1024 * 1024,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("projects bounded alias selection as a resource-limit unknown", () => {
|
||||
const source = aliasSelectionBudgetSource();
|
||||
const ir = analyzeJavaScriptSemantics(source);
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import {
|
||||
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
|
||||
JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES,
|
||||
JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM,
|
||||
javaScriptAnalysisCoverageLimits,
|
||||
javaScriptSemanticCeilingCoverage,
|
||||
@@ -71,6 +72,7 @@ import {
|
||||
semanticFamilyStatus,
|
||||
} from "./JavaScriptSemanticGraphProjection.js";
|
||||
import {
|
||||
constructSemanticGraphNode,
|
||||
addSemanticFallbackRoot as addFallbackRoot,
|
||||
addSemanticGraphUnknown,
|
||||
recordSemanticUnknownOmission,
|
||||
@@ -771,14 +773,102 @@ function* createCallSiteNodes(
|
||||
call.callerCallableId === null
|
||||
? null
|
||||
: (callables.get(call.callerCallableId)?.node_id ?? null);
|
||||
const node = retainNode(state, file, {
|
||||
kind: "call-site",
|
||||
const nodeInput = {
|
||||
kind: "call-site" as const,
|
||||
roleKey: `call:${call.callSiteId}`,
|
||||
location: call.location,
|
||||
label: call.kind,
|
||||
functionNodeId: owner,
|
||||
};
|
||||
// Include the complete node and worker JSON envelope before retaining any
|
||||
// arguments. Each entry is charged by its encoded size and comma, so even
|
||||
// worst-case escaping cannot overflow the worker's single-record ceiling.
|
||||
let recordBytes = 0;
|
||||
if (call.arguments.some((argument) => Object.hasOwn(argument, "literal"))) {
|
||||
const baseline = constructSemanticGraphNode(file, nodeInput, state);
|
||||
recordBytes = Buffer.byteLength(
|
||||
`${JSON.stringify({ kind: "node", value: { ...baseline, properties: { arguments: [] } } })}\n`,
|
||||
);
|
||||
}
|
||||
const argumentsValue: JsonValue[] = [];
|
||||
let transferOmitted = 0;
|
||||
for (const argument of call.arguments) {
|
||||
if (!Object.hasOwn(argument, "literal")) continue;
|
||||
const item = {
|
||||
index: argument.index,
|
||||
value: argument.literal ?? null,
|
||||
source_range:
|
||||
argument.valueExpressionLocation === null
|
||||
? null
|
||||
: {
|
||||
start: { ...argument.valueExpressionLocation.start },
|
||||
end: { ...argument.valueExpressionLocation.end },
|
||||
},
|
||||
};
|
||||
const size =
|
||||
Buffer.byteLength(JSON.stringify(item)) +
|
||||
(argumentsValue.length === 0 ? 0 : 1);
|
||||
if (recordBytes + size > JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES) {
|
||||
transferOmitted += 1;
|
||||
continue;
|
||||
}
|
||||
argumentsValue.push(item);
|
||||
recordBytes += size;
|
||||
}
|
||||
const node = retainNode(state, file, {
|
||||
...nodeInput,
|
||||
...(argumentsValue.length === 0
|
||||
? {}
|
||||
: { properties: { arguments: argumentsValue } }),
|
||||
});
|
||||
if (node !== null) result.set(call.callSiteId, node);
|
||||
if (node !== null) {
|
||||
result.set(call.callSiteId, node);
|
||||
if (transferOmitted > 0) {
|
||||
const detail = `${String(transferOmitted)} direct call arguments exceeded the ${String(JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES)}-byte worker transfer record ceiling.`;
|
||||
addSemanticGraphUnknown(state, {
|
||||
node_id: node.node_id,
|
||||
family: "call-flow",
|
||||
relation_kinds: ["argument-to-parameter"],
|
||||
reason: "resource-limit",
|
||||
detail,
|
||||
candidate_node_ids: [],
|
||||
evidence: {
|
||||
...unknownSemanticEvidence(file, call.location),
|
||||
coverage: partialApplicationCoverage(
|
||||
javaScriptAnalysisCoverageLimits({
|
||||
transfer_record_bytes:
|
||||
JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES,
|
||||
}),
|
||||
null,
|
||||
),
|
||||
limitations: [detail],
|
||||
},
|
||||
});
|
||||
}
|
||||
if (
|
||||
call.arguments.some(
|
||||
(argument) => argument.literalResourceLimit === "primitive-bytes",
|
||||
)
|
||||
) {
|
||||
const limit = "primitive-bytes";
|
||||
addSemanticGraphUnknown(state, {
|
||||
node_id: node.node_id,
|
||||
family: "call-flow",
|
||||
relation_kinds: ["argument-to-parameter"],
|
||||
reason: "resource-limit",
|
||||
detail: semanticResourceLimitReason(limit),
|
||||
candidate_node_ids: [],
|
||||
evidence: {
|
||||
...unknownSemanticEvidence(file, call.location),
|
||||
coverage: partialApplicationCoverage(
|
||||
semanticResourceLimitCoverage([limit]),
|
||||
null,
|
||||
),
|
||||
limitations: [semanticResourceLimitReason(limit)],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user