Files
bbedward bb20528f68 popouts: grow surfaces at their origin
cc: slide grid with the detail page
settings: tweak appearance header style
settings: tweak sidebar profile style
2026-09-21 15:47:01 -04:00

105 lines
3.7 KiB
JavaScript

import assert from "node:assert/strict";
import { readdirSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import test from "node:test";
const root = fileURLToPath(new URL("../", import.meta.url));
const KNOWN_CYCLES = [
["Paths", "SettingsData"],
["SessionData", "SessionService"],
["AqueousService", "CompositorService"],
["CompositorService", "HyprlandService"],
["CompositorService", "MangoService"],
["CompositorService", "NiriService"],
["CompositorService", "NiriService", "Theme"]
].map(members => members.join(","));
function stripLiterals(source) {
let out = "";
let state = null;
for (let i = 0; i < source.length; i++) {
const c = source[i];
if (state === null) {
if (source.startsWith("//", i)) { state = "line"; i++; continue; }
if (source.startsWith("/*", i)) { state = "block"; i++; continue; }
if (c === '"' || c === "'" || c === "`") { state = c; out += " "; continue; }
out += c;
} else if (state === "line") {
if (c === "\n") { state = null; out += c; }
} else if (state === "block") {
if (source.startsWith("*/", i)) { state = null; i++; }
} else {
if (c === "\\") { i++; continue; }
if (c === state) { state = null; out += " "; }
}
}
return out;
}
function stripDeferredBodies(source) {
const pattern = /\bfunction\s+\w+\s*\([^)]*\)\s*\{|\bon[A-Z]\w*\s*:\s*(?:\w+\s*=>\s*|function\s*\([^)]*\)\s*)?\{/;
for (;;) {
const match = pattern.exec(source);
if (!match) return source;
let depth = 0;
let i = match.index + match[0].length - 1;
for (; i < source.length; i++) {
if (source[i] === "{") depth++;
else if (source[i] === "}" && --depth === 0) break;
}
source = source.slice(0, match.index) + " ".repeat(i + 1 - match.index) + source.slice(i + 1);
}
}
function singletonSources() {
const sources = new Map();
for (const dir of ["Common", "Services"]) {
for (const name of readdirSync(root + dir)) {
if (!name.endsWith(".qml")) continue;
const text = readFileSync(root + dir + "/" + name, "utf8");
if (text.trimStart().startsWith("pragma Singleton"))
sources.set(name.slice(0, -4), text);
}
}
return sources;
}
function creationGraph(sources) {
const graph = new Map();
for (const [name, text] of sources) {
const scanned = stripDeferredBodies(stripLiterals(text));
const refs = new Set();
for (const [identifier] of scanned.matchAll(/\b[A-Z]\w+\b/g))
if (sources.has(identifier) && identifier !== name) refs.add(identifier);
graph.set(name, refs);
}
return graph;
}
function findCycles(graph) {
const found = new Set();
const walk = (node, stack, seen) => {
for (const next of graph.get(node) ?? []) {
const at = stack.indexOf(next);
if (at !== -1) {
found.add([...new Set(stack.slice(at))].sort().join(","));
} else if (!seen.has(next)) {
seen.add(next);
walk(next, [...stack, next], seen);
}
}
};
for (const node of graph.keys()) walk(node, [node], new Set([node]));
return found;
}
test("no new creation-time cycles between Common and Services singletons", () => {
const sources = singletonSources();
const cycles = findCycles(creationGraph(sources));
const added = [...cycles].filter(cycle => !KNOWN_CYCLES.includes(cycle));
assert.deepEqual(added, [], "new singleton cycle(s) reachable during construction: " + added.join(" | "));
});