refactor(javascript): remove superseded full-IR analysis pipeline

This commit is contained in:
morluto committed 2026-10-24 23:49:25 +08:00
1 parent cc7cc8155a
commit e9abf39848
21 files changed
+410 -479

No files matched your search

@@ -2,7 +2,7 @@ import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
import type { ElectronIpcFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
resolveArtifactPathByContext,
@@ -31,7 +31,7 @@ export interface ElectronIpcPairingState {
/** Flatten all per-file IPC facts into deterministic application records. */
export const collectElectronIpcRecords = (
analysis: JavaScriptModuleArtifactAnalysis,
analysis: JavaScriptArtifactAnalysis,
): readonly ElectronIpcRecord[] =>
analysis.files
.flatMap(({ file, javascript }) =>
@@ -83,7 +83,7 @@ export const unambiguousElectronIpcPairings = (
/** Summarize static Electron findings without treating gaps as absence. */
export const summarizeElectronBoundaries = (
analysis: JavaScriptModuleArtifactAnalysis,
analysis: JavaScriptArtifactAnalysis,
directoryPaths?: ReadonlySet<string>,
): ElectronBoundarySummary => {
const files = new Map(analysis.files.map(({ file }) => [file.path, file]));
@@ -1,9 +1,9 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { analyzeArtifactFiles } from "../../../tests/fixtures/javascriptArtifactAnalysis.js";
const packageObservation = (text: string) =>
analyzeJavaScriptArtifactFiles({
const packageObservation = async (text: string) => {
const { analysis } = await analyzeArtifactFiles({
files: [
{
path: "app/package.json",
@@ -20,44 +20,15 @@ const packageObservation = (text: string) =>
containers: [],
text_bytes_read: Buffer.byteLength(text),
invalid_utf8_files: 0,
}).packages[0];
});
return analysis.packages[0];
};
it("retains only declared package metadata despite inherited host fields", () => {
const inherited = {
name: "host-name",
version: "host-version",
main: "host-entry.cjs",
renderer: "host-renderer.cjs",
browser: "host-browser.cjs",
module: "host-module.mjs",
};
const previous = Object.fromEntries(
Object.keys(inherited).map((key) => [
key,
Object.getOwnPropertyDescriptor(Object.prototype, key),
]),
it("retains declared package metadata and normal defaults", async () => {
const absent = await packageObservation("{}");
const declared = await packageObservation(
'{"name":"source-name","version":"1.0.0","main":null,"browser":"source-browser.cjs"}',
);
let absent: ReturnType<typeof packageObservation>;
let declared: ReturnType<typeof packageObservation>;
try {
for (const [key, value] of Object.entries(inherited))
Object.defineProperty(Object.prototype, key, {
configurable: true,
writable: true,
value,
});
absent = packageObservation("{}");
declared = packageObservation(
'{"name":"source-name","version":"1.0.0","main":null,"browser":"source-browser.cjs"}',
);
} finally {
for (const key of Object.keys(inherited)) {
const descriptor = previous[key];
if (descriptor === undefined)
Reflect.deleteProperty(Object.prototype, key);
else Object.defineProperty(Object.prototype, key, descriptor);
}
}
expect(absent).toMatchObject({
status: "included",
name: null,
@@ -80,8 +51,8 @@ it("retains only declared package metadata despite inherited host fields", () =>
it.each([null, false, 123, {}, []].map((main) => ({ main })))(
"retains non-string main $main evidence separately from Node index fallback",
({ main }) => {
expect(packageObservation(JSON.stringify({ main }))).toMatchObject({
async ({ main }) => {
expect(await packageObservation(JSON.stringify({ main }))).toMatchObject({
status: "included",
main: null,
main_status: "invalid",
@@ -90,8 +61,8 @@ it.each([null, false, 123, {}, []].map((main) => ({ main })))(
},
);
it("keeps valid JSON numeric overflow invalid as a package main", () => {
expect(packageObservation('{"main":1e400}')).toMatchObject({
it("keeps valid JSON numeric overflow invalid as a package main", async () => {
expect(await packageObservation('{"main":1e400}')).toMatchObject({
status: "included",
main: null,
main_status: "invalid",
@@ -99,8 +70,8 @@ it("keeps valid JSON numeric overflow invalid as a package main", () => {
});
});
it("treats a valid numeric JSON root as a package metadata shape failure", () => {
expect(packageObservation("1e400")).toMatchObject({
it("treats a valid numeric JSON root as a package metadata shape failure", async () => {
expect(await packageObservation("1e400")).toMatchObject({
status: "invalid",
main: null,
main_status: "unavailable",
@@ -108,9 +79,9 @@ it("treats a valid numeric JSON root as a package metadata shape failure", () =>
});
});
it("retains valid numeric overflow as an ordinary JSON module", () => {
it("retains valid numeric overflow as an ordinary JSON module", async () => {
const text = "1e400";
const analysis = analyzeJavaScriptArtifactFiles({
const { analysis } = await analyzeArtifactFiles({
files: [
{
path: "settings.json",
@@ -142,8 +113,8 @@ it.each([
['{"main":"C:main.cjs"}', "declared", "C:main.cjs"],
] as const)(
"preserves package launch declarations from %s",
(text, status, entry) => {
expect(packageObservation(text)).toMatchObject({
async (text, status, entry) => {
expect(await packageObservation(text)).toMatchObject({
status: "included",
main_status: status,
main_entrypoint: entry,
@@ -153,8 +124,8 @@ it.each([
it.each(["{", "null", "[]", "[{}]", "123", '"root"', "false"])(
"keeps malformed package metadata %s out of known launch fallback",
(text) => {
expect(packageObservation(text)).toMatchObject({
async (text) => {
expect(await packageObservation(text)).toMatchObject({
status: "invalid",
main: null,
main_status: "unavailable",
@@ -1,10 +1,10 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { analyzeArtifactFiles } from "../../../tests/fixtures/javascriptArtifactAnalysis.js";
const HASH = "a".repeat(64);
it("retains resolved source declarations, Unicode spelling and null source positions", () => {
it("retains resolved source declarations, Unicode spelling and null source positions", async () => {
const sourceMapText = JSON.stringify({
version: 3,
file: "main.js",
@@ -14,7 +14,7 @@ it("retains resolved source declarations, Unicode spelling and null source posit
names: [],
mappings: "",
});
const analysis = analyzeJavaScriptArtifactFiles({
const { analysis } = await analyzeArtifactFiles({
files: [
{
path: "dist/main.js.map",
@@ -1,17 +1,8 @@
import { createHash } from "node:crypto";
import { moduleResolutionModeForJavaScriptReference } from "../../domain/javascript/javascriptModuleResolutionMode.js";
import { setImmediate } from "node:timers/promises";
import { resolveJavaScriptSourceMapReferences } from "../../domain/javascript/javascriptSourceMapPaths.js";
import { analyzeParsedJavaScriptStaticSourceSteps } from "../../domain/javascript/javascriptStaticAnalysis.js";
import { analyzeParsedJavaScriptSemanticsSteps } from "../../domain/javascript/javascriptSemanticAnalysis.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import {
type JavaScriptModuleSemanticIr,
projectJavaScriptModuleSemantics,
} from "../../domain/javascript/javascriptModuleSemanticIr.js";
import { parseJavaScriptSource } from "../../domain/javascript/javascriptSourceParser.js";
import { hasValidSourceMapContents } from "../../domain/sourceMapContents.js";
import {
inspectSourceMapEnvelope,
@@ -20,7 +11,6 @@ import {
} from "../../domain/sourceMapEnvelope.js";
import type { SourceMapLeaf } from "../../domain/sourceMapEnvelope.js";
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import { failedJavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
import type {
JavaScriptArtifactFile,
JavaScriptArtifactFileSet,
@@ -34,7 +24,6 @@ import type {
JavaScriptArtifactAnalysisFailure,
JavaScriptArtifactAnalysisScope,
JavaScriptArtifactAnalysis,
JavaScriptModuleArtifactAnalysis,
JavaScriptHtmlInlineScriptsObservation,
JavaScriptHtmlScriptObservation,
JavaScriptJsonModuleObservation,
@@ -48,7 +37,7 @@ import {
JavaScriptJsonDocuments,
type JavaScriptJsonDocumentFacts,
} from "./JavaScriptJsonDocuments.js";
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
import { assertJavaScriptAnalysisActive } from "./JavaScriptAnalysisControl.js";
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
import type { JavaScriptSourceAnalysisPort } from "../../domain/javascript/javascriptSourceAnalysis.js";
import type { JavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
@@ -62,11 +51,9 @@ import {
AnalysisTimeoutError,
} from "../../domain/analysisErrorCore.js";
interface MutableArtifactAnalysis<
SemanticIr extends JavaScriptModuleSemanticIr,
> {
interface MutableArtifactAnalysis {
readonly jsonDocuments: JavaScriptJsonDocuments;
readonly files: AnalyzedJavaScriptArtifactFile<SemanticIr>[];
readonly files: AnalyzedJavaScriptArtifactFile[];
readonly packages: JavaScriptPackageObservation[];
readonly jsonModules: JavaScriptJsonModuleObservation[];
readonly htmlScripts: JavaScriptHtmlScriptObservation[];
@@ -78,26 +65,6 @@ interface MutableArtifactAnalysis<
parseFailures: number;
}
interface ArtifactAnalysisContext<
SemanticIr extends JavaScriptModuleSemanticIr,
> {
readonly state: MutableArtifactAnalysis<SemanticIr>;
readonly projectSemantics: (
file: JavaScriptArtifactFile,
ir: JavaScriptSemanticIr,
) => Generator<void, SemanticIr>;
}
/** Analyze every text source from the artifact without a result quota. */
export const analyzeJavaScriptArtifactFiles = (
fileSet: JavaScriptArtifactFileSet,
): JavaScriptArtifactAnalysis =>
analyzeArtifactFiles(fileSet, function* (_file, ir) {
// The full IR needs no projection; one step keeps the shared protocol.
yield;
return ir;
});
/** Analyze sources behind a hard heap boundary, retaining collected facts on interruption. */
export const analyzeAndAdoptJavaScriptArtifactFiles = async (
fileSet: JavaScriptArtifactFileSet,
@@ -111,10 +78,10 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
signal?: AbortSignal,
onPhase?: (file: JavaScriptArtifactFile, phase: string) => Promise<void>,
): Promise<{
readonly analysis: JavaScriptModuleArtifactAnalysis;
readonly analysis: JavaScriptArtifactAnalysis;
readonly interruption: AnalysisError | null;
}> => {
const state = emptyArtifactAnalysis<JavaScriptModuleSemanticIr>();
const state = emptyArtifactAnalysis();
let interruption: AnalysisError | null = null;
const selectedFileCount =
fileSet.files.length - (fileSet.source_selection?.unselected_files ?? 0);
@@ -140,14 +107,9 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
await beforeFile(file, selectedFileIndex++, selectedFileCount);
analysisStarted = true;
if (file.kind !== "javascript" || !file.text.included) {
const steps = analyzeArtifactFileSteps(file, {
state,
projectSemantics: function* (_file, ir) {
yield;
return projectJavaScriptModuleSemantics(ir);
},
});
await completeJavaScriptAnalysisSteps(steps, signal);
assertJavaScriptAnalysisActive(signal);
analyzeArtifactMetadata(file, state);
assertJavaScriptAnalysisActive(signal);
continue;
}
const result = await worker.analyze(
@@ -317,24 +279,9 @@ export const analyzeAndAdoptJavaScriptArtifactFiles = async (
return { analysis: finalizeArtifactAnalysis(state), interruption };
};
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) {
const steps = analyzeArtifactFileSteps(file, context);
while (steps.next().done !== true);
}
return finalizeArtifactAnalysis(state);
};
const finalizeArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr,
>(
state: MutableArtifactAnalysis<SemanticIr>,
): JavaScriptArtifactAnalysis<SemanticIr> => {
const finalizeArtifactAnalysis = (
state: MutableArtifactAnalysis,
): JavaScriptArtifactAnalysis => {
return {
jsonDocuments: state.jsonDocuments,
files: state.files,
@@ -367,14 +314,10 @@ const finalizeArtifactAnalysis = <
};
};
function* analyzeArtifactFileSteps<
SemanticIr extends JavaScriptModuleSemanticIr,
>(
const analyzeArtifactMetadata = (
file: JavaScriptArtifactFile,
context: ArtifactAnalysisContext<SemanticIr>,
): Generator<void, void> {
const { state } = context;
if (classifyUnavailableSource(file, state)) return;
state: MutableArtifactAnalysis,
): void => {
addStructuredObservations(file, state);
if (file.kind === "html" && file.text.included) {
const html = parseHtmlScripts(file.path, file.text.value);
@@ -383,78 +326,23 @@ function* analyzeArtifactFileSteps<
if (html.inlineScripts !== null)
state.htmlInlineScripts.push(html.inlineScripts);
}
if (file.kind === "source-map") addSourceMap(file, context);
if (file.kind !== "javascript" || !file.text.included) {
state.files.push({ file, javascript: null, semantic: null });
return;
}
const parsed = parseJavaScriptSource(file.text.value, file.path);
yield;
if (parsed === null) {
const analysis = failedJavaScriptStaticAnalysis();
state.files.push({ file, javascript: analysis, semantic: null });
state.parseFailures += 1;
return;
}
const analysis = yield* analyzeParsedJavaScriptStaticSourceSteps(
file.text.value,
parsed,
);
const staticFindings = findingCount(analysis);
yield;
const semantics =
analysis.parse_status === "complete" || analysis.parse_status === "partial"
? yield* analyzeParsedJavaScriptSemanticsSteps(
parsed,
moduleResolutionModeForJavaScriptReference(
file.path,
"static-import",
),
)
: null;
const projected =
semantics === null
? null
: yield* context.projectSemantics(file, semantics);
state.files.push({
file,
javascript: analysis,
semantic: projected === null ? null : { ir: projected },
});
state.visitedNodes += analysis.visited_ast_nodes;
state.findings += staticFindings + (semantics?.moduleLinks.length ?? 0);
state.modules += analysis.bundler_registrations.reduce(
(count, registration) => count + registration.modules.length,
0,
);
if (analysis.parse_status === "failed") state.parseFailures += 1;
}
if (file.kind === "source-map") state.sourceMaps.push(parseSourceMap(file));
state.files.push({ file, javascript: null, semantic: null });
};
const classifyUnavailableSource = <
SemanticIr extends JavaScriptModuleSemanticIr,
>(
const classifyUnavailableSource = (
file: JavaScriptArtifactFile,
state: MutableArtifactAnalysis<SemanticIr>,
projection?: JavaScriptSemanticGraphProjection,
state: MutableArtifactAnalysis,
projection: JavaScriptSemanticGraphProjection,
): boolean => {
const reason = javaScriptArtifactTextExclusion(file);
if (reason === null) return false;
state.files.push({ file, javascript: null, semantic: null });
projection?.recordUnavailableFile(file, reason, null, "incomplete-module");
projection.recordUnavailableFile(file, reason, null, "incomplete-module");
return true;
};
const addSourceMap = (
file: JavaScriptArtifactFile,
context: ArtifactAnalysisContext<JavaScriptModuleSemanticIr>,
): void => {
const sourceMap = parseSourceMap(file);
context.state.sourceMaps.push(sourceMap);
};
const emptyArtifactAnalysis = <
SemanticIr extends JavaScriptModuleSemanticIr,
>(): MutableArtifactAnalysis<SemanticIr> => ({
const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({
jsonDocuments: new JavaScriptJsonDocuments(),
files: [],
packages: [],
@@ -470,7 +358,7 @@ const emptyArtifactAnalysis = <
const addStructuredObservations = (
file: JavaScriptArtifactFile,
state: MutableArtifactAnalysis<JavaScriptModuleSemanticIr>,
state: MutableArtifactAnalysis,
): void => {
if (file.kind === "package-json")
state.packages.push(parsePackage(file, state.jsonDocuments.read(file)));
@@ -3,7 +3,6 @@ import type {
JavaScriptSourceRange,
JavaScriptStaticAnalysis,
} from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptSemanticIr } from "../../domain/javascript/javascriptSemanticIr.js";
import type { JavaScriptModuleSemanticIr } from "../../domain/javascript/javascriptModuleSemanticIr.js";
import type {
ExternalSourceMapSection,
@@ -162,14 +161,12 @@ export interface JavaScriptArtifactAnalysisFailure {
readonly limits: Readonly<Record<string, JsonValue>> | null;
}
/** One relevant file plus optional AST-only JavaScript facts. */
export interface AnalyzedJavaScriptArtifactFile<
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
> {
/** One relevant file and the module facts retained after file-local projection. */
export interface AnalyzedJavaScriptArtifactFile {
readonly file: JavaScriptArtifactFile;
readonly javascript: JavaScriptStaticAnalysis | null;
readonly semantic: {
readonly ir: SemanticIr;
readonly ir: JavaScriptModuleSemanticIr;
} | null;
/** Operational incompleteness, distinct from a malformed source file. */
readonly analysis_failure?: JavaScriptArtifactAnalysisFailure;
@@ -197,13 +194,11 @@ export const isSkippedJavaScriptAnalysis = (
entry.javascript === null &&
entry.analysis_failure !== undefined;
/** Complete static-analysis projection before graph construction. */
export interface JavaScriptArtifactAnalysis<
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
> {
/** Retained source facts shared by application graph construction. */
export interface JavaScriptArtifactAnalysis {
/** Internal source-facts owner shared with graph path resolution. */
readonly jsonDocuments: JavaScriptJsonDocuments;
readonly files: readonly AnalyzedJavaScriptArtifactFile<SemanticIr>[];
readonly files: readonly AnalyzedJavaScriptArtifactFile[];
readonly packages: readonly JavaScriptPackageObservation[];
readonly json_modules: readonly JavaScriptJsonModuleObservation[];
readonly html_scripts: readonly JavaScriptHtmlScriptObservation[];
@@ -215,7 +210,3 @@ export interface JavaScriptArtifactAnalysis<
readonly parse_failures: number;
readonly limitations: readonly string[];
}
/** Artifact facts retaining only semantics used by application relationships. */
export type JavaScriptModuleArtifactAnalysis =
JavaScriptArtifactAnalysis<JavaScriptModuleSemanticIr>;
@@ -4,7 +4,7 @@ import {
sealTransferredJavaScriptApplicationGraphSteps,
type JavaScriptApplicationGraph,
} from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactFileSet } from "../../domain/javascript/javascriptArtifactFiles.js";
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
import {
@@ -57,7 +57,7 @@ import {
export function* buildImmutableJavaScriptArtifactGraphSteps(
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
analysis: JavaScriptArtifactAnalysis,
): Generator<void, JavaScriptApplicationGraph> {
const input = yield* buildJavaScriptArtifactGraphInputSteps(
snapshot,
@@ -71,7 +71,7 @@ export function* buildImmutableJavaScriptArtifactGraphSteps(
function* buildJavaScriptArtifactGraphInputSteps(
snapshot: ArtifactInventorySnapshot,
fileSet: JavaScriptArtifactFileSet,
analysis: JavaScriptModuleArtifactAnalysis,
analysis: JavaScriptArtifactAnalysis,
): Generator<void, unknown> {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
@@ -14,7 +14,7 @@ import type { ApplicationNode } from "../../domain/javascript/javascriptApplicat
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type {
JavaScriptHtmlInlineScriptsObservation,
JavaScriptModuleArtifactAnalysis,
JavaScriptArtifactAnalysis,
JavaScriptArtifactAnalysisScope,
} from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptUnanalyzedArtifactLookup } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -41,7 +41,7 @@ export interface JavaScriptArtifactGraphContext {
readonly accumulator: JavaScriptArtifactGraphAccumulator;
readonly snapshot: ArtifactInventorySnapshot;
readonly fileSet: JavaScriptArtifactFileSet;
readonly analysis: JavaScriptModuleArtifactAnalysis;
readonly analysis: JavaScriptArtifactAnalysis;
readonly directoryPaths: ReadonlySet<string>;
readonly root: ApplicationNode;
readonly filesByPath: ReadonlyMap<string, JavaScriptArtifactFile>;
@@ -380,7 +380,7 @@ export const addUnavailableStaticParseScope = (
/** Coverage for one bounded JavaScript AST analysis. */
export const javascriptAnalysisCoverage = (
analysis: NonNullable<
JavaScriptModuleArtifactAnalysis["files"][number]["javascript"]
JavaScriptArtifactAnalysis["files"][number]["javascript"]
>,
): JavaScriptArtifactGraphCoverage =>
analysis.parse_status === "complete"
@@ -6,7 +6,7 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA
import { analyzeJavaScriptStaticSource } from "../../domain/javascript/javascriptStaticAnalysis.js";
import type {
AnalyzedJavaScriptArtifactFile,
JavaScriptModuleArtifactAnalysis,
JavaScriptArtifactAnalysis,
} from "./JavaScriptArtifactAnalysisTypes.js";
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
import { JavaScriptArtifactGraphAccumulator } from "./JavaScriptArtifactGraphAccumulator.js";
@@ -51,7 +51,7 @@ const skippedEntry = (
const analysisFor = (
entries: readonly AnalyzedJavaScriptArtifactFile[],
): JavaScriptModuleArtifactAnalysis => ({
): JavaScriptArtifactAnalysis => ({
jsonDocuments: new JavaScriptJsonDocuments(),
files: entries,
packages: [],
@@ -68,7 +68,7 @@ const analysisFor = (
const contextFor = (
file: JavaScriptArtifactFile,
analysis: JavaScriptModuleArtifactAnalysis,
analysis: JavaScriptArtifactAnalysis,
): JavaScriptArtifactGraphContext => {
const accumulator = new JavaScriptArtifactGraphAccumulator();
const snapshot = {
@@ -6,7 +6,7 @@ import {
completeApplicationCoverage,
partialApplicationCoverage,
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { freezeJsonSnapshot } from "../../domain/immutableJson.js";
@@ -623,8 +623,8 @@ export const addJavaScriptPackageNodes = (
const createFileTarget = (
context: JavaScriptArtifactGraphContext,
file: JavaScriptArtifactFile,
javascript: JavaScriptModuleArtifactAnalysis["files"][number]["javascript"],
json: JavaScriptModuleArtifactAnalysis["json_modules"][number] | undefined,
javascript: JavaScriptArtifactAnalysis["files"][number]["javascript"],
json: JavaScriptArtifactAnalysis["json_modules"][number] | undefined,
skipped: boolean,
): ApplicationNode => {
const kind = artifactFileNodeKind(file.kind);
@@ -18,6 +18,67 @@ const mutableFile = (): MutableArtifactFile => ({
text: { included: true, value: '{"main":"a.cjs"}' },
});
it("retains only declared package metadata despite inherited host fields", () => {
const documents = new JavaScriptJsonDocuments();
const absentFile = mutableFile();
absentFile.text = { included: true, value: "{}" };
const declaredFile = mutableFile();
declaredFile.text = {
included: true,
value:
'{"name":"source-name","version":"1.0.0","main":null,"browser":"source-browser.cjs"}',
};
const inherited = {
name: "host-name",
version: "host-version",
main: "host-entry.cjs",
renderer: "host-renderer.cjs",
browser: "host-browser.cjs",
module: "host-module.mjs",
};
const previous = Object.fromEntries(
Object.keys(inherited).map((key) => [
key,
Object.getOwnPropertyDescriptor(Object.prototype, key),
]),
);
let absent: ReturnType<JavaScriptJsonDocuments["read"]>;
let declared: ReturnType<JavaScriptJsonDocuments["read"]>;
try {
for (const [key, value] of Object.entries(inherited))
Object.defineProperty(Object.prototype, key, {
configurable: true,
writable: true,
value,
});
absent = documents.read(absentFile);
declared = documents.read(declaredFile);
} finally {
for (const key of Object.keys(inherited)) {
const descriptor = previous[key];
if (descriptor === undefined)
Reflect.deleteProperty(Object.prototype, key);
else Object.defineProperty(Object.prototype, key, descriptor);
}
}
expect(absent.status).toBe("included");
expect(declared.status).toBe("included");
if (absent.packageFields === null || declared.packageFields === null)
throw new Error("Expected package metadata fields");
expect(Object.getPrototypeOf(absent.packageFields)).toBeNull();
expect(Object.getPrototypeOf(declared.packageFields)).toBeNull();
for (const key of Object.keys(inherited))
expect(Object.hasOwn(absent.packageFields, key)).toBe(false);
expect(declared.packageFields).toEqual({
name: "source-name",
version: "1.0.0",
main: null,
browser: "source-browser.cjs",
});
expect(Object.hasOwn(declared.packageFields, "renderer")).toBe(false);
expect(Object.hasOwn(declared.packageFields, "module")).toBe(false);
});
it("reparses changed text and availability on a reused file object", () => {
const documents = new JavaScriptJsonDocuments();
const file = mutableFile();
@@ -18,11 +18,7 @@ import type {
} from "../../domain/javascript/javascriptSemanticIr.js";
import { sourceRangesEqual as rangesEqual } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import {
javaScriptSemanticSourceExclusion,
type JavaScriptArtifactFile,
} from "../../domain/javascript/javascriptArtifactFiles.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
import {
projectSemanticEvents,
projectSemanticTimers,
@@ -91,14 +87,7 @@ import type {
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import { projectElectronSemanticCallPaths } from "./JavaScriptSemanticGraphElectronProjection.js";
interface BuilderInput {
readonly rootArtifactSha256: string;
readonly applicationGraph: Pick<
JavaScriptApplicationGraph,
"graph_id" | "nodes"
>;
readonly analysis: JavaScriptArtifactAnalysis;
}
type ApplicationGraph = Pick<JavaScriptApplicationGraph, "graph_id" | "nodes">;
interface FileContext extends SemanticFlowProjectionContext {
readonly file: JavaScriptArtifactFile;
@@ -147,36 +136,6 @@ export const SEMANTIC_GRAPH_NODE_CEILING =
export const SEMANTIC_GRAPH_FILE_NODE_CEILING =
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.semanticNodesPerFile;
export const buildJavaScriptSemanticGraph = ({
rootArtifactSha256,
applicationGraph,
analysis,
}: BuilderInput): JavaScriptSemanticGraph => {
const projection = createJavaScriptSemanticGraphProjection();
for (const analyzed of analysis.files) {
if (analyzed.semantic !== null)
projection.projectFile(
analyzed.file,
analyzed.semantic.ir,
analyzed.javascript?.electron.call_paths,
);
else {
const reason = javaScriptSemanticSourceExclusion(
analyzed.file,
analyzed.javascript?.parse_status ?? null,
);
if (reason !== null)
projection.recordUnavailableFile(
analyzed.file,
reason,
null,
"incomplete-module",
);
}
}
return projection.finish(rootArtifactSha256, applicationGraph);
};
/** File-local semantic projection that does not retain consumed source IR. */
export interface JavaScriptSemanticGraphProjection {
/** Node capacity offered to the next independently analyzed source file. */
@@ -205,11 +164,11 @@ export interface JavaScriptSemanticGraphProjection {
) => Generator<void, void>;
readonly finish: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
applicationGraph: ApplicationGraph,
) => JavaScriptSemanticGraph;
readonly finishImmutableSteps: (
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
applicationGraph: ApplicationGraph,
) => Generator<void, JavaScriptSemanticGraph>;
}
@@ -336,7 +295,7 @@ export const createJavaScriptSemanticGraphProjection = (
const finishWith = <Value>(
factory: (input: unknown) => Value,
rootArtifactSha256: string,
applicationGraph: BuilderInput["applicationGraph"],
applicationGraph: ApplicationGraph,
): Value => {
bindSemanticGraphApplicationNodes(state, applicationGraph);
if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state);
@@ -2,13 +2,9 @@ import { javaScriptSemanticQueryInputSchema } from "../../domain/javascript/java
import { expect, it } from "vitest";
import { graphForJavaScript as graphFor } from "../../../tests/fixtures/javascriptSemanticGraph.js";
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
import { analyzeJavaScriptArtifactFiles } from "./JavaScriptArtifactAnalysis.js";
import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
const SHA256 = "a".repeat(64);
const GRAPH_ID = `jag_${"b".repeat(64)}`;
@@ -387,10 +383,9 @@ it("does not promote transformed return expressions to resolved aliases", () =>
});
it("uses unavailable evidence when no source produced semantic IR", () => {
const graph = buildJavaScriptSemanticGraph({
rootArtifactSha256: SHA256,
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis: emptyAnalysis(),
const graph = createJavaScriptSemanticGraphProjection().finish(SHA256, {
graph_id: GRAPH_ID,
nodes: [],
});
const root = graph.nodes[0];
if (root === undefined) throw new Error("Expected unavailable semantic root");
@@ -402,68 +397,3 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
},
);
});
it("preserves an unparseable source identity and unknown semantic extents", () => {
const source = "export const broken = ;";
const analysis = analyzeJavaScriptArtifactFiles({
files: [
{
path: "broken.mjs",
container_sha256: SHA256,
sha256: SHA256,
bytes: Buffer.byteLength(source),
inventory_artifact_id: `art_${SHA256}`,
kind: "javascript",
unpacked: false,
text: { included: true, value: source },
},
],
unavailable_relevant_files: [],
containers: [],
text_bytes_read: Buffer.byteLength(source),
invalid_utf8_files: 0,
});
expect(analysis.files[0]?.javascript?.parse_status).toBe("failed");
const graph = buildJavaScriptSemanticGraph({
rootArtifactSha256: SHA256,
applicationGraph: { graph_id: GRAPH_ID, nodes: [] },
analysis,
});
const module = graph.nodes.find(
({ kind, identity }) =>
kind === "module" && identity.module_path === "broken.mjs",
);
expect(module).toBeDefined();
expect(graph.unknowns).toHaveLength(
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.length,
);
for (const unknown of graph.unknowns) {
expect(unknown).toMatchObject({
node_id: module?.node_id,
reason: "incomplete-module",
});
expect(unknown.detail).toContain("syntax could not be parsed");
}
expect(graph.coverage).toMatchObject({
status: "partial",
truncated: false,
omitted_nodes: null,
omitted_relations: null,
limits: [],
});
});
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
jsonDocuments: new JavaScriptJsonDocuments(),
files: [],
packages: [],
json_modules: [],
html_scripts: [],
html_inline_scripts: [],
source_maps: [],
visited_ast_nodes: 0,
findings: 0,
modules: 0,
parse_failures: 0,
limitations: [],
});