fix(javascript): complete large-ASAR analysis with file-local projection (#1055)
This commit is contained in:
parent
7af7bc88ce
commit
b9fb438835
|
|
@ -74,6 +74,18 @@ for the tool-result contract.
|
|||
|
||||
## Large results
|
||||
|
||||
Application analysis projects semantic relationships one source file at a time.
|
||||
After projection, it retains only the lexical module facts and exported return
|
||||
shapes needed to build application relationships. Full source IR does not stay
|
||||
resident for every file together. Callable ownership and contained-reference
|
||||
queries use per-file range indexes, while call sites use exact-range lookups.
|
||||
Existing node budgets and coverage reporting remain in effect.
|
||||
|
||||
Progress identifies the source file being parsed and projected. Cancellation is
|
||||
checked between files, with event-loop yields that allow notifications and
|
||||
completed file-local allocations to be released. A single file's synchronous
|
||||
parser and semantic extraction still need to finish before that boundary.
|
||||
|
||||
Graph and Evidence identifiers hash canonical JSON incrementally, without
|
||||
assembling a single string for the whole graph. The canonical bytes and existing
|
||||
identifiers remain unchanged. The opt-in regression check is
|
||||
|
|
@ -93,6 +105,9 @@ assemble whole strings. Field selection remains useful when the caller needs a
|
|||
smaller view, for example `--format json --filter-output
|
||||
evidence_id,normalized_result.statistics`. Streaming output does not bound the
|
||||
memory needed to construct the analysis graph itself.
|
||||
The separate large-response transport work is tracked in
|
||||
[#1053](https://github.com/morluto/rea/issues/1053) and
|
||||
[#1050](https://github.com/morluto/rea/issues/1050).
|
||||
Client framing limits also apply: the pinned Node MCP SDK's stdio transport
|
||||
defaults to a 10 MiB buffer. Its caller-selected `maxBufferSize` must accommodate
|
||||
the complete response, including text and structured Evidence projections.
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ function* bufferedParts(parts: Iterable<string>): Generator<string> {
|
|||
}
|
||||
|
||||
function* encodeString(value: string): Generator<string> {
|
||||
if (value.length <= 8192) {
|
||||
yield JSON.stringify(value);
|
||||
return;
|
||||
}
|
||||
yield '"';
|
||||
for (let start = 0; start < value.length;) {
|
||||
let end = Math.min(start + 8192, value.length);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,31 @@ const TARGET: BinaryTarget = {
|
|||
const PROVIDER = { id: "fixture", name: "Fixture provider", version: "1" };
|
||||
const PROFILE = createAnalysisProfile(PROVIDER, { loader: "default" });
|
||||
|
||||
it("snapshots caller-owned payloads before deriving their identity", () => {
|
||||
const result = { nested: { values: ["observed"] } };
|
||||
const rawResult = { entries: [{ value: "raw" }] };
|
||||
const parameters = { selected: ["target"] };
|
||||
const evidence = createEvidence(TARGET, PROVIDER, {
|
||||
operation: "inspect",
|
||||
parameters,
|
||||
result,
|
||||
rawResult,
|
||||
});
|
||||
result.nested.values.push("later");
|
||||
const rawEntry = rawResult.entries[0];
|
||||
if (rawEntry !== undefined) rawEntry.value = "changed";
|
||||
parameters.selected.push("other");
|
||||
expect(evidence.normalized_result).toEqual({
|
||||
nested: { values: ["observed"] },
|
||||
});
|
||||
expect(evidence.raw_result).toEqual({ entries: [{ value: "raw" }] });
|
||||
expect(evidence.parameters).toEqual({ selected: ["target"] });
|
||||
expect(parseEvidence(evidence)).toEqual(evidence);
|
||||
expect(() =>
|
||||
parseEvidence({ ...evidence, normalized_result: result }),
|
||||
).toThrow(/semantic identifier/u);
|
||||
});
|
||||
|
||||
describe("analysis evidence identity", () => {
|
||||
it("normalizes prototype-named parameter keys", () => {
|
||||
const evidence = createEvidence(TARGET, PROVIDER, {
|
||||
|
|
|
|||
|
|
@ -274,13 +274,23 @@ export const createEvidence = (
|
|||
locations: [...(observation.locations ?? [])],
|
||||
evidence_links: [...(observation.evidenceLinks ?? [])],
|
||||
} satisfies JsonValue;
|
||||
const normalized = evidenceRecordSchema.parse({
|
||||
// Select the known envelope before parsing. A union otherwise traverses and
|
||||
// clones a legacy result for the profiled branch before rejecting its absent
|
||||
// profile and trying the legacy branch.
|
||||
const schema =
|
||||
observation.analysisProfile === undefined
|
||||
? evidenceBaseSchema
|
||||
: profiledEvidenceSchema;
|
||||
const normalized = schema.parse({
|
||||
...semantic,
|
||||
evidence_id: `ev_${"0".repeat(64)}`,
|
||||
subject,
|
||||
});
|
||||
return parseEvidence({
|
||||
// The envelope has already been parsed into an independent snapshot. Only
|
||||
// its derived identifier changes here; parsing again clones the full payload
|
||||
// and recomputes the same digest while the previous snapshot is still live.
|
||||
return {
|
||||
...normalized,
|
||||
evidence_id: computeEvidenceId(normalized),
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ const normalizeEvidence = (
|
|||
evidence_ids: uniqueSorted(evidence.evidence_ids),
|
||||
});
|
||||
|
||||
const nodeId = (
|
||||
/** Derive a semantic node ID from its kind and exact artifact identity. */
|
||||
export const javaScriptSemanticNodeId = (
|
||||
node: Pick<JavaScriptSemanticGraphNode, "kind" | "identity">,
|
||||
): string =>
|
||||
`jsrg_node_${canonicalDigest({ kind: node.kind, identity: node.identity }, "JavaScript semantic graph")}`;
|
||||
|
|
@ -59,7 +60,7 @@ export const createJavaScriptSemanticGraphNode = (
|
|||
};
|
||||
return javaScriptSemanticNodeSchema.parse({
|
||||
...semantic,
|
||||
node_id: nodeId(semantic),
|
||||
node_id: javaScriptSemanticNodeId(semantic),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -129,11 +130,21 @@ export const createJavaScriptSemanticFingerprint = (input: unknown) => {
|
|||
|
||||
type GraphRecord = z.infer<typeof javaScriptSemanticGraphRecordSchema>;
|
||||
|
||||
type SemanticGraphIssue = {
|
||||
readonly code: "custom";
|
||||
readonly path: PropertyKey[];
|
||||
readonly message: string;
|
||||
};
|
||||
|
||||
interface GraphIssueReporter {
|
||||
readonly addIssue: (issue: SemanticGraphIssue) => void;
|
||||
}
|
||||
|
||||
const sortedUniqueIssue = (
|
||||
values: readonly string[],
|
||||
path: PropertyKey[],
|
||||
label: string,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (let index = 1; index < values.length; index += 1) {
|
||||
if (compareCodePoints(values[index - 1] ?? "", values[index] ?? "") < 0)
|
||||
|
|
@ -147,7 +158,10 @@ const sortedUniqueIssue = (
|
|||
}
|
||||
};
|
||||
|
||||
const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
||||
const checkCoverage = (
|
||||
graph: GraphRecord,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
const families = graph.coverage.families.map(({ family }) => family);
|
||||
if (
|
||||
canonicalJson(families, "JavaScript semantic graph") !==
|
||||
|
|
@ -221,7 +235,7 @@ const checkCoverage = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
|||
|
||||
const checkCanonicalOrder = (
|
||||
graph: GraphRecord,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
sortedUniqueIssue(
|
||||
graph.root_node_ids,
|
||||
|
|
@ -259,10 +273,10 @@ const checkCanonicalOrder = (
|
|||
const checkNodes = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (const [index, node] of graph.nodes.entries()) {
|
||||
if (node.node_id !== nodeId(node))
|
||||
if (node.node_id !== javaScriptSemanticNodeId(node))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["nodes", index, "node_id"],
|
||||
|
|
@ -283,7 +297,7 @@ const checkNodes = (
|
|||
const checkRoots = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (const root of graph.root_node_ids)
|
||||
if (!nodes.has(root))
|
||||
|
|
@ -297,7 +311,7 @@ const checkRoots = (
|
|||
const checkRelations = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (const [index, relation] of graph.relations.entries()) {
|
||||
const { relation_id: identifier, ...semantic } = relation;
|
||||
|
|
@ -340,7 +354,7 @@ const checkRelations = (
|
|||
const checkUnknowns = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (const [index, unknown] of graph.unknowns.entries()) {
|
||||
const { unknown_id: identifier, ...semantic } = unknown;
|
||||
|
|
@ -386,7 +400,7 @@ const checkUnknowns = (
|
|||
|
||||
const checkUnknownReferences = (
|
||||
graph: GraphRecord,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
const unknownIds = new Set(
|
||||
graph.unknowns.map(({ unknown_id }) => unknown_id),
|
||||
|
|
@ -403,7 +417,7 @@ const checkUnknownReferences = (
|
|||
const checkFingerprints = (
|
||||
graph: GraphRecord,
|
||||
nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
context: z.RefinementCtx,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
for (const [index, fingerprint] of graph.fingerprints.entries()) {
|
||||
if (nodes.get(fingerprint.function_node_id)?.kind !== "function")
|
||||
|
|
@ -449,7 +463,10 @@ const checkFingerprints = (
|
|||
}
|
||||
};
|
||||
|
||||
const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
||||
const checkGraphContent = (
|
||||
graph: GraphRecord,
|
||||
context: GraphIssueReporter,
|
||||
): void => {
|
||||
checkCanonicalOrder(graph, context);
|
||||
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
|
||||
checkNodes(graph, nodes, context);
|
||||
|
|
@ -465,6 +482,10 @@ const checkGraph = (graph: GraphRecord, context: z.RefinementCtx): void => {
|
|||
path: ["limitations"],
|
||||
message: "Non-complete graph coverage requires a limitation",
|
||||
});
|
||||
};
|
||||
|
||||
const checkGraph = (graph: GraphRecord, context: GraphIssueReporter): void => {
|
||||
checkGraphContent(graph, context);
|
||||
const { graph_id: identifier, ...semantic } = graph;
|
||||
if (
|
||||
identifier !==
|
||||
|
|
@ -523,10 +544,21 @@ export const createJavaScriptSemanticGraph = (
|
|||
},
|
||||
limitations: uniqueSorted(parsed.limitations),
|
||||
};
|
||||
return javaScriptSemanticGraphSchema.parse({
|
||||
// The input schema already cloned and validated every field. Validate the
|
||||
// normalized relationships in place, then derive the only added field. The
|
||||
// public schema still verifies arbitrary records and their commitments.
|
||||
const record: GraphRecord = {
|
||||
...semantic,
|
||||
graph_id: `jsrg_${canonicalDigest(semantic, "JavaScript semantic graph")}`,
|
||||
};
|
||||
const issues: SemanticGraphIssue[] = [];
|
||||
checkGraphContent(record, {
|
||||
addIssue: (issue) => {
|
||||
issues.push(issue);
|
||||
},
|
||||
});
|
||||
if (issues.length > 0) throw new z.ZodError(issues);
|
||||
return record;
|
||||
};
|
||||
|
||||
export type { JavaScriptSemanticGraphNode, JavaScriptSemanticGraphRelation };
|
||||
|
|
|
|||
|
|
@ -7,6 +7,18 @@ import { expect, it } from "vitest";
|
|||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
import { reconstructJavaScriptArtifact } from "../../../src/application/javascript/JavaScriptArtifactReconstruction.js";
|
||||
import {
|
||||
analyzeJavaScriptArtifactFiles,
|
||||
analyzeAndProjectJavaScriptArtifactFiles,
|
||||
} from "../../../src/application/javascript/JavaScriptArtifactAnalysis.js";
|
||||
import { buildJavaScriptArtifactGraph } from "../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js";
|
||||
import {
|
||||
buildJavaScriptSemanticGraph,
|
||||
createJavaScriptSemanticGraphProjection,
|
||||
} from "../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js";
|
||||
import { createJavaScriptArtifactReader } from "../../../src/artifacts/javascript/JavaScriptArtifactReader.js";
|
||||
import { readJavaScriptArtifactFiles } from "../../../src/artifacts/javascript/JavaScriptArtifactFiles.js";
|
||||
import { scanCanonicalArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js";
|
||||
import { scanArtifactInventory } from "../../../src/artifacts/inventory/ArtifactInventory.js";
|
||||
import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js";
|
||||
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
||||
|
|
@ -14,6 +26,83 @@ import { createJavaScriptSemanticGraph } from "../../../src/domain/javascript/ja
|
|||
import { parseJavaScriptSemanticGraph } from "../../../src/domain/javascript/javascriptSemanticGraphSerialization.js";
|
||||
import { writeJavaScriptArtifactFixture } from "../../fixtures/javascriptArtifactApplication.js";
|
||||
|
||||
it("reports file progress and honors cancellation before the next source", async () => {
|
||||
const root = await fixtureDirectory();
|
||||
const controller = new AbortController();
|
||||
const progress: string[] = [];
|
||||
await expect(
|
||||
reconstructJavaScriptArtifact({ input_path: root }, controller.signal, {
|
||||
report: async (event) => {
|
||||
if (event.phase === "parse_javascript_source") {
|
||||
progress.push(event.message);
|
||||
if (progress.length === 2) controller.abort();
|
||||
}
|
||||
},
|
||||
}),
|
||||
).rejects.toMatchObject({ reason: "cancelled" });
|
||||
expect(progress).toHaveLength(2);
|
||||
expect(progress[0]).not.toBe(progress[1]);
|
||||
});
|
||||
|
||||
it("preserves graph commitments and export shapes when consuming file-local IR", async () => {
|
||||
const root = await fixtureDirectory();
|
||||
await writeFile(
|
||||
join(root, "exports.mjs"),
|
||||
`
|
||||
import { readFile } from "node:fs";
|
||||
export { readFile as read };
|
||||
export function create(value) { return { value, nested: { enabled: true } }; }
|
||||
function unexported() { return "private"; }
|
||||
`,
|
||||
);
|
||||
const snapshot = await scanCanonicalArtifactInventory(root, {});
|
||||
const reader = createJavaScriptArtifactReader(root, "directory");
|
||||
try {
|
||||
const files = await readJavaScriptArtifactFiles(reader, snapshot);
|
||||
const full = analyzeJavaScriptArtifactFiles(files);
|
||||
const projection = createJavaScriptSemanticGraphProjection();
|
||||
const compact = await analyzeAndProjectJavaScriptArtifactFiles(
|
||||
files,
|
||||
projection.projectFile,
|
||||
);
|
||||
const graph = buildJavaScriptArtifactGraph(snapshot, files, full);
|
||||
expect(buildJavaScriptArtifactGraph(snapshot, files, compact)).toEqual(
|
||||
graph,
|
||||
);
|
||||
const semantic = projection.finish(
|
||||
snapshot.manifest.root_sha256,
|
||||
graph,
|
||||
compact,
|
||||
);
|
||||
expect(semantic).toEqual(
|
||||
buildJavaScriptSemanticGraph({
|
||||
rootArtifactSha256: snapshot.manifest.root_sha256,
|
||||
applicationGraph: graph,
|
||||
analysis: full,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
semantic.nodes.some(
|
||||
({ application_node_ids }) => application_node_ids.length > 0,
|
||||
),
|
||||
).toBe(true);
|
||||
const exported = compact.files.find(
|
||||
({ file }) => file.path === "exports.mjs",
|
||||
);
|
||||
expect(exported?.semantic?.ir.callables.map(({ name }) => name)).toEqual([
|
||||
"create",
|
||||
]);
|
||||
expect(
|
||||
full.files
|
||||
.find(({ file }) => file.path === "exports.mjs")
|
||||
?.semantic?.ir.callables.map(({ name }) => name),
|
||||
).toContain("unexported");
|
||||
expect(parseJavaScriptSemanticGraph(semantic)).toEqual(semantic);
|
||||
} finally {
|
||||
await reader.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("reconstructs package, Electron roles, Webpack/Rspack modules, and cross-layer facts without execution", async () => {
|
||||
const root = await fixtureDirectory();
|
||||
Reflect.deleteProperty(globalThis, "__rea_bundle_executed");
|
||||
|
|
|
|||
Loading…
Reference in New Issue