Files
ask262/setup/build_graph.ts
T
bendtherules cbe6bfdb10 refactor(build_graph): limit emu-clause selection to #spec-container
Search for `emu-clause` elements now performed within the `#spec-container` element instead of the entire document, preventing accidental matches outside the spec container.
2026-03-30 12:43:48 +05:30

126 lines
4.0 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import * as cheerio from "cheerio";
import { glob } from "glob";
import Graph from "graphology";
const SPEC_DIR = "./spec-built/multipage";
const CODE_DIR = "./engine262/src";
import { GRAPH_FILE } from "../constants";
async function buildGraph() {
const graph = new Graph({ multi: true });
const htmlFiles = await glob(path.join(SPEC_DIR, "*.html"));
console.log(
`Found ${htmlFiles.length} specification HTML file(s) in ${SPEC_DIR}`,
);
if (htmlFiles.length === 0) {
console.warn(`Warning: No specification HTML files found in ${SPEC_DIR}`);
}
console.log("Processing specification for graph...");
for (const file of htmlFiles) {
const fileName = path.basename(file);
const content = fs.readFileSync(file, "utf-8");
const $ = cheerio.load(content);
const $container = $("#spec-container");
$container.find("emu-clause").each((_i, elem) => {
const id = $(elem).attr("id");
const title = $(elem).find("h1").first().text().trim();
if (id) {
if (!graph.hasNode(id)) {
graph.addNode(id, { title, type: "SpecSection", file: fileName });
}
// Extract internal links
$(elem)
.find("a[href]")
.each((_j, link) => {
const href = $(link).attr("href");
if (href?.includes("#")) {
const [_targetFile, targetId] = href.split("#");
// We only care about links to other sections for now
if (targetId?.startsWith("sec-")) {
// Add relationship later if nodes exist
}
}
});
}
});
}
// Add edges for internal links
for (const file of htmlFiles) {
const content = fs.readFileSync(file, "utf-8");
const $ = cheerio.load(content);
const $container = $("#spec-container");
$container.find("emu-clause").each((_i, elem) => {
const sourceId = $(elem).attr("id");
if (sourceId && graph.hasNode(sourceId)) {
$(elem)
.find("a[href]")
.each((_j, link) => {
const href = $(link).attr("href");
if (href?.includes("#")) {
const [, targetId] = href.split("#");
if (
targetId &&
graph.hasNode(targetId) &&
sourceId !== targetId
) {
if (!graph.hasEdge(sourceId, targetId)) {
graph.addEdge(sourceId, targetId, { type: "LINKS_TO" });
}
}
}
});
}
});
}
console.log("Processing code for graph...");
const jsFiles = await glob(path.join(CODE_DIR, "**/*.mts"));
console.log(`Found ${jsFiles.length} code file(s) in ${CODE_DIR}`);
if (jsFiles.length === 0) {
console.warn(`Warning: No code files found in ${CODE_DIR}`);
}
for (const file of jsFiles) {
const fileName = path.basename(file);
const content = fs.readFileSync(file, "utf-8");
// Simple heuristic: look for function names starting with Evaluate_
const functionMatches = content.matchAll(
/export function\*? (Evaluate_([a-zA-Z0-9_]+))/g,
);
for (const match of functionMatches) {
const fullFuncName = match[1];
const shortName = match[2];
const funcNodeId = `func-${fullFuncName}`;
if (!graph.hasNode(funcNodeId)) {
graph.addNode(funcNodeId, {
name: fullFuncName,
type: "JSFunction",
file: fileName,
});
}
// Try to link to a spec section
// Heuristic: Evaluate_IfStatement -> sec-if-statement
const specId = `sec-${shortName.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()}`;
if (graph.hasNode(specId)) {
graph.addEdge(funcNodeId, specId, { type: "IMPLEMENTS" });
}
}
}
console.log(`Graph built with ${graph.order} nodes and ${graph.size} edges.`);
fs.writeFileSync(GRAPH_FILE, JSON.stringify(graph.export(), null, 2));
console.log(`Graph saved to ${GRAPH_FILE}`);
}
buildGraph().catch(console.error);