mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(javascript): preserve loader authority for exact suffixes (#9477)
* fix(javascript): require exact loader suffix case * fix(javascript): share exact loader mode across import resolution * test(process): align symlink fixture with filesystem checkpoints * test(javascript): normalize the module resolution oracle path separators * test(frida): assert the CLI spawn Evidence contract * test(mcp): use an unsupported analysis-view parent * test(browser): separate source-location line bounds from parity * fix(javascript): preserve uncertain loader provenance * test(javascript): cover uncertain require resolution * fix(javascript): retain conditional Electron evidence * fix(javascript): preserve timed semantic provenance * fix(javascript): retain conditional builtin effects * test(javascript): distinguish bundled host authority * fix(javascript): preserve nested loader invocation identity * test(javascript): qualify Electron fixture authority * fix(javascript): preserve loader invocation authority * fix(javascript): respect source order for resolver escapes * fix(javascript): check nested loader callable mutations * fix(javascript): retain conditional constructor requests * fix(reference): retain constructor loader relationships * fix(reference): follow nested loader callable paths * refactor(javascript): share callable mutation interpretation * fix(javascript): require exact package loader suffixes * refactor(javascript): unify conditional relation evidence * refactor(javascript): unify conditional node evidence * fix(javascript): retain unknown authority for mixed case sources * fix(javascript): preserve loader uncertainty for dotfiles * fix(reference): classify javascript suffix dotfiles * test(reference): cover suffix dotfiles through cli * fix(javascript): retain equivalent conditional built-in origins * fix(javascript): preserve wrapped resolver operation * fix(javascript): retain nested conditional built-in receivers * test(javascript): declare CommonJS in native addon fixtures * fix(javascript): retain wrapped resolver candidates * style: format merged native command schema * fix(javascript): preserve uncertain loader authority * test(javascript): require candidate preload evidence * test(javascript): cover reference loader member writes --------- Co-authored-by: ShuaixinHuang <1836656514@qq.com>
This commit is contained in:
1 parent
78a84d75c0
commit
b46f4fcebd
112 files changed
+5156
-797
No files matched your search
@@ -90,14 +90,20 @@ export const createElectronIpcContext = (
|
||||
const uncertain =
|
||||
!coverageComplete ||
|
||||
owner == null ||
|
||||
transmission.finding.loader_authority === "candidate" ||
|
||||
matching.some(
|
||||
({ finding }) => finding.loader_authority === "candidate",
|
||||
) ||
|
||||
matching.some((handler) => owners.get(handler.key) == null);
|
||||
return {
|
||||
transmission,
|
||||
candidates,
|
||||
coverageLimitedCandidate:
|
||||
!coverageComplete &&
|
||||
transmission.finding.loader_authority !== "candidate" &&
|
||||
owner != null &&
|
||||
candidates.length === 1 &&
|
||||
candidates[0]?.finding.loader_authority !== "candidate" &&
|
||||
isExactIpcHandler(candidates[0]) &&
|
||||
!matching.some((handler) => owners.get(handler.key) == null)
|
||||
? (candidates[0] ?? null)
|
||||
@@ -143,16 +149,18 @@ export const electronModuleReferenced = (
|
||||
if (
|
||||
links !== undefined &&
|
||||
links.some(
|
||||
({ specifier, kind }) =>
|
||||
({ specifier, kind, loaderAuthority }) =>
|
||||
kind !== "type-import" &&
|
||||
loaderAuthority !== "candidate" &&
|
||||
specifier !== null &&
|
||||
ELECTRON_MODULE.test(specifier),
|
||||
)
|
||||
)
|
||||
return true;
|
||||
return (javascript?.references ?? []).some(
|
||||
({ specifier, kind, operation }) =>
|
||||
({ specifier, kind, operation, loader_authority }) =>
|
||||
operation === "load" &&
|
||||
loader_authority !== "candidate" &&
|
||||
ELECTRON_REFERENCE_KINDS.has(kind) &&
|
||||
specifier !== null &&
|
||||
ELECTRON_MODULE.test(specifier),
|
||||
@@ -168,16 +176,48 @@ export const electronBoundaryFactCount = (
|
||||
count +
|
||||
(javascript === null
|
||||
? 0
|
||||
: javascript.electron.browser_windows.length +
|
||||
javascript.electron.context_bridge_apis.length +
|
||||
javascript.electron.ipc.length +
|
||||
javascript.electron.sender_validations.length +
|
||||
javascript.electron.utility_processes.length +
|
||||
javascript.electron.native_addon_bindings.length +
|
||||
javascript.electron.protocols.length),
|
||||
: javascript.electron.browser_windows.filter(provenElectronFinding)
|
||||
.length +
|
||||
javascript.electron.context_bridge_apis.filter(provenElectronFinding)
|
||||
.length +
|
||||
javascript.electron.ipc.filter(provenElectronFinding).length +
|
||||
javascript.electron.sender_validations.filter(provenElectronFinding)
|
||||
.length +
|
||||
javascript.electron.utility_processes.filter(provenElectronFinding)
|
||||
.length +
|
||||
javascript.electron.native_addon_bindings.filter(
|
||||
provenElectronFinding,
|
||||
).length +
|
||||
javascript.electron.protocols.filter(provenElectronFinding).length),
|
||||
0,
|
||||
);
|
||||
|
||||
const provenElectronFinding = (finding: {
|
||||
readonly loader_authority?: "candidate" | undefined;
|
||||
}): boolean => finding.loader_authority !== "candidate";
|
||||
|
||||
/** Candidate findings remain in the graph, but are not proven summary counts. */
|
||||
export const hasCandidateElectronFindings = (
|
||||
analysis: JavaScriptArtifactAnalysis,
|
||||
): boolean =>
|
||||
analysis.files.some(
|
||||
({ javascript }) =>
|
||||
javascript !== null &&
|
||||
[
|
||||
javascript.electron.browser_windows,
|
||||
javascript.electron.context_bridge_apis,
|
||||
javascript.electron.ipc,
|
||||
javascript.electron.sender_validations,
|
||||
javascript.electron.utility_processes,
|
||||
javascript.electron.native_addon_bindings,
|
||||
javascript.electron.protocols,
|
||||
javascript.electron.app_path_changes ?? [],
|
||||
javascript.electron.call_paths ?? [],
|
||||
].some((findings) =>
|
||||
findings.some((finding) => !provenElectronFinding(finding)),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Whether the Electron boundary summary came back empty. `undefined` means no
|
||||
* analyzed source names Electron, which is unknown rather than absent: unread
|
||||
@@ -209,36 +249,45 @@ export const summarizeElectronBoundaries = (
|
||||
const javascript = analysis.files.flatMap(({ javascript: value }) =>
|
||||
value === null ? [] : [value],
|
||||
);
|
||||
const windows = javascript.flatMap(
|
||||
({ electron }) => electron.browser_windows,
|
||||
const windows = javascript.flatMap(({ electron }) =>
|
||||
electron.browser_windows.filter(provenElectronFinding),
|
||||
);
|
||||
const bridges = javascript.flatMap(
|
||||
({ electron }) => electron.context_bridge_apis,
|
||||
const bridges = javascript.flatMap(({ electron }) =>
|
||||
electron.context_bridge_apis.filter(provenElectronFinding),
|
||||
);
|
||||
const validations = javascript.flatMap(
|
||||
({ electron }) => electron.sender_validations,
|
||||
const validations = javascript.flatMap(({ electron }) =>
|
||||
electron.sender_validations.filter(provenElectronFinding),
|
||||
);
|
||||
const utilities = analysis.files.flatMap(({ file, javascript: value }) =>
|
||||
(value?.electron.utility_processes ?? []).map((finding) => ({
|
||||
file,
|
||||
finding,
|
||||
})),
|
||||
(value?.electron.utility_processes ?? [])
|
||||
.filter(provenElectronFinding)
|
||||
.map((finding) => ({
|
||||
file,
|
||||
finding,
|
||||
})),
|
||||
);
|
||||
const nativeBindings = analysis.files.flatMap(({ file, javascript: value }) =>
|
||||
(value?.electron.native_addon_bindings ?? []).map((finding) => ({
|
||||
file,
|
||||
finding,
|
||||
})),
|
||||
(value?.electron.native_addon_bindings ?? [])
|
||||
.filter(provenElectronFinding)
|
||||
.map((finding) => ({
|
||||
file,
|
||||
finding,
|
||||
})),
|
||||
);
|
||||
const ipc = ipcContext.records.filter(({ finding }) =>
|
||||
provenElectronFinding(finding),
|
||||
);
|
||||
const pairing = ipcContext.states.filter(({ transmission }) =>
|
||||
provenElectronFinding(transmission.finding),
|
||||
);
|
||||
const ipc = ipcContext.records;
|
||||
const pairing = ipcContext.states;
|
||||
const literalChannels = new Set(
|
||||
ipc.flatMap(({ finding }) =>
|
||||
finding.channel === null ? [] : [finding.channel],
|
||||
),
|
||||
);
|
||||
const protocolCount = javascript.reduce(
|
||||
(count, { electron }) => count + electron.protocols.length,
|
||||
(count, { electron }) =>
|
||||
count + electron.protocols.filter(provenElectronFinding).length,
|
||||
0,
|
||||
);
|
||||
// The coverage state is read from the retained analysis so that a rejected
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
import {
|
||||
addElectronInferenceEdge,
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
electronFindingSourceNode,
|
||||
} from "./ElectronBoundaryGraphContext.js";
|
||||
import {
|
||||
@@ -64,6 +65,9 @@ export const addElectronCallPaths = (
|
||||
module_runtime: step.module_runtime,
|
||||
conditions: path.conditions,
|
||||
resolution: "candidate",
|
||||
...(path.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: staticInferenceEvidence({
|
||||
sha256: file.sha256,
|
||||
@@ -71,8 +75,14 @@ export const addElectronCallPaths = (
|
||||
range: step.location,
|
||||
operation: "substitute-electron-wrapper",
|
||||
coverage,
|
||||
...(path.loader_authority === "candidate"
|
||||
? { confidence: "low" as const }
|
||||
: {}),
|
||||
limitations: [
|
||||
"Lexical wrapper substitution is a static candidate; guards, runtime overrides and execution remain unverified.",
|
||||
...(path.loader_authority === "candidate"
|
||||
? [ELECTRON_CANDIDATE_LIMITATION]
|
||||
: []),
|
||||
],
|
||||
}),
|
||||
},
|
||||
@@ -98,6 +108,7 @@ export const addElectronCallPaths = (
|
||||
coverage,
|
||||
relation: "contains",
|
||||
operation: "locate-electron-wrapper-path",
|
||||
loaderAuthority: path.loader_authority,
|
||||
properties: {
|
||||
path_id: pathId,
|
||||
entry: path.entry,
|
||||
@@ -123,6 +134,7 @@ export const addElectronCallPaths = (
|
||||
? "maps_to"
|
||||
: "calls",
|
||||
operation: "trace-electron-wrapper-path",
|
||||
loaderAuthority: path.loader_authority,
|
||||
properties: {
|
||||
path_id: pathId,
|
||||
entry: path.entry,
|
||||
@@ -161,6 +173,11 @@ export const addElectronCallPaths = (
|
||||
);
|
||||
for (const from of sources)
|
||||
for (const to of targets) {
|
||||
if (
|
||||
from.path.loader_authority === "candidate" ||
|
||||
to.path.loader_authority === "candidate"
|
||||
)
|
||||
continue;
|
||||
const source = from.nodes.at(-1),
|
||||
target = to.nodes[0];
|
||||
if (!source || !target) continue;
|
||||
|
||||
@@ -33,8 +33,29 @@ export interface ElectronGraphEdgeInput {
|
||||
readonly properties: Readonly<Record<string, unknown>>;
|
||||
readonly confidence?: "high" | "medium" | "low";
|
||||
readonly limitations?: readonly string[];
|
||||
readonly loaderAuthority?: "candidate" | undefined;
|
||||
}
|
||||
|
||||
/** Candidate syntax does not establish that the receiver is Electron. */
|
||||
export const ELECTRON_CANDIDATE_LIMITATION =
|
||||
"The Electron module loader is unknown, so this relationship is only a candidate.";
|
||||
|
||||
/** Preserve syntax with uncertainty when its Electron receiver is unproven. */
|
||||
export const electronFindingEvidence = (
|
||||
input: Parameters<typeof astObservationEvidence>[0],
|
||||
loaderAuthority: "candidate" | undefined,
|
||||
) =>
|
||||
loaderAuthority === "candidate"
|
||||
? staticInferenceEvidence({
|
||||
...input,
|
||||
confidence: "low",
|
||||
limitations: [
|
||||
...(input.limitations ?? []),
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
],
|
||||
})
|
||||
: astObservationEvidence(input);
|
||||
|
||||
/** Resolve the file or recovered bundle module that owns one finding. */
|
||||
export const electronFindingSourceNode = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
@@ -69,17 +90,25 @@ export const addElectronAstEdge = (
|
||||
source_node_id: input.source.node_id,
|
||||
target_node_id: input.target.node_id,
|
||||
relation: input.relation,
|
||||
properties: input.properties,
|
||||
evidence: astObservationEvidence({
|
||||
sha256: input.file.sha256,
|
||||
path: input.file.path,
|
||||
range: input.range,
|
||||
operation: input.operation,
|
||||
coverage: input.coverage,
|
||||
...(input.limitations === undefined
|
||||
? {}
|
||||
: { limitations: input.limitations }),
|
||||
}),
|
||||
properties: {
|
||||
...input.properties,
|
||||
...(input.loaderAuthority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: electronFindingEvidence(
|
||||
{
|
||||
sha256: input.file.sha256,
|
||||
path: input.file.path,
|
||||
range: input.range,
|
||||
operation: input.operation,
|
||||
coverage: input.coverage,
|
||||
...(input.limitations === undefined
|
||||
? {}
|
||||
: { limitations: input.limitations }),
|
||||
},
|
||||
input.loaderAuthority,
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -92,19 +121,33 @@ export const addElectronInferenceEdge = (
|
||||
source_node_id: input.source.node_id,
|
||||
target_node_id: input.target.node_id,
|
||||
relation: input.relation,
|
||||
properties: input.properties,
|
||||
properties: {
|
||||
...input.properties,
|
||||
...(input.loaderAuthority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: staticInferenceEvidence({
|
||||
sha256: input.file.sha256,
|
||||
path: input.file.path,
|
||||
range: input.range,
|
||||
operation: input.operation,
|
||||
coverage: input.coverage,
|
||||
...(input.confidence === undefined
|
||||
? {}
|
||||
: { confidence: input.confidence }),
|
||||
...(input.limitations === undefined
|
||||
? {}
|
||||
: { limitations: input.limitations }),
|
||||
...(input.loaderAuthority === "candidate"
|
||||
? { confidence: "low" as const }
|
||||
: input.confidence === undefined
|
||||
? {}
|
||||
: { confidence: input.confidence }),
|
||||
...(input.loaderAuthority === "candidate"
|
||||
? {
|
||||
limitations: [
|
||||
...(input.limitations ?? []),
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
],
|
||||
}
|
||||
: input.limitations === undefined
|
||||
? {}
|
||||
: { limitations: input.limitations }),
|
||||
}),
|
||||
});
|
||||
};
|
||||
@@ -12,10 +12,7 @@ import {
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import {
|
||||
astObservationEvidence,
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { staticInferenceEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import type {
|
||||
ElectronIpcContext,
|
||||
ElectronIpcRecord,
|
||||
@@ -23,6 +20,8 @@ import type {
|
||||
import {
|
||||
addElectronAstEdge,
|
||||
addElectronInferenceEdge,
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
electronFindingEvidence,
|
||||
electronFindingSourceNode,
|
||||
electronObservationIdentity,
|
||||
} from "./ElectronBoundaryGraphContext.js";
|
||||
@@ -111,6 +110,7 @@ const addIpcRecord = (
|
||||
coverage,
|
||||
relation: record.finding.mode === "invoke" ? "invokes" : "sends",
|
||||
operation: "map-ipc-transmission",
|
||||
loaderAuthority: record.finding.loader_authority,
|
||||
properties: {
|
||||
side: record.finding.side,
|
||||
operation: record.finding.operation,
|
||||
@@ -150,10 +150,13 @@ const addChannelNode = (
|
||||
return context.accumulator.addNode({
|
||||
kind: "ipc-channel",
|
||||
identity:
|
||||
finding.resolution_basis === "wrapper-substitution"
|
||||
finding.resolution_basis === "wrapper-substitution" ||
|
||||
finding.loader_authority === "candidate"
|
||||
? artifactLocalIdentity(
|
||||
file.sha256,
|
||||
"electron-inferred-ipc-channel",
|
||||
finding.loader_authority === "candidate"
|
||||
? "electron-candidate-ipc-channel"
|
||||
: "electron-inferred-ipc-channel",
|
||||
identityKey,
|
||||
)
|
||||
: electronObservationIdentity(
|
||||
@@ -174,36 +177,67 @@ const addChannelNode = (
|
||||
channel: finding.channel,
|
||||
channel_expression: finding.channel_expression,
|
||||
resolution: finding.channel === null ? "dynamic" : "literal",
|
||||
...(finding.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
...(finding.invocation_location === undefined
|
||||
? {}
|
||||
: { invocation_location: finding.invocation_location }),
|
||||
},
|
||||
evidence: (finding.resolution_basis === "wrapper-substitution"
|
||||
? staticInferenceEvidence
|
||||
: astObservationEvidence)({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: finding.location,
|
||||
operation: "observe-ipc-channel-expression",
|
||||
coverage,
|
||||
limitations: [
|
||||
...(finding.receiver === undefined
|
||||
? []
|
||||
: [
|
||||
"The receiver is a source-level allocation, not a runtime frame identity; registration and dispatch remain unverified.",
|
||||
]),
|
||||
...(finding.resolution_basis !== "wrapper-substitution"
|
||||
? []
|
||||
: [
|
||||
"The channel is inferred through lexical wrapper substitution; runtime registration and dispatch remain unverified.",
|
||||
]),
|
||||
...(finding.channel !== null
|
||||
? []
|
||||
: [
|
||||
"The channel expression is dynamic; its runtime value and pairings remain unknown.",
|
||||
]),
|
||||
],
|
||||
}),
|
||||
evidence:
|
||||
finding.resolution_basis !== "wrapper-substitution"
|
||||
? electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: finding.location,
|
||||
operation: "observe-ipc-channel-expression",
|
||||
coverage,
|
||||
limitations: [
|
||||
...(finding.receiver === undefined
|
||||
? []
|
||||
: [
|
||||
"The receiver is a source-level allocation, not a runtime frame identity; registration and dispatch remain unverified.",
|
||||
]),
|
||||
...(finding.channel !== null
|
||||
? []
|
||||
: [
|
||||
"The channel expression is dynamic; its runtime value and pairings remain unknown.",
|
||||
]),
|
||||
],
|
||||
},
|
||||
finding.loader_authority,
|
||||
)
|
||||
: staticInferenceEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: finding.location,
|
||||
operation: "observe-ipc-channel-expression",
|
||||
coverage,
|
||||
...(finding.loader_authority === "candidate"
|
||||
? { confidence: "low" as const }
|
||||
: {}),
|
||||
limitations: [
|
||||
...(finding.loader_authority === "candidate"
|
||||
? [ELECTRON_CANDIDATE_LIMITATION]
|
||||
: []),
|
||||
...(finding.receiver === undefined
|
||||
? []
|
||||
: [
|
||||
"The receiver is a source-level allocation, not a runtime frame identity; registration and dispatch remain unverified.",
|
||||
]),
|
||||
...(finding.resolution_basis !== "wrapper-substitution"
|
||||
? []
|
||||
: [
|
||||
"The channel is inferred through lexical wrapper substitution; runtime registration and dispatch remain unverified.",
|
||||
]),
|
||||
...(finding.channel !== null
|
||||
? []
|
||||
: [
|
||||
"The channel expression is dynamic; its runtime value and pairings remain unknown.",
|
||||
]),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -245,19 +279,38 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
||||
channel_expression: finding.channel_expression,
|
||||
handler_kind: finding.handler_kind,
|
||||
handler_location: finding.handler_location,
|
||||
...(finding.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
...(finding.invocation_location === undefined
|
||||
? {}
|
||||
: { invocation_location: finding.invocation_location }),
|
||||
},
|
||||
evidence: (finding.resolution_basis === "wrapper-substitution"
|
||||
? staticInferenceEvidence
|
||||
: astObservationEvidence)({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: handlerRange,
|
||||
operation: "observe-ipc-handler",
|
||||
coverage,
|
||||
}),
|
||||
evidence:
|
||||
finding.resolution_basis !== "wrapper-substitution"
|
||||
? electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: handlerRange,
|
||||
operation: "observe-ipc-handler",
|
||||
coverage,
|
||||
},
|
||||
finding.loader_authority,
|
||||
)
|
||||
: staticInferenceEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: handlerRange,
|
||||
operation: "observe-ipc-handler",
|
||||
coverage,
|
||||
...(finding.loader_authority === "candidate"
|
||||
? {
|
||||
confidence: "low" as const,
|
||||
limitations: [ELECTRON_CANDIDATE_LIMITATION],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -270,6 +323,7 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
||||
relation: "contains",
|
||||
operation: "locate-ipc-handler",
|
||||
properties: { side: finding.side, operation: finding.operation },
|
||||
loaderAuthority: finding.loader_authority,
|
||||
});
|
||||
addElectronInferenceEdge(context, {
|
||||
source: handler,
|
||||
@@ -279,6 +333,7 @@ const addHandlerNode = (input: IpcHandlerInput): ApplicationNode => {
|
||||
coverage,
|
||||
relation: "handles",
|
||||
operation: "map-ipc-handler-registration",
|
||||
loaderAuthority: finding.loader_authority,
|
||||
properties: {
|
||||
channel: finding.channel,
|
||||
channel_expression: finding.channel_expression,
|
||||
@@ -362,17 +417,23 @@ const addValidationObservation = (input: ValidationObservationInput): void => {
|
||||
mechanism: validation.mechanism,
|
||||
expected: validation.expected,
|
||||
enforcement: validation.enforcement,
|
||||
...(validation.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: validation.location,
|
||||
operation: "observe-ipc-sender-validation-candidate",
|
||||
coverage,
|
||||
limitations: [
|
||||
"A comparison or string check is visible, but static syntax alone does not prove rejection, ordering, or complete sender validation.",
|
||||
],
|
||||
}),
|
||||
evidence: electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: validation.location,
|
||||
operation: "observe-ipc-sender-validation-candidate",
|
||||
coverage,
|
||||
limitations: [
|
||||
"A comparison or string check is visible, but static syntax alone does not prove rejection, ordering, or complete sender validation.",
|
||||
],
|
||||
},
|
||||
validation.loader_authority,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,15 +3,16 @@ import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSour
|
||||
import type { ElectronNativeAddonBindingFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
artifactLocalIdentity,
|
||||
javascriptAnalysisCoverage,
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
|
||||
import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValues.js";
|
||||
import {
|
||||
addElectronInferenceEdge,
|
||||
electronFindingEvidence,
|
||||
electronFindingSourceNode,
|
||||
electronObservationIdentity,
|
||||
} from "./ElectronBoundaryGraphContext.js";
|
||||
@@ -45,15 +46,18 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
value.module_runtime,
|
||||
);
|
||||
if (source === undefined) return;
|
||||
const resolved = resolveArtifactPathByContext({
|
||||
declaredPath: value.specifier,
|
||||
sourcePath: file.path,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
moduleKind: value.module_kind,
|
||||
}).resolved_path;
|
||||
const resolved =
|
||||
value.loader_authority === "candidate"
|
||||
? null
|
||||
: resolveArtifactPathByContext({
|
||||
declaredPath: value.specifier,
|
||||
sourcePath: file.path,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
moduleKind: value.module_kind,
|
||||
}).resolved_path;
|
||||
// Extension probing can resolve `./addon.node` to `addon.node.js`; only a
|
||||
// native-addon artifact can expose the requested binding.
|
||||
const addon =
|
||||
@@ -61,21 +65,29 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
context.filesByPath.get(resolved)?.kind !== "native-addon"
|
||||
? undefined
|
||||
: context.fileNodes.get(resolved);
|
||||
const bindingKey = compositeKey([
|
||||
file.path,
|
||||
value.specifier,
|
||||
value.binding_kind,
|
||||
value.members,
|
||||
value.namespace_access ?? null,
|
||||
value.dynamic_member_access ?? null,
|
||||
javascriptSourceRangeKey(value.location),
|
||||
]);
|
||||
const binding = context.accumulator.addNode({
|
||||
kind: "native-export",
|
||||
identity: electronObservationIdentity(
|
||||
context,
|
||||
"javascript-requested-native-export",
|
||||
compositeKey([
|
||||
file.path,
|
||||
value.specifier,
|
||||
value.binding_kind,
|
||||
value.members,
|
||||
value.namespace_access ?? null,
|
||||
value.dynamic_member_access ?? null,
|
||||
javascriptSourceRangeKey(value.location),
|
||||
]),
|
||||
),
|
||||
identity:
|
||||
value.loader_authority === "candidate"
|
||||
? artifactLocalIdentity(
|
||||
file.sha256,
|
||||
"javascript-requested-native-export",
|
||||
bindingKey,
|
||||
)
|
||||
: electronObservationIdentity(
|
||||
context,
|
||||
"javascript-requested-native-export",
|
||||
bindingKey,
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label:
|
||||
@@ -89,17 +101,23 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
dynamic_member_access: value.dynamic_member_access ?? null,
|
||||
resolved_path: resolved,
|
||||
native_export_verification: "not-performed",
|
||||
...(value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-native-addon-binding",
|
||||
coverage,
|
||||
limitations: [
|
||||
"JavaScript syntax identifies requested member names; it does not prove that the native binary exports or implements them.",
|
||||
],
|
||||
}),
|
||||
evidence: electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-native-addon-binding",
|
||||
coverage,
|
||||
limitations: [
|
||||
"JavaScript syntax identifies requested member names; it does not prove that the native binary exports or implements them.",
|
||||
],
|
||||
},
|
||||
value.loader_authority,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -111,6 +129,7 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
coverage,
|
||||
relation: value.binding_kind === "re-export" ? "exposes" : "imports",
|
||||
operation: "map-native-addon-binding",
|
||||
loaderAuthority: value.loader_authority,
|
||||
properties: {
|
||||
specifier: value.specifier,
|
||||
binding_kind: value.binding_kind,
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import { astObservationEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
addElectronInferenceEdge,
|
||||
electronFindingEvidence,
|
||||
electronFindingSourceNode,
|
||||
electronObservationIdentity,
|
||||
} from "./ElectronBoundaryGraphContext.js";
|
||||
@@ -67,6 +67,9 @@ const addProtocolRegistration = (input: ProtocolInput): void => {
|
||||
role: value.role,
|
||||
session: value.session,
|
||||
enabled_privileges: value.enabled_privileges,
|
||||
...(value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
};
|
||||
const limitations =
|
||||
scheme !== null
|
||||
@@ -86,10 +89,12 @@ const addProtocolRegistration = (input: ProtocolInput): void => {
|
||||
const node = context.accumulator.addNode({
|
||||
kind: "url-scheme",
|
||||
identity:
|
||||
scheme === null
|
||||
scheme === null || value.loader_authority === "candidate"
|
||||
? artifactLocalIdentity(
|
||||
file.sha256,
|
||||
"electron-dynamic-url-scheme",
|
||||
value.loader_authority === "candidate"
|
||||
? "electron-candidate-url-scheme"
|
||||
: "electron-dynamic-url-scheme",
|
||||
// Identical bytes at two paths are separate occurrences whose
|
||||
// expressions can resolve differently.
|
||||
`${file.path}:${value.api}:${javascriptSourceRangeKey(value.location)}`,
|
||||
@@ -103,14 +108,17 @@ const addProtocolRegistration = (input: ProtocolInput): void => {
|
||||
{
|
||||
label: scheme ?? value.scheme.expression ?? String(value.scheme.value),
|
||||
properties: facts,
|
||||
evidence: astObservationEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-protocol-registration",
|
||||
coverage,
|
||||
limitations,
|
||||
}),
|
||||
evidence: electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-protocol-registration",
|
||||
coverage,
|
||||
limitations,
|
||||
},
|
||||
value.loader_authority,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -123,6 +131,7 @@ const addProtocolRegistration = (input: ProtocolInput): void => {
|
||||
// Granting privileges exposes the scheme; the other roles claim its links.
|
||||
relation: value.role === "privileged-scheme" ? "exposes" : "handles",
|
||||
operation: "map-protocol-registration",
|
||||
loaderAuthority: value.loader_authority,
|
||||
properties: facts,
|
||||
limitations,
|
||||
});
|
||||
|
||||
@@ -14,13 +14,12 @@ import {
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
import {
|
||||
astObservationEvidence,
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { staticInferenceEvidence } from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
addElectronAstEdge,
|
||||
addElectronInferenceEdge,
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
electronFindingEvidence,
|
||||
electronFindingSourceNode,
|
||||
} from "./ElectronBoundaryGraphContext.js";
|
||||
import {
|
||||
@@ -98,6 +97,9 @@ const addBrowserWindow = (
|
||||
web_preferences: value.web_preferences,
|
||||
preload_path: value.preload_path,
|
||||
preload_resolution_context: value.preload_resolution_context,
|
||||
...(value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
...(value.resolution_sources === undefined
|
||||
? {}
|
||||
: {
|
||||
@@ -110,21 +112,40 @@ const addBrowserWindow = (
|
||||
}),
|
||||
absence_means_default: false,
|
||||
},
|
||||
evidence: (value.resolution_sources === undefined
|
||||
? astObservationEvidence
|
||||
: staticInferenceEvidence)({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation:
|
||||
value.resolution_sources === undefined
|
||||
? "observe-browser-window-construction"
|
||||
: "resolve-browser-window-constant-options",
|
||||
coverage,
|
||||
limitations: [
|
||||
"Only explicitly present webPreferences are reported; omitted properties are not interpreted as Electron defaults.",
|
||||
],
|
||||
}),
|
||||
evidence:
|
||||
value.resolution_sources === undefined
|
||||
? electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-browser-window-construction",
|
||||
coverage,
|
||||
limitations: [
|
||||
"Only explicitly present webPreferences are reported; omitted properties are not interpreted as Electron defaults.",
|
||||
],
|
||||
},
|
||||
value.loader_authority,
|
||||
)
|
||||
: staticInferenceEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation:
|
||||
value.resolution_sources === undefined
|
||||
? "observe-browser-window-construction"
|
||||
: "resolve-browser-window-constant-options",
|
||||
coverage,
|
||||
limitations: [
|
||||
"Only explicitly present webPreferences are reported; omitted properties are not interpreted as Electron defaults.",
|
||||
...(value.loader_authority === "candidate"
|
||||
? [ELECTRON_CANDIDATE_LIMITATION]
|
||||
: []),
|
||||
],
|
||||
...(value.loader_authority === "candidate"
|
||||
? { confidence: "low" as const }
|
||||
: {}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -137,6 +158,7 @@ const addBrowserWindow = (
|
||||
relation: "contains",
|
||||
operation: "locate-browser-window-construction",
|
||||
properties: { source_path: file.path },
|
||||
loaderAuthority: value.loader_authority,
|
||||
});
|
||||
addWindowPreload(input, window);
|
||||
};
|
||||
@@ -145,7 +167,11 @@ const addWindowPreload = (
|
||||
input: FindingInput<ElectronBrowserWindowFinding>,
|
||||
window: ApplicationNode,
|
||||
): void => {
|
||||
if (input.value.preload_path === null) return;
|
||||
if (
|
||||
input.value.preload_path === null ||
|
||||
input.value.loader_authority === "candidate"
|
||||
)
|
||||
return;
|
||||
const {
|
||||
preload_path: declared,
|
||||
preload_resolution_context: resolutionContext,
|
||||
@@ -217,16 +243,35 @@ const addContextBridge = (
|
||||
members: value.members,
|
||||
unknown_members: value.unknown_members,
|
||||
resolution_basis: value.resolution_basis ?? "direct-syntax",
|
||||
...(value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: (value.resolution_basis === "wrapper-substitution"
|
||||
? staticInferenceEvidence
|
||||
: astObservationEvidence)({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-context-bridge-api",
|
||||
coverage,
|
||||
}),
|
||||
evidence:
|
||||
value.resolution_basis !== "wrapper-substitution"
|
||||
? electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-context-bridge-api",
|
||||
coverage,
|
||||
},
|
||||
value.loader_authority,
|
||||
)
|
||||
: staticInferenceEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-context-bridge-api",
|
||||
coverage,
|
||||
...(value.loader_authority === "candidate"
|
||||
? {
|
||||
confidence: "low" as const,
|
||||
limitations: [ELECTRON_CANDIDATE_LIMITATION],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -238,6 +283,7 @@ const addContextBridge = (
|
||||
coverage,
|
||||
relation: "exposes",
|
||||
operation: "map-context-bridge-exposure",
|
||||
loaderAuthority: value.loader_authority,
|
||||
properties: {
|
||||
world: value.world,
|
||||
api_key: value.api_key,
|
||||
@@ -251,7 +297,7 @@ const addUtilityProcess = (
|
||||
): void => {
|
||||
const { context, file, value, coverage } = input;
|
||||
const resolution =
|
||||
value.module_path === null
|
||||
value.module_path === null || value.loader_authority === "candidate"
|
||||
? undefined
|
||||
: resolveArtifactPathByContext({
|
||||
declaredPath: value.module_path,
|
||||
@@ -283,14 +329,20 @@ const addUtilityProcess = (
|
||||
module_resolution_context: value.module_resolution_context,
|
||||
module_expression: value.module_expression,
|
||||
service_name: value.service_name,
|
||||
...(value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-utility-process-fork",
|
||||
coverage,
|
||||
}),
|
||||
evidence: electronFindingEvidence(
|
||||
{
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: value.location,
|
||||
operation: "observe-utility-process-fork",
|
||||
coverage,
|
||||
},
|
||||
value.loader_authority,
|
||||
),
|
||||
},
|
||||
...(resolution === undefined
|
||||
? []
|
||||
@@ -318,6 +370,7 @@ const addUtilityProcess = (
|
||||
coverage,
|
||||
relation: "loads",
|
||||
operation: "map-utility-process-launch",
|
||||
loaderAuthority: value.loader_authority,
|
||||
limitations: resolution?.limitations ?? [],
|
||||
properties: {
|
||||
declared_path: value.module_path,
|
||||
|
||||
@@ -129,7 +129,7 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
|
||||
signal,
|
||||
onPhase === undefined ? undefined : (phase) => onPhase(file, phase),
|
||||
preloadPaths.has(`${file.container_sha256}:${file.path}`) &&
|
||||
/\.js$/iu.test(file.path)
|
||||
posix.extname(file.path) === ".js"
|
||||
? "commonjs"
|
||||
: packageTypeFor(file),
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ElectronBoundarySummary } from "../../domain/javascript/javascript
|
||||
import {
|
||||
electronSemanticsState,
|
||||
createElectronIpcContext,
|
||||
hasCandidateElectronFindings,
|
||||
summarizeElectronBoundaries,
|
||||
type ElectronIpcContext,
|
||||
} from "./ElectronBoundaryAnalysis.js";
|
||||
@@ -381,6 +382,11 @@ const graphLimitations = (
|
||||
"CommonJS and ESM binding relationships were recovered from inert syntax and resolved only within the inventoried artifact container.",
|
||||
"Webpack/Rspack factories were recovered from AST literals; REA did not invoke push handlers or bundle bootstrap code.",
|
||||
"Static imports, entrypoints, workers, endpoints, and storage relationships do not prove runtime execution.",
|
||||
...(hasCandidateElectronFindings(context.analysis)
|
||||
? [
|
||||
"Conditional Electron findings are retained in the graph but excluded from proven boundary summary counts; their loader identity remains unknown.",
|
||||
]
|
||||
: []),
|
||||
...(context.analysis.files.some(({ javascript }) =>
|
||||
javascript?.limitations.includes(ELECTRON_IDENTITY_LIMITATION),
|
||||
)
|
||||
|
||||
@@ -118,9 +118,25 @@ const graphFor = async (
|
||||
};
|
||||
|
||||
describe("Electron semantic coverage", () => {
|
||||
it.each(["main.js", "main.MJS"])(
|
||||
"does not present candidate Electron boundaries as proven coverage for %s",
|
||||
async (path) => {
|
||||
const { limitations, electronSemantics } = await graphFor(
|
||||
'const { BrowserWindow } = require("electron"); new BrowserWindow();',
|
||||
undefined,
|
||||
path,
|
||||
);
|
||||
expect(electronSemantics).toBeUndefined();
|
||||
expect(limitations.join(" ")).toContain("Conditional Electron findings");
|
||||
expect(limitations).not.toContain(EMPTY_COVERAGE);
|
||||
},
|
||||
);
|
||||
|
||||
it("reports empty Electron coverage when the module is referenced but nothing is recovered", async () => {
|
||||
const { limitations, electronSemantics } = await graphFor(
|
||||
`const { app } = require("electron");\napp.whenReady();\n`,
|
||||
undefined,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(limitations).toContain(EMPTY_COVERAGE);
|
||||
expect(electronSemantics).toBe("empty");
|
||||
@@ -129,6 +145,8 @@ describe("Electron semantic coverage", () => {
|
||||
it("reports idle-coverage limits, not empty coverage, once a boundary fact is recovered", async () => {
|
||||
const { limitations, electronSemantics } = await graphFor(
|
||||
`const { ipcMain } = require("electron");\nipcMain.handle("ping", () => 1);\n`,
|
||||
undefined,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(limitations).not.toContain(EMPTY_COVERAGE);
|
||||
expect(limitations).toContainEqual(
|
||||
@@ -171,6 +189,8 @@ describe("Electron semantic coverage", () => {
|
||||
it("matches a supported Electron subpath and ignores an unrelated package", async () => {
|
||||
const subpath = await graphFor(
|
||||
`const { app } = require("electron/main");\napp.whenReady();\n`,
|
||||
undefined,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(subpath.electronSemantics).toBe("empty");
|
||||
const unrelated = await graphFor(
|
||||
@@ -193,6 +213,7 @@ describe("Electron semantic coverage", () => {
|
||||
limits: null,
|
||||
},
|
||||
}),
|
||||
"main.cjs",
|
||||
);
|
||||
expect(electronSemantics).toBe("empty");
|
||||
expect(limitations).toContain(EMPTY_COVERAGE);
|
||||
@@ -207,6 +228,7 @@ describe("Electron semantic coverage", () => {
|
||||
...entry,
|
||||
semantic: null,
|
||||
}),
|
||||
"main.cjs",
|
||||
);
|
||||
expect(electronSemantics).toBe("empty");
|
||||
});
|
||||
@@ -304,6 +326,8 @@ it("classifies a bundle module by its loader, not by its name", async () => {
|
||||
`100:(e,t,n)=>{"use strict";const s=n(45840);s.ipcMain.handle("x",()=>1)}`,
|
||||
`n(100);`,
|
||||
),
|
||||
undefined,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(external.electronSemantics).toBe("recovered");
|
||||
|
||||
@@ -315,6 +339,8 @@ it("classifies a bundle module by its loader, not by its name", async () => {
|
||||
`100:(e,t,n)=>{"use strict";const s=n("electron");s.ipcMain.handle("x",()=>1)}`,
|
||||
`n(100);`,
|
||||
),
|
||||
undefined,
|
||||
"main.cjs",
|
||||
);
|
||||
expect(externalUnderKey.electronSemantics).toBe("recovered");
|
||||
});
|
||||
@@ -205,7 +205,8 @@ const addReference = (
|
||||
input.value.module_runtime,
|
||||
) ?? input.asset;
|
||||
const resolved =
|
||||
input.value.specifier === null
|
||||
input.value.specifier === null ||
|
||||
input.value.loader_authority === "candidate"
|
||||
? null
|
||||
: resolveReference(context, input.file.path, input.value, input.coverage);
|
||||
if (
|
||||
@@ -319,11 +320,15 @@ const addReference = (
|
||||
operation: input.value.operation,
|
||||
specifier: input.value.specifier,
|
||||
expression: input.value.expression,
|
||||
...(input.value.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
resolution_base_path: input.value.resolution_base_path ?? null,
|
||||
resolution_mode:
|
||||
input.value.kind === "static-import" ||
|
||||
input.value.kind === "dynamic-import" ||
|
||||
input.value.kind === "require"
|
||||
(input.value.kind === "require" &&
|
||||
input.value.loader_authority !== "candidate")
|
||||
? moduleResolutionModeForJavaScriptReference(
|
||||
input.file.path,
|
||||
input.value.kind,
|
||||
@@ -333,7 +338,18 @@ const addReference = (
|
||||
resolution_status: resolved?.resolution?.resolution_status ?? null,
|
||||
...artifactPathResolutionProperties(resolved?.resolution),
|
||||
},
|
||||
...(resolved === null ? {} : { limitations: resolved.limitations }),
|
||||
...(input.value.loader_authority === "candidate"
|
||||
? { confidence: "low" as const }
|
||||
: {}),
|
||||
...(resolved === null
|
||||
? input.value.loader_authority === "candidate"
|
||||
? {
|
||||
limitations: [
|
||||
"The module loader is unknown, so this dependency cannot be resolved against the source file.",
|
||||
],
|
||||
}
|
||||
: {}
|
||||
: { limitations: resolved.limitations }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -354,7 +370,7 @@ const addEndpoint = (
|
||||
const candidateLimitation =
|
||||
input.value.kind === "route"
|
||||
? "Only the property name suggests a route; the enclosing object has no router fields."
|
||||
: "The unbound method-like argument is only a candidate; its receiver does not establish a network call.";
|
||||
: "The receiver or loader identity is unproven; this network endpoint is only a candidate.";
|
||||
const partialLimitation =
|
||||
"The request target is not an exact literal; the endpoint value retains the unresolved source expression.";
|
||||
const label = `${input.value.kind} ${candidate ? "endpoint candidate" : "endpoint"}`;
|
||||
@@ -388,7 +404,10 @@ const addEndpoint = (
|
||||
operation: "classify-static-endpoint-candidate",
|
||||
coverage: input.coverage,
|
||||
confidence: "low",
|
||||
limitations: [candidateLimitation],
|
||||
limitations: [
|
||||
candidateLimitation,
|
||||
...(partial ? [partialLimitation] : []),
|
||||
],
|
||||
})
|
||||
: astObservationEvidence({
|
||||
sha256: input.file.sha256,
|
||||
@@ -416,7 +435,10 @@ const addEndpoint = (
|
||||
...(candidate
|
||||
? {
|
||||
confidence: "low" as const,
|
||||
limitations: [candidateLimitation],
|
||||
limitations: [
|
||||
candidateLimitation,
|
||||
...(partial ? [partialLimitation] : []),
|
||||
],
|
||||
}
|
||||
: partial
|
||||
? { limitations: [partialLimitation] }
|
||||
@@ -496,6 +518,10 @@ const addDiscoveredRole = (
|
||||
input: FindingInput<StaticRolePath>,
|
||||
changedRoots: ReadonlySet<string> | null,
|
||||
): void => {
|
||||
if (input.value.loader_authority === "candidate") {
|
||||
addCandidateRole(context, input);
|
||||
return;
|
||||
}
|
||||
const root = applicationRootPath(context, input.file);
|
||||
const resolved = resolveArtifactPathByContext({
|
||||
declaredPath: input.value.path,
|
||||
@@ -575,6 +601,67 @@ const addDiscoveredRole = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Retain the requested path without asserting that an unknown loader creates an Electron role. */
|
||||
const addCandidateRole = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
input: FindingInput<StaticRolePath>,
|
||||
): void => {
|
||||
const limitation =
|
||||
"The Electron loader is unknown, so the renderer role and target ownership remain unproven.";
|
||||
const source =
|
||||
sourceNodeFor(
|
||||
context,
|
||||
input.file.path,
|
||||
input.value.module_key,
|
||||
input.value.module_runtime,
|
||||
) ?? input.asset;
|
||||
const properties = {
|
||||
candidate_role: input.value.role,
|
||||
loader_authority: "candidate",
|
||||
declared_path: input.value.path,
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
mechanism: input.value.mechanism,
|
||||
...(input.value.path_base === undefined
|
||||
? {}
|
||||
: { path_base: input.value.path_base }),
|
||||
};
|
||||
const role = context.accumulator.addNode({
|
||||
kind: "unknown",
|
||||
identity: artifactLocalIdentity(
|
||||
input.file.sha256,
|
||||
"candidate-electron-role",
|
||||
JSON.stringify([input.file.path, input.value.path, input.value.location]),
|
||||
),
|
||||
observations: [
|
||||
{
|
||||
label: input.value.path,
|
||||
properties,
|
||||
evidence: staticInferenceEvidence({
|
||||
sha256: input.file.sha256,
|
||||
path: input.file.path,
|
||||
range: input.value.location,
|
||||
operation: "retain-candidate-electron-role",
|
||||
coverage: input.coverage,
|
||||
confidence: "low",
|
||||
limitations: [limitation],
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
addStaticInferenceEdge(context, {
|
||||
source,
|
||||
target: role,
|
||||
file: input.file,
|
||||
range: input.value.location,
|
||||
coverage: input.coverage,
|
||||
relation: "loads",
|
||||
properties,
|
||||
confidence: "low",
|
||||
limitations: [limitation],
|
||||
});
|
||||
};
|
||||
|
||||
const addSourceMapEdge = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
input: FindingInput<StaticSourceMap>,
|
||||
@@ -925,6 +1012,9 @@ const unresolvedReferenceNode = (
|
||||
reference_kind: reference.kind,
|
||||
specifier: reference.specifier,
|
||||
expression: reference.expression,
|
||||
...(reference.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
resolution: "unresolved",
|
||||
...(resolution === undefined
|
||||
? {}
|
||||
@@ -940,6 +1030,9 @@ const unresolvedReferenceNode = (
|
||||
confidence: "low",
|
||||
limitations: [
|
||||
"The static reference could not be resolved within the inventoried artifact.",
|
||||
...(reference.loader_authority === "candidate"
|
||||
? ["The module loader is unknown."]
|
||||
: []),
|
||||
...(resolution?.limitations ?? []),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,11 @@ import { createHash } from "node:crypto";
|
||||
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
|
||||
import {
|
||||
analyzeJavaScriptStaticSource,
|
||||
analyzeParsedJavaScriptStaticSource,
|
||||
} from "../../domain/javascript/javascriptStaticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
|
||||
import type {
|
||||
AnalyzedJavaScriptArtifactFile,
|
||||
JavaScriptArtifactAnalysis,
|
||||
@@ -25,6 +29,13 @@ const SHA256 = "a".repeat(64);
|
||||
const REASON =
|
||||
"main.js: Resource constraint during analyze_javascript_application: Owned JavaScript worker exhausted its heap while analyzing main.js during static";
|
||||
|
||||
/** Preserve the explicit artifact filename when analyzing owned test fixtures. */
|
||||
const analyzeOwnedSource = (source: string, path: string) => {
|
||||
const parsed = parseJavaScriptSource(source, path);
|
||||
if (parsed === null) throw new Error(`Unable to parse ${path}`);
|
||||
return analyzeParsedJavaScriptStaticSource(source, parsed);
|
||||
};
|
||||
|
||||
const skippedFile = (): JavaScriptArtifactFile => ({
|
||||
path: "main.js",
|
||||
container_sha256: SHA256,
|
||||
@@ -245,15 +256,17 @@ it.each([false, true])(
|
||||
const analysis = analysisFor([
|
||||
{
|
||||
file: setter,
|
||||
javascript: analyzeJavaScriptStaticSource(
|
||||
javascript: analyzeOwnedSource(
|
||||
'const { app } = require("electron"); app.setAppPath("/changed");',
|
||||
setter.path,
|
||||
),
|
||||
semantic: null,
|
||||
},
|
||||
{
|
||||
file: window,
|
||||
javascript: analyzeJavaScriptStaticSource(
|
||||
javascript: analyzeOwnedSource(
|
||||
'const { BrowserWindow } = require("electron"); new BrowserWindow().loadFile("index.html");',
|
||||
window.path,
|
||||
),
|
||||
semantic: null,
|
||||
},
|
||||
@@ -361,20 +374,21 @@ it.each([
|
||||
const entries: AnalyzedJavaScriptArtifactFile[] = [
|
||||
{
|
||||
file: main,
|
||||
javascript: analyzeJavaScriptStaticSource(statement + window),
|
||||
javascript: analyzeOwnedSource(statement + window, main.path),
|
||||
semantic: null,
|
||||
},
|
||||
{
|
||||
file: other,
|
||||
javascript: analyzeJavaScriptStaticSource(window),
|
||||
javascript: analyzeOwnedSource(window, other.path),
|
||||
semantic: null,
|
||||
},
|
||||
...(gap === "transitive-unselected"
|
||||
? [
|
||||
{
|
||||
file: makeFile("a/middle.cjs"),
|
||||
javascript: analyzeJavaScriptStaticSource(
|
||||
javascript: analyzeOwnedSource(
|
||||
'require("./change.cjs");',
|
||||
"a/middle.cjs",
|
||||
),
|
||||
semantic: null,
|
||||
},
|
||||
|
||||
@@ -19,6 +19,8 @@ import type {
|
||||
JavaScriptSemanticModuleLink,
|
||||
} from "../../domain/javascript/javascriptSemanticIr.js";
|
||||
import {
|
||||
explicitJavaScriptLoaderMode,
|
||||
hasCaseVariantJavaScriptLoaderSuffix,
|
||||
isJsonModuleTarget,
|
||||
moduleResolutionModeForJavaScriptReference,
|
||||
} from "../../domain/javascript/javascriptModuleResolutionMode.js";
|
||||
@@ -88,8 +90,12 @@ export const addJavaScriptSourceModules = (
|
||||
if (asset === undefined || semantic === null || program === undefined)
|
||||
continue;
|
||||
const exports = semantic.ir.moduleLinks
|
||||
.flatMap(({ exportedName, kind }) =>
|
||||
exportedName === null || kind === "type-import" ? [] : [exportedName],
|
||||
.flatMap(({ exportedName, kind, loaderAuthority }) =>
|
||||
exportedName === null ||
|
||||
kind === "type-import" ||
|
||||
loaderAuthority === "candidate"
|
||||
? []
|
||||
: [exportedName],
|
||||
)
|
||||
.sort(compareUnicodeCodePoints);
|
||||
const typeExports = semantic.ir.moduleLinks
|
||||
@@ -97,7 +103,17 @@ export const addJavaScriptSourceModules = (
|
||||
exportedName === null || kind !== "type-import" ? [] : [exportedName],
|
||||
)
|
||||
.sort(compareUnicodeCodePoints);
|
||||
const retainedExports = [...new Set(exports)];
|
||||
const retainedExportSet = new Set(exports);
|
||||
const retainedExports = [...retainedExportSet];
|
||||
const candidateExportNames = new Set(
|
||||
semantic.ir.moduleLinks.flatMap(({ exportedName, loaderAuthority }) =>
|
||||
exportedName !== null &&
|
||||
loaderAuthority === "candidate" &&
|
||||
!retainedExportSet.has(exportedName)
|
||||
? [exportedName]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
const source = context.accumulator.addNode({
|
||||
kind: "javascript-module",
|
||||
identity: artifactLocalIdentity(
|
||||
@@ -124,7 +140,7 @@ export const addJavaScriptSourceModules = (
|
||||
export_relationships: exports.length,
|
||||
export_names: retainedExports,
|
||||
type_export_names: [...new Set(typeExports)],
|
||||
omitted_export_names: 0,
|
||||
omitted_export_names: candidateExportNames.size,
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
sha256: file.sha256,
|
||||
@@ -132,7 +148,14 @@ export const addJavaScriptSourceModules = (
|
||||
range: program.location,
|
||||
operation: "recover-source-module",
|
||||
coverage: semanticCoverage(semantic),
|
||||
limitations: semantic.ir.limitations,
|
||||
limitations: [
|
||||
...semantic.ir.limitations,
|
||||
...(candidateExportNames.size === 0
|
||||
? []
|
||||
: [
|
||||
"Candidate CommonJS export names are not included in the proven export names; the candidate relationships are retained separately.",
|
||||
]),
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -189,12 +212,17 @@ const addExportRelationship = (
|
||||
const { context, file, semantic, source, link } = input;
|
||||
if (link.exportedName === null) return;
|
||||
const baseCoverage = semanticCoverage(semantic);
|
||||
const returnShapes = projectJavaScriptExportReturnShapes({
|
||||
ir: semantic.ir,
|
||||
link,
|
||||
modulePath: file.path,
|
||||
baseCoverage,
|
||||
});
|
||||
const candidate = link.loaderAuthority === "candidate";
|
||||
const candidateLimitation =
|
||||
"The module wrapper is unknown, so this export is only a candidate.";
|
||||
const returnShapes = candidate
|
||||
? null
|
||||
: projectJavaScriptExportReturnShapes({
|
||||
ir: semantic.ir,
|
||||
link,
|
||||
modulePath: file.path,
|
||||
baseCoverage,
|
||||
});
|
||||
const exported = context.accumulator.addNode({
|
||||
kind: "javascript-module",
|
||||
identity: artifactLocalIdentity(
|
||||
@@ -214,14 +242,20 @@ const addExportRelationship = (
|
||||
local_name: link.localName,
|
||||
imported_name: link.importedName,
|
||||
declared_specifier: link.specifier,
|
||||
...(candidate ? { loader_authority: "candidate" } : {}),
|
||||
},
|
||||
evidence: astObservationEvidence({
|
||||
evidence: (candidate
|
||||
? staticInferenceEvidence
|
||||
: astObservationEvidence)({
|
||||
sha256: file.sha256,
|
||||
path: file.path,
|
||||
range: link.location,
|
||||
operation: "recover-module-export",
|
||||
coverage: baseCoverage,
|
||||
limitations: semantic.ir.limitations,
|
||||
limitations: candidate
|
||||
? [...semantic.ir.limitations, candidateLimitation]
|
||||
: semantic.ir.limitations,
|
||||
...(candidate ? { confidence: "low" as const } : {}),
|
||||
}),
|
||||
},
|
||||
...(returnShapes === null
|
||||
@@ -247,7 +281,12 @@ const addExportRelationship = (
|
||||
target_node_id: exported.node_id,
|
||||
relation: "exposes",
|
||||
properties: relationshipProperties(link, null),
|
||||
evidence: relationshipEvidence(input, "expose-module-export", []),
|
||||
evidence: relationshipEvidence(
|
||||
input,
|
||||
"expose-module-export",
|
||||
candidate ? [candidateLimitation] : [],
|
||||
candidate ? "low" : "high",
|
||||
),
|
||||
});
|
||||
const origin = moduleOriginForExport(semantic.ir, link);
|
||||
if (origin !== null)
|
||||
@@ -260,7 +299,7 @@ const addImportRelationship = (
|
||||
omissions: RelationshipOmissionCounter,
|
||||
origin: JavaScriptModuleOrigin,
|
||||
): void => {
|
||||
const target = resolveModuleTarget(input, origin.specifier);
|
||||
const target = resolveModuleTarget(input, origin);
|
||||
// A specifier that resolves back to the importing module itself (a literal
|
||||
// self-import, self-require, or artifact-confined path alias)
|
||||
// cannot become an edge: the application graph forbids self-referential
|
||||
@@ -289,8 +328,34 @@ const addImportRelationship = (
|
||||
|
||||
const resolveModuleTarget = (
|
||||
input: RelationshipInput,
|
||||
specifier: string,
|
||||
origin: JavaScriptModuleOrigin,
|
||||
): ResolvedModuleTarget => {
|
||||
const specifier = origin.specifier;
|
||||
if (
|
||||
input.link.loaderAuthority === "candidate" ||
|
||||
origin.loaderAuthority === "candidate"
|
||||
) {
|
||||
// The syntax retains a possible dependency, but an unproven wrapper
|
||||
// cannot establish the loader or its lookup base.
|
||||
const resolution: ArtifactPathResolution = {
|
||||
declared_path: specifier,
|
||||
resolution_context: "module-specifier",
|
||||
resolution_status: "unavailable",
|
||||
resolved_path: null,
|
||||
limitations: [
|
||||
"The module loader is unknown, so this dependency cannot be resolved against the source file.",
|
||||
],
|
||||
};
|
||||
return {
|
||||
node: unresolvedModuleNode(input, specifier, resolution),
|
||||
path: null,
|
||||
file: null,
|
||||
jsonStatus: null,
|
||||
status: resolution.resolution_status,
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
};
|
||||
}
|
||||
const resolution = resolveArtifactPathByContext({
|
||||
declaredPath: specifier,
|
||||
sourcePath: input.file.path,
|
||||
@@ -370,7 +435,10 @@ const unresolvedModuleNode = (
|
||||
operation: "retain-unresolved-semantic-module",
|
||||
coverage: semanticCoverage(input.semantic),
|
||||
confidence: "low",
|
||||
limitations: input.semantic.ir.limitations,
|
||||
limitations: [
|
||||
...input.semantic.ir.limitations,
|
||||
...resolution.limitations,
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
@@ -387,6 +455,9 @@ const moduleOriginForExport = (
|
||||
: { resolutionMode: link.resolutionMode }),
|
||||
specifier: link.specifier,
|
||||
importedPath: link.importedName === null ? [] : [link.importedName],
|
||||
...(link.loaderAuthority === "candidate"
|
||||
? { loaderAuthority: "candidate" as const }
|
||||
: {}),
|
||||
};
|
||||
if (link.localName === null) return null;
|
||||
const program = ir.scopes.find(({ kind }) => kind === "program");
|
||||
@@ -394,9 +465,14 @@ const moduleOriginForExport = (
|
||||
({ name, scopeId }) =>
|
||||
name === link.localName && scopeId === program?.scopeId,
|
||||
);
|
||||
return binding?.provenance.status === "module" &&
|
||||
binding.provenance.origins.length === 1
|
||||
? (binding.provenance.origins[0] ?? null)
|
||||
const provenance = binding?.provenance;
|
||||
if (provenance === undefined || provenance.origins.length !== 1) return null;
|
||||
const origin = provenance.origins[0];
|
||||
return origin !== undefined &&
|
||||
(provenance.status === "module" ||
|
||||
(provenance.status === "ambiguous" &&
|
||||
origin.loaderAuthority === "candidate"))
|
||||
? origin
|
||||
: null;
|
||||
};
|
||||
|
||||
@@ -406,7 +482,14 @@ const relationshipProperties = (
|
||||
origin?: JavaScriptModuleOrigin,
|
||||
) => ({
|
||||
module_link_kind: link.kind,
|
||||
...moduleResolutionModeProperties(link.resolutionMode),
|
||||
...(link.loaderAuthority === "candidate" ||
|
||||
origin?.loaderAuthority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
...(link.loaderAuthority === "candidate" ||
|
||||
origin?.loaderAuthority === "candidate"
|
||||
? { resolution_mode: null }
|
||||
: moduleResolutionModeProperties(link.resolutionMode)),
|
||||
specifier: origin?.specifier ?? link.specifier,
|
||||
imported_name: origin?.importedPath.at(-1) ?? link.importedName,
|
||||
imported_path: origin?.importedPath ?? [],
|
||||
@@ -457,15 +540,19 @@ const moduleFormat = (
|
||||
path: string,
|
||||
ir: JavaScriptModuleSemanticIr,
|
||||
): "commonjs" | "esm" | "mixed" | "unknown" => {
|
||||
const loader = explicitJavaScriptLoaderMode(path);
|
||||
if (loader === "module") return "esm";
|
||||
if (loader === "commonjs") return "commonjs";
|
||||
if (hasCaseVariantJavaScriptLoaderSuffix(path)) return "unknown";
|
||||
const extension = posix.extname(path).toLowerCase();
|
||||
if (extension === ".mjs" || extension === ".mts") return "esm";
|
||||
if (extension === ".cjs" || extension === ".cts") return "commonjs";
|
||||
if (extension === ".ts" || extension === ".tsx") return "unknown";
|
||||
const esm = ir.moduleLinks.some(({ kind }) =>
|
||||
["import", "export", "re-export"].includes(kind),
|
||||
);
|
||||
const commonJs = ir.moduleLinks.some(({ kind }) =>
|
||||
["require", "commonjs-export"].includes(kind),
|
||||
const commonJs = ir.moduleLinks.some(
|
||||
({ kind, loaderAuthority }) =>
|
||||
loaderAuthority !== "candidate" &&
|
||||
["require", "commonjs-export"].includes(kind),
|
||||
);
|
||||
return esm && commonJs
|
||||
? "mixed"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { posix } from "node:path";
|
||||
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
|
||||
import {
|
||||
@@ -41,7 +43,7 @@ export const discoverJavaScriptPreloadPaths = async (
|
||||
(file) =>
|
||||
file.kind === "javascript" &&
|
||||
file.text.included &&
|
||||
/\.js$/iu.test(file.path) &&
|
||||
posix.extname(file.path) === ".js" &&
|
||||
packageTypeFor(file) === "module",
|
||||
)
|
||||
)
|
||||
@@ -87,7 +89,11 @@ export const discoverJavaScriptPreloadPaths = async (
|
||||
break;
|
||||
}
|
||||
for (const window of result.value.javascript.electron.browser_windows) {
|
||||
if (window.preload_path === null) continue;
|
||||
if (
|
||||
window.preload_path === null ||
|
||||
window.loader_authority === "candidate"
|
||||
)
|
||||
continue;
|
||||
const resolution = resolveArtifactPathByContext({
|
||||
declaredPath: window.preload_path,
|
||||
sourcePath: file.path,
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
addSemanticGraphRelation,
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
unknownSemanticEvidence,
|
||||
builtinSemanticNode,
|
||||
builtinSemanticRelation,
|
||||
} from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
projectSemanticResultBindings,
|
||||
type SemanticFlowProjectionContext,
|
||||
@@ -158,10 +162,10 @@ export const projectSemanticTimers = (
|
||||
location: operation.location,
|
||||
label: operation.method,
|
||||
functionNodeId: owner?.node_id ?? null,
|
||||
properties: {
|
||||
...builtinSemanticNode(context.file, operation, {
|
||||
delay_milliseconds: operation.delayMilliseconds,
|
||||
method: operation.method,
|
||||
},
|
||||
}),
|
||||
});
|
||||
return timer === null ? [] : [[operation.timerId, timer] as const];
|
||||
}),
|
||||
@@ -177,17 +181,39 @@ const projectTimerSchedule = (
|
||||
operation: JavaScriptSemanticTimerOperation,
|
||||
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
): void => {
|
||||
const authority = builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
"resolved",
|
||||
);
|
||||
const candidateEvidence = authority.evidence;
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: context.callSiteAt(operation.location),
|
||||
target: timerNodes.get(operation.timerId),
|
||||
relation: "schedules-timer",
|
||||
resolution: "resolved",
|
||||
...authority,
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: {
|
||||
properties: { builtin_authority: "candidate" },
|
||||
}),
|
||||
});
|
||||
projectSemanticResultBindings(
|
||||
context,
|
||||
timerNodes.get(operation.timerId),
|
||||
operation.projectedResultBindings,
|
||||
candidateEvidence,
|
||||
);
|
||||
if (candidateEvidence !== undefined)
|
||||
addSemanticGraphUnknown(context.state, {
|
||||
node_id: timerNodes.get(operation.timerId)?.node_id ?? null,
|
||||
family: "timer",
|
||||
relation_kinds: ["schedules-timer"],
|
||||
reason: "ambiguous-target",
|
||||
detail: "The timer identity is conditional on loader or mutation facts.",
|
||||
candidate_node_ids: [],
|
||||
evidence: unknownSemanticEvidence(context.file, operation.location),
|
||||
});
|
||||
};
|
||||
|
||||
const projectTimerCancellation = (
|
||||
@@ -196,6 +222,12 @@ const projectTimerCancellation = (
|
||||
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
): void => {
|
||||
const callSite = context.callSiteAt(operation.location);
|
||||
const authority = builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
);
|
||||
const candidateEvidence = authority.evidence;
|
||||
const target =
|
||||
operation.linkedTimerId === null
|
||||
? undefined
|
||||
@@ -204,15 +236,24 @@ const projectTimerCancellation = (
|
||||
source: callSite,
|
||||
target,
|
||||
relation: "cancels-timer",
|
||||
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
...authority,
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: {
|
||||
properties: { builtin_authority: "candidate" },
|
||||
}),
|
||||
});
|
||||
if (operation.resolution === "complete") return;
|
||||
if (operation.resolution === "complete" && candidateEvidence === undefined)
|
||||
return;
|
||||
addSemanticGraphUnknown(context.state, {
|
||||
node_id: callSite?.node_id ?? null,
|
||||
family: "timer",
|
||||
relation_kinds: ["cancels-timer"],
|
||||
reason: "ambiguous-target",
|
||||
detail: `Static ${operation.method} handle resolution was ${operation.resolution}.`,
|
||||
detail:
|
||||
candidateEvidence === undefined
|
||||
? `Static ${operation.method} handle resolution was ${operation.resolution}.`
|
||||
: `The ${operation.method} identity is conditional on loader or mutation facts.`,
|
||||
candidate_node_ids: [],
|
||||
evidence: unknownSemanticEvidence(context.file, operation.location),
|
||||
});
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
addSemanticGraphRelation,
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
unknownSemanticEvidence,
|
||||
builtinSemanticNode,
|
||||
builtinSemanticRelation,
|
||||
} from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
projectSemanticResultBindings,
|
||||
type SemanticFlowProjectionContext,
|
||||
@@ -62,13 +66,13 @@ const addChildProcessNode = (
|
||||
spawn.ownerCallableId === null
|
||||
? null
|
||||
: (context.callableNodes.get(spawn.ownerCallableId)?.node_id ?? null),
|
||||
properties: {
|
||||
...builtinSemanticNode(context.file, spawn, {
|
||||
argv_count: spawn.argvCount,
|
||||
command: spawn.command,
|
||||
environment_supplied: spawn.environmentSupplied,
|
||||
method: spawn.method,
|
||||
stdio_mode: spawn.stdioMode,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const projectSpawn = (
|
||||
@@ -77,19 +81,38 @@ const projectSpawn = (
|
||||
child: JavaScriptSemanticGraphNode,
|
||||
): void => {
|
||||
const callSite = context.callSiteAt(spawn.location);
|
||||
const authority = builtinSemanticRelation(
|
||||
context.file,
|
||||
spawn,
|
||||
spawn.resolution === "complete" ? "resolved" : "candidate",
|
||||
);
|
||||
const candidateEvidence = authority.evidence;
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: callSite,
|
||||
target: child,
|
||||
relation: "spawns",
|
||||
resolution: spawn.resolution === "complete" ? "resolved" : "candidate",
|
||||
...authority,
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: {
|
||||
properties: { builtin_authority: "candidate" },
|
||||
}),
|
||||
});
|
||||
projectSemanticResultBindings(context, child, spawn.projectedResultBindings);
|
||||
projectSemanticResultBindings(
|
||||
context,
|
||||
child,
|
||||
spawn.projectedResultBindings,
|
||||
candidateEvidence,
|
||||
);
|
||||
if (spawn.argvCount !== null)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: callSite,
|
||||
target: child,
|
||||
relation: "supplies-argv",
|
||||
resolution: "resolved",
|
||||
resolution: candidateEvidence === undefined ? "resolved" : "candidate",
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: { evidence: candidateEvidence }),
|
||||
properties: { argument_count: spawn.argvCount },
|
||||
});
|
||||
if (spawn.environmentSupplied)
|
||||
@@ -97,14 +120,21 @@ const projectSpawn = (
|
||||
source: callSite,
|
||||
target: child,
|
||||
relation: "supplies-env",
|
||||
resolution: "resolved",
|
||||
resolution: candidateEvidence === undefined ? "resolved" : "candidate",
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: { evidence: candidateEvidence }),
|
||||
});
|
||||
const stdio = addStdioNode(context, spawn, child);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: child,
|
||||
target: stdio,
|
||||
relation: "connects-stdio",
|
||||
resolution: spawn.stdioMode === "dynamic" ? "candidate" : "resolved",
|
||||
resolution:
|
||||
candidateEvidence !== undefined || spawn.stdioMode === "dynamic"
|
||||
? "candidate"
|
||||
: "resolved",
|
||||
...(candidateEvidence === undefined ? {} : { evidence: candidateEvidence }),
|
||||
});
|
||||
if (spawn.stdioMode === "dynamic")
|
||||
addChildUnknown({
|
||||
@@ -116,6 +146,16 @@ const projectSpawn = (
|
||||
detail: `Static ${spawn.method} stdio options remain unresolved.`,
|
||||
candidateNodeIds: [],
|
||||
});
|
||||
if (candidateEvidence !== undefined)
|
||||
addChildUnknown({
|
||||
context,
|
||||
node: child,
|
||||
location: spawn.location,
|
||||
relationKinds: ["spawns"],
|
||||
detail:
|
||||
"The child-process identity is conditional on loader or mutation facts.",
|
||||
candidateNodeIds: [child.node_id],
|
||||
});
|
||||
if (spawn.resolution !== "complete")
|
||||
addChildUnknown({
|
||||
context,
|
||||
@@ -138,7 +178,9 @@ const addStdioNode = (
|
||||
location: spawn.location,
|
||||
label: spawn.stdioMode,
|
||||
functionNodeId: child.function_node_id,
|
||||
properties: { mode: spawn.stdioMode },
|
||||
...builtinSemanticNode(context.file, spawn, {
|
||||
mode: spawn.stdioMode,
|
||||
}),
|
||||
});
|
||||
|
||||
const projectInteraction = (
|
||||
@@ -146,6 +188,12 @@ const projectInteraction = (
|
||||
interaction: JavaScriptSemanticChildProcessInteraction,
|
||||
processNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
): void => {
|
||||
const authority = builtinSemanticRelation(
|
||||
context.file,
|
||||
interaction,
|
||||
interaction.resolution === "complete" ? "resolved" : "candidate",
|
||||
);
|
||||
const candidateEvidence = authority.evidence;
|
||||
const children = interaction.linkedProcessIds.flatMap((identifier) => {
|
||||
const child = processNodes.get(identifier);
|
||||
return child === undefined ? [] : [child];
|
||||
@@ -164,8 +212,12 @@ const projectInteraction = (
|
||||
: interaction.eventName === "exit"
|
||||
? "listens-exit"
|
||||
: "listens-error",
|
||||
resolution:
|
||||
interaction.resolution === "complete" ? "resolved" : "candidate",
|
||||
...authority,
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: {
|
||||
properties: { builtin_authority: "candidate" },
|
||||
}),
|
||||
});
|
||||
if (interaction.resolution !== "complete")
|
||||
addChildUnknown({
|
||||
@@ -198,10 +250,10 @@ const addChildListenerNode = (
|
||||
? null
|
||||
: (context.callableNodes.get(interaction.ownerCallableId)?.node_id ??
|
||||
null),
|
||||
properties: {
|
||||
...builtinSemanticNode(context.file, interaction, {
|
||||
event_name: interaction.eventName,
|
||||
method: interaction.method,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const addSignalNode = (
|
||||
@@ -218,7 +270,9 @@ const addSignalNode = (
|
||||
? null
|
||||
: (context.callableNodes.get(interaction.ownerCallableId)?.node_id ??
|
||||
null),
|
||||
properties: { signal_name: interaction.signalName },
|
||||
...builtinSemanticNode(context.file, interaction, {
|
||||
signal_name: interaction.signalName,
|
||||
}),
|
||||
});
|
||||
|
||||
const addChildUnknown = (input: ChildUnknownInput): void => {
|
||||
|
||||
@@ -64,6 +64,8 @@ export interface SemanticNodeConstructionInput {
|
||||
readonly label: string | null;
|
||||
readonly functionNodeId: string | null;
|
||||
readonly properties?: Readonly<Record<string, JsonValue>>;
|
||||
/** Evidence for inferred semantic identities; defaults to exact syntax. */
|
||||
readonly evidence?: ApplicationGraphEvidence;
|
||||
}
|
||||
|
||||
/** Input for one directed static semantic relationship. */
|
||||
@@ -131,7 +133,8 @@ export const constructSemanticGraphNode = (
|
||||
application_node_ids: [],
|
||||
label: input.label === null ? null : javascriptDisplayText(input.label),
|
||||
properties: input.properties ?? {},
|
||||
evidence: observedSemanticEvidence(file, input.location),
|
||||
evidence:
|
||||
input.evidence ?? observedSemanticEvidence(file, input.location),
|
||||
},
|
||||
state.evidenceContexts,
|
||||
);
|
||||
|
||||
@@ -9,7 +9,11 @@ import {
|
||||
addSemanticGraphRelation,
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
builtinSemanticNode,
|
||||
builtinSemanticRelation,
|
||||
unknownSemanticEvidence,
|
||||
} from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
projectSemanticResultBindings,
|
||||
type SemanticFlowProjectionContext,
|
||||
@@ -38,16 +42,22 @@ export const projectSemanticConfiguration = (
|
||||
source: config,
|
||||
target,
|
||||
relation: configurationRelation(operation),
|
||||
resolution:
|
||||
...builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
),
|
||||
});
|
||||
if (operation.kind === "default" && operation.sourceConfigId !== null)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: configNodes.get(operation.sourceConfigId),
|
||||
target: config,
|
||||
relation: "overrides",
|
||||
resolution:
|
||||
...builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
),
|
||||
});
|
||||
if (operation.resolution !== "complete")
|
||||
addDataUnknown({
|
||||
@@ -89,14 +99,14 @@ const addConfigurationNode = (
|
||||
? null
|
||||
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
|
||||
null),
|
||||
properties: {
|
||||
...builtinSemanticNode(context.file, operation, {
|
||||
key: operation.key,
|
||||
source_kind: operation.kind,
|
||||
value: operation.value,
|
||||
...(operation.valueResourceLimit === undefined
|
||||
? {}
|
||||
: { value_resource_limit: operation.valueResourceLimit }),
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const configurationRelation = (
|
||||
@@ -163,11 +173,11 @@ const addRequestNode = (
|
||||
? null
|
||||
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
|
||||
null),
|
||||
properties: {
|
||||
...builtinSemanticNode(context.file, operation, {
|
||||
endpoint: operation.endpoint,
|
||||
method: operation.method,
|
||||
operation_kind: operation.kind,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const projectRequestConstruction = (
|
||||
@@ -176,16 +186,22 @@ const projectRequestConstruction = (
|
||||
request: JavaScriptSemanticGraphNode,
|
||||
): void => {
|
||||
const callSite = context.callSiteAt(operation.location);
|
||||
const authority = builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: callSite,
|
||||
target: request,
|
||||
relation: "constructs-request",
|
||||
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
...authority,
|
||||
});
|
||||
projectSemanticResultBindings(
|
||||
context,
|
||||
request,
|
||||
operation.projectedResultBindings,
|
||||
authority.evidence,
|
||||
);
|
||||
for (const field of operation.fields)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
@@ -195,7 +211,11 @@ const projectRequestConstruction = (
|
||||
: context.bindingNodes.get(field.sourceBindingId),
|
||||
target: request,
|
||||
relation: "supplies-request-field",
|
||||
resolution: field.sourceBindingId === null ? "candidate" : "resolved",
|
||||
...builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
field.sourceBindingId === null ? "candidate" : "resolved",
|
||||
),
|
||||
properties: { field: field.name },
|
||||
});
|
||||
};
|
||||
@@ -209,7 +229,11 @@ const projectResponseConsumer = (
|
||||
source: response,
|
||||
target: context.callSiteAt(operation.location),
|
||||
relation: "consumed-by",
|
||||
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
...builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
),
|
||||
properties: { method: operation.method },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type SemanticGraphProjectionState,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { inferredSemanticEvidenceAt } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import { ELECTRON_CANDIDATE_LIMITATION } from "./ElectronBoundaryGraphContext.js";
|
||||
|
||||
/** Give discovered process-boundary paths budget before bulk library definitions. */
|
||||
export function* projectElectronSemanticCallPaths(
|
||||
@@ -20,6 +21,18 @@ export function* projectElectronSemanticCallPaths(
|
||||
let previous: ReturnType<typeof retainSemanticGraphNode> = null;
|
||||
let previousKind: ElectronCallPath["steps"][number]["kind"] | undefined;
|
||||
for (const [index, step] of path.steps.entries()) {
|
||||
const inferred = inferredSemanticEvidenceAt(file, step.location);
|
||||
const candidateEvidence =
|
||||
path.loader_authority === "candidate"
|
||||
? {
|
||||
...inferred,
|
||||
confidence: "low" as const,
|
||||
limitations: [
|
||||
...inferred.limitations,
|
||||
ELECTRON_CANDIDATE_LIMITATION,
|
||||
],
|
||||
}
|
||||
: undefined;
|
||||
const node = retainSemanticGraphNode(state, file, {
|
||||
kind:
|
||||
step.kind === "function"
|
||||
@@ -31,6 +44,9 @@ export function* projectElectronSemanticCallPaths(
|
||||
location: step.location,
|
||||
label: step.label,
|
||||
functionNodeId: null,
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: { evidence: candidateEvidence }),
|
||||
properties: {
|
||||
path_id: pathId,
|
||||
entry: path.entry,
|
||||
@@ -39,6 +55,9 @@ export function* projectElectronSemanticCallPaths(
|
||||
: { entry_path: [...path.entry_path] }),
|
||||
channel: path.channel,
|
||||
terminal: path.terminal,
|
||||
...(path.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
step_kind: step.kind,
|
||||
resolution: "candidate",
|
||||
conditions: path.conditions.map((c) => ({
|
||||
@@ -61,7 +80,7 @@ export function* projectElectronSemanticCallPaths(
|
||||
? "aliases"
|
||||
: "calls",
|
||||
resolution: "candidate",
|
||||
evidence: inferredSemanticEvidenceAt(file, step.location),
|
||||
evidence: candidateEvidence ?? inferred,
|
||||
properties: {
|
||||
path_id: pathId,
|
||||
entry: path.entry,
|
||||
@@ -70,6 +89,9 @@ export function* projectElectronSemanticCallPaths(
|
||||
: { entry_path: [...path.entry_path] }),
|
||||
channel: path.channel,
|
||||
basis: "lexical-wrapper-substitution",
|
||||
...(path.loader_authority === "candidate"
|
||||
? { loader_authority: "candidate" }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
previous = node;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
|
||||
const INFERENCE_LIMITATION =
|
||||
"This relationship is a conservative static candidate; it does not prove runtime execution or causal flow.";
|
||||
@@ -64,6 +65,59 @@ export const inferredSemanticEvidenceAt = (
|
||||
limitations: [INFERENCE_LIMITATION],
|
||||
});
|
||||
|
||||
/** A built-in operation whose identity is conditional on loader or mutation facts. */
|
||||
export const candidateBuiltinSemanticEvidence = (
|
||||
file: JavaScriptArtifactFile,
|
||||
location: JavaScriptSourceRange,
|
||||
): ApplicationGraphEvidence => {
|
||||
const evidence = inferredSemanticEvidenceAt(file, location);
|
||||
return {
|
||||
...evidence,
|
||||
confidence: "low",
|
||||
limitations: [
|
||||
...evidence.limitations,
|
||||
"The built-in identity or its mutation state is unproven; this operation is conditional.",
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
type SemanticBuiltinOperation = {
|
||||
readonly builtinAuthority?: "candidate" | undefined;
|
||||
readonly location: JavaScriptSourceRange;
|
||||
};
|
||||
|
||||
/** Keep the candidate marker and its Evidence together on built-in nodes. */
|
||||
export const builtinSemanticNode = (
|
||||
file: JavaScriptArtifactFile,
|
||||
operation: SemanticBuiltinOperation,
|
||||
properties: Readonly<Record<string, JsonValue>>,
|
||||
): {
|
||||
readonly properties: Readonly<Record<string, JsonValue>>;
|
||||
readonly evidence?: ApplicationGraphEvidence;
|
||||
} =>
|
||||
operation.builtinAuthority === "candidate"
|
||||
? {
|
||||
evidence: candidateBuiltinSemanticEvidence(file, operation.location),
|
||||
properties: { ...properties, builtin_authority: "candidate" },
|
||||
}
|
||||
: { properties };
|
||||
|
||||
/** Keep conditional built-in authority and relation resolution together. */
|
||||
export const builtinSemanticRelation = (
|
||||
file: JavaScriptArtifactFile,
|
||||
operation: SemanticBuiltinOperation,
|
||||
resolution: "resolved" | "candidate",
|
||||
): {
|
||||
readonly resolution: "resolved" | "candidate";
|
||||
readonly evidence?: ApplicationGraphEvidence;
|
||||
} =>
|
||||
operation.builtinAuthority === "candidate"
|
||||
? {
|
||||
resolution: "candidate",
|
||||
evidence: candidateBuiltinSemanticEvidence(file, operation.location),
|
||||
}
|
||||
: { resolution };
|
||||
|
||||
/** Explicit absence of semantic syntax for an artifact-level fallback root. */
|
||||
export const unavailableSemanticRootEvidence = (
|
||||
rootArtifactSha256: string,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import { javaScriptSemanticNodeId } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type {
|
||||
JavaScriptSemanticIr,
|
||||
@@ -45,6 +46,7 @@ export const projectSemanticResultBindings = (
|
||||
context: Pick<SemanticFlowProjectionContext, "state" | "bindingNodes">,
|
||||
source: JavaScriptSemanticGraphNode | undefined,
|
||||
bindings: readonly JavaScriptSemanticProjectedResultBinding[],
|
||||
candidateEvidence?: ApplicationGraphEvidence,
|
||||
): void => {
|
||||
for (const projected of bindings)
|
||||
addSemanticGraphRelation(context.state, {
|
||||
@@ -52,7 +54,12 @@ export const projectSemanticResultBindings = (
|
||||
target: context.bindingNodes.get(projected.bindingId),
|
||||
relation: "destructures",
|
||||
resolution:
|
||||
projected.resolution === "complete" ? "resolved" : "candidate",
|
||||
candidateEvidence === undefined && projected.resolution === "complete"
|
||||
? "resolved"
|
||||
: "candidate",
|
||||
...(candidateEvidence === undefined
|
||||
? {}
|
||||
: { evidence: candidateEvidence }),
|
||||
properties: {
|
||||
projection_path: [...projected.projectionPath],
|
||||
projection_resolution: projected.resolution,
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
addSemanticGraphRelation,
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
builtinSemanticNode,
|
||||
builtinSemanticRelation,
|
||||
unknownSemanticEvidence,
|
||||
} from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
|
||||
/** Project built-in resource acquisition and exact local release handles. */
|
||||
@@ -25,7 +29,7 @@ export const projectSemanticResources = (
|
||||
source: context.callSiteAt(operation.location),
|
||||
target: resourceNodes.get(operation.resourceId),
|
||||
relation: "acquires",
|
||||
resolution: "resolved",
|
||||
...builtinSemanticRelation(context.file, operation, "resolved"),
|
||||
});
|
||||
else projectRelease(context, operation, resourceNodes);
|
||||
};
|
||||
@@ -44,7 +48,9 @@ const addResourceNode = (
|
||||
? null
|
||||
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
|
||||
null),
|
||||
properties: { method: operation.method },
|
||||
...builtinSemanticNode(context.file, operation, {
|
||||
method: operation.method,
|
||||
}),
|
||||
});
|
||||
|
||||
const projectRelease = (
|
||||
@@ -62,8 +68,11 @@ const projectRelease = (
|
||||
source: callSite,
|
||||
target: resource,
|
||||
relation: "releases",
|
||||
resolution:
|
||||
...builtinSemanticRelation(
|
||||
context.file,
|
||||
operation,
|
||||
operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
),
|
||||
});
|
||||
if (operation.resolution === "complete") return;
|
||||
addSemanticGraphUnknown(context.state, {
|
||||
|
||||
@@ -401,3 +401,33 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("retains conditional Node timer and child effects without proving a loader", () => {
|
||||
const graph = graphFor(`
|
||||
const { spawn } = require('child_process');
|
||||
const { setTimeout } = require('timers');
|
||||
const child = spawn('tool', []); child.kill();
|
||||
setTimeout(() => {}, 1);
|
||||
`);
|
||||
const child = graph.nodes.find((node) => node.kind === "child-process");
|
||||
const timer = graph.nodes.find((node) => node.kind === "timer");
|
||||
for (const node of [child, timer]) {
|
||||
expect(node?.properties.builtin_authority).toBe("candidate");
|
||||
if (node === undefined) throw new Error("Expected candidate node");
|
||||
expect(
|
||||
resolveJavaScriptSemanticEvidence(graph, node.evidence),
|
||||
).toMatchObject({ state: "inferred", confidence: "low" });
|
||||
}
|
||||
for (const kind of [
|
||||
"spawns",
|
||||
"schedules-timer",
|
||||
"forwards-signal",
|
||||
] as const) {
|
||||
const relation = graph.relations.find((item) => item.relation === kind);
|
||||
expect(relation).toMatchObject({ resolution: "candidate" });
|
||||
if (relation === undefined) throw new Error(`Expected ${kind} relation`);
|
||||
expect(
|
||||
resolveJavaScriptSemanticEvidence(graph, relation.evidence),
|
||||
).toMatchObject({ state: "inferred", confidence: "low" });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user