From ddce4bb446dc64469df5256e47c9a1d2865e2063 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 22 Oct 2026 10:57:27 +0800 Subject: [PATCH] refactor(javascript): centralize source coordinate operations --- .../ElectronBoundaryGraphCallPaths.ts | 8 +-- .../ElectronBoundaryGraphContext.ts | 20 ------- .../javascript/ElectronBoundaryGraphIpc.ts | 10 ++-- .../javascript/ElectronBoundaryGraphNative.ts | 4 +- .../ElectronBoundaryGraphWindows.ts | 10 ++-- .../JavaScriptSemanticGraphConstruction.ts | 5 +- .../JavaScriptSemanticGraphFlowProjection.ts | 8 ++- .../JavaScriptSemanticGraphProjection.ts | 52 +++++++++---------- .../javascriptSemanticCallResolution.ts | 14 +---- .../javascript/javascriptSemanticPromises.ts | 10 ++-- .../javascript/javascriptSourceCoordinates.ts | 30 +++++++++-- 11 files changed, 78 insertions(+), 93 deletions(-) diff --git a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts index d1f1886dbf..f835ea092a 100644 --- a/src/application/javascript/ElectronBoundaryGraphCallPaths.ts +++ b/src/application/javascript/ElectronBoundaryGraphCallPaths.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; @@ -10,7 +11,6 @@ import { import { addElectronInferenceEdge, electronFindingSourceNode, - electronRangeKey, } from "./ElectronBoundaryGraphContext.js"; import { artifactLocalIdentity, @@ -191,13 +191,15 @@ const matches = ( path.steps.some( (step) => step.kind === sourceStepKind && - electronRangeKey(step.location) === electronRangeKey(finding.location), + javascriptSourceRangeKey(step.location) === + javascriptSourceRangeKey(finding.location), ) && (invocation === undefined || path.steps.some( (step) => (step.kind === "call" || step.kind === "exposure") && - electronRangeKey(step.location) === electronRangeKey(invocation), + javascriptSourceRangeKey(step.location) === + javascriptSourceRangeKey(invocation), )) ); }; diff --git a/src/application/javascript/ElectronBoundaryGraphContext.ts b/src/application/javascript/ElectronBoundaryGraphContext.ts index a02b5f0161..f8f96b343a 100644 --- a/src/application/javascript/ElectronBoundaryGraphContext.ts +++ b/src/application/javascript/ElectronBoundaryGraphContext.ts @@ -108,23 +108,3 @@ export const addElectronInferenceEdge = ( }), }); }; - -/** Deterministic source-range key for artifact-local graph identities. */ -export const electronRangeKey = (range: JavaScriptSourceRange): string => - `${String(range.start.line)}:${String(range.start.column)}-${String(range.end.line)}:${String(range.end.column)}`; - -/** Test exact source-range containment without assuming runtime reachability. */ -export const electronRangeContains = ( - outer: JavaScriptSourceRange, - inner: JavaScriptSourceRange, -): boolean => - comparePoint(outer.start, inner.start) <= 0 && - comparePoint(outer.end, inner.end) >= 0; - -const comparePoint = ( - left: JavaScriptSourceRange["start"], - right: JavaScriptSourceRange["start"], -): number => - left.line === right.line - ? left.column - right.column - : left.line - right.line; diff --git a/src/application/javascript/ElectronBoundaryGraphIpc.ts b/src/application/javascript/ElectronBoundaryGraphIpc.ts index 24dbffba44..3cc383914d 100644 --- a/src/application/javascript/ElectronBoundaryGraphIpc.ts +++ b/src/application/javascript/ElectronBoundaryGraphIpc.ts @@ -1,3 +1,7 @@ +import { + javascriptSourceRangeKey, + javascriptSourceRangeContains, +} from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import type { ElectronSenderValidationFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; @@ -21,8 +25,6 @@ import { addElectronInferenceEdge, electronFindingSourceNode, electronObservationIdentity, - electronRangeContains, - electronRangeKey, } from "./ElectronBoundaryGraphContext.js"; interface IpcGraphState { @@ -182,7 +184,7 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => { identity: artifactLocalIdentity( file.sha256, "electron-ipc-handler", - `${finding.side}:${finding.operation}:${finding.channel ?? finding.channel_expression}:${electronRangeKey(handlerRange)}`, + `${finding.side}:${finding.operation}:${finding.channel ?? finding.channel_expression}:${javascriptSourceRangeKey(handlerRange)}`, ), observations: [ { @@ -338,7 +340,7 @@ const containingHandler = ( (record) => record.file.path === file.path && record.finding.handler_location !== null && - electronRangeContains( + javascriptSourceRangeContains( record.finding.handler_location, validation.location, ), diff --git a/src/application/javascript/ElectronBoundaryGraphNative.ts b/src/application/javascript/ElectronBoundaryGraphNative.ts index f356c7d1b4..446ab32338 100644 --- a/src/application/javascript/ElectronBoundaryGraphNative.ts +++ b/src/application/javascript/ElectronBoundaryGraphNative.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { ElectronNativeAddonBindingFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import { @@ -13,7 +14,6 @@ import { addElectronInferenceEdge, electronFindingSourceNode, electronObservationIdentity, - electronRangeKey, } from "./ElectronBoundaryGraphContext.js"; interface NativeBindingInput { @@ -65,7 +65,7 @@ const addNativeBinding = (input: NativeBindingInput): void => { value.members, value.namespace_access ?? null, value.dynamic_member_access ?? null, - electronRangeKey(value.location), + javascriptSourceRangeKey(value.location), ]), ), observations: [ diff --git a/src/application/javascript/ElectronBoundaryGraphWindows.ts b/src/application/javascript/ElectronBoundaryGraphWindows.ts index 7c0f73e8fc..7b4c23af2e 100644 --- a/src/application/javascript/ElectronBoundaryGraphWindows.ts +++ b/src/application/javascript/ElectronBoundaryGraphWindows.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import type { ElectronBrowserWindowFinding, @@ -21,7 +22,6 @@ import { addElectronAstEdge, addElectronInferenceEdge, electronFindingSourceNode, - electronRangeKey, } from "./ElectronBoundaryGraphContext.js"; import { artifactPathResolutionProperties, @@ -86,11 +86,11 @@ const addBrowserWindow = ( identity: artifactLocalIdentity( file.sha256, "electron-browser-window", - electronRangeKey(value.location), + javascriptSourceRangeKey(value.location), ), observations: [ { - label: `BrowserWindow@${electronRangeKey(value.location)}`, + label: `BrowserWindow@${javascriptSourceRangeKey(value.location)}`, properties: { source_path: file.path, options_status: value.options_status, @@ -202,7 +202,7 @@ const addContextBridge = ( identity: artifactLocalIdentity( file.sha256, "electron-context-bridge", - `${value.world}:${value.api_key ?? value.api_key_expression ?? "[missing]"}:${electronRangeKey(value.location)}`, + `${value.world}:${value.api_key ?? value.api_key_expression ?? "[missing]"}:${javascriptSourceRangeKey(value.location)}`, ), observations: [ { @@ -271,7 +271,7 @@ const addUtilityProcess = ( identity: artifactLocalIdentity( file.sha256, "electron-utility-process", - `${value.module_path ?? value.module_expression}:${electronRangeKey(value.location)}`, + `${value.module_path ?? value.module_expression}:${javascriptSourceRangeKey(value.location)}`, ), observations: [ { diff --git a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts index dc3a9e55e5..87525acdb3 100644 --- a/src/application/javascript/JavaScriptSemanticGraphConstruction.ts +++ b/src/application/javascript/JavaScriptSemanticGraphConstruction.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { z } from "zod"; import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js"; import type { JsonValue } from "../../domain/jsonValue.js"; @@ -338,7 +339,5 @@ const applicationLocationKey = ( [ artifactSha256, path, - range === null - ? "artifact-path" - : `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`, + range === null ? "artifact-path" : javascriptSourceRangeKey(range), ].join("\u0000"); diff --git a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts index 6b2acc8de6..cc28a23a52 100644 --- a/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphFlowProjection.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js"; import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import { javaScriptSemanticNodeId } from "../../domain/javascript/javascriptSemanticGraph.js"; import type { @@ -221,9 +222,6 @@ interface ObjectReadIndex { const objectReadIndexes = new WeakMap(); -const objectReadRangeKey = (location: JavaScriptSourceRange): string => - `${String(location.start.line)}:${String(location.start.column)}:${String(location.end.line)}:${String(location.end.column)}`; - /** * The object projection owns a member read's occurrence. Index it once per * file so return and argument flow can connect that expression without @@ -250,7 +248,7 @@ const objectReadOccurrences = ( }), ); if (node !== undefined) - nodes.set(objectReadRangeKey(operation.location), node); + nodes.set(javascriptSourceRangeKey(operation.location), node); } objectReadIndexes.set(context.ir, { state: context.state, nodes }); return nodes; @@ -263,7 +261,7 @@ export const semanticObjectReadOccurrence = ( ): JavaScriptSemanticGraphNode | undefined => location === null ? undefined - : objectReadOccurrences(context).get(objectReadRangeKey(location)); + : objectReadOccurrences(context).get(javascriptSourceRangeKey(location)); /** Link direct return expressions to their retained semantic references. */ export const projectSemanticReturnValues = ( diff --git a/src/application/javascript/JavaScriptSemanticGraphProjection.ts b/src/application/javascript/JavaScriptSemanticGraphProjection.ts index bc1ade2976..a3612e3969 100644 --- a/src/application/javascript/JavaScriptSemanticGraphProjection.ts +++ b/src/application/javascript/JavaScriptSemanticGraphProjection.ts @@ -1,3 +1,8 @@ +import { + compareJavaScriptSourcePoints, + javascriptSourceRangeContains, + javascriptSourceRangeKey, +} from "../../domain/javascript/javascriptSourceCoordinates.js"; import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js"; import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js"; @@ -40,7 +45,7 @@ export const createSemanticCallableOwnerLookup = ( })) .sort( (left, right) => - pointCompare(left.range.start, right.range.start) || + compareJavaScriptSourcePoints(left.range.start, right.range.start) || left.order - right.order, ); const tree = buildSourceIntervalTree(entries, 0, entries.length); @@ -54,12 +59,13 @@ export const createSemanticCallableOwnerLookup = ( const current = pending.pop(); if ( current === undefined || - pointCompare(current.minimumStart, location.start) > 0 || - pointCompare(current.maximumEnd, location.end) < 0 + compareJavaScriptSourcePoints(current.minimumStart, location.start) > + 0 || + compareJavaScriptSourcePoints(current.maximumEnd, location.end) < 0 ) continue; const candidate = current.entry; - if (contains(candidate.range, location)) { + if (javascriptSourceRangeContains(candidate.range, location)) { const node = nodes.get(candidate.callableId); const rank = best === undefined @@ -93,9 +99,15 @@ const buildSourceIntervalTree = ( let minimumEnd = entry.range.end; let maximumEnd = entry.range.end; for (const child of [left, right]) { - if (child !== null && pointCompare(child.minimumEnd, minimumEnd) < 0) + if ( + child !== null && + compareJavaScriptSourcePoints(child.minimumEnd, minimumEnd) < 0 + ) minimumEnd = child.minimumEnd; - if (child !== null && pointCompare(child.maximumEnd, maximumEnd) > 0) + if ( + child !== null && + compareJavaScriptSourcePoints(child.maximumEnd, maximumEnd) > 0 + ) maximumEnd = child.maximumEnd; } return { @@ -120,7 +132,7 @@ export const createSemanticNodeRangeLookup = ( }) .sort( (left, right) => - pointCompare(left.range.start, right.range.start) || + compareJavaScriptSourcePoints(left.range.start, right.range.start) || left.order - right.order, ); const tree = buildSourceIntervalTree(entries, 0, entries.length); @@ -132,11 +144,12 @@ export const createSemanticNodeRangeLookup = ( const current = pending.pop(); if ( current === undefined || - pointCompare(current.maximumStart, range.start) < 0 || - pointCompare(current.minimumEnd, range.end) > 0 + compareJavaScriptSourcePoints(current.maximumStart, range.start) < 0 || + compareJavaScriptSourcePoints(current.minimumEnd, range.end) > 0 ) continue; - if (contains(range, current.entry.range)) matches.push(current.entry); + if (javascriptSourceRangeContains(range, current.entry.range)) + matches.push(current.entry); if (current.left !== null) pending.push(current.left); if (current.right !== null) pending.push(current.right); } @@ -155,18 +168,15 @@ export const createSemanticCallSiteLookup = ( ) => JavaScriptSemanticGraphNode | undefined) => { const identifiers = new Map(); for (const call of ir.callSites) { - const key = rangeKey(call.location); + const key = javascriptSourceRangeKey(call.location); if (!identifiers.has(key)) identifiers.set(key, call.callSiteId); } return (range) => { - const identifier = identifiers.get(rangeKey(range)); + const identifier = identifiers.get(javascriptSourceRangeKey(range)); return identifier === undefined ? undefined : nodes.get(identifier); }; }; -const rangeKey = (range: JavaScriptSourceRange): string => - `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; - /** Report extractor support without treating missing families as absence. */ export const semanticFamilyStatus = ( family: (typeof JAVASCRIPT_SEMANTIC_RELATION_FAMILIES)[number], @@ -191,17 +201,5 @@ export const semanticFamilyStatus = ( return "partial"; }; -const contains = ( - outer: JavaScriptSourceRange, - inner: JavaScriptSourceRange, -): boolean => - pointCompare(outer.start, inner.start) <= 0 && - pointCompare(outer.end, inner.end) >= 0; - -const pointCompare = ( - left: JavaScriptSourceRange["start"], - right: JavaScriptSourceRange["start"], -): number => left.line - right.line || left.column - right.column; - const positionKey = (range: JavaScriptSourceRange): string => `${String(range.start.line).padStart(12, "0")}:${String(range.start.column).padStart(12, "0")}`; diff --git a/src/domain/javascript/javascriptSemanticCallResolution.ts b/src/domain/javascript/javascriptSemanticCallResolution.ts index d3df8327c2..0dde354078 100644 --- a/src/domain/javascript/javascriptSemanticCallResolution.ts +++ b/src/domain/javascript/javascriptSemanticCallResolution.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeContains } from "./javascriptSourceCoordinates.js"; import * as t from "@babel/types"; import type { @@ -270,7 +271,7 @@ const parameterAcceptsArgument = ( if (!t.isFunction(node)) return false; const parameters = runtimeFunctionParameters(node); const parameterIndex = parameters.findIndex((parameter) => - rangeContains(range(parameter), location), + javascriptSourceRangeContains(range(parameter), location), ); if (parameterIndex === argumentIndex) return true; return ( @@ -280,16 +281,5 @@ const parameterAcceptsArgument = ( ); }; -const rangeContains = ( - outer: JavaScriptSemanticArgumentFlow["parameterLocation"], - inner: JavaScriptSemanticArgumentFlow["parameterLocation"], -): boolean => - (outer.start.line < inner.start.line || - (outer.start.line === inner.start.line && - outer.start.column <= inner.start.column)) && - (outer.end.line > inner.end.line || - (outer.end.line === inner.end.line && - outer.end.column >= inner.end.column)); - const uniqueCallableIds = (values: readonly string[]): string[] => [...new Set(values)].sort(compareUnicodeCodePoints); diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 062e593560..5178c04dff 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -1,3 +1,4 @@ +import { javascriptSourceRangeKey } from "./javascriptSourceCoordinates.js"; import * as t from "@babel/types"; import type { @@ -340,7 +341,7 @@ const indexReturnSites = ( ): ReadonlyMap => { const output = new Map(); for (const site of callable.returnSites) { - const key = returnSiteKey(site.location); + const key = javascriptSourceRangeKey(site.location); if (!output.has(key)) output.set(key, site.returnSiteId); } return output; @@ -366,7 +367,7 @@ const createReturnSiteLookup = ( returnSiteIds = indexReturnSites(callable); returnSiteIdsByCallable.set(callableId, returnSiteIds); } - return returnSiteIds.get(returnSiteKey(location)) ?? null; + return returnSiteIds.get(javascriptSourceRangeKey(location)) ?? null; }; }; @@ -381,11 +382,6 @@ const indexCallablesById = ( return output; }; -const returnSiteKey = ( - location: JavaScriptSemanticPromiseOperation["location"], -): string => - `${location.start.line}:${location.start.column}:${location.end.line}:${location.end.column}`; - const promiseSources = ( candidate: PromiseCandidate, candidateByNode: WeakMap, diff --git a/src/domain/javascript/javascriptSourceCoordinates.ts b/src/domain/javascript/javascriptSourceCoordinates.ts index f9c3978185..9378cf974e 100644 --- a/src/domain/javascript/javascriptSourceCoordinates.ts +++ b/src/domain/javascript/javascriptSourceCoordinates.ts @@ -1,4 +1,28 @@ import { z } from "zod"; +import type { + JavaScriptSourcePoint, + JavaScriptSourceRange, +} from "./javascriptStaticAnalysisTypes.js"; + +/** Compare parser coordinates by line, then UTF-16 column. */ +export const compareJavaScriptSourcePoints = ( + left: JavaScriptSourcePoint, + right: JavaScriptSourcePoint, +): number => left.line - right.line || left.column - right.column; + +/** Include equal endpoints without implying runtime reachability. */ +export const javascriptSourceRangeContains = ( + outer: JavaScriptSourceRange, + inner: JavaScriptSourceRange, +): boolean => + compareJavaScriptSourcePoints(outer.start, inner.start) <= 0 && + compareJavaScriptSourcePoints(outer.end, inner.end) >= 0; + +/** Exact artifact-local coordinate key; callers supply the artifact identity. */ +export const javascriptSourceRangeKey = ( + range: JavaScriptSourceRange, +): string => + `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; const sourcePointSchema = z.strictObject({ line: z.number().int().min(1), @@ -9,11 +33,7 @@ const sourcePointSchema = z.strictObject({ export const javascriptSourceRangeSchema = z .strictObject({ start: sourcePointSchema, end: sourcePointSchema }) .superRefine((range, context) => { - if ( - range.end.line < range.start.line || - (range.end.line === range.start.line && - range.end.column < range.start.column) - ) + if (compareJavaScriptSourcePoints(range.end, range.start) < 0) context.addIssue({ code: "custom", path: ["end"],