mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-14 02:40:45 +08:00
297 lines
8.4 KiB
JavaScript
297 lines
8.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const CHECKS = [
|
|
{
|
|
modulePath: "sync/graph-persistence-io.js",
|
|
builderName: "createGraphPersistenceIoRuntime",
|
|
},
|
|
{
|
|
modulePath: "sync/graph-load-persist.js",
|
|
builderName: "createGraphLoadPersistRuntime",
|
|
},
|
|
{
|
|
modulePath: "sync/graph-mutation-gate.js",
|
|
builderName: "createGraphMutationGateRuntime",
|
|
},
|
|
];
|
|
|
|
function readProjectFile(relativePath) {
|
|
return readFileSync(new URL(`../${relativePath}`, import.meta.url), "utf8");
|
|
}
|
|
|
|
function stripCommentsAndStrings(source) {
|
|
let output = "";
|
|
let i = 0;
|
|
|
|
const appendSpacePreservingNewlines = (text) => {
|
|
output += text.replace(/[^\n\r]/g, " ");
|
|
};
|
|
|
|
while (i < source.length) {
|
|
const char = source[i];
|
|
const next = source[i + 1];
|
|
|
|
if (char === "/" && next === "/") {
|
|
const start = i;
|
|
i += 2;
|
|
while (i < source.length && source[i] !== "\n" && source[i] !== "\r") i += 1;
|
|
appendSpacePreservingNewlines(source.slice(start, i));
|
|
continue;
|
|
}
|
|
|
|
if (char === "/" && next === "*") {
|
|
const start = i;
|
|
i += 2;
|
|
while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1;
|
|
i = Math.min(source.length, i + 2);
|
|
appendSpacePreservingNewlines(source.slice(start, i));
|
|
continue;
|
|
}
|
|
|
|
if (char === '"' || char === "'") {
|
|
const quote = char;
|
|
const start = i;
|
|
i += 1;
|
|
while (i < source.length) {
|
|
if (source[i] === "\\") {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (source[i] === quote) {
|
|
i += 1;
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
appendSpacePreservingNewlines(source.slice(start, i));
|
|
continue;
|
|
}
|
|
|
|
if (char === "`") {
|
|
const start = i;
|
|
i += 1;
|
|
while (i < source.length) {
|
|
if (source[i] === "\\") {
|
|
i += 2;
|
|
continue;
|
|
}
|
|
if (source[i] === "`") {
|
|
i += 1;
|
|
break;
|
|
}
|
|
i += 1;
|
|
}
|
|
appendSpacePreservingNewlines(source.slice(start, i));
|
|
continue;
|
|
}
|
|
|
|
output += char;
|
|
i += 1;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
function findMatchingBrace(strippedSource, openIndex) {
|
|
if (strippedSource[openIndex] !== "{") {
|
|
throw new Error(`Expected opening brace at offset ${openIndex}`);
|
|
}
|
|
|
|
let depth = 0;
|
|
for (let i = openIndex; i < strippedSource.length; i += 1) {
|
|
if (strippedSource[i] === "{") depth += 1;
|
|
if (strippedSource[i] === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) return i;
|
|
}
|
|
}
|
|
|
|
throw new Error(`No matching closing brace for offset ${openIndex}`);
|
|
}
|
|
|
|
function extractRuntimeKeys(moduleSource) {
|
|
const stripped = stripCommentsAndStrings(moduleSource);
|
|
const unsupportedComputedAccess = /\bruntime\s*(?:\?\.)?\s*\[/.exec(stripped);
|
|
if (unsupportedComputedAccess) {
|
|
throw new Error(
|
|
`Unsupported computed runtime dependency access near offset ${unsupportedComputedAccess.index}. ` +
|
|
"Use direct runtime.someDependency access so completeness can be checked.",
|
|
);
|
|
}
|
|
const keys = new Set();
|
|
const runtimePropertyPattern = /\bruntime\s*(?:\?\.|\.)\s*([A-Za-z_$][\w$]*)/g;
|
|
let match = null;
|
|
|
|
while ((match = runtimePropertyPattern.exec(stripped))) {
|
|
keys.add(match[1]);
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
function extractBuilderReturnObjectRange(indexSource, builderName) {
|
|
const stripped = stripCommentsAndStrings(indexSource);
|
|
const functionPattern = new RegExp(`\\bfunction\\s+${builderName}\\s*\\(`);
|
|
const functionMatch = functionPattern.exec(stripped);
|
|
if (!functionMatch) {
|
|
throw new Error(`Could not locate builder function ${builderName}`);
|
|
}
|
|
|
|
const functionBodyOpen = stripped.indexOf("{", functionMatch.index + functionMatch[0].length);
|
|
if (functionBodyOpen < 0) {
|
|
throw new Error(`Could not locate function body for ${builderName}`);
|
|
}
|
|
const functionBodyClose = findMatchingBrace(stripped, functionBodyOpen);
|
|
|
|
const returnPattern = /\breturn\b/g;
|
|
returnPattern.lastIndex = functionBodyOpen + 1;
|
|
let returnMatch = null;
|
|
|
|
while ((returnMatch = returnPattern.exec(stripped)) && returnMatch.index < functionBodyClose) {
|
|
let cursor = returnMatch.index + returnMatch[0].length;
|
|
while (cursor < functionBodyClose && /\s/.test(stripped[cursor])) cursor += 1;
|
|
if (stripped[cursor] === "{") {
|
|
const objectOpen = cursor;
|
|
const objectClose = findMatchingBrace(stripped, objectOpen);
|
|
if (objectClose > functionBodyClose) {
|
|
throw new Error(`Return object for ${builderName} extends beyond function body`);
|
|
}
|
|
return { open: objectOpen, close: objectClose, stripped };
|
|
}
|
|
}
|
|
|
|
throw new Error(`Could not locate returned object literal for ${builderName}`);
|
|
}
|
|
|
|
function splitTopLevelObjectEntries(strippedSource, open, close) {
|
|
const entries = [];
|
|
let depth = 0;
|
|
let entryStart = open + 1;
|
|
|
|
for (let i = open + 1; i < close; i += 1) {
|
|
const char = strippedSource[i];
|
|
if (char === "{" || char === "(" || char === "[") depth += 1;
|
|
if (char === "}" || char === ")" || char === "]") depth -= 1;
|
|
if (char === "," && depth === 0) {
|
|
entries.push(strippedSource.slice(entryStart, i));
|
|
entryStart = i + 1;
|
|
}
|
|
}
|
|
|
|
entries.push(strippedSource.slice(entryStart, close));
|
|
return entries;
|
|
}
|
|
|
|
function extractBuilderProvidedKeys(indexSource, builderName) {
|
|
const { open, close, stripped } = extractBuilderReturnObjectRange(indexSource, builderName);
|
|
const entries = splitTopLevelObjectEntries(stripped, open, close);
|
|
const keys = new Set();
|
|
|
|
for (const rawEntry of entries) {
|
|
const entry = rawEntry.trim();
|
|
if (!entry || entry.startsWith("...")) continue;
|
|
|
|
const keyedEntryMatch = /^([A-Za-z_$][\w$]*)\s*:/.exec(entry);
|
|
if (keyedEntryMatch) {
|
|
keys.add(keyedEntryMatch[1]);
|
|
continue;
|
|
}
|
|
|
|
const methodEntryMatch = /^([A-Za-z_$][\w$]*)\s*\(/.exec(entry);
|
|
if (methodEntryMatch) {
|
|
keys.add(methodEntryMatch[1]);
|
|
continue;
|
|
}
|
|
|
|
const shorthandEntryMatch = /^([A-Za-z_$][\w$]*)$/.exec(entry);
|
|
if (shorthandEntryMatch) {
|
|
keys.add(shorthandEntryMatch[1]);
|
|
}
|
|
}
|
|
|
|
return keys;
|
|
}
|
|
|
|
function diffSets(left, right) {
|
|
return [...left].filter((value) => !right.has(value)).sort();
|
|
}
|
|
|
|
function assertRuntimeDepsComplete({ modulePath, builderName, moduleSource, indexSource }) {
|
|
const requiredKeys = extractRuntimeKeys(moduleSource);
|
|
const providedKeys = extractBuilderProvidedKeys(indexSource, builderName);
|
|
const missingKeys = diffSets(requiredKeys, providedKeys);
|
|
|
|
if (missingKeys.length > 0) {
|
|
throw new Error(
|
|
`${modulePath} requires runtime deps missing from ${builderName}: ${missingKeys.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
requiredKeys,
|
|
providedKeys,
|
|
unusedProvidedKeys: diffSets(providedKeys, requiredKeys),
|
|
};
|
|
}
|
|
|
|
function runExtractorSelfChecks() {
|
|
const runtimeKeys = extractRuntimeKeys(`
|
|
runtime.used();
|
|
runtime.value?.();
|
|
runtime?.optional;
|
|
// runtime.commentOnly();
|
|
const stringOnly = "runtime.stringOnly";
|
|
const templateOnly = ` + "`runtime.templateOnly`" + `;
|
|
`);
|
|
assert.deepEqual([...runtimeKeys].sort(), ["optional", "used", "value"]);
|
|
assert.throws(
|
|
() => extractRuntimeKeys("runtime[dynamicKey]();"),
|
|
/Unsupported computed runtime dependency access/,
|
|
);
|
|
|
|
const syntheticIndex = `
|
|
function createSyntheticRuntime() {
|
|
return {
|
|
used,
|
|
value: () => ({ nested: true }),
|
|
extra: (input) => {
|
|
return { input };
|
|
},
|
|
};
|
|
}
|
|
`;
|
|
const providedKeys = extractBuilderProvidedKeys(syntheticIndex, "createSyntheticRuntime");
|
|
assert.deepEqual([...providedKeys].sort(), ["extra", "used", "value"]);
|
|
|
|
assert.throws(
|
|
() =>
|
|
assertRuntimeDepsComplete({
|
|
modulePath: "synthetic.js",
|
|
builderName: "createSyntheticRuntime",
|
|
moduleSource: "runtime.used(); runtime.missing();",
|
|
indexSource: syntheticIndex,
|
|
}),
|
|
/synthetic\.js requires runtime deps missing from createSyntheticRuntime: missing/,
|
|
);
|
|
}
|
|
|
|
runExtractorSelfChecks();
|
|
|
|
const indexSource = readProjectFile("index.js");
|
|
for (const check of CHECKS) {
|
|
const result = assertRuntimeDepsComplete({
|
|
...check,
|
|
moduleSource: readProjectFile(check.modulePath),
|
|
indexSource,
|
|
});
|
|
|
|
if (result.unusedProvidedKeys.length > 0) {
|
|
console.info(
|
|
`[runtime-deps] ${check.builderName} provides unused keys for ${check.modulePath}: ${result.unusedProvidedKeys.join(", ")}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
console.log("runtime dependency completeness checks passed");
|