shared/analyzers/segment.analyzer.ts

shared/analyzers/segment.analyzer.ts is a file in GovLab Extension Host. 99 lines of code and 22 definitions.

import {
    calleeName,
    handlerKey,
    isType,
    literalString,
    nameOf,
    nodeAt,
    nodesAt,
    stringAt,
} from "../selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import type { SegmentTable } from "../../types/location.types.ts";
import { cookedOf } from "./location.analyzer.ts";
import { tokenIn } from "../registries/location.registry.ts";

const BUILDERS = new Set(["join", "resolve"]);

type Folder = (node: AstNode, consts: SegmentTable) => string[] | null;

const foldParts = function foldParts(nodes: readonly AstNode[], consts: SegmentTable): string[] | null {
    const parts: string[] = [];
    for (const node of nodes) {
        const folded = foldSegments(node, consts);
        if (folded === null) {
            return null;
        }
        parts.push(...folded);
    }
    return parts;
};

const arrayJoinObject = function arrayJoinObject(node: AstNode): AstNode | null {
    const callee = nodeAt(node, "callee");
    if (!isType(callee, "MemberExpression") || nameOf(nodeAt(callee, "property")) !== "join") {
        return null;
    }
    return nodeAt(callee, "object");
};

const foldTemplate: Folder = function foldTemplate(node, consts) {
    const holes: string[] = [];
    for (const expression of nodesAt(node, "expressions")) {
        const folded = foldSegments(expression, consts);
        if (folded === null) {
            return null;
        }
        holes.push(folded.join("/"));
    }
    return [
        nodesAt(node, "quasis")
            .map((quasi, index) => cookedOf(quasi) + (holes[index] ?? ""))
            .join(""),
    ];
};

const foldConcat: Folder = function foldConcat(node, consts) {
    if (stringAt(node, "operator") !== "+") {
        return null;
    }
    const left = foldSegments(nodeAt(node, "left"), consts);
    const right = foldSegments(nodeAt(node, "right"), consts);
    return left === null || right === null ? null : [left.join("/") + right.join("/")];
};

const foldCall: Folder = function foldCall(node, consts) {
    if (BUILDERS.has(calleeName(node))) {
        return foldParts(nodesAt(node, "arguments"), consts);
    }
    return foldSegments(arrayJoinObject(node), consts);
};

const foldLiteral: Folder = function foldLiteral(node) {
    const literal = literalString(node);
    return literal === null ? null : [literal];
};

const FOLDERS: Record<string, Folder> = {
    arrayExpression: (node, consts) => foldParts(nodesAt(node, "elements"), consts),
    binaryExpression: foldConcat,
    callExpression: foldCall,
    identifier: (node, consts) => consts.get(nameOf(node)) ?? null,
    literal: foldLiteral,
    spreadElement: (node, consts) => foldSegments(nodeAt(node, "argument"), consts),
    templateLiteral: foldTemplate,
};

export const foldSegments = function foldSegments(node: AstNode | null, consts: SegmentTable): string[] | null {
    if (node === null) {
        return null;
    }
    const fold = FOLDERS[handlerKey(node.type)];
    return fold === undefined ? null : fold(node, consts);
};

export const composedTokenOf = function composedTokenOf(node: AstNode, consts: SegmentTable): string | null {
    if (node.type !== "CallExpression") {
        return null;
    }
    const joinObject = arrayJoinObject(node);
    const isBuilder = BUILDERS.has(calleeName(node));
    if (!isBuilder && joinObject === null) {
        return null;
    }
    const segments =
        joinObject === null ? foldParts(nodesAt(node, "arguments"), consts) : foldSegments(joinObject, consts);
    if (segments === null || segments.length < 2) {
        return null;
    }
    return tokenIn(segments.filter((part) => part.length > 0).join("/"));
};