mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(javascript): complete large-ASAR analysis with file-local projection (#1055)
This commit is contained in:
@@ -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>>,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user