fix(javascript): leave empty application values unlabeled
A synthetic `""` label for an empty application-graph value could still
be matched by a literal `""` feature seed, which also matches the label of
a real two-quote value, so `trace_application_feature` combined
`fetch('')` with `fetch('""')`. Emit no label (`null`) for empty values;
the exact value stays in the identity and properties.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
63012eeb60
commit
527b76ef78
|
|
@ -5,6 +5,7 @@ import { expect, it } from "vitest";
|
|||
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../domain/javascriptApplicationAnalysis.js";
|
||||
import { findApplicationFeatureSeeds } from "../domain/javascriptFeatureSeed.js";
|
||||
import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js";
|
||||
|
||||
const analyzeSource = async (
|
||||
|
|
@ -50,7 +51,7 @@ it.each([
|
|||
await expect(analyzeSource(source)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it("displays empty literals as nonempty text while retaining exact values", async () => {
|
||||
it("keeps exact values for empty literals without synthetic application labels", async () => {
|
||||
const output = await analyzeSource(
|
||||
"fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);",
|
||||
);
|
||||
|
|
@ -61,11 +62,11 @@ it("displays empty literals as nonempty text while retaining exact values", asyn
|
|||
labels: observations.map(({ label }) => label),
|
||||
values: observations.map(({ properties }) => properties.value),
|
||||
}));
|
||||
// The empty endpoint and a literal two-quote endpoint share display text
|
||||
// but remain distinct nodes with exact identities.
|
||||
// The empty endpoint has no display label, so it cannot be confused with
|
||||
// a literal two-quote endpoint; both keep exact identities.
|
||||
expect(endpoints).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ key: "", labels: ['""'], values: [""] },
|
||||
{ key: "", labels: [null], values: [""] },
|
||||
{ key: '""', labels: ['""'], values: ['""'] },
|
||||
{ key: "/api", labels: ["/api"], values: ["/api"] },
|
||||
]),
|
||||
|
|
@ -128,3 +129,26 @@ it("keeps empty and two-quote source-map names distinct", async () => {
|
|||
.sort(),
|
||||
).toEqual(["", '""']);
|
||||
});
|
||||
|
||||
it("does not match an empty endpoint with a literal two-quote seed", async () => {
|
||||
const output = await analyzeSource("fetch(''); fetch('\"\"');");
|
||||
const endpoints = new Map(
|
||||
output.graph.nodes
|
||||
.filter(({ kind }) => kind === "endpoint")
|
||||
.map((node) => [
|
||||
node.node_id,
|
||||
node.identity.strategy === "artifact-local-key"
|
||||
? node.identity.key
|
||||
: null,
|
||||
]),
|
||||
);
|
||||
const matches = findApplicationFeatureSeeds(output.graph.nodes, {
|
||||
kind: "string",
|
||||
value: '""',
|
||||
match: "exact",
|
||||
case_sensitive: true,
|
||||
}).filter(({ node_id: nodeId }) => endpoints.has(nodeId));
|
||||
expect(matches.map(({ node_id: nodeId }) => endpoints.get(nodeId))).toEqual([
|
||||
'""',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import {
|
|||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
} from "../domain/javascriptApplicationGraph.js";
|
||||
import { javascriptDisplayText } from "../domain/javascriptAstValues.js";
|
||||
|
||||
/** Deduplicate graph entities while retaining distinct evidence observations. */
|
||||
export class JavaScriptArtifactGraphAccumulator {
|
||||
|
|
@ -73,9 +72,10 @@ interface DisplayableNodeInput {
|
|||
|
||||
/**
|
||||
* Static findings can carry legal empty strings, such as `fetch("")`,
|
||||
* `require("")`, or a source map's `sources: [""]`. Labels are nonempty display
|
||||
* text, so the empty value is shown as `""`. Identities and observation
|
||||
* properties keep the exact value, so distinct values never merge.
|
||||
* `require("")`, or a source map's `sources: [""]`. Labels must be nonempty,
|
||||
* so an empty value has no label rather than synthetic text that a real value
|
||||
* could share; literal feature seeds match labels. Identities and observation
|
||||
* properties keep the exact value.
|
||||
*/
|
||||
const displayableNode = (
|
||||
input: DisplayableNodeInput,
|
||||
|
|
@ -83,9 +83,6 @@ const displayableNode = (
|
|||
...input,
|
||||
observations: input.observations.map((observation) => ({
|
||||
...observation,
|
||||
label:
|
||||
observation.label === null
|
||||
? null
|
||||
: javascriptDisplayText(observation.label),
|
||||
label: observation.label === "" ? null : observation.label,
|
||||
})),
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue