mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
refactor(javascript): centralize package and JSON document facts
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -164,6 +165,7 @@ export const summarizeElectronBoundaries = (
|
||||
specifier: finding.module_path,
|
||||
context: finding.module_resolution_context,
|
||||
files,
|
||||
jsonDocuments: analysis.jsonDocuments,
|
||||
directoryPaths,
|
||||
runtimePathKind: "node-entrypoint",
|
||||
}),
|
||||
@@ -175,6 +177,7 @@ export const summarizeElectronBoundaries = (
|
||||
specifier: finding.specifier,
|
||||
context: "module-specifier",
|
||||
files,
|
||||
jsonDocuments: analysis.jsonDocuments,
|
||||
directoryPaths,
|
||||
kind: "native-addon",
|
||||
moduleKind: finding.module_kind,
|
||||
@@ -209,6 +212,7 @@ interface ResolveToFileInput {
|
||||
readonly specifier: string;
|
||||
readonly context: "module-specifier" | "filesystem-expression";
|
||||
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly directoryPaths?: ReadonlySet<string> | undefined;
|
||||
readonly kind?: JavaScriptArtifactFile["kind"];
|
||||
readonly moduleKind?: "import" | "require";
|
||||
@@ -221,6 +225,7 @@ const resolvesToFile = (input: ResolveToFileInput): boolean => {
|
||||
sourcePath: input.file.path,
|
||||
context: input.context,
|
||||
files: input.files,
|
||||
jsonDocuments: input.jsonDocuments,
|
||||
...(input.directoryPaths === undefined
|
||||
? {}
|
||||
: { directoryPaths: input.directoryPaths }),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
|
||||
import type { ElectronNativeAddonBindingFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
javascriptAnalysisCoverage,
|
||||
resolveArtifactPath,
|
||||
type JavaScriptArtifactGraphContext,
|
||||
type JavaScriptArtifactGraphCoverage,
|
||||
} from "./JavaScriptArtifactGraphContext.js";
|
||||
@@ -45,13 +45,17 @@ const addNativeBinding = (input: NativeBindingInput): void => {
|
||||
value.module_runtime,
|
||||
);
|
||||
if (source === undefined) return;
|
||||
const resolved = resolveArtifactPath(
|
||||
value.specifier,
|
||||
file.path,
|
||||
context.filesByPath,
|
||||
value.module_kind,
|
||||
context.directoryPaths,
|
||||
);
|
||||
const resolved = resolveArtifactPathByContext({
|
||||
declaredPath: value.specifier,
|
||||
sourcePath: file.path,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
...(value.module_kind === undefined
|
||||
? {}
|
||||
: { moduleKind: value.module_kind }),
|
||||
}).resolved_path;
|
||||
const addon = resolved === null ? undefined : context.fileNodes.get(resolved);
|
||||
const binding = context.accumulator.addNode({
|
||||
kind: "native-export",
|
||||
|
||||
@@ -155,6 +155,7 @@ const addWindowPreload = (
|
||||
sourcePath: input.file.path,
|
||||
context: resolutionContext,
|
||||
files: input.context.filesByPath,
|
||||
jsonDocuments: input.context.analysis.jsonDocuments,
|
||||
directoryPaths: input.context.directoryPaths,
|
||||
unanalyzedFiles: input.context.unanalyzedFiles,
|
||||
runtimePathKind: "exact-file",
|
||||
@@ -257,6 +258,7 @@ const addUtilityProcess = (
|
||||
sourcePath: file.path,
|
||||
context: value.module_resolution_context,
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
runtimePathKind: "node-entrypoint",
|
||||
|
||||
@@ -34,6 +34,50 @@ it.each([null, false, 123, {}, []].map((main) => ({ main })))(
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps valid JSON numeric overflow invalid as a package main", () => {
|
||||
expect(packageObservation('{"main":1e400}')).toMatchObject({
|
||||
status: "included",
|
||||
main: null,
|
||||
main_status: "invalid",
|
||||
main_entrypoint: "",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats a valid numeric JSON root as a package metadata shape failure", () => {
|
||||
expect(packageObservation("1e400")).toMatchObject({
|
||||
status: "invalid",
|
||||
main: null,
|
||||
main_status: "unavailable",
|
||||
main_entrypoint: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains valid numeric overflow as an ordinary JSON module", () => {
|
||||
const text = "1e400";
|
||||
const analysis = analyzeJavaScriptArtifactFiles({
|
||||
files: [
|
||||
{
|
||||
path: "settings.json",
|
||||
container_sha256: "a".repeat(64),
|
||||
sha256: "b".repeat(64),
|
||||
bytes: Buffer.byteLength(text),
|
||||
inventory_artifact_id: "numeric-json-fixture",
|
||||
kind: "json",
|
||||
unpacked: false,
|
||||
text: { included: true, value: text },
|
||||
},
|
||||
],
|
||||
unavailable_relevant_files: [],
|
||||
containers: [],
|
||||
text_bytes_read: Buffer.byteLength(text),
|
||||
invalid_utf8_files: 0,
|
||||
});
|
||||
|
||||
expect(analysis.json_modules).toMatchObject([
|
||||
{ status: "included", top_level_keys: [] },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["{}", "default", ""],
|
||||
['{"main":""}', "default", ""],
|
||||
|
||||
@@ -40,6 +40,10 @@ import type {
|
||||
} from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { javascriptArtifactAnalysisScopes } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { analyzeJavaScriptJsonModule } from "./JavaScriptJsonModules.js";
|
||||
import {
|
||||
JavaScriptJsonDocuments,
|
||||
type JavaScriptJsonDocumentFacts,
|
||||
} from "./JavaScriptJsonDocuments.js";
|
||||
import { completeJavaScriptAnalysisSteps } from "./JavaScriptAnalysisControl.js";
|
||||
import { htmlArtifactReferences } from "../../domain/javascript/htmlArtifactReferences.js";
|
||||
import type { JavaScriptSourceAnalysisPort } from "../../domain/javascript/javascriptSourceAnalysis.js";
|
||||
@@ -57,6 +61,7 @@ import {
|
||||
interface MutableArtifactAnalysis<
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
> {
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly files: AnalyzedJavaScriptArtifactFile<SemanticIr>[];
|
||||
readonly packages: JavaScriptPackageObservation[];
|
||||
readonly jsonModules: JavaScriptJsonModuleObservation[];
|
||||
@@ -359,6 +364,7 @@ const finalizeArtifactAnalysis = <
|
||||
state: MutableArtifactAnalysis<SemanticIr>,
|
||||
): JavaScriptArtifactAnalysis<SemanticIr> => {
|
||||
return {
|
||||
jsonDocuments: state.jsonDocuments,
|
||||
files: state.files,
|
||||
packages: state.packages,
|
||||
json_modules: state.jsonModules,
|
||||
@@ -482,6 +488,7 @@ const addSourceMap = (
|
||||
const emptyArtifactAnalysis = <
|
||||
SemanticIr extends JavaScriptModuleSemanticIr,
|
||||
>(): MutableArtifactAnalysis<SemanticIr> => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [],
|
||||
packages: [],
|
||||
jsonModules: [],
|
||||
@@ -498,27 +505,25 @@ const addStructuredObservations = (
|
||||
file: JavaScriptArtifactFile,
|
||||
state: MutableArtifactAnalysis<JavaScriptModuleSemanticIr>,
|
||||
): void => {
|
||||
if (file.kind === "package-json") state.packages.push(parsePackage(file));
|
||||
if (file.kind === "package-json")
|
||||
state.packages.push(parsePackage(file, state.jsonDocuments.read(file)));
|
||||
if (file.kind !== "json") return;
|
||||
const json = analyzeJavaScriptJsonModule(file);
|
||||
const json = analyzeJavaScriptJsonModule(file, state.jsonDocuments);
|
||||
state.jsonModules.push(json);
|
||||
if (json.status === "invalid") state.parseFailures += 1;
|
||||
};
|
||||
|
||||
const parsePackage = (
|
||||
file: JavaScriptArtifactFile,
|
||||
document: JavaScriptJsonDocumentFacts,
|
||||
): JavaScriptPackageObservation => {
|
||||
if (!file.text.included)
|
||||
if (document.status === "unavailable")
|
||||
return unavailablePackage(file, "Package metadata text was unavailable.");
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
if (document.status === "invalid")
|
||||
return invalidPackage(file, "package.json is not valid JSON.");
|
||||
}
|
||||
if (!isRecord(value))
|
||||
if (document.packageFields === null)
|
||||
return invalidPackage(file, "package.json root is not an object.");
|
||||
const value = document.packageFields;
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
@@ -780,6 +785,3 @@ const optionalString = (value: unknown): string | null =>
|
||||
|
||||
const sha256 = (value: string): string =>
|
||||
createHash("sha256").update(value).digest("hex");
|
||||
|
||||
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
JavaScriptArtifactFileKind,
|
||||
} from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
interface JavaScriptStructuredObservation {
|
||||
readonly path: string;
|
||||
@@ -200,6 +201,8 @@ export const isSkippedJavaScriptAnalysis = (
|
||||
export interface JavaScriptArtifactAnalysis<
|
||||
SemanticIr extends JavaScriptModuleSemanticIr = JavaScriptSemanticIr,
|
||||
> {
|
||||
/** Internal source-facts owner shared with graph path resolution. */
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly files: readonly AnalyzedJavaScriptArtifactFile<SemanticIr>[];
|
||||
readonly packages: readonly JavaScriptPackageObservation[];
|
||||
readonly json_modules: readonly JavaScriptJsonModuleObservation[];
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import {
|
||||
artifactPathResolutionProperties,
|
||||
resolveArtifactPathByContext,
|
||||
type ArtifactPathResolution,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
|
||||
@@ -513,23 +512,6 @@ export const chunkLookupKey = (
|
||||
chunkKey: string,
|
||||
): string => compositeKey([path, runtime, chunkKey]);
|
||||
|
||||
/** Resolve a relative static specifier without escaping the artifact root. */
|
||||
export const resolveArtifactPath = (
|
||||
specifier: string,
|
||||
sourcePath: string,
|
||||
files: ReadonlyMap<string, JavaScriptArtifactFile>,
|
||||
moduleKind?: "import" | "require" | "compiler-dependent",
|
||||
directoryPaths?: ReadonlySet<string>,
|
||||
): string | null =>
|
||||
resolveArtifactPathByContext({
|
||||
declaredPath: specifier,
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
files,
|
||||
...(directoryPaths === undefined ? {} : { directoryPaths }),
|
||||
...(moduleKind === undefined ? {} : { moduleKind }),
|
||||
}).resolved_path;
|
||||
|
||||
/** Locate the recovered module that owns a static finding. */
|
||||
export const sourceNodeFor = (
|
||||
context: JavaScriptArtifactGraphContext,
|
||||
|
||||
@@ -58,6 +58,7 @@ export const addJavaScriptHtmlRoles = (
|
||||
sourcePath: html.path,
|
||||
context: "html-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
htmlBaseHref: script.base_href,
|
||||
|
||||
@@ -488,6 +488,7 @@ const addDiscoveredRole = (
|
||||
sourcePath: input.file.path,
|
||||
context: input.value.resolution_context,
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
...(input.value.role === "preload"
|
||||
@@ -556,6 +557,7 @@ const addSourceMapEdge = (
|
||||
sourcePath: input.file.path,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
});
|
||||
@@ -660,6 +662,7 @@ const resolveReference = (
|
||||
sourcePath,
|
||||
context: "url-reference",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
})
|
||||
@@ -668,6 +671,7 @@ const resolveReference = (
|
||||
sourcePath,
|
||||
context: "module-specifier",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
...("require" === reference.kind ||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from "./JavaScriptArtifactGraphStructure.js";
|
||||
import { addJavaScriptStaticFindingsSteps } from "./JavaScriptArtifactGraphFindings.js";
|
||||
import { applicationContainerIdentity } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
const REASON =
|
||||
@@ -51,6 +52,7 @@ const skippedEntry = (
|
||||
const analysisFor = (
|
||||
entries: readonly AnalyzedJavaScriptArtifactFile[],
|
||||
): JavaScriptModuleArtifactAnalysis => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: entries,
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
|
||||
@@ -562,6 +562,7 @@ export const addJavaScriptPackageNodes = (
|
||||
context: "package-entrypoint",
|
||||
runtimePathKind: "node-entrypoint",
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
}),
|
||||
@@ -689,6 +690,7 @@ const addPackageRole = (
|
||||
? { runtimePathKind: "node-entrypoint" as const }
|
||||
: {}),
|
||||
files: context.filesByPath,
|
||||
jsonDocuments: context.analysis.jsonDocuments,
|
||||
directoryPaths: context.directoryPaths,
|
||||
unanalyzedFiles: context.unanalyzedFiles,
|
||||
});
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
|
||||
const files = new Map(
|
||||
@@ -20,6 +21,7 @@ const files = new Map(
|
||||
],
|
||||
),
|
||||
);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
|
||||
it.each([
|
||||
"../../rea-artifact/shared/window.cjs",
|
||||
@@ -38,6 +40,7 @@ it.each([
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
jsonDocuments,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
|
||||
},
|
||||
@@ -61,6 +64,7 @@ it.each([
|
||||
context: "module-specifier",
|
||||
moduleKind: "import",
|
||||
files,
|
||||
jsonDocuments,
|
||||
}),
|
||||
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
resolveArtifactPathByContext,
|
||||
type ResolveArtifactPathInput,
|
||||
} from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { artifactLocalIdentity } from "./JavaScriptArtifactGraphContext.js";
|
||||
import { applicationNodeIdentitySchema } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
@@ -3565,8 +3566,9 @@ describe("module-backed package main host paths", () => {
|
||||
);
|
||||
});
|
||||
|
||||
const resolve = (input: ResolveArtifactPathInput) =>
|
||||
resolveArtifactPathByContext(input);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
const resolve = (input: Omit<ResolveArtifactPathInput, "jsonDocuments">) =>
|
||||
resolveArtifactPathByContext({ ...input, jsonDocuments });
|
||||
|
||||
const fileMap = (files: readonly JavaScriptArtifactFile[]) =>
|
||||
new Map(files.map((value) => [value.path, value]));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import { isBuiltin } from "node:module";
|
||||
import { posix, win32 } from "node:path";
|
||||
import { ELECTRON_MODULE } from "../../domain/javascript/javascriptElectronMemberWrites.js";
|
||||
@@ -79,6 +80,7 @@ export type ArtifactPathResolution = ArtifactPathResolutionBase &
|
||||
|
||||
/** Values needed to resolve a declaration without filesystem access. */
|
||||
export interface ResolveArtifactPathInput {
|
||||
readonly jsonDocuments: JavaScriptJsonDocuments;
|
||||
readonly declaredPath: string;
|
||||
readonly sourcePath: string;
|
||||
readonly context: ArtifactPathResolutionContext;
|
||||
@@ -1449,25 +1451,17 @@ const readPackageMetadata = (
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} was inventoried but its text is unavailable: ${file.text.reason}.`,
|
||||
};
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
const document = input.jsonDocuments.read(file);
|
||||
if (document.status !== "included" || document.packageFields === null)
|
||||
return {
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} is not valid package JSON.`,
|
||||
};
|
||||
}
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value))
|
||||
return {
|
||||
kind: "unavailable",
|
||||
limitation: `Directory package metadata ${path} is not valid package JSON.`,
|
||||
};
|
||||
const name = Reflect.get(value, "name");
|
||||
const exports = Reflect.get(value, "exports");
|
||||
const imports = Reflect.get(value, "imports");
|
||||
const main = Reflect.get(value, "main");
|
||||
const value = document.packageFields;
|
||||
const name = value.name;
|
||||
const exports = value.exports;
|
||||
const imports = value.imports;
|
||||
const main = value.main;
|
||||
return {
|
||||
kind: "available",
|
||||
path,
|
||||
@@ -1488,7 +1482,7 @@ const readPackageMetadata = (
|
||||
input.moduleKind !== undefined && typeof main !== "string"
|
||||
? undefined
|
||||
: main,
|
||||
module: Reflect.get(value, "module"),
|
||||
module: value.module,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1565,6 +1559,7 @@ const resolveDirectory = (
|
||||
sourcePath: metadata.path,
|
||||
context: "package-entrypoint",
|
||||
files: input.files,
|
||||
jsonDocuments: input.jsonDocuments,
|
||||
...(input.directoryPaths === undefined
|
||||
? {}
|
||||
: { directoryPaths: input.directoryPaths }),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
const paths = [
|
||||
"renderer/index.html",
|
||||
@@ -24,6 +25,7 @@ const files = new Map<string, JavaScriptArtifactFile>(
|
||||
},
|
||||
]),
|
||||
);
|
||||
const jsonDocuments = new JavaScriptJsonDocuments();
|
||||
const resolve = (declaredPath: string, htmlBaseHref: string) =>
|
||||
resolveArtifactPathByContext({
|
||||
declaredPath,
|
||||
@@ -31,6 +33,7 @@ const resolve = (declaredPath: string, htmlBaseHref: string) =>
|
||||
sourcePath: "renderer/index.html",
|
||||
context: "html-reference",
|
||||
files,
|
||||
jsonDocuments,
|
||||
});
|
||||
const documentUrl = "https://artifact.test/renderer/index.html";
|
||||
describe("HTML base href URL components", () => {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
type MutableArtifactFile = {
|
||||
-readonly [Key in keyof JavaScriptArtifactFile]: JavaScriptArtifactFile[Key];
|
||||
};
|
||||
|
||||
const mutableFile = (): MutableArtifactFile => ({
|
||||
path: "package.json",
|
||||
container_sha256: "container-a",
|
||||
sha256: "file-a",
|
||||
bytes: 13,
|
||||
inventory_artifact_id: "inventory-a",
|
||||
kind: "package-json",
|
||||
unpacked: false,
|
||||
text: { included: true, value: '{"main":"a.cjs"}' },
|
||||
});
|
||||
|
||||
it("reparses changed text and availability on a reused file object", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
const original = documents.read(file);
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
|
||||
file.text = { included: true, value: "{" };
|
||||
const invalid = documents.read(file);
|
||||
expect(invalid.status).toBe("invalid");
|
||||
|
||||
file.text = { included: false, reason: "invalid-utf8" };
|
||||
const unavailable = documents.read(file);
|
||||
expect(unavailable).toMatchObject({
|
||||
status: "unavailable",
|
||||
reason: "invalid-utf8",
|
||||
});
|
||||
|
||||
file.text = { included: true, value: '{"main":"b.cjs"}' };
|
||||
const updated = documents.read(file);
|
||||
expect(updated).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "b.cjs" },
|
||||
});
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
source: { path: "package.json" },
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
expect(invalid.status).toBe("invalid");
|
||||
expect(unavailable.status).toBe("unavailable");
|
||||
});
|
||||
|
||||
it("rebuilds source provenance when path and content digests change", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
const original = documents.read(file);
|
||||
|
||||
file.path = "nested/package.json";
|
||||
const moved = documents.read(file);
|
||||
expect(moved.source.path).toBe("nested/package.json");
|
||||
|
||||
file.container_sha256 = "container-b";
|
||||
const recontained = documents.read(file);
|
||||
expect(recontained.source).toMatchObject({
|
||||
path: "nested/package.json",
|
||||
container_sha256: "container-b",
|
||||
sha256: "file-a",
|
||||
});
|
||||
|
||||
file.sha256 = "file-b";
|
||||
const rehashed = documents.read(file);
|
||||
expect(rehashed.source).toMatchObject({
|
||||
path: "nested/package.json",
|
||||
container_sha256: "container-b",
|
||||
sha256: "file-b",
|
||||
});
|
||||
expect(original.source).toEqual({
|
||||
path: "package.json",
|
||||
container_sha256: "container-a",
|
||||
sha256: "file-a",
|
||||
});
|
||||
expect(original).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "a.cjs" },
|
||||
});
|
||||
});
|
||||
|
||||
it("retains only syntax and keys for ordinary JSON, with package fields scoped to its format", () => {
|
||||
const documents = new JavaScriptJsonDocuments();
|
||||
const file = mutableFile();
|
||||
file.kind = "json";
|
||||
file.text = {
|
||||
included: true,
|
||||
value: '{"exports":{"nested":["large config"]},"main":"entry.cjs"}',
|
||||
};
|
||||
const ordinary = documents.read(file);
|
||||
expect(ordinary).toMatchObject({
|
||||
status: "included",
|
||||
topLevelKeys: ["exports", "main"],
|
||||
packageFields: null,
|
||||
});
|
||||
file.kind = "package-json";
|
||||
expect(documents.read(file)).toMatchObject({
|
||||
status: "included",
|
||||
packageFields: { main: "entry.cjs", exports: { nested: ["large config"] } },
|
||||
});
|
||||
expect(ordinary.packageFields).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
|
||||
interface JavaScriptJsonDocumentSource {
|
||||
readonly path: string;
|
||||
readonly container_sha256: string;
|
||||
readonly sha256: string;
|
||||
}
|
||||
|
||||
interface JavaScriptJsonDocumentFactBase {
|
||||
readonly source: JavaScriptJsonDocumentSource;
|
||||
readonly topLevelKeys: readonly string[];
|
||||
}
|
||||
|
||||
export type JavaScriptJsonDocumentFacts =
|
||||
| (JavaScriptJsonDocumentFactBase & {
|
||||
readonly status: "included";
|
||||
readonly packageFields: Readonly<
|
||||
Partial<Record<(typeof PACKAGE_PROPERTIES)[number], unknown>>
|
||||
> | null;
|
||||
})
|
||||
| (JavaScriptJsonDocumentFactBase & {
|
||||
readonly status: "invalid";
|
||||
readonly packageFields: null;
|
||||
readonly syntaxError: SyntaxError;
|
||||
})
|
||||
| (JavaScriptJsonDocumentFactBase & {
|
||||
readonly status: "unavailable";
|
||||
readonly packageFields: null;
|
||||
readonly reason:
|
||||
| "not-applicable"
|
||||
| "invalid-utf8"
|
||||
| "outside-analysis-scope";
|
||||
});
|
||||
|
||||
interface SourceSnapshot {
|
||||
readonly source: JavaScriptJsonDocumentSource;
|
||||
readonly kind: JavaScriptArtifactFile["kind"];
|
||||
readonly text: JavaScriptArtifactFile["text"];
|
||||
}
|
||||
|
||||
interface CachedFacts {
|
||||
readonly snapshot: SourceSnapshot;
|
||||
readonly facts: JavaScriptJsonDocumentFacts;
|
||||
}
|
||||
|
||||
const PACKAGE_PROPERTIES = [
|
||||
"name",
|
||||
"version",
|
||||
"main",
|
||||
"module",
|
||||
"renderer",
|
||||
"browser",
|
||||
"exports",
|
||||
"imports",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Lazy JSON facts shared by one analysis and its graph projection. Entries follow
|
||||
* the admitted source files' lifetime; changed identity, format or text replaces them.
|
||||
*/
|
||||
export class JavaScriptJsonDocuments {
|
||||
readonly #byFile = new Map<JavaScriptArtifactFile, CachedFacts>();
|
||||
|
||||
read(file: JavaScriptArtifactFile): JavaScriptJsonDocumentFacts {
|
||||
const cached = this.#byFile.get(file);
|
||||
if (cached !== undefined && sameSnapshot(cached.snapshot, file))
|
||||
return cached.facts;
|
||||
|
||||
const snapshot = sourceSnapshot(file);
|
||||
const facts = parseDocument(snapshot);
|
||||
this.#byFile.set(file, { snapshot, facts });
|
||||
return facts;
|
||||
}
|
||||
}
|
||||
|
||||
const sourceSnapshot = (file: JavaScriptArtifactFile): SourceSnapshot => ({
|
||||
source: Object.freeze({
|
||||
path: file.path,
|
||||
container_sha256: file.container_sha256,
|
||||
sha256: file.sha256,
|
||||
}),
|
||||
kind: file.kind,
|
||||
text: { ...file.text },
|
||||
});
|
||||
|
||||
const sameSnapshot = (
|
||||
left: SourceSnapshot,
|
||||
right: JavaScriptArtifactFile,
|
||||
): boolean =>
|
||||
left.source.path === right.path &&
|
||||
left.source.container_sha256 === right.container_sha256 &&
|
||||
left.source.sha256 === right.sha256 &&
|
||||
left.kind === right.kind &&
|
||||
left.text.included === right.text.included &&
|
||||
(left.text.included
|
||||
? right.text.included && left.text.value === right.text.value
|
||||
: !right.text.included && left.text.reason === right.text.reason);
|
||||
|
||||
const parseDocument = (
|
||||
snapshot: SourceSnapshot,
|
||||
): JavaScriptJsonDocumentFacts => {
|
||||
if (!snapshot.text.included)
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "unavailable",
|
||||
packageFields: null,
|
||||
topLevelKeys: Object.freeze([]),
|
||||
reason: snapshot.text.reason,
|
||||
});
|
||||
|
||||
let root: unknown;
|
||||
try {
|
||||
root = JSON.parse(snapshot.text.value);
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof SyntaxError)) throw cause;
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "invalid",
|
||||
packageFields: null,
|
||||
topLevelKeys: Object.freeze([]),
|
||||
syntaxError: cause,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof root !== "object" || root === null || Array.isArray(root))
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
packageFields: null,
|
||||
topLevelKeys: Object.freeze([]),
|
||||
});
|
||||
|
||||
const record = root as Record<string, unknown>;
|
||||
const topLevelKeys = Object.freeze(
|
||||
Object.keys(record).sort(compareUnicodeCodePoints),
|
||||
);
|
||||
const packageFields =
|
||||
snapshot.kind === "package-json"
|
||||
? Object.freeze(
|
||||
Object.fromEntries(
|
||||
PACKAGE_PROPERTIES.flatMap((key) =>
|
||||
Object.hasOwn(record, key)
|
||||
? [[key, Reflect.get(record, key)]]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
return Object.freeze({
|
||||
source: snapshot.source,
|
||||
status: "included",
|
||||
packageFields,
|
||||
topLevelKeys,
|
||||
});
|
||||
};
|
||||
@@ -1,25 +1,23 @@
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import type { JavaScriptJsonModuleObservation } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import type { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
/** Parse one approved JSON module without evaluating JavaScript or resolving imports. */
|
||||
export const analyzeJavaScriptJsonModule = (
|
||||
file: JavaScriptArtifactFile,
|
||||
documents: JavaScriptJsonDocuments,
|
||||
): JavaScriptJsonModuleObservation => {
|
||||
if (!file.text.included)
|
||||
const document = documents.read(file);
|
||||
if (document.status === "unavailable")
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
status: "unavailable",
|
||||
top_level_keys: [],
|
||||
omitted_top_level_keys: null,
|
||||
limitation: `JSON module text was unavailable: ${file.text.reason}.`,
|
||||
limitation: `JSON module text was unavailable: ${document.reason}.`,
|
||||
};
|
||||
let value: unknown;
|
||||
try {
|
||||
value = JSON.parse(file.text.value);
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
if (document.status === "invalid")
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
@@ -28,17 +26,11 @@ export const analyzeJavaScriptJsonModule = (
|
||||
omitted_top_level_keys: 0,
|
||||
limitation: "JSON module is not valid JSON.",
|
||||
};
|
||||
}
|
||||
const keys =
|
||||
typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
? Object.keys(value).sort(compareUnicodeCodePoints)
|
||||
: [];
|
||||
const retained = keys;
|
||||
return {
|
||||
path: file.path,
|
||||
sha256: file.sha256,
|
||||
status: "included",
|
||||
top_level_keys: retained,
|
||||
top_level_keys: document.topLevelKeys,
|
||||
omitted_top_level_keys: 0,
|
||||
limitation: null,
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ interface RelationshipInput {
|
||||
type ResolvedModuleTarget = {
|
||||
readonly node: ApplicationNode;
|
||||
readonly limitations: readonly string[];
|
||||
readonly jsonStatus: "included" | "invalid" | "unavailable" | null;
|
||||
readonly resolution?: ArtifactPathResolution;
|
||||
} & (
|
||||
| {
|
||||
@@ -289,6 +290,7 @@ const resolveModuleTarget = (
|
||||
sourcePath: input.file.path,
|
||||
context: "module-specifier",
|
||||
files: input.context.filesByPath,
|
||||
jsonDocuments: input.context.analysis.jsonDocuments,
|
||||
directoryPaths: input.context.directoryPaths,
|
||||
unanalyzedFiles: input.context.unanalyzedFiles,
|
||||
moduleKind: moduleResolutionModeForJavaScriptReference(
|
||||
@@ -304,6 +306,7 @@ const resolveModuleTarget = (
|
||||
node: unresolvedModuleNode(input, specifier, resolution),
|
||||
path: null,
|
||||
file: null,
|
||||
jsonStatus: null,
|
||||
status: resolution.resolution_status,
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
@@ -321,6 +324,10 @@ const resolveModuleTarget = (
|
||||
node,
|
||||
path,
|
||||
file,
|
||||
jsonStatus:
|
||||
file.kind === "json" || file.kind === "package-json"
|
||||
? input.context.analysis.jsonDocuments.read(file).status
|
||||
: null,
|
||||
status: "resolved",
|
||||
limitations: resolution.limitations,
|
||||
resolution,
|
||||
@@ -404,10 +411,7 @@ const relationshipProperties = (
|
||||
resolution_status: target?.status ?? null,
|
||||
...artifactPathResolutionProperties(target?.resolution),
|
||||
target_file_kind: target?.file?.kind ?? null,
|
||||
target_json_status:
|
||||
target?.file?.kind === "json" || target?.file?.kind === "package-json"
|
||||
? jsonStatus(target.file)
|
||||
: null,
|
||||
target_json_status: target?.jsonStatus ?? null,
|
||||
});
|
||||
|
||||
const relationshipEvidence = (
|
||||
@@ -467,20 +471,6 @@ const moduleFormat = (
|
||||
: "unknown";
|
||||
};
|
||||
|
||||
const jsonStatus = (
|
||||
file: JavaScriptArtifactFile,
|
||||
): "included" | "invalid" | "unavailable" => {
|
||||
if (!file.text.included) return "unavailable";
|
||||
try {
|
||||
JSON.parse(file.text.value);
|
||||
return "included";
|
||||
} catch (cause: unknown) {
|
||||
// Invalid JSON is classified without further detail.
|
||||
void cause;
|
||||
return "invalid";
|
||||
}
|
||||
};
|
||||
|
||||
const isExportLink = (link: JavaScriptSemanticModuleLink): boolean =>
|
||||
link.kind === "export" ||
|
||||
link.kind === "re-export" ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.j
|
||||
import type { JavaScriptArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js";
|
||||
import { resolveJavaScriptSemanticEvidence } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { JavaScriptJsonDocuments } from "./JavaScriptJsonDocuments.js";
|
||||
|
||||
const SHA256 = "a".repeat(64);
|
||||
const GRAPH_ID = `jag_${"b".repeat(64)}`;
|
||||
@@ -347,6 +348,7 @@ it("uses unavailable evidence when no source produced semantic IR", () => {
|
||||
});
|
||||
|
||||
const emptyAnalysis = (): JavaScriptArtifactAnalysis => ({
|
||||
jsonDocuments: new JavaScriptJsonDocuments(),
|
||||
files: [],
|
||||
packages: [],
|
||||
json_modules: [],
|
||||
|
||||
Reference in New Issue
Block a user