fix(javascript): complete large-ASAR analysis with file-local projection (#1055)

This commit is contained in:
Ysopwnable
2026-10-08 16:15:35 +08:00
committed by GitHub
parent 7af7bc88ce
commit b9fb438835
27 changed files with 1231 additions and 631 deletions
@@ -1,7 +1,7 @@
import { compareCodePoints } from "../../domain/canonicalOrdering.js";
import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
@@ -27,7 +27,7 @@ export interface ElectronIpcPairingState {
/** Flatten all per-file IPC facts into deterministic application records. */
export const collectElectronIpcRecords = (
analysis: JavaScriptArtifactAnalysis,
analysis: JavaScriptModuleArtifactAnalysis,
): readonly ElectronIpcRecord[] =>
analysis.files
.flatMap(({ file, javascript }) =>
@@ -79,7 +79,7 @@ export const unambiguousElectronIpcPairings = (
/** Summarize static Electron findings without treating gaps as absence. */
export const summarizeElectronBoundaries = (
analysis: JavaScriptArtifactAnalysis,
analysis: JavaScriptModuleArtifactAnalysis,
): ElectronBoundarySummary => {
const files = new Map(analysis.files.map(({ file }) => [file.path, file]));
const javascript = analysis.files.flatMap(({ javascript: value }) =>
@@ -7,7 +7,6 @@ import type {
AnalyzeJavaScriptApplicationInput,
JavaScriptApplicationAnalysisResult,
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
/** Create Evidence for one deterministic local JavaScript application graph. */
@@ -26,7 +25,7 @@ export const createJavaScriptApplicationEvidence = (
predicateType: "rea.javascript-application-analysis",
operation: "analyze_javascript_application",
parameters: parameters(input),
result: jsonValueSchema.parse(result),
result,
rawResult: null,
confidence: "derived",
authority: "shipped-artifact",
@@ -1,7 +1,9 @@
import { createHash } from "node:crypto";
import { setImmediate } from "node:timers/promises";
import { analyzeParsedJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
import { flattenSourceMapLeaves } from "../../domain/sourceMapEnvelope.js";
@@ -18,6 +20,8 @@ import type {
import type {
AnalyzedJavaScriptArtifactFile,
JavaScriptArtifactAnalysis,
JavaScriptModuleArtifactAnalysis,
JavaScriptModuleSemanticIr,
JavaScriptHtmlScriptObservation,
JavaScriptJsonModuleObservation,
JavaScriptPackageObservation,
@@ -27,8 +31,10 @@ import type {
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
interface MutableArtifactAnalysis {
readonly files: AnalyzedJavaScriptArtifactFile[];
interface MutableArtifactAnalysis<
SemanticIr extends JavaScriptModuleSemanticIr,
> {
readonly files: AnalyzedJavaScriptArtifactFile<SemanticIr>[];
readonly packages: JavaScriptPackageObservation[];
readonly jsonModules: JavaScriptJsonModuleObservation[];
readonly htmlScripts: JavaScriptHtmlScriptObservation[];
@@ -40,23 +46,88 @@ interface MutableArtifactAnalysis {
truncatedScopes: number;
}
interface ArtifactAnalysisContext {
readonly state: MutableArtifactAnalysis;
interface ArtifactAnalysisContext<
SemanticIr extends JavaScriptModuleSemanticIr,
> {
readonly state: MutableArtifactAnalysis<SemanticIr>;
readonly projectSemantics: (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => SemanticIr;
}
/** Analyze every text source from the artifact without a result quota. */
export const analyzeJavaScriptArtifactFiles = (
fileSet: JavaScriptArtifactFileSet,
): JavaScriptArtifactAnalysis => {
const state = emptyArtifactAnalysis();
const context = { state };
): JavaScriptArtifactAnalysis =>
analyzeArtifactFiles(fileSet, (_file, ir) => ir);
/** Project each file's full IR immediately, retaining only later module facts. */
export const analyzeAndProjectJavaScriptArtifactFiles = async (
fileSet: JavaScriptArtifactFileSet,
projectSemantics: (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => void,
beforeFile?: (
file: JavaScriptArtifactFile,
completed: number,
total: number,
) => Promise<void>,
): Promise<JavaScriptModuleArtifactAnalysis> => {
const state = emptyArtifactAnalysis<JavaScriptModuleSemanticIr>();
const projectFileSemantics = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
): JavaScriptModuleSemanticIr => {
projectSemantics(file, ir);
const programScopes = ir.scopes.filter(({ kind }) => kind === "program");
const programScopeId = programScopes[0]?.scopeId;
const localNames = new Set(
ir.moduleLinks.map(({ localName }) => localName),
);
const callableIds = new Set(
ir.moduleLinks.map(({ callableId }) => callableId),
);
return {
scopes: programScopes,
bindings: ir.bindings.filter(
({ name, scopeId }) =>
scopeId === programScopeId && localNames.has(name),
),
callables: ir.callables.filter(({ callableId }) =>
callableIds.has(callableId),
),
moduleLinks: ir.moduleLinks,
coverage: ir.coverage,
limitations: ir.limitations,
};
};
const context = { state, projectSemantics: projectFileSemantics };
for (const [index, file] of fileSet.files.entries()) {
// Let progress, cancellation and garbage collection run between files.
await setImmediate();
await beforeFile?.(file, index, fileSet.files.length);
analyzeArtifactFile(file, context);
}
return finalizeArtifactAnalysis(state);
};
const analyzeArtifactFiles = <SemanticIr extends JavaScriptModuleSemanticIr>(
fileSet: JavaScriptArtifactFileSet,
projectSemantics: ArtifactAnalysisContext<SemanticIr>["projectSemantics"],
): JavaScriptArtifactAnalysis<SemanticIr> => {
const state = emptyArtifactAnalysis<SemanticIr>();
const context = { state, projectSemantics };
for (const file of fileSet.files) analyzeArtifactFile(file, context);
return finalizeArtifactAnalysis(state);
};
const finalizeArtifactAnalysis = (
state: MutableArtifactAnalysis,
): JavaScriptArtifactAnalysis => {
const finalizeArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr,
>(
state: MutableArtifactAnalysis<SemanticIr>,
): JavaScriptArtifactAnalysis<SemanticIr> => {
const truncatedScopes = state.truncatedScopes;
return {
files: state.files,
@@ -79,9 +150,9 @@ const finalizeArtifactAnalysis = (
};
};
const analyzeArtifactFile = (
const analyzeArtifactFile = <SemanticIr extends JavaScriptModuleSemanticIr>(
file: JavaScriptArtifactFile,
context: ArtifactAnalysisContext,
context: ArtifactAnalysisContext<SemanticIr>,
): void => {
const { state } = context;
addStructuredObservations(file, state);
@@ -108,7 +179,10 @@ const analyzeArtifactFile = (
state.files.push({
file,
javascript: analysis,
semantic: semantics === null ? null : { ir: semantics },
semantic:
semantics === null
? null
: { ir: context.projectSemantics(file, semantics) },
});
state.visitedNodes += analysis.visited_ast_nodes;
state.findings += staticFindings + (semantics?.moduleLinks.length ?? 0);
@@ -121,13 +195,15 @@ const analyzeArtifactFile = (
const addSourceMap = (
file: JavaScriptArtifactFile,
context: ArtifactAnalysisContext,
context: ArtifactAnalysisContext<JavaScriptModuleSemanticIr>,
): void => {
const sourceMap = parseSourceMap(file);
context.state.sourceMaps.push(sourceMap);
};
const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({
const emptyArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr,
>(): MutableArtifactAnalysis<SemanticIr> => ({
files: [],
packages: [],
jsonModules: [],
@@ -142,7 +218,7 @@ const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({
const addStructuredObservations = (
file: JavaScriptArtifactFile,
state: MutableArtifactAnalysis,
state: MutableArtifactAnalysis<JavaScriptModuleSemanticIr>,
): void => {
if (file.kind === "package-json") state.packages.push(parsePackage(file));
if (file.kind !== "json") return;
@@ -87,18 +87,33 @@ export type JavaScriptJsonModuleObservation = JavaScriptStructuredObservation &
}
);
/** Module facts needed after file-local semantic projection has finished. */
export type JavaScriptModuleSemanticIr = Pick<
JavaScriptSemanticIr,
| "scopes"
| "bindings"
| "callables"
| "moduleLinks"
| "coverage"
| "limitations"
>;
/** One relevant file plus optional AST-only JavaScript facts. */
export interface AnalyzedJavaScriptArtifactFile {
export interface AnalyzedJavaScriptArtifactFile<
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
> {
readonly file: JavaScriptArtifactFile;
readonly javascript: JavaScriptStaticAnalysis | null;
readonly semantic: {
readonly ir: JavaScriptSemanticIr;
readonly ir: SemanticIr;
} | null;
}
/** Complete static-analysis projection before graph construction. */
export interface JavaScriptArtifactAnalysis {
readonly files: readonly AnalyzedJavaScriptArtifactFile[];
export interface JavaScriptArtifactAnalysis<
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
> {
readonly files: readonly AnalyzedJavaScriptArtifactFile<SemanticIr>[];
readonly packages: readonly JavaScriptPackageObservation[];
readonly json_modules: readonly JavaScriptJsonModuleObservation[];
readonly html_scripts: readonly JavaScriptHtmlScriptObservation[];
@@ -110,3 +125,7 @@ export interface JavaScriptArtifactAnalysis {
readonly truncated_scopes: number;
readonly limitations: readonly string[];
}
/** Artifact facts retaining only semantics used by application relationships. */
export type JavaScriptModuleArtifactAnalysis =
JavaScriptArtifactAnalysis<JavaScriptModuleSemanticIr>;
@@ -3,7 +3,7 @@ import {
createJavaScriptApplicationGraph,
type JavaScriptApplicationGraph,
} from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFileSet } from "../../domain/javascript/javascriptArtifactFiles.js";
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
import type { JavaScriptArtifactGraphContext } from "./JavaScriptArtifactGraphContext.js";
@@ -37,7 +37,7 @@ import {
export const buildJavaScriptArtifactGraph = (
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptArtifactAnalysis,
analysis: JavaScriptModuleArtifactAnalysis,
): JavaScriptApplicationGraph => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
@@ -6,7 +6,7 @@ import {
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
@@ -29,7 +29,7 @@ export interface JavaScriptArtifactGraphContext {
readonly accumulator: JavaScriptArtifactGraphAccumulator;
readonly snapshot: ArtifactInventorySnapshot;
readonly fileSet: JavaScriptArtifactFileSet;
readonly analysis: JavaScriptArtifactAnalysis;
readonly analysis: JavaScriptModuleArtifactAnalysis;
readonly root: ApplicationNode;
readonly filesByPath: ReadonlyMap<string, JavaScriptArtifactFile>;
readonly fileNodes: Map<string, ApplicationNode>;
@@ -209,7 +209,7 @@ export const addUnavailableStaticParseScope = (
/** Coverage for one bounded JavaScript AST analysis. */
export const javascriptAnalysisCoverage = (
analysis: NonNullable<
JavaScriptArtifactAnalysis["files"][number]["javascript"]
JavaScriptModuleArtifactAnalysis["files"][number]["javascript"]
>,
): JavaScriptArtifactGraphCoverage =>
analysis.parse_status === "complete"
@@ -1,7 +1,7 @@
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type {
JavaScriptArtifactContainer,
JavaScriptArtifactFile,
@@ -270,7 +270,7 @@ export const addJavaScriptPackageNodes = (
const createFileTarget = (
context: JavaScriptArtifactGraphContext,
file: JavaScriptArtifactFile,
javascript: JavaScriptArtifactAnalysis["files"][number]["javascript"],
javascript: JavaScriptModuleArtifactAnalysis["files"][number]["javascript"],
): ApplicationNode => {
const kind = artifactFileNodeKind(file.kind);
const json = context.analysis.json_modules.find(
@@ -5,7 +5,7 @@ import { createJavaScriptArtifactReader as createReader } from "../../artifacts/
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { analyzeAndProjectJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { readJavaScriptArtifactFiles } from "../../artifacts/javascript/JavaScriptArtifactFiles.js";
import { buildJavaScriptArtifactGraph } from "./JavaScriptArtifactGraphBuilder.js";
import {
@@ -14,7 +14,7 @@ import {
} from "./JavaScriptArtifactReconstructionInput.js";
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js";
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import type { ProgressReporter } from "../ProgressReporter.js";
/** Application-layer result retaining local diagnostics outside the canonical graph. */
@@ -70,9 +70,23 @@ export const reconstructJavaScriptArtifact = async (
const files = await readJavaScriptArtifactFiles(reader, snapshot, signal);
await reportPhase(
"parse_javascript_sources",
`Parsing ${String(files.files.length)} application source files`,
`Parsing and projecting ${String(files.files.length)} application source files`,
);
const semanticProjection = createJavaScriptSemanticGraphProjection();
const analysis = await analyzeAndProjectJavaScriptArtifactFiles(
files,
semanticProjection.projectFile,
async (file, completed, total) => {
abortIfNeeded(signal);
await progress?.report({
phase: "parse_javascript_source",
completed: 0,
total: 1,
message: `Parsing and projecting ${file.path} (${String(completed + 1)}/${String(total)})`,
});
abortIfNeeded(signal);
},
);
const analysis = analyzeJavaScriptArtifactFiles(files);
abortIfNeeded(signal);
await reportPhase(
"build_javascript_application_graph",
@@ -81,13 +95,13 @@ export const reconstructJavaScriptArtifact = async (
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
await reportPhase(
"build_javascript_semantic_graph",
"Constructing static semantic relationships",
"Binding and validating static semantic relationships",
);
const semanticGraph = buildJavaScriptSemanticGraph({
rootArtifactSha256: snapshot.manifest.root_sha256,
applicationGraph: graph,
const semanticGraph = semanticProjection.finish(
snapshot.manifest.root_sha256,
graph,
analysis,
});
);
return {
input_path: path,
format,
@@ -8,7 +8,6 @@ import {
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type {
JavaScriptModuleOrigin,
JavaScriptSemanticIr,
JavaScriptSemanticModuleLink,
} from "../../domain/javascript/javascriptSemanticIr.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -28,8 +27,10 @@ import {
} from "./JavaScriptArtifactPathResolution.js";
import { projectJavaScriptExportReturnShapes } from "./JavaScriptReturnShapeProjection.js";
import type { JavaScriptModuleSemanticIr } from "./JavaScriptArtifactAnalysisTypes.js";
interface SemanticAnalysis {
readonly ir: JavaScriptSemanticIr;
readonly ir: JavaScriptModuleSemanticIr;
}
interface RelationshipInput {
@@ -310,7 +311,7 @@ const unresolvedModuleNode = (
});
const moduleOriginForExport = (
ir: JavaScriptSemanticIr,
ir: JavaScriptModuleSemanticIr,
link: JavaScriptSemanticModuleLink,
): JavaScriptModuleOrigin | null => {
if (link.specifier !== null)
@@ -374,7 +375,7 @@ const semanticCoverage = (
const moduleFormat = (
path: string,
ir: JavaScriptSemanticIr,
ir: JavaScriptModuleSemanticIr,
): "commonjs" | "esm" | "mixed" | "unknown" => {
const extension = posix.extname(path).toLowerCase();
if (extension === ".mjs") return "esm";
@@ -4,13 +4,13 @@ import {
truncatedApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type {
JavaScriptSemanticIr,
JavaScriptSemanticCallable,
JavaScriptSemanticModuleLink,
} from "../../domain/javascript/javascriptSemanticIr.js";
import { flattenSemanticReturnValue } from "../../domain/javascript/javascriptSemanticReturns.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactGraphCoverage } from "./JavaScriptArtifactGraphContext.js";
import type { JavaScriptModuleSemanticIr } from "./JavaScriptArtifactAnalysisTypes.js";
export interface JavaScriptReturnShapeProjection {
readonly properties: ReturnType<typeof jsonObjectSchema.parse>;
@@ -21,7 +21,7 @@ export interface JavaScriptReturnShapeProjection {
/** Project one exact export/callable link into shallow bounded graph values. */
export const projectJavaScriptExportReturnShapes = (input: {
readonly ir: JavaScriptSemanticIr;
readonly ir: JavaScriptModuleSemanticIr;
readonly link: JavaScriptSemanticModuleLink;
readonly modulePath: string;
readonly baseCoverage: JavaScriptArtifactGraphCoverage;
@@ -5,10 +5,9 @@ import type {
JavaScriptSemanticTimerOperation,
} from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
@@ -59,55 +58,41 @@ const addEventNode = (
operation: JavaScriptSemanticEventOperation,
eventKey: string,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "event",
roleKey: `event:${eventKey}`,
location: operation.eventName === null ? operation.location : null,
label: operation.eventName,
functionNodeId: null,
properties: {
emitter_key: operation.emitterKey,
event_name: operation.eventName,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "event",
roleKey: `event:${eventKey}`,
location: operation.eventName === null ? operation.location : null,
label: operation.eventName,
functionNodeId: null,
properties: {
emitter_key: operation.emitterKey,
event_name: operation.eventName,
},
});
const addListenerNode = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticEventOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "listener",
roleKey: `listener:${operation.eventId}`,
location: operation.listenerLocation ?? operation.location,
label:
operation.eventName === null
? operation.method
: `${operation.method}:${operation.eventName}`,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
emitter_key: operation.emitterKey,
event_name: operation.eventName,
method: operation.method,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "listener",
roleKey: `listener:${operation.eventId}`,
location: operation.listenerLocation ?? operation.location,
label:
operation.eventName === null
? operation.method
: `${operation.method}:${operation.eventName}`,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
emitter_key: operation.emitterKey,
event_name: operation.eventName,
method: operation.method,
},
});
const eventEmitterNode = (
context: SemanticFlowProjectionContext,
@@ -164,24 +149,17 @@ export const projectSemanticTimers = (
operation.ownerCallableId === null
? undefined
: context.callableNodes.get(operation.ownerCallableId);
const timer = addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "timer",
roleKey: operation.timerId,
location: operation.location,
label: operation.method,
functionNodeId: owner?.node_id ?? null,
properties: {
delay_milliseconds: operation.delayMilliseconds,
method: operation.method,
},
},
context.state,
),
);
const timer = retainSemanticGraphNode(context.state, context.file, {
kind: "timer",
roleKey: operation.timerId,
location: operation.location,
label: operation.method,
functionNodeId: owner?.node_id ?? null,
properties: {
delay_milliseconds: operation.delayMilliseconds,
method: operation.method,
},
});
return timer === null ? [] : [[operation.timerId, timer] as const];
}),
);
@@ -38,16 +38,17 @@ import {
type SemanticFlowProjectionContext,
} from "./JavaScriptSemanticGraphFlowProjection.js";
import {
owningSemanticCallableNode,
createSemanticCallableOwnerLookup,
createSemanticCallSiteLookup,
createSemanticNodeRangeLookup,
semanticFamilyStatus,
semanticNodesWithinRange,
} from "./JavaScriptSemanticGraphProjection.js";
import {
addSemanticFallbackRoot as addFallbackRoot,
addSemanticGraphNode as addNode,
retainSemanticGraphNode as retainNode,
addSemanticGraphRelation as addRelation,
constructSemanticGraphNode as semanticNode,
createSemanticGraphProjectionState as emptyState,
bindSemanticGraphApplicationNodes,
type SemanticGraphProjectionState as BuilderState,
} from "./JavaScriptSemanticGraphConstruction.js";
@@ -72,6 +73,9 @@ interface FileContext extends SemanticFlowProjectionContext {
readonly argumentNodes: Map<string, JavaScriptSemanticGraphNode>;
readonly referenceNodes: JavaScriptSemanticGraphNode[];
readonly callResolutions: ReadonlyMap<string, "candidate" | "resolved">;
readonly callableOwnerAt: ReturnType<
typeof createSemanticCallableOwnerLookup
>;
}
/**
@@ -108,152 +112,170 @@ export const buildJavaScriptSemanticGraph = ({
applicationGraph,
analysis,
}: BuilderInput): JavaScriptSemanticGraph => {
const state = emptyState(applicationGraph);
const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = [];
let truncatedFiles = 0;
const projection = createJavaScriptSemanticGraphProjection();
for (const analyzed of analysis.files) {
if (analyzed.semantic === null) continue;
if (state.nodes.size >= SEMANTIC_GRAPH_NODE_CEILING) {
truncatedFiles += 1;
continue;
}
const remainingTreeBudget = SEMANTIC_GRAPH_NODE_CEILING - state.nodes.size;
state.fileNodeBudget = Math.min(
SEMANTIC_GRAPH_FILE_NODE_CEILING,
remainingTreeBudget,
);
state.fileNodesDropped = false;
fingerprints.push(
...projectFile(analyzed.file, analyzed.semantic.ir, state),
);
// Nodes were dropped only if the budget actually blocked creation. A file
// that exactly fills its share ends with a zero budget without dropping
// anything, so the remaining budget alone cannot decide truncation.
if (state.fileNodesDropped) truncatedFiles += 1;
state.fileNodeBudget = null;
if (analyzed.semantic !== null)
projection.projectFile(analyzed.file, analyzed.semantic.ir);
}
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
return createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] === family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
return projection.finish(rootArtifactSha256, applicationGraph, analysis);
};
/** File-local semantic projection that does not retain consumed source IR. */
export interface JavaScriptSemanticGraphProjection {
readonly projectFile: (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => void;
readonly finish: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
) => JavaScriptSemanticGraph;
}
/** Accumulate semantic nodes while each source IR is still file-local. */
export const createJavaScriptSemanticGraphProjection =
(): JavaScriptSemanticGraphProjection => {
const state = emptyState({ nodes: [] });
const fingerprints: JavaScriptSemanticGraph["fingerprints"][number][] = [];
let truncatedFiles = 0;
const projectSource = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
): void => {
if (state.nodes.size >= SEMANTIC_GRAPH_NODE_CEILING) {
truncatedFiles += 1;
return;
}
const remainingTreeBudget =
SEMANTIC_GRAPH_NODE_CEILING - state.nodes.size;
state.fileNodeBudget = Math.min(
SEMANTIC_GRAPH_FILE_NODE_CEILING,
remainingTreeBudget,
);
state.fileNodesDropped = false;
fingerprints.push(...projectFile(file, ir, state));
// Nodes were dropped only if the budget actually blocked creation. A file
// that exactly fills its share ends with a zero budget without dropping
// anything, so the remaining budget alone cannot decide truncation.
if (state.fileNodesDropped) truncatedFiles += 1;
state.fileNodeBudget = null;
};
return {
projectFile: projectSource,
finish: (rootArtifactSha256, applicationGraph, analysis) => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
const unknowns = [...state.unknowns.values()];
const graph = createJavaScriptSemanticGraph({
schema: "JavaScriptSemanticRelationGraph",
root_artifact_sha256: rootArtifactSha256,
application_graph_id: applicationGraph.graph_id,
root_node_ids: [...state.roots],
nodes: [...state.nodes.values()],
relations: [...state.relations.values()],
fingerprints,
unknowns,
coverage: {
status: truncatedFiles > 0 ? "partial" : "unknown",
truncated: truncatedFiles > 0,
omitted_nodes: truncatedFiles > 0 ? null : 0,
omitted_relations: truncatedFiles > 0 ? null : 0,
limits:
truncatedFiles > 0
? [
{
name: "semantic_graph_node_ceiling",
value: SEMANTIC_GRAPH_NODE_CEILING,
unit: "items" as const,
},
]
: [],
families: JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => ({
family,
status: semanticFamilyStatus(family, analysis),
retained_relations: [...state.relations.values()].filter(
(relation) =>
JAVASCRIPT_SEMANTIC_RELATION_FAMILY[relation.relation] ===
family,
).length,
omitted_relations: truncatedFiles > 0 ? null : 0,
unknown_ids: unknowns
.filter((unknown) => unknown.family === family)
.map(({ unknown_id: identifier }) => identifier),
})),
},
limitations: [
"The semantic graph contains static syntax observations and conservative relationship candidates; it does not claim runtime execution.",
"Local data flow does not claim control-flow-sensitive reaching definitions or arbitrary dynamic property resolution.",
"Promise ownership covers explicit unshadowed Promise construction, static factories, aggregation, chaining, and await syntax only.",
"Function fingerprints are static candidates; equal digests can remain ambiguous and do not prove behavioral equivalence.",
"Event extraction covers EventEmitter-style literal registrations, removals, and dispatch candidates; dynamic names remain unknown.",
"Timer extraction covers global or node:timers scheduling and exact local-handle cancellation.",
"Child-process extraction covers asynchronous node:child_process creation, literal argv/env/stdio options, exit/error listeners, and kill signals.",
"Configuration extraction covers process.env, process.argv, node:fs reads, and direct logical defaults.",
"Request extraction covers fetch, WebSocket, node:http/node:https construction, direct option fields, and exact local response consumers.",
"Boundary extraction covers unshadowed JSON/global coercions plus parse and validation method candidates.",
"Resource extraction covers built-in filesystem/network acquisition and exact local close/destroy/end handles.",
],
});
state.nodes.clear();
state.relations.clear();
state.unknowns.clear();
state.roots.clear();
fingerprints.length = 0;
return graph;
},
};
};
const projectFile = (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
state: BuilderState,
): JavaScriptSemanticGraph["fingerprints"][number][] => {
const moduleNode = addNode(
state,
semanticNode(
file,
{
kind: "module",
roleKey: "module",
location: null,
label: file.path,
functionNodeId: null,
},
state,
),
);
const moduleNode = retainNode(state, file, {
kind: "module",
roleKey: "module",
location: null,
label: file.path,
functionNodeId: null,
});
if (moduleNode === null) return [];
state.roots.add(moduleNode.node_id);
const callableNodes = new Map(
ir.callables.flatMap((callable) => {
const node = addNode(
state,
semanticNode(
file,
{
kind: "function",
roleKey: `callable:${callable.callableId}`,
location: callable.location,
label: callable.name,
functionNodeId: null,
// The label is display text; keep the exact name, which may be "".
properties: { name: callable.name },
},
state,
),
);
const node = retainNode(state, file, {
kind: "function",
roleKey: `callable:${callable.callableId}`,
location: callable.location,
label: callable.name,
functionNodeId: null,
// The label is display text; keep the exact name, which may be "".
properties: { name: callable.name },
});
return node === null ? [] : [[callable.callableId, node] as const];
}),
);
const callableOwnerAt = createSemanticCallableOwnerLookup(ir, callableNodes);
const bindingNodes = new Map(
ir.bindings.flatMap((binding) => {
const location = binding.definitions[0]?.location ?? null;
const kind = binding.kind === "parameter" ? "parameter" : "binding";
const owner = owningSemanticCallableNode(location, ir, callableNodes);
const node = addNode(
state,
semanticNode(
file,
{
kind,
roleKey: `binding:${binding.bindingId}`,
location,
label: binding.name,
functionNodeId: owner?.node_id ?? null,
},
state,
),
);
const owner = callableOwnerAt(location);
const node = retainNode(state, file, {
kind,
roleKey: `binding:${binding.bindingId}`,
location,
label: binding.name,
functionNodeId: owner?.node_id ?? null,
});
return node === null ? [] : [[binding.bindingId, node] as const];
}),
);
const returnSiteNodes = createReturnSiteNodes(file, ir, callableNodes, state);
const callSiteNodes = createCallSiteNodes(file, ir, callableNodes, state);
const context: FileContext = {
const definitions: Omit<FileContext, "referenceNodesWithin"> = {
file,
ir,
state,
@@ -270,8 +292,16 @@ const projectFile = (
call.resolution === "exact" ? "resolved" : "candidate",
]),
),
callableOwnerAt,
callSiteAt: createSemanticCallSiteLookup(ir, callSiteNodes),
};
projectDefinitionsAndReferences(definitions);
const context: FileContext = {
...definitions,
referenceNodesWithin: createSemanticNodeRangeLookup(
definitions.referenceNodes,
),
};
projectDefinitionsAndReferences(context);
projectSemanticValues(context);
projectSemanticObjects(context);
projectCalls(context);
@@ -300,20 +330,13 @@ const createReturnSiteNodes = (
if (owner === undefined) continue;
for (const site of callable.returnSites) {
const identifier = site.returnSiteId;
const node = addNode(
state,
semanticNode(
file,
{
kind: "return-site",
roleKey: identifier,
location: site.location,
label: "return",
functionNodeId: owner.node_id,
},
state,
),
);
const node = retainNode(state, file, {
kind: "return-site",
roleKey: identifier,
location: site.location,
label: "return",
functionNodeId: owner.node_id,
});
if (node !== null) result.set(identifier, node);
}
}
@@ -332,44 +355,32 @@ const createCallSiteNodes = (
call.callerCallableId === null
? null
: (callables.get(call.callerCallableId)?.node_id ?? null);
const node = addNode(
state,
semanticNode(
file,
{
kind: "call-site",
roleKey: `call:${call.callSiteId}`,
location: call.location,
label: call.kind,
functionNodeId: owner,
},
state,
),
);
const node = retainNode(state, file, {
kind: "call-site",
roleKey: `call:${call.callSiteId}`,
location: call.location,
label: call.kind,
functionNodeId: owner,
});
if (node !== null) result.set(call.callSiteId, node);
}
return result;
};
const projectDefinitionsAndReferences = (context: FileContext): void => {
const projectDefinitionsAndReferences = (
context: Omit<FileContext, "referenceNodesWithin">,
): void => {
for (const binding of context.ir.bindings) {
const bindingNode = context.bindingNodes.get(binding.bindingId);
if (bindingNode === undefined) continue;
for (const [index, definition] of binding.definitions.entries()) {
const definitionNode = addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `definition:${binding.bindingId}:${String(index)}`,
location: definition.location,
label: definition.kind,
functionNodeId: bindingNode.function_node_id,
},
context.state,
),
);
const definitionNode = retainNode(context.state, context.file, {
kind: "expression",
roleKey: `definition:${binding.bindingId}:${String(index)}`,
location: definition.location,
label: definition.kind,
functionNodeId: bindingNode.function_node_id,
});
addRelation(context.state, {
source: definitionNode,
target: bindingNode,
@@ -379,25 +390,14 @@ const projectDefinitionsAndReferences = (context: FileContext): void => {
}
}
for (const [index, reference] of context.ir.references.entries()) {
const expression = addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `reference:${String(index)}:${reference.role}:${reference.name}`,
location: reference.location,
label: reference.name,
functionNodeId:
owningSemanticCallableNode(
reference.location,
context.ir,
context.callableNodes,
)?.node_id ?? null,
},
context.state,
),
);
const expression = retainNode(context.state, context.file, {
kind: "expression",
roleKey: `reference:${String(index)}:${reference.role}:${reference.name}`,
location: reference.location,
label: reference.name,
functionNodeId:
context.callableOwnerAt(reference.location)?.node_id ?? null,
});
const binding =
reference.bindingId === null
? undefined
@@ -447,10 +447,7 @@ const projectCalls = (context: FileContext): void => {
`${call.callSiteId}\u0000${String(argument.index)}`,
argumentNode,
);
const references = semanticNodesWithinRange(
context.referenceNodes,
argument.location,
);
const references = context.referenceNodesWithin(argument.location);
for (const reference of references)
addRelation(context.state, {
source: reference,
@@ -499,19 +496,12 @@ const createArgumentNode = (
callSiteId: string,
argument: JavaScriptSemanticCallArgument,
): JavaScriptSemanticGraphNode | null =>
addNode(
context.state,
semanticNode(
context.file,
{
kind: "expression",
roleKey: `argument:${callSiteId}:${String(argument.index)}`,
location: argument.location,
label: argument.spread
? "spread argument"
: `argument ${String(argument.index)}`,
functionNodeId: callNode.function_node_id,
},
context.state,
),
);
retainNode(context.state, context.file, {
kind: "expression",
roleKey: `argument:${callSiteId}:${String(argument.index)}`,
location: argument.location,
label: argument.spread
? "spread argument"
: `argument ${String(argument.index)}`,
functionNodeId: callNode.function_node_id,
});
@@ -5,10 +5,9 @@ import type {
JavaScriptSemanticChildProcessSpawn,
} from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
@@ -53,31 +52,23 @@ const addChildProcessNode = (
context: SemanticFlowProjectionContext,
spawn: JavaScriptSemanticChildProcessSpawn,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "child-process",
roleKey: spawn.processId,
location: spawn.location,
label: spawn.command ?? spawn.method,
functionNodeId:
spawn.ownerCallableId === null
? null
: (context.callableNodes.get(spawn.ownerCallableId)?.node_id ??
null),
properties: {
argv_count: spawn.argvCount,
command: spawn.command,
environment_supplied: spawn.environmentSupplied,
method: spawn.method,
stdio_mode: spawn.stdioMode,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "child-process",
roleKey: spawn.processId,
location: spawn.location,
label: spawn.command ?? spawn.method,
functionNodeId:
spawn.ownerCallableId === null
? null
: (context.callableNodes.get(spawn.ownerCallableId)?.node_id ?? null),
properties: {
argv_count: spawn.argvCount,
command: spawn.command,
environment_supplied: spawn.environmentSupplied,
method: spawn.method,
stdio_mode: spawn.stdioMode,
},
});
const projectSpawn = (
context: SemanticFlowProjectionContext,
@@ -141,21 +132,14 @@ const addStdioNode = (
spawn: JavaScriptSemanticChildProcessSpawn,
child: JavaScriptSemanticGraphNode,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "stdio",
roleKey: `stdio:${spawn.processId}`,
location: spawn.location,
label: spawn.stdioMode,
functionNodeId: child.function_node_id,
properties: { mode: spawn.stdioMode },
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "stdio",
roleKey: `stdio:${spawn.processId}`,
location: spawn.location,
label: spawn.stdioMode,
functionNodeId: child.function_node_id,
properties: { mode: spawn.stdioMode },
});
const projectInteraction = (
context: SemanticFlowProjectionContext,
@@ -204,52 +188,38 @@ const addChildListenerNode = (
context: SemanticFlowProjectionContext,
interaction: JavaScriptSemanticChildProcessInteraction,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "listener",
roleKey: interaction.interactionId,
location: interaction.listenerLocation ?? interaction.location,
label: interaction.eventName,
functionNodeId:
interaction.ownerCallableId === null
? null
: (context.callableNodes.get(interaction.ownerCallableId)
?.node_id ?? null),
properties: {
event_name: interaction.eventName,
method: interaction.method,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "listener",
roleKey: interaction.interactionId,
location: interaction.listenerLocation ?? interaction.location,
label: interaction.eventName,
functionNodeId:
interaction.ownerCallableId === null
? null
: (context.callableNodes.get(interaction.ownerCallableId)?.node_id ??
null),
properties: {
event_name: interaction.eventName,
method: interaction.method,
},
});
const addSignalNode = (
context: SemanticFlowProjectionContext,
interaction: JavaScriptSemanticChildProcessInteraction,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "signal",
roleKey: interaction.interactionId,
location: interaction.location,
label: interaction.signalName,
functionNodeId:
interaction.ownerCallableId === null
? null
: (context.callableNodes.get(interaction.ownerCallableId)
?.node_id ?? null),
properties: { signal_name: interaction.signalName },
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "signal",
roleKey: interaction.interactionId,
location: interaction.location,
label: interaction.signalName,
functionNodeId:
interaction.ownerCallableId === null
? null
: (context.callableNodes.get(interaction.ownerCallableId)?.node_id ??
null),
properties: { signal_name: interaction.signalName },
});
const addChildUnknown = (input: ChildUnknownInput): void => {
const { context, node, location, relationKinds, detail, candidateNodeIds } =
@@ -4,6 +4,7 @@ import { javascriptDisplayText } from "../../domain/javascript/javascriptAstValu
import {
createJavaScriptSemanticGraphNode,
createJavaScriptSemanticGraphRelation,
javaScriptSemanticNodeId,
type JavaScriptSemanticGraphNode,
} from "../../domain/javascript/javascriptSemanticGraph.js";
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
@@ -80,6 +81,28 @@ export const createSemanticGraphProjectionState = (
};
};
/** Bind retained semantic identities after all application observations exist. */
export const bindSemanticGraphApplicationNodes = (
state: SemanticGraphProjectionState,
applicationGraph: Pick<JavaScriptApplicationGraph, "nodes">,
): void => {
const index = indexApplicationNodes(applicationGraph.nodes).identifiers;
for (const [identifier, node] of state.nodes) {
state.nodes.set(identifier, {
...node,
application_node_ids: [
...(index.get(
applicationLocationKey(
node.identity.artifact_sha256,
node.identity.module_path,
node.identity.source_range,
),
) ?? []),
],
});
}
};
/** Construct one canonical semantic node backed by an exact artifact file. */
export const constructSemanticGraphNode = (
file: JavaScriptArtifactFile,
@@ -101,6 +124,33 @@ export const constructSemanticGraphNode = (
evidence: observedSemanticEvidence(file, input.location),
});
/** Reuse retained identities and reject exhausted budgets before allocation. */
export const retainSemanticGraphNode = (
state: SemanticGraphProjectionState,
file: JavaScriptArtifactFile,
input: SemanticNodeConstructionInput,
): JavaScriptSemanticGraphNode | null => {
const identifier = javaScriptSemanticNodeId({
kind: input.kind,
identity: {
artifact_sha256: file.sha256,
module_path: file.path,
source_range: input.location,
role_key: input.roleKey,
},
});
const existing = state.nodes.get(identifier);
if (existing !== undefined) return existing;
if (typeof state.fileNodeBudget === "number" && state.fileNodeBudget <= 0) {
state.fileNodesDropped = true;
return null;
}
return addSemanticGraphNode(
state,
constructSemanticGraphNode(file, input, state),
);
};
/** Retain one canonical semantic node. */
export const addSemanticGraphNode = (
state: SemanticGraphProjectionState,
@@ -6,10 +6,9 @@ import type {
JavaScriptSemanticRequestOperation,
} from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
@@ -70,29 +69,22 @@ const addConfigurationNode = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticConfigurationOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "config-source",
roleKey: operation.configId,
location: operation.location,
label: operation.key ?? operation.kind,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
key: operation.key,
source_kind: operation.kind,
value: operation.value,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "config-source",
roleKey: operation.configId,
location: operation.location,
label: operation.key ?? operation.kind,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
key: operation.key,
source_kind: operation.kind,
value: operation.value,
},
});
const configurationRelation = (
operation: JavaScriptSemanticConfigurationOperation,
@@ -144,29 +136,22 @@ const addRequestNode = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticRequestOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: operation.kind === "request" ? "request" : "response",
roleKey: operation.requestId,
location: operation.location,
label: operation.endpoint ?? operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
endpoint: operation.endpoint,
method: operation.method,
operation_kind: operation.kind,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: operation.kind === "request" ? "request" : "response",
roleKey: operation.requestId,
location: operation.location,
label: operation.endpoint ?? operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
endpoint: operation.endpoint,
method: operation.method,
operation_kind: operation.kind,
},
});
const projectRequestConstruction = (
context: SemanticFlowProjectionContext,
@@ -265,28 +250,21 @@ const addBoundaryNode = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticBoundaryOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "boundary",
roleKey: operation.boundaryId,
location: operation.location,
label: operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
boundary_kind: operation.kind,
method: operation.method,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "boundary",
roleKey: operation.boundaryId,
location: operation.location,
label: operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
boundary_kind: operation.kind,
method: operation.method,
},
});
const bindingOrOwner = (
context: SemanticFlowProjectionContext,
@@ -5,17 +5,15 @@ import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnaly
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
type SemanticGraphProjectionState,
} from "./JavaScriptSemanticGraphConstruction.js";
import {
inferredSemanticEvidenceAt,
unknownSemanticEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
import { semanticNodesWithinRange } from "./JavaScriptSemanticGraphProjection.js";
/** File-local graph state needed by return, capture, and frontier projection. */
export interface SemanticFlowProjectionContext {
@@ -28,6 +26,12 @@ export interface SemanticFlowProjectionContext {
readonly callSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly returnSiteNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly referenceNodes: readonly JavaScriptSemanticGraphNode[];
readonly referenceNodesWithin: (
range: JavaScriptSourceRange,
) => JavaScriptSemanticGraphNode[];
readonly callSiteAt: (
range: JavaScriptSourceRange,
) => JavaScriptSemanticGraphNode | undefined;
}
/** Find the retained call-site node at one exact semantic source range. */
@@ -35,12 +39,7 @@ export const semanticCallSiteAt = (
context: SemanticFlowProjectionContext,
location: JavaScriptSourceRange,
): JavaScriptSemanticGraphNode | undefined => {
const call = context.ir.callSites.find(({ location: candidate }) =>
sourceRangesEqual(candidate, location),
);
return call === undefined
? undefined
: context.callSiteNodes.get(call.callSiteId);
return context.callSiteAt(location);
};
/** Project explicit static Promise/task ownership without runtime claims. */
@@ -53,26 +52,19 @@ export const projectSemanticPromises = (
operation.ownerCallableId === null
? undefined
: context.callableNodes.get(operation.ownerCallableId);
const node = addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: operation.kind === "awaited-expression" ? "task" : "promise",
roleKey: operation.promiseId,
location: operation.location,
label: promiseLabel(operation),
functionNodeId: owner?.node_id ?? null,
properties: {
method: operation.method,
operation_kind: operation.kind,
ownership: operation.ownership,
source_resolution: operation.sourceResolution,
},
},
context.state,
),
);
const node = retainSemanticGraphNode(context.state, context.file, {
kind: operation.kind === "awaited-expression" ? "task" : "promise",
roleKey: operation.promiseId,
location: operation.location,
label: promiseLabel(operation),
functionNodeId: owner?.node_id ?? null,
properties: {
method: operation.method,
operation_kind: operation.kind,
ownership: operation.ownership,
source_resolution: operation.sourceResolution,
},
});
return node === null ? [] : [[operation.promiseId, node] as const];
}),
);
@@ -217,10 +209,7 @@ export const projectSemanticReturnValues = (
for (const callable of context.ir.callables)
for (const site of callable.returnSites) {
const returnNode = context.returnSiteNodes.get(site.returnSiteId);
const references = semanticNodesWithinRange(
context.referenceNodes,
site.location,
);
const references = context.referenceNodesWithin(site.location);
for (const reference of references)
addSemanticGraphRelation(context.state, {
source: reference,
@@ -2,10 +2,9 @@ import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/ja
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticObjectOperation } from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
@@ -72,31 +71,24 @@ const addObjectOccurrence = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticObjectOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "expression",
roleKey: operation.objectOperationId,
location: operation.location,
label:
operation.propertyName === null
? operation.kind
: `${operation.kind}:${operation.propertyName}`,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
operation_kind: operation.kind,
property_name: operation.propertyName,
},
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "expression",
roleKey: operation.objectOperationId,
location: operation.location,
label:
operation.propertyName === null
? operation.kind
: `${operation.kind}:${operation.propertyName}`,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: {
operation_kind: operation.kind,
property_name: operation.propertyName,
},
});
const addObjectUnknown = (
context: SemanticFlowProjectionContext,
@@ -0,0 +1,259 @@
import { expect, it } from "vitest";
import { compareCodePoints } from "../../domain/canonicalOrdering.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import { analyzeJavaScriptSemantics } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import {
constructSemanticGraphNode,
createSemanticGraphProjectionState,
} from "./JavaScriptSemanticGraphConstruction.js";
import {
createSemanticCallableOwnerLookup,
createSemanticNodeRangeLookup,
createSemanticCallSiteLookup,
} from "./JavaScriptSemanticGraphProjection.js";
import { sourceRangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
const fixtureFile: JavaScriptArtifactFile = {
path: "app.js",
container_sha256: "a".repeat(64),
sha256: "b".repeat(64),
bytes: 0,
inventory_artifact_id: "fixture-app",
kind: "javascript",
unpacked: false,
text: { included: true, value: "" },
};
const nodesFor = (ir: JavaScriptSemanticIr) => {
const state = createSemanticGraphProjectionState({ nodes: [] });
return new Map(
ir.callables.map((callable) => [
callable.callableId,
constructSemanticGraphNode(
fixtureFile,
{
kind: "function",
roleKey: callable.callableId,
label: callable.name,
location: callable.location,
functionNodeId: null,
},
state,
),
]),
);
};
// Reference behavior before the optimization, including stable ties and
// fallback when the most deeply nested callable has no retained graph node.
const previousOwner = (
location: JavaScriptSourceRange | null,
ir: JavaScriptSemanticIr,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
) => {
if (location === null) return undefined;
const compare = (
left: JavaScriptSourceRange["start"],
right: JavaScriptSourceRange["start"],
) => left.line - right.line || left.column - right.column;
const key = (range: JavaScriptSourceRange) =>
`${String(range.start.line).padStart(12, "0")}:${String(range.start.column).padStart(12, "0")}`;
return ir.callables
.filter(
(callable) =>
compare(callable.location.start, location.start) <= 0 &&
compare(callable.location.end, location.end) >= 0,
)
.sort((left, right) =>
compareCodePoints(key(right.location), key(left.location)),
)
.map((callable) => nodes.get(callable.callableId))
.find((node) => node !== undefined);
};
const rangesFor = (ir: JavaScriptSemanticIr) => [
null,
...ir.callables.map((callable) => callable.location),
...ir.bindings.flatMap((binding) =>
binding.definitions.map((definition) => definition.location),
),
...ir.references.map((reference) => reference.location),
{ start: { line: 999, column: 0 }, end: { line: 999, column: 1 } },
];
it("matches the prior owner selection across nested functions, arrows and methods", () => {
const ir = analyzeJavaScriptSemantics(`
const outside = 1;
function outer(parameter) {
function inner(value) { return value + parameter; }
const callback = argument => inner(argument);
return callback(outside);
}
class Example { method(value) { return () => outer(value); } }
`);
const nodes = nodesFor(ir);
const lookup = createSemanticCallableOwnerLookup(ir, nodes);
for (const range of rangesFor(ir))
expect(lookup(range)).toBe(previousOwner(range, ir, nodes));
});
it("reads retained node membership at query time and keeps file indexes independent", () => {
const ir = analyzeJavaScriptSemantics(
"function outer() { function inner() { return 1; } return inner(); }",
);
const inner = ir.callables.find((callable) => callable.name === "inner");
if (inner === undefined) throw new Error("Missing inner fixture callable");
const nodes = nodesFor(ir);
const otherNodes = nodesFor(ir);
const lookup = createSemanticCallableOwnerLookup(ir, nodes);
const otherLookup = createSemanticCallableOwnerLookup(ir, otherNodes);
const before = lookup(inner.location);
expect(before?.label).toBe("inner");
nodes.delete(inner.callableId);
expect(lookup(inner.location)?.label).toBe("outer");
expect(otherLookup(inner.location)?.label).toBe("inner");
if (before === undefined) throw new Error("Missing retained fixture node");
nodes.set(inner.callableId, before);
expect(lookup(inner.location)).toBe(before);
});
it("preserves stable ties and non-nested overlapping ranges", () => {
const original = analyzeJavaScriptSemantics(
"function first() { return 1; } function second() { return 2; } function third() { return 3; }",
);
const range = (start: number, end: number): JavaScriptSourceRange => ({
start: { line: 1, column: start },
end: { line: 1, column: end },
});
const ir = {
...original,
callables: original.callables.map((callable, index) => ({
...callable,
location: index === 0 ? range(0, 90) : range(20, index === 1 ? 60 : 80),
})),
};
const nodes = nodesFor(ir);
const lookup = createSemanticCallableOwnerLookup(ir, nodes);
for (const query of [
range(25, 30),
range(25, 70),
range(0, 80),
range(80, 90),
range(91, 92),
])
expect(lookup(query)).toBe(previousOwner(query, ir, nodes));
});
it("matches the prior result across a compiled-style collection of sibling callables", () => {
const ir = analyzeJavaScriptSemantics(
Array.from(
{ length: 300 },
(_, index) =>
`function f${String(index)}(value) { return value + ${String(index)}; }`,
).join(""),
);
const nodes = nodesFor(ir);
const lookup = createSemanticCallableOwnerLookup(ir, nodes);
for (const range of rangesFor(ir))
expect(lookup(range)).toBe(previousOwner(range, ir, nodes));
});
it("returns contained nodes in original order, including repeated and overlapping entries", () => {
const ir = analyzeJavaScriptSemantics(`
function outer(value) { function inner() { return value; } return inner(); }
const sibling = () => outer(1);
`);
const nodes = [...nodesFor(ir).values()].reverse();
const repeated = nodes[0];
if (repeated === undefined) throw new Error("Missing range fixture node");
nodes.push(
repeated,
constructSemanticGraphNode(
fixtureFile,
{
kind: "module",
roleKey: "module",
location: null,
label: "module",
functionNodeId: null,
},
createSemanticGraphProjectionState({ nodes: [] }),
),
);
const lookup = createSemanticNodeRangeLookup(nodes);
const compare = (
left: JavaScriptSourceRange["start"],
right: JavaScriptSourceRange["start"],
) => left.line - right.line || left.column - right.column;
for (const range of rangesFor(ir)) {
if (range === null) continue;
const expected = nodes.filter(
({ identity }) =>
identity.source_range !== null &&
compare(range.start, identity.source_range.start) <= 0 &&
compare(range.end, identity.source_range.end) >= 0,
);
expect(lookup(range)).toEqual(expected);
}
expect(
createSemanticNodeRangeLookup([])(
repeated.identity.source_range ?? {
start: { line: 1, column: 0 },
end: { line: 1, column: 1 },
},
),
).toEqual([]);
});
it("keeps first-call matching and current retained membership for exact locations", () => {
const original = analyzeJavaScriptSemantics(
"function target(value) { return value; } target(1); target(2);",
);
const first = original.callSites[0];
const node = [...nodesFor(original).values()][0];
if (first === undefined || node === undefined)
throw new Error("Missing call fixture");
const duplicate = { ...first, callSiteId: `${first.callSiteId}:duplicate` };
const ir = {
...original,
callSites: [first, duplicate, ...original.callSites.slice(1)],
};
const nodes = new Map(
ir.callSites.map((call) => [
call.callSiteId,
constructSemanticGraphNode(
fixtureFile,
{
kind: "call-site",
roleKey: call.callSiteId,
location: call.location,
label: call.callSiteId,
functionNodeId: null,
},
createSemanticGraphProjectionState({ nodes: [] }),
),
]),
);
const lookup = createSemanticCallSiteLookup(ir, nodes);
const previous = (range: JavaScriptSourceRange) => {
const call = ir.callSites.find((candidate) =>
sourceRangesEqual(candidate.location, range),
);
return call === undefined ? undefined : nodes.get(call.callSiteId);
};
for (const call of ir.callSites)
expect(lookup(call.location)).toBe(previous(call.location));
nodes.delete(first.callSiteId);
expect(lookup(first.location)).toBeUndefined();
expect(nodes.has(duplicate.callSiteId)).toBe(true);
nodes.set(first.callSiteId, node);
expect(lookup(first.location)).toBe(node);
expect(
lookup({ start: { line: 99, column: 0 }, end: { line: 99, column: 1 } }),
).toBeUndefined();
});
@@ -5,40 +5,173 @@ import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSem
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
/** Find the innermost callable that contains one exact source range. */
export const owningSemanticCallableNode = (
location: JavaScriptSourceRange | null,
interface SourceInterval {
readonly range: JavaScriptSourceRange;
readonly order: number;
}
interface CallableInterval extends SourceInterval {
readonly callableId: string;
readonly priority: string;
}
interface SourceIntervalTree<Interval extends SourceInterval> {
readonly entry: Interval;
readonly minimumStart: JavaScriptSourceRange["start"];
readonly maximumStart: JavaScriptSourceRange["start"];
readonly minimumEnd: JavaScriptSourceRange["end"];
readonly maximumEnd: JavaScriptSourceRange["end"];
readonly left: SourceIntervalTree<Interval> | null;
readonly right: SourceIntervalTree<Interval> | null;
}
/** Index one immutable file IR; read retained callable membership at query time. */
export const createSemanticCallableOwnerLookup = (
ir: JavaScriptSemanticIr,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): JavaScriptSemanticGraphNode | undefined => {
if (location === null) return undefined;
return ir.callables
.filter((callable) => contains(callable.location, location))
.sort((left, right) =>
compareCodePoints(
positionKey(right.location),
positionKey(left.location),
),
)
.map(({ callableId }) => nodes.get(callableId))
.find((node) => node !== undefined);
): ((
location: JavaScriptSourceRange | null,
) => JavaScriptSemanticGraphNode | undefined) => {
const entries = ir.callables
.map((callable, order): CallableInterval => ({
callableId: callable.callableId,
range: callable.location,
order,
priority: positionKey(callable.location),
}))
.sort(
(left, right) =>
pointCompare(left.range.start, right.range.start) ||
left.order - right.order,
);
const tree = buildSourceIntervalTree(entries, 0, entries.length);
return (location) => {
if (location === null || tree === null || nodes.size === 0)
return undefined;
const pending = [tree];
let best: CallableInterval | undefined;
let result: JavaScriptSemanticGraphNode | undefined;
while (pending.length > 0) {
const current = pending.pop();
if (
current === undefined ||
pointCompare(current.minimumStart, location.start) > 0 ||
pointCompare(current.maximumEnd, location.end) < 0
)
continue;
const candidate = current.entry;
if (contains(candidate.range, location)) {
const node = nodes.get(candidate.callableId);
const rank =
best === undefined
? 1
: compareCodePoints(candidate.priority, best.priority) ||
best.order - candidate.order;
if (node !== undefined && rank > 0) {
best = candidate;
result = node;
}
}
if (current.left !== null) pending.push(current.left);
if (current.right !== null) pending.push(current.right);
}
return result;
};
};
/** Find retained semantic nodes whose exact ranges are within one expression. */
export const semanticNodesWithinRange = (
const buildSourceIntervalTree = <Interval extends SourceInterval>(
entries: readonly Interval[],
start: number,
end: number,
): SourceIntervalTree<Interval> | null => {
if (start >= end) return null;
const middle = Math.floor((start + end) / 2);
const entry = entries[middle];
if (entry === undefined) return null;
// This recursion follows a balanced index, not the producer's nesting depth.
const left = buildSourceIntervalTree(entries, start, middle);
const right = buildSourceIntervalTree(entries, middle + 1, end);
let minimumEnd = entry.range.end;
let maximumEnd = entry.range.end;
for (const child of [left, right]) {
if (child !== null && pointCompare(child.minimumEnd, minimumEnd) < 0)
minimumEnd = child.minimumEnd;
if (child !== null && pointCompare(child.maximumEnd, maximumEnd) > 0)
maximumEnd = child.maximumEnd;
}
return {
entry,
minimumStart: left?.minimumStart ?? entry.range.start,
maximumStart: right?.maximumStart ?? entry.range.start,
minimumEnd,
maximumEnd,
left,
right,
};
};
/** Index an immutable node list; return contained ranges in producer order. */
export const createSemanticNodeRangeLookup = (
nodes: readonly JavaScriptSemanticGraphNode[],
): ((range: JavaScriptSourceRange) => JavaScriptSemanticGraphNode[]) => {
const entries = nodes
.flatMap((node, order) => {
const range = node.identity.source_range;
return range === null ? [] : [{ node, range, order }];
})
.sort(
(left, right) =>
pointCompare(left.range.start, right.range.start) ||
left.order - right.order,
);
const tree = buildSourceIntervalTree(entries, 0, entries.length);
return (range) => {
if (tree === null) return [];
const pending = [tree];
const matches: (typeof entries)[number][] = [];
while (pending.length > 0) {
const current = pending.pop();
if (
current === undefined ||
pointCompare(current.maximumStart, range.start) < 0 ||
pointCompare(current.minimumEnd, range.end) > 0
)
continue;
if (contains(range, current.entry.range)) matches.push(current.entry);
if (current.left !== null) pending.push(current.left);
if (current.right !== null) pending.push(current.right);
}
return matches
.sort((left, right) => left.order - right.order)
.map(({ node }) => node);
};
};
/** Index exact call ranges, preserving the first producer occurrence. */
export const createSemanticCallSiteLookup = (
ir: JavaScriptSemanticIr,
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
): ((
range: JavaScriptSourceRange,
): JavaScriptSemanticGraphNode[] =>
nodes.filter(
(node) =>
node.identity.source_range !== null &&
contains(range, node.identity.source_range),
);
) => JavaScriptSemanticGraphNode | undefined) => {
const identifiers = new Map<string, string>();
for (const call of ir.callSites) {
const key = rangeKey(call.location);
if (!identifiers.has(key)) identifiers.set(key, call.callSiteId);
}
return (range) => {
const identifier = identifiers.get(rangeKey(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],
analysis: JavaScriptArtifactAnalysis,
analysis: Pick<JavaScriptArtifactAnalysis, "truncated_scopes">,
): "complete" | "partial" | "unknown" | "unsupported" => {
if (
![
@@ -2,10 +2,9 @@ import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/ja
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
import type { JavaScriptSemanticResourceOperation } from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import {
@@ -39,25 +38,18 @@ const addResourceNode = (
context: SemanticFlowProjectionContext,
operation: JavaScriptSemanticResourceOperation,
): JavaScriptSemanticGraphNode | null =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "resource",
roleKey: operation.resourceId,
location: operation.location,
label: operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: { method: operation.method },
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "resource",
roleKey: operation.resourceId,
location: operation.location,
label: operation.method,
functionNodeId:
operation.ownerCallableId === null
? null
: (context.callableNodes.get(operation.ownerCallableId)?.node_id ??
null),
properties: { method: operation.method },
});
const projectRelease = (
context: SemanticFlowProjectionContext,
@@ -4,9 +4,8 @@ import type {
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticIr.js";
import {
addSemanticGraphNode,
retainSemanticGraphNode,
addSemanticGraphRelation,
constructSemanticGraphNode,
} from "./JavaScriptSemanticGraphConstruction.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
@@ -85,22 +84,15 @@ const addLiteralNode = (
value: string | number | boolean | null,
role: string,
) =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "literal",
roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`,
location: binding.definitions[0]?.location ?? null,
label: JSON.stringify(value),
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "literal",
roleKey: `literal:${binding.bindingId}:${role}:${JSON.stringify(value)}`,
location: binding.definitions[0]?.location ?? null,
label: JSON.stringify(value),
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
});
/** Create one canonical property slot for a binding/property identity. */
export const semanticPropertySlot = (
@@ -108,22 +100,15 @@ export const semanticPropertySlot = (
objectBindingId: string,
name: string,
) =>
addSemanticGraphNode(
context.state,
constructSemanticGraphNode(
context.file,
{
kind: "property-slot",
roleKey: `property:${objectBindingId}:${name}`,
location: null,
label: name,
functionNodeId:
context.bindingNodes.get(objectBindingId)?.function_node_id ?? null,
properties: {
name,
object_binding_id: objectBindingId,
} satisfies Readonly<Record<string, JsonValue>>,
},
context.state,
),
);
retainSemanticGraphNode(context.state, context.file, {
kind: "property-slot",
roleKey: `property:${objectBindingId}:${name}`,
location: null,
label: name,
functionNodeId:
context.bindingNodes.get(objectBindingId)?.function_node_id ?? null,
properties: {
name,
object_binding_id: objectBindingId,
} satisfies Readonly<Record<string, JsonValue>>,
});