diff --git a/src/application/javascript/ElectronBoundaryAnalysis.ts b/src/application/javascript/ElectronBoundaryAnalysis.ts index 55b964cbb6..7b7b5c4bc6 100644 --- a/src/application/javascript/ElectronBoundaryAnalysis.ts +++ b/src/application/javascript/ElectronBoundaryAnalysis.ts @@ -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, ): ElectronBoundarySummary => { const files = new Map(analysis.files.map(({ file }) => [file.path, file])); diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.packageMain.test.ts b/src/application/javascript/JavaScriptArtifactAnalysis.packageMain.test.ts index 5e4394941d..2d97028027 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.packageMain.test.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.packageMain.test.ts @@ -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; - let declared: ReturnType; - 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", diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.sourceMaps.test.ts b/src/application/javascript/JavaScriptArtifactAnalysis.sourceMaps.test.ts index e5786c8b62..3ab37f4990 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.sourceMaps.test.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.sourceMaps.test.ts @@ -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", diff --git a/src/application/javascript/JavaScriptArtifactAnalysis.ts b/src/application/javascript/JavaScriptArtifactAnalysis.ts index 7eeb7e3456..75d1079415 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysis.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysis.ts @@ -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[]; + 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; - readonly projectSemantics: ( - file: JavaScriptArtifactFile, - ir: JavaScriptSemanticIr, - ) => Generator; -} - -/** 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, ): Promise<{ - readonly analysis: JavaScriptModuleArtifactAnalysis; + readonly analysis: JavaScriptArtifactAnalysis; readonly interruption: AnalysisError | null; }> => { - const state = emptyArtifactAnalysis(); + 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 = ( - fileSet: JavaScriptArtifactFileSet, - projectSemantics: ArtifactAnalysisContext["projectSemantics"], -): JavaScriptArtifactAnalysis => { - const state = emptyArtifactAnalysis(); - 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, -): JavaScriptArtifactAnalysis => { +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, -): Generator { - 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, - 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, -): void => { - const sourceMap = parseSourceMap(file); - context.state.sourceMaps.push(sourceMap); -}; - -const emptyArtifactAnalysis = < - SemanticIr extends JavaScriptModuleSemanticIr, ->(): MutableArtifactAnalysis => ({ +const emptyArtifactAnalysis = (): MutableArtifactAnalysis => ({ jsonDocuments: new JavaScriptJsonDocuments(), files: [], packages: [], @@ -470,7 +358,7 @@ const emptyArtifactAnalysis = < const addStructuredObservations = ( file: JavaScriptArtifactFile, - state: MutableArtifactAnalysis, + state: MutableArtifactAnalysis, ): void => { if (file.kind === "package-json") state.packages.push(parsePackage(file, state.jsonDocuments.read(file))); diff --git a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts index a395380b6f..840ca83111 100644 --- a/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts +++ b/src/application/javascript/JavaScriptArtifactAnalysisTypes.ts @@ -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> | 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[]; + 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; diff --git a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts index b240be555d..6086968853 100644 --- a/src/application/javascript/JavaScriptArtifactGraphBuilder.ts +++ b/src/application/javascript/JavaScriptArtifactGraphBuilder.ts @@ -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 { const input = yield* buildJavaScriptArtifactGraphInputSteps( snapshot, @@ -71,7 +71,7 @@ export function* buildImmutableJavaScriptArtifactGraphSteps( function* buildJavaScriptArtifactGraphInputSteps( snapshot: ArtifactInventorySnapshot, fileSet: JavaScriptArtifactFileSet, - analysis: JavaScriptModuleArtifactAnalysis, + analysis: JavaScriptArtifactAnalysis, ): Generator { const accumulator = new JavaScriptArtifactGraphAccumulator(); const root = createJavaScriptArtifactRootNode(accumulator, snapshot); diff --git a/src/application/javascript/JavaScriptArtifactGraphContext.ts b/src/application/javascript/JavaScriptArtifactGraphContext.ts index 5a5365f3fc..e7f225e6a5 100644 --- a/src/application/javascript/JavaScriptArtifactGraphContext.ts +++ b/src/application/javascript/JavaScriptArtifactGraphContext.ts @@ -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; readonly root: ApplicationNode; readonly filesByPath: ReadonlyMap; @@ -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" diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts index 318b1184cb..90e2008497 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.test.ts @@ -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 = { diff --git a/src/application/javascript/JavaScriptArtifactGraphStructure.ts b/src/application/javascript/JavaScriptArtifactGraphStructure.ts index 18c912c93b..24355cee98 100644 --- a/src/application/javascript/JavaScriptArtifactGraphStructure.ts +++ b/src/application/javascript/JavaScriptArtifactGraphStructure.ts @@ -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); diff --git a/src/application/javascript/JavaScriptJsonDocuments.test.ts b/src/application/javascript/JavaScriptJsonDocuments.test.ts index 27cd10e655..2a09772a4b 100644 --- a/src/application/javascript/JavaScriptJsonDocuments.test.ts +++ b/src/application/javascript/JavaScriptJsonDocuments.test.ts @@ -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; + let declared: ReturnType; + 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(); diff --git a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts index 94fe00bad3..ae1dc849b7 100644 --- a/src/application/javascript/JavaScriptSemanticGraphBuilder.ts +++ b/src/application/javascript/JavaScriptSemanticGraphBuilder.ts @@ -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; 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; readonly finish: ( rootArtifactSha256: string, - applicationGraph: BuilderInput["applicationGraph"], + applicationGraph: ApplicationGraph, ) => JavaScriptSemanticGraph; readonly finishImmutableSteps: ( rootArtifactSha256: string, - applicationGraph: BuilderInput["applicationGraph"], + applicationGraph: ApplicationGraph, ) => Generator; } @@ -336,7 +295,7 @@ export const createJavaScriptSemanticGraphProjection = ( const finishWith = ( factory: (input: unknown) => Value, rootArtifactSha256: string, - applicationGraph: BuilderInput["applicationGraph"], + applicationGraph: ApplicationGraph, ): Value => { bindSemanticGraphApplicationNodes(state, applicationGraph); if (state.roots.size === 0) addFallbackRoot(rootArtifactSha256, state); diff --git a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts index a6d2b964d0..e8c1aa4270 100644 --- a/src/application/javascript/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/javascript/JavaScriptSemanticGraphResources.test.ts @@ -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: [], -}); diff --git a/tests/boundary/filesystem/javascript-applications/javascriptArtifactGraphEndpoints.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptArtifactGraphEndpoints.test.ts index 91a1bfc686..374dcb034b 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptArtifactGraphEndpoints.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptArtifactGraphEndpoints.test.ts @@ -7,7 +7,7 @@ import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js import { scanCanonicalArtifactInventory } from "../../../fixtures/artifactInventory.js"; import { readJavaScriptArtifactFiles } from "../../../support/javascriptApplicationScope.js"; import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; -import { analyzeJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../../../fixtures/javascriptArtifactAnalysis.js"; import { immutableArtifactGraph } from "../../../fixtures/javascriptArtifactGraph.js"; it("keeps an endpoint uncertain after mutation through a declared inherited field", async () => { @@ -27,7 +27,7 @@ it("keeps an endpoint uncertain after mutation through a declared inherited fiel const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const endpoints = graph.nodes.filter(({ kind }) => kind === "endpoint"); expect(endpoints).toHaveLength(1); @@ -51,7 +51,7 @@ it("keeps ambiguous endpoint candidates inferred through artifact graph projecti const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const endpoints = graph.nodes.filter(({ kind }) => kind === "endpoint"); const candidate = endpoints.find(({ observations }) => @@ -121,7 +121,7 @@ it("preserves partial and exact endpoint identity through graph projection", asy const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const endpoints = graph.nodes.filter(({ kind }) => kind === "endpoint"); const partialNodes = endpoints.filter(({ observations }) => @@ -171,7 +171,7 @@ it("projects path data records as inferred route candidates", async () => { const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const route = (value: string) => graph.nodes diff --git a/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts index fe4866e58f..43dce81474 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts @@ -17,20 +17,12 @@ import { reconstructJavaScriptArtifact, readJavaScriptArtifactFiles, } from "../../../support/javascriptApplicationScope.js"; -import { - analyzeJavaScriptArtifactFiles, - analyzeAndAdoptJavaScriptArtifactFiles, -} from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeAndAdoptJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../../../fixtures/javascriptArtifactAnalysis.js"; +import type { JavaScriptAnalyzedSource } from "../../../../src/domain/javascript/javascriptSourceAnalysis.js"; import { immutableArtifactGraph } from "../../../fixtures/javascriptArtifactGraph.js"; -import { - buildJavaScriptSemanticGraph, - createJavaScriptSemanticGraphProjection, - projectJavaScriptSemanticFileSteps, -} from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; -import { - projectJavaScriptModuleSemantics, - type JavaScriptModuleSemanticIr, -} from "../../../../src/domain/javascript/javascriptModuleSemanticIr.js"; +import { createJavaScriptSemanticGraphProjection } from "../../../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; +import type { JavaScriptModuleSemanticIr } from "../../../../src/domain/javascript/javascriptModuleSemanticIr.js"; import type { JavaScriptStaticAnalysis } from "../../../../src/domain/javascript/javascriptStaticAnalysisTypes.js"; import type { JavaScriptArtifactFile } from "../../../../src/domain/javascript/javascriptArtifactFiles.js"; import { completeJavaScriptAnalysisSteps } from "../../../../src/application/javascript/JavaScriptAnalysisControl.js"; @@ -43,9 +35,13 @@ import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javasc import { createJavaScriptSourceAnalysis } from "../../../../src/artifacts/javascript/JavaScriptSourceAnalysis.js"; import { scanCanonicalArtifactInventory, + withArtifactResourceScope, scanArtifactInventory, } from "../../../fixtures/artifactInventory.js"; -import { parseJavaScriptApplicationGraph } from "../../../../src/domain/javascript/javascriptApplicationGraph.js"; +import { + parseJavaScriptApplicationGraph, + type JavaScriptApplicationGraph, +} from "../../../../src/domain/javascript/javascriptApplicationGraph.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../../src/domain/javascript/javascriptApplicationAnalysis.js"; import { createJavaScriptSemanticGraph, @@ -215,7 +211,7 @@ it.each([ const graph = await immutableArtifactGraph( snapshot, partial, - analyzeJavaScriptArtifactFiles(partial), + (await analyzeArtifactFiles(partial)).analysis, ); const roles = graph.nodes.filter( ({ kind }) => kind === "electron-renderer", @@ -291,7 +287,7 @@ it.each(["unselected", "unavailable"] as const)( const graph = await immutableArtifactGraph( snapshot, partial, - analyzeJavaScriptArtifactFiles(partial), + (await analyzeArtifactFiles(partial)).analysis, ); const roles = graph.nodes.filter( ({ kind }) => kind === "electron-renderer", @@ -335,7 +331,7 @@ it("keeps imported loadFile ownership unknown across an application projection o const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const entry = analysis.files.find( ({ file }) => file.path === "shared/main.cjs", ); @@ -424,7 +420,7 @@ it("keeps a loadFile owner unknown when a sibling manifest is unavailable in the const graph = await immutableArtifactGraph( snapshot, partial, - analyzeJavaScriptArtifactFiles(partial), + (await analyzeArtifactFiles(partial)).analysis, ); const roles = graph.nodes.filter( ({ kind }) => kind === "electron-renderer", @@ -453,7 +449,7 @@ it("keeps a loadFile owner unknown when a sibling manifest is unavailable in the } }); -it("keeps excluded non-JavaScript files out of both semantic construction paths", async () => { +it("keeps excluded non-JavaScript files out of the semantic graph", async () => { const root = await createTestTempDirectory("rea-js-semantic-applicability-"); await Promise.all( ( @@ -490,34 +486,22 @@ it("keeps excluded non-JavaScript files out of both semantic construction paths" "native.node", "package.json", ]); - const analysis = analyzeJavaScriptArtifactFiles(files); - const applicationGraph = await immutableArtifactGraph( - snapshot, - files, - analysis, - ); - const full = buildJavaScriptSemanticGraph({ - rootArtifactSha256: snapshot.manifest.root_sha256, - applicationGraph, - analysis, - }); - const incremental = await reconstructJavaScriptArtifact({ + const result = await reconstructJavaScriptArtifact({ input_path: root, source_paths: ["a.js"], }); - for (const graph of [full, incremental.semantic_graph]) { - expect( - graph.nodes - .filter(({ kind }) => kind === "module") - .map(({ identity }) => identity.module_path) - .sort(), - ).toEqual(["a.js", "b.js"]); - expect(graph.coverage.status).toBe("partial"); - expect(graph.unknowns.length).toBeGreaterThan(0); - expect( - new Set(graph.nodes.map(({ identity }) => identity.module_path)), - ).toEqual(new Set(["a.js", "b.js"])); - } + const graph = result.semantic_graph; + expect( + graph.nodes + .filter(({ kind }) => kind === "module") + .map(({ identity }) => identity.module_path) + .sort(), + ).toEqual(["a.js", "b.js"]); + expect(graph.coverage.status).toBe("partial"); + expect(graph.unknowns.length).toBeGreaterThan(0); + expect( + new Set(graph.nodes.map(({ identity }) => identity.module_path)), + ).toEqual(new Set(["a.js", "b.js"])); } finally { await reader.close(); } @@ -574,80 +558,71 @@ it("preserves graph commitments and export shapes through worker adoption", asyn const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const full = analyzeJavaScriptArtifactFiles(files); const projection = createJavaScriptSemanticGraphProjection(); - const worker = await createJavaScriptSourceAnalysis( - { heapMb: 512, timeoutMs: 30_000 }, - files.files.filter( - (file) => file.kind === "javascript" && file.text.included, - ).length, - ); - let outcome: Awaited< - ReturnType - >; - try { - outcome = await analyzeAndAdoptJavaScriptArtifactFiles( + const sources = new Map(); + const outcome = await withArtifactResourceScope(async (scope) => { + const worker = await createJavaScriptSourceAnalysis( + { heapMb: 512, timeoutMs: 30_000 }, + files.files.filter( + (file) => file.kind === "javascript" && file.text.included, + ).length, + { scope }, + ); + scope.retain({ + kind: "javascript-source-analysis", + resource: "worker adoption fixture analyzer", + analysis: worker, + }); + return analyzeAndAdoptJavaScriptArtifactFiles( files, - worker, + { + analyze: async (...args) => { + const result = await worker.analyze(...args); + if (result.ok) sources.set(args[0].path, result.value); + return result; + }, + }, projection, async () => {}, ); - } finally { - await worker.close(); - } + }); if (outcome.interruption !== null) throw outcome.interruption; const compact = outcome.analysis; - const graph = await immutableArtifactGraph(snapshot, files, full); - expect(await immutableArtifactGraph(snapshot, files, compact)).toEqual( - graph, + const graph = await immutableArtifactGraph(snapshot, files, compact); + const semantic = await completeJavaScriptAnalysisSteps( + projection.finishImmutableSteps(snapshot.manifest.root_sha256, graph), ); - const semantic = projection.finish(snapshot.manifest.root_sha256, graph); - 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); + expect(parseJavaScriptApplicationGraph(graph)).toEqual(graph); + expect(semantic.application_graph_id).toBe(graph.graph_id); + expect(semantic.root_artifact_sha256).toBe(snapshot.manifest.root_sha256); const exported = compact.files.find( ({ file }) => file.path === "exports.mjs", ); - expect(exported?.semantic?.ir.callables.map(({ name }) => name)).toEqual([ + if (exported === undefined) + throw new Error("Expected exports source facts"); + expect(exported.semantic?.ir.callables.map(({ name }) => name)).toEqual([ "create", "configured", ]); - expect( - full.files - .find(({ file }) => file.path === "exports.mjs") - ?.semantic?.ir.callables.map(({ name }) => name), - ).toContain("unexported"); + expectWorkerExportFacts(graph, semantic, exported.file); expect(javaScriptSemanticGraphSchema.parse(semantic)).toEqual(semantic); const transferred = createJavaScriptSemanticGraphProjection(); const wireRoot = await createTestTempDirectory("rea-javascript-transfer-"); const decodedModules = new Map(); const decodedStatic = new Map(); let rejectedInvalidTransfer = false; - for (const [index, analyzed] of full.files.entries()) { + for (const [index, analyzed] of compact.files.entries()) { + const source = sources.get(analyzed.file.path); + if (source === undefined) continue; if (analyzed.javascript !== null) { const raw: unknown = JSON.parse(JSON.stringify(analyzed.javascript)); const decodedFacts = javaScriptWorkerStaticAnalysisSchema.parse(raw); expect(decodedFacts).toEqual(analyzed.javascript); decodedStatic.set(analyzed.file.path, decodedFacts); } - if (analyzed.semantic === null) continue; - const fragment = await completeJavaScriptAnalysisSteps( - projectJavaScriptSemanticFileSteps( - analyzed.file, - analyzed.semantic.ir, - transferred.remainingFileCapacity(), - ), - ); - const module = projectJavaScriptModuleSemantics(analyzed.semantic.ir); + if (source.projection === null || source.module === null) continue; + const fragment = source.projection; + const module = source.module; const wirePath = join(wireRoot, `${String(index)}.jsonl`); const descriptor = await writeJavaScriptAnalysisTransfer( wirePath, @@ -672,6 +647,7 @@ it("preserves graph commitments and export shapes through worker adoption", asyn rejectedInvalidTransfer = true; } } + expect(rejectedInvalidTransfer).toBe(true); const decodedGraph = await immutableArtifactGraph(snapshot, files, { ...compact, files: compact.files.map((analyzed) => { @@ -698,6 +674,84 @@ it("preserves graph commitments and export shapes through worker adoption", asyn } }); +const expectWorkerExportFacts = ( + graph: JavaScriptApplicationGraph, + semantic: JavaScriptSemanticGraph, + file: JavaScriptArtifactFile, +): void => { + // File-local projection precedes pruning to exported module facts. + const privateFunction = semantic.nodes.find( + ({ kind, label, identity }) => + kind === "function" && + label === "unexported" && + identity.module_path === "exports.mjs", + ); + expect(privateFunction?.identity.artifact_sha256).toBe(file.sha256); + const exports = graph.nodes.flatMap((node) => + node.observations + .filter( + ({ properties }) => + properties.semantic_role === "export-binding" && + properties.module_path === "exports.mjs", + ) + .map(({ properties }) => ({ node, name: properties.exported_name })), + ); + expect(exports.map(({ name }) => name).sort()).toEqual([ + "configured", + "create", + "read", + ]); + const createExport = exports.find(({ name }) => name === "create"); + const createFunction = semantic.nodes.find( + ({ kind, label, identity }) => + kind === "function" && + label === "create" && + identity.module_path === "exports.mjs", + ); + if (createExport === undefined || createFunction === undefined) + throw new Error("Expected create export and semantic callable"); + expect(createFunction.application_node_ids).toContain( + createExport.node.node_id, + ); + const returnShapes = graph.nodes.flatMap(({ observations }) => + observations + .map(({ properties }) => properties) + .filter( + (properties) => + properties.semantic_role === "export-return-shapes" && + properties.module_path === "exports.mjs", + ), + ); + expect(returnShapes).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + exported_name: "create", + static_return_shapes: [ + expect.objectContaining({ + fields: expect.arrayContaining([ + expect.objectContaining({ + path: "/nested/enabled", + state: "literal", + value: true, + }), + ]), + }), + ], + }), + expect.objectContaining({ + exported_name: "configured", + static_return_shapes: [ + expect.objectContaining({ + property_coverage: expect.arrayContaining([ + { path: "", status: "partial", omitted: null }, + ]), + }), + ], + }), + ]), + ); +}; + const rejectInvalidTransfer = async ( wirePath: string, descriptor: Awaited>, @@ -753,7 +807,7 @@ it("reports semantic value resource limits in application graph coverage", async const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); expect(graph.coverage).toMatchObject({ diff --git a/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts index 4a0fcc7c31..cd103df8a3 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts @@ -13,7 +13,7 @@ import { primitiveCandidateExpansionExpression } from "../../../fixtures/javascr import { scanCanonicalArtifactInventory } from "../../../fixtures/artifactInventory.js"; import { readJavaScriptArtifactFiles } from "../../../support/javascriptApplicationScope.js"; import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; -import { analyzeJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../../../fixtures/javascriptArtifactAnalysis.js"; import { immutableArtifactGraph } from "../../../fixtures/javascriptArtifactGraph.js"; import { @@ -29,11 +29,8 @@ const relationshipGraph = async (root: string) => { const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - return await immutableArtifactGraph( - snapshot, - files, - analyzeJavaScriptArtifactFiles(files), - ); + const { analysis } = await analyzeArtifactFiles(files); + return await immutableArtifactGraph(snapshot, files, analysis); } finally { await reader.close(); } diff --git a/tests/boundary/filesystem/javascript-applications/javascriptNonStringPackageMain.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptNonStringPackageMain.test.ts index bab140aec4..9cce24e1e0 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptNonStringPackageMain.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptNonStringPackageMain.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; -import { analyzeJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../../../fixtures/javascriptArtifactAnalysis.js"; import { immutableArtifactGraph } from "../../../fixtures/javascriptArtifactGraph.js"; import { resolveArtifactPathByContext } from "../../../../src/application/javascript/JavaScriptArtifactPathResolution.js"; import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; @@ -42,7 +42,7 @@ it.each( const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); expect( analysis.packages.find(({ path }) => path === "b/package.json"), ).toMatchObject({ main_status: "invalid", main_entrypoint: "" }); @@ -103,7 +103,7 @@ it.each( const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); expect( analysis.packages.find(({ path }) => path === "b/package.json"), ).toMatchObject({ diff --git a/tests/boundary/filesystem/javascriptArtifactGraphObservationLookup.test.ts b/tests/boundary/filesystem/javascriptArtifactGraphObservationLookup.test.ts index 8fc5844810..6cbf2c6ea9 100644 --- a/tests/boundary/filesystem/javascriptArtifactGraphObservationLookup.test.ts +++ b/tests/boundary/filesystem/javascriptArtifactGraphObservationLookup.test.ts @@ -6,7 +6,7 @@ import { expect, it } from "vitest"; import { scanCanonicalArtifactInventory } from "../../fixtures/artifactInventory.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { readJavaScriptArtifactFiles } from "../../support/javascriptApplicationScope.js"; -import { analyzeJavaScriptArtifactFiles } from "../../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../../fixtures/javascriptArtifactAnalysis.js"; import { immutableArtifactGraph } from "../../fixtures/javascriptArtifactGraph.js"; import { createJavaScriptArtifactReader } from "../../../src/artifacts/javascript/JavaScriptArtifactReader.js"; import type { @@ -35,7 +35,7 @@ it("uses the first structured observation when paths repeat", async () => { const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const packageObservation = analysis.packages.find( ({ path }) => path === "package.json", ); @@ -137,7 +137,7 @@ it("preserves unknown omitted-key count for unavailable JSON", async () => { try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const configFile = graph.nodes.find(({ observations }) => @@ -182,7 +182,7 @@ it("preserves unknown omitted-key count for unselected JSON", async () => { ["package.json"], ); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const configFile = graph.nodes.find(({ observations }) => @@ -227,7 +227,7 @@ it("preserves unknown omitted-key count for unselected uppercase JSON", async () ["package.json"], ); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const graph = await immutableArtifactGraph(snapshot, files, analysis); const configFile = graph.nodes.find(({ observations }) => diff --git a/tests/boundary/filesystem/javascriptArtifactSemanticUnknowns.test.ts b/tests/boundary/filesystem/javascriptArtifactSemanticUnknowns.test.ts new file mode 100644 index 0000000000..287c23f0f7 --- /dev/null +++ b/tests/boundary/filesystem/javascriptArtifactSemanticUnknowns.test.ts @@ -0,0 +1,56 @@ +import { expect, it } from "vitest"; + +import { analyzeArtifactFiles } from "../../fixtures/javascriptArtifactAnalysis.js"; +import { completeJavaScriptAnalysisSteps } from "../../../src/application/javascript/JavaScriptAnalysisControl.js"; +import { JAVASCRIPT_SEMANTIC_RELATION_FAMILIES } from "../../../src/domain/javascript/javascriptSemanticGraphSchemas.js"; + +const SHA256 = "a".repeat(64); +const GRAPH_ID = `jag_${"b".repeat(64)}`; + +it("preserves an unparseable source identity and unknown semantic extents", async () => { + const source = "export const broken = ;"; + const { analysis, projection } = await analyzeArtifactFiles({ + 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 = await completeJavaScriptAnalysisSteps( + projection.finishImmutableSteps(SHA256, { graph_id: GRAPH_ID, nodes: [] }), + ); + 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: [], + }); +}); diff --git a/tests/fixtures/javascriptArtifactAnalysis.ts b/tests/fixtures/javascriptArtifactAnalysis.ts new file mode 100644 index 0000000000..9abfcfad0a --- /dev/null +++ b/tests/fixtures/javascriptArtifactAnalysis.ts @@ -0,0 +1,40 @@ +import { analyzeAndAdoptJavaScriptArtifactFiles } from "../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import type { JavaScriptArtifactAnalysis } from "../../src/application/javascript/JavaScriptArtifactAnalysisTypes.js"; +import { + createJavaScriptSemanticGraphProjection, + type JavaScriptSemanticGraphProjection, +} from "../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; +import { createJavaScriptSourceAnalysis } from "../../src/artifacts/javascript/JavaScriptSourceAnalysis.js"; +import type { JavaScriptArtifactFileSet } from "../../src/domain/javascript/javascriptArtifactFiles.js"; +import { withArtifactResourceScope } from "./artifactInventory.js"; + +/** Analyze inert fixture sources through the real worker and join its owning scope. */ +export const analyzeArtifactFiles = ( + fileSet: JavaScriptArtifactFileSet, +): Promise<{ + readonly analysis: JavaScriptArtifactAnalysis; + readonly projection: JavaScriptSemanticGraphProjection; +}> => + withArtifactResourceScope(async (scope) => { + const worker = await createJavaScriptSourceAnalysis( + { heapMb: 512, timeoutMs: 30_000 }, + fileSet.files.filter( + (file) => file.kind === "javascript" && file.text.included, + ).length, + { scope }, + ); + scope.retain({ + kind: "javascript-source-analysis", + resource: "fixture JavaScript source analyzer", + analysis: worker, + }); + const projection = createJavaScriptSemanticGraphProjection(); + const outcome = await analyzeAndAdoptJavaScriptArtifactFiles( + fileSet, + worker, + projection, + async () => {}, + ); + if (outcome.interruption !== null) throw outcome.interruption; + return { analysis: outcome.analysis, projection }; + }); diff --git a/tests/fixtures/javascriptSemanticGraph.ts b/tests/fixtures/javascriptSemanticGraph.ts index 6ac66416ae..f25aab7cad 100644 --- a/tests/fixtures/javascriptSemanticGraph.ts +++ b/tests/fixtures/javascriptSemanticGraph.ts @@ -1,6 +1,4 @@ -import { JavaScriptJsonDocuments } from "../../src/application/javascript/JavaScriptJsonDocuments.js"; -import { buildJavaScriptSemanticGraph } from "../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; -import type { JavaScriptArtifactAnalysis } from "../../src/application/javascript/JavaScriptArtifactAnalysisTypes.js"; +import { createJavaScriptSemanticGraphProjection } from "../../src/application/javascript/JavaScriptSemanticGraphBuilder.js"; import type { JavaScriptArtifactFile } from "../../src/domain/javascript/javascriptArtifactFiles.js"; import type { JavaScriptSemanticIr } from "../../src/domain/javascript/javascriptSemanticIr.js"; import { analyzeJavaScriptSemantics } from "../../src/domain/javascript/javascriptSemanticAnalysis.js"; @@ -22,24 +20,10 @@ export const graphForJavaScript = ( unpacked: false, text: { included: true, value: source }, }; - const analysis: JavaScriptArtifactAnalysis = { - jsonDocuments: new JavaScriptJsonDocuments(), - files: [{ file, javascript: null, semantic: { ir } }], - packages: [], - json_modules: [], - html_scripts: [], - html_inline_scripts: [], - source_maps: [], - visited_ast_nodes: 0, - findings: 0, - modules: 0, - parse_failures: 0, - limitations: [], - }; - - return buildJavaScriptSemanticGraph({ - rootArtifactSha256: SHA256, - applicationGraph: { graph_id: `jag_${"b".repeat(64)}`, nodes: [] }, - analysis, + const projection = createJavaScriptSemanticGraphProjection(); + projection.projectFile(file, ir); + return projection.finish(SHA256, { + graph_id: `jag_${"b".repeat(64)}`, + nodes: [], }); }; diff --git a/tests/process-global/javascriptJsonDocumentFailures.test.ts b/tests/process-global/javascriptJsonDocumentFailures.test.ts index b702f10233..ba41620225 100644 --- a/tests/process-global/javascriptJsonDocumentFailures.test.ts +++ b/tests/process-global/javascriptJsonDocumentFailures.test.ts @@ -1,7 +1,7 @@ import { expect, it, onTestFinished, vi } from "vitest"; import { createJavaScriptArtifactReader } from "../../src/artifacts/javascript/JavaScriptArtifactReader.js"; -import { analyzeJavaScriptArtifactFiles } from "../../src/application/javascript/JavaScriptArtifactAnalysis.js"; +import { analyzeArtifactFiles } from "../fixtures/javascriptArtifactAnalysis.js"; import { immutableArtifactGraph } from "../fixtures/javascriptArtifactGraph.js"; import type { JavaScriptArtifactFile, @@ -65,7 +65,7 @@ it.each([ }, ])( "propagates unexpected JSON.parse faults from $document", - ({ text, create }) => { + async ({ text, create }) => { const files = fileSet(); const originalParse = JSON.parse; const thrown = create(); @@ -79,7 +79,7 @@ it.each([ let caught: unknown; try { - analyzeJavaScriptArtifactFiles(files); + await analyzeArtifactFiles(files); } catch (cause: unknown) { caught = cause; } @@ -87,10 +87,10 @@ it.each([ }, ); -it("keeps malformed JSON as an invalid observation", () => { +it("keeps malformed JSON as an invalid observation", async () => { const malformedPackage = artifactFile("package.json", "package-json", "{"); const malformedJson = artifactFile("settings.json", "json", "{"); - const analysis = analyzeJavaScriptArtifactFiles({ + const { analysis } = await analyzeArtifactFiles({ ...fileSet(), files: [malformedPackage, malformedJson], }); @@ -111,7 +111,7 @@ it("reuses parsed package and JSON facts while building the public graph", async const reader = createJavaScriptArtifactReader(root, "directory"); try { const files = await readJavaScriptArtifactFiles(reader, snapshot); - const analysis = analyzeJavaScriptArtifactFiles(files); + const { analysis } = await analyzeArtifactFiles(files); const depPackageText = '{"main":"index.cjs"}'; const configText = '{"enabled":true}'; const originalParse = JSON.parse;