refactor(javascript): centralize source coordinate operations

This commit is contained in:
morluto
2026-10-22 10:57:27 +08:00
parent dd3497e133
commit ddce4bb446
11 changed files with 78 additions and 93 deletions
@@ -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),
))
);
};
@@ -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;
@@ -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,
),
@@ -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: [
@@ -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: [
{
@@ -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");
@@ -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<JavaScriptSemanticIr, ObjectReadIndex>();
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 = (
@@ -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 = <Interval extends SourceInterval>(
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<string, string>();
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")}`;