From 527b76ef78e84d29bb41bd45f1f83fae05ca66ca Mon Sep 17 00:00:00 2001 From: Iwakurakaoru0822 Date: Tue, 6 Oct 2026 21:11:51 -0700 Subject: [PATCH] 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 --- ...JavaScriptApplicationEmptyLiterals.test.ts | 32 ++++++++++++++++--- .../JavaScriptArtifactGraphAccumulator.ts | 13 +++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/application/JavaScriptApplicationEmptyLiterals.test.ts b/src/application/JavaScriptApplicationEmptyLiterals.test.ts index f2b87269f..d9fbde02f 100644 --- a/src/application/JavaScriptApplicationEmptyLiterals.test.ts +++ b/src/application/JavaScriptApplicationEmptyLiterals.test.ts @@ -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([ + '""', + ]); +}); diff --git a/src/application/JavaScriptArtifactGraphAccumulator.ts b/src/application/JavaScriptArtifactGraphAccumulator.ts index 403ed9e65..59ee76ff1 100644 --- a/src/application/JavaScriptArtifactGraphAccumulator.ts +++ b/src/application/JavaScriptArtifactGraphAccumulator.ts @@ -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, })), });