Files
rea/src/application/ReferenceSourceImportEntries.test.ts
T

356 lines
10 KiB
TypeScript

import { createHash } from "node:crypto";
import { expect, it } from "vitest";
import { parseReferenceSourceEntries } from "./ReferenceSourceImportEntries.js";
import { historicalSourceLanguages } from "../domain/referenceSourceGraph.js";
import type { ReferenceSourceRead } from "../reference/ReferenceSourceReaderTypes.js";
it("projects an unsupported non-regular entry as unknown kind instead of file", () => {
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: [
{
status: "failed",
kind: "other",
path: "src/pipe",
code: "unsupported",
message: "Entry is not a regular file",
size: 0,
},
],
bytesRead: 0,
limitations: [],
};
const parsed = parseReferenceSourceEntries(read, new Set());
expect(parsed.entries).toContainEqual({
path: "src/pipe",
kind: "unknown",
classifications: ["unknown"],
limitations: [
"Entry is not a regular file Exclude this entry or replace it with a regular file.",
],
});
expect(parsed.entries.some((entry) => entry.kind === "file")).toBe(false);
});
it("keeps a failed regular file as file kind", () => {
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: [
{
status: "failed",
kind: "file",
path: "src/broken.ts",
code: "io",
message: "Entry could not be read",
},
],
bytesRead: 0,
limitations: [],
};
const parsed = parseReferenceSourceEntries(read, new Set());
expect(parsed.entries).toContainEqual({
path: "src/broken.ts",
kind: "file",
sha256: null,
size: null,
language: null,
classifications: ["source"],
content_state: "unreadable",
limitations: [
"Entry could not be read This file could not be read. Check its permissions, then try again.",
],
});
});
it("keeps source entries when a module has an empty specifier", () => {
const files = [
["main.mjs", 'import "";\n'],
["other.txt", "retained\n"],
] as const;
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: files.map(([path, content]) => ({
status: "read",
kind: "file",
path,
bytes: Buffer.from(content),
size: Buffer.byteLength(content),
})),
bytesRead: files.reduce(
(sum, [, content]) => sum + Buffer.byteLength(content),
0,
),
limitations: [],
};
const parsed = parseReferenceSourceEntries(
read,
new Set(files.map(([path]) => path)),
);
expect(parsed.entries.map(({ path }) => path)).toEqual([
"main.mjs",
"other.txt",
]);
expect(parsed.relationships).toEqual([]);
expect(parsed.limitations).toEqual([
"Empty module specifier in main.mjs was not recorded as a relationship.",
]);
});
it("retains Kotlin source identity and language across the inventory projection", () => {
const files = [
[
"MainActivity.kt",
"import android.app.Activity\nclass MainActivity : Activity()\n",
["source"],
"Kotlin",
],
["task.kts", 'println("hello")\n', ["source"], "Kotlin"],
[
"build.gradle.kts",
'plugins { kotlin("jvm") version "2.3.0" }\n',
["manifest", "source"],
"Kotlin",
],
["MainActivity.kt.backup", "backup\n", ["unknown"], null],
["Legacy.java", "class Legacy {}\n", ["source"], "Java"],
] as const;
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: files.map(([path, content]) => ({
status: "read",
kind: "file",
path,
bytes: Buffer.from(content),
size: Buffer.byteLength(content),
})),
bytesRead: files.reduce(
(sum, [, content]) => sum + Buffer.byteLength(content),
0,
),
limitations: [],
};
const parsed = parseReferenceSourceEntries(
read,
new Set(files.map(([path]) => path)),
);
for (const [path, content, classifications, language] of files)
expect(parsed.entries).toContainEqual({
path,
kind: "file",
language,
classifications,
size: Buffer.byteLength(content),
sha256: createHash("sha256").update(content).digest("hex"),
content_state: "hashed",
limitations: [],
});
expect(historicalSourceLanguages(parsed.entries)).toEqual(["Java", "Kotlin"]);
expect(parsed.parseFailures).toEqual([]);
expect(parsed.relationships).toEqual([]);
});
it("resolves runtime require package mains without an index and before typed indexes", () => {
const files = [
["main.cjs", 'require("./pkg");\nrequire("./dep");\n'],
[
"main.ts",
'import dep = require("./typed");\n' +
'require("./typed");\n' +
'import data = require("./json-package");\n' +
'require("./emitted.js");\n',
],
["pkg/package.json", '{"main":"actual.js"}\n'],
["pkg/actual.js", 'module.exports = "package main";\n'],
["dep.js", 'module.exports = "file";\n'],
["dep/index.ts", 'export const value = "index";\n'],
["typed.js", 'module.exports = "runtime file";\n'],
["typed/index.ts", 'export const value = "source index";\n'],
["json-package/package.json", '{"main":"data"}\n'],
["json-package/data.json", '{"value":"package main"}\n'],
["json-package/index.js", 'module.exports = "fallback index";\n'],
["emitted.ts", 'export const value = "source counterpart";\n'],
] as const;
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: files.map(([path, content]) => ({
status: "read",
kind: "file",
path,
bytes: Buffer.from(content),
size: Buffer.byteLength(content),
})),
bytesRead: files.reduce(
(sum, [, content]) => sum + Buffer.byteLength(content),
0,
),
limitations: [],
};
const parsed = parseReferenceSourceEntries(
read,
new Set(files.map(([path]) => path)),
);
expect(parsed.relationships).toContainEqual({
from_path: "main.cjs",
to: "pkg/actual.js",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
expect(parsed.relationships).toContainEqual({
from_path: "main.cjs",
to: "dep.js",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
expect(parsed.relationships).toContainEqual({
from_path: "main.ts",
to: "typed/index.ts",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
expect(parsed.relationships).toContainEqual({
from_path: "main.ts",
to: "typed.js",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
expect(parsed.relationships).toContainEqual({
from_path: "main.ts",
to: "json-package/data.json",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
expect(parsed.relationships).toContainEqual({
from_path: "main.ts",
to: "emitted.ts",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
});
});
it("matches package manifests by exact or unique canonical directory identity", () => {
const composed = "caf\u00e9";
const decomposed = "cafe\u0301";
const composedTwin = "r\u00e9sum\u00e9";
const decomposedTwin = "re\u0301sume\u0301";
const files = [
[
"main.cjs",
[composed, composedTwin, decomposedTwin]
.map((name) => `require(${JSON.stringify(`./${name}`)});`)
.join("\n"),
],
[`${decomposed}/package.json`, '{"main":"unique.js"}\n'],
[`${decomposed}/unique.js`, 'module.exports = "unique";\n'],
[`${composed}/unique.js`, 'module.exports = "unrelated exact spelling";\n'],
[`${composedTwin}/package.json`, '{"main":"exact-composed.js"}\n'],
[`${composedTwin}/exact-composed.js`, 'module.exports = "composed";\n'],
[`${decomposedTwin}/package.json`, '{"main":"exact-decomposed.js"}\n'],
[
`${decomposedTwin}/exact-decomposed.js`,
'module.exports = "decomposed";\n',
],
] as const;
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: files.map(([path, content]) => ({
status: "read",
kind: "file",
path,
bytes: Buffer.from(content),
size: Buffer.byteLength(content),
})),
bytesRead: files.reduce(
(sum, [, content]) => sum + Buffer.byteLength(content),
0,
),
limitations: [],
};
const parsed = parseReferenceSourceEntries(
read,
new Set(files.map(([path]) => path)),
);
expect(parsed.relationships.map(({ to }) => to)).toEqual([
`${decomposed}/unique.js`,
`${composedTwin}/exact-composed.js`,
`${decomposedTwin}/exact-decomposed.js`,
]);
});
it("resolves extensionless imports and requires to .mts and .cts files and directory indexes", () => {
const files = [
[
"src/index.ts",
'import "./esm";\nimport "./pkg-esm";\nimport cjs = require("./cjs");\nimport pkgCjs = require("./pkg-cjs");\n',
],
["src/esm.mts", "export const esm = 1;\n"],
["src/pkg-esm/index.mts", "export const pkgEsm = 2;\n"],
["src/cjs.cts", "module.exports = { cjs: 3 };\n"],
["src/pkg-cjs/index.cts", "module.exports = { pkgCjs: 4 };\n"],
] as const;
const read: ReferenceSourceRead = {
root: "/selected/source",
entries: files.map(([path, content]) => ({
status: "read",
kind: "file",
path,
bytes: Buffer.from(content),
size: Buffer.byteLength(content),
})),
bytesRead: files.reduce(
(sum, [, content]) => sum + Buffer.byteLength(content),
0,
),
limitations: [],
};
const parsed = parseReferenceSourceEntries(
read,
new Set(files.map(([path]) => path)),
);
expect(parsed.relationships).toEqual(
expect.arrayContaining([
{
from_path: "src/index.ts",
to: "src/esm.mts",
kind: "imports",
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "src/index.ts",
to: "src/pkg-esm/index.mts",
kind: "imports",
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "src/index.ts",
to: "src/cjs.cts",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
},
{
from_path: "src/index.ts",
to: "src/pkg-cjs/index.cts",
kind: "requires",
resolution: "internal",
parse_state: "parsed",
},
]),
);
});