356 lines
10 KiB
TypeScript
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",
|
|
},
|
|
]),
|
|
);
|
|
});
|