mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 06:15:59 +00:00
refactor(javascript): remove superseded full-IR analysis pipeline
This commit is contained in:
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: [],
|
||||
});
|
||||
Reference in new issue
Block a user