shared/selectors/syntax.selector.ts

shared/selectors/syntax.selector.ts is a file in GovLab Extension Host. 164 lines of code and 41 definitions.

import type { AstNode, ExportedName, SourceLoc } from "../../types/syntax.types.ts";

export const locOf = function locOf(node: AstNode): SourceLoc {
    return node.loc;
};

export const isAstNode = function isAstNode(value: unknown): value is AstNode {
    if (typeof value !== "object" || value === null) {
        return false;
    }
    const candidate = value as { type?: unknown; loc?: unknown };
    return typeof candidate.type === "string" && typeof candidate.loc === "object" && candidate.loc !== null;
};

export const asNode = function asNode(value: unknown): AstNode | null {
    return isAstNode(value) ? value : null;
};

export const nodeAt = function nodeAt(node: AstNode | null, key: string): AstNode | null {
    return node === null ? null : asNode(node[key]);
};

export const nodesAt = function nodesAt(node: AstNode | null, key: string): AstNode[] {
    const value = node === null ? undefined : node[key];
    return Array.isArray(value) ? value.filter(isAstNode) : [];
};

export const stringAt = function stringAt(node: AstNode | null, key: string): string {
    const value = node === null ? undefined : node[key];
    return typeof value === "string" ? value : "";
};

const isRecord = function isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === "object" && value !== null;
};

export const recordAt = function recordAt(node: AstNode | null, key: string): Record<string, unknown> | null {
    const value = node === null ? undefined : node[key];
    return isRecord(value) ? value : null;
};

export const stringIn = function stringIn(record: Record<string, unknown> | null, key: string): string {
    const value = record === null ? undefined : record[key];
    return typeof value === "string" ? value : "";
};

export const numberAt = function numberAt(node: AstNode | null, key: string): number | null {
    const value = node === null ? undefined : node[key];
    return typeof value === "number" ? value : null;
};

export const booleanAt = function booleanAt(node: AstNode | null, key: string): boolean {
    return node?.[key] === true;
};

export const handlerKey = function handlerKey(type: string): string {
    return type.slice(0, 1).toLowerCase() + type.slice(1);
};

export const typeOf = function typeOf(node: AstNode | null): string {
    return node === null ? "" : node.type;
};

export const isType = function isType(node: AstNode | null, type: string): boolean {
    return node?.type === type;
};

export const literalString = function literalString(node: AstNode | null): string | null {
    if (!isType(node, "Literal")) {
        return null;
    }
    const value = node === null ? undefined : node["value"];
    return typeof value === "string" ? value : null;
};

export const staticTextOf = function staticTextOf(node: AstNode | null): string | null {
    if (node === null) {
        return null;
    }
    if (node.type === "Literal") {
        return literalString(node);
    }
    if (node.type !== "TemplateLiteral" || nodesAt(node, "expressions").length > 0) {
        return null;
    }
    const [part] = nodesAt(node, "quasis");
    return part === undefined ? null : stringIn(recordAt(part, "value"), "cooked");
};

export const nameOf = function nameOf(node: AstNode | null): string {
    if (node === null) {
        return "";
    }
    if (node.type === "Identifier" || node.type === "PrivateIdentifier") {
        return stringAt(node, "name");
    }
    return literalString(node) ?? "";
};

export const calleeName = function calleeName(node: AstNode | null): string {
    const callee = nodeAt(node, "callee");
    if (callee === null) {
        return "";
    }
    if (callee.type === "MemberExpression") {
        return nameOf(nodeAt(callee, "property"));
    }
    return nameOf(callee);
};

export const propertyKeyName = function propertyKeyName(node: AstNode | null): string {
    return nameOf(nodeAt(node, "key"));
};

export const argumentAt = function argumentAt(node: AstNode | null, index: number): AstNode | null {
    return nodesAt(node, "arguments")[index] ?? null;
};

const NAMED_DECLARATIONS = new Set(["FunctionDeclaration", "ClassDeclaration"]);

const declaredNames = function declaredNames(declaration: AstNode | null): ExportedName[] {
    if (declaration === null) {
        return [];
    }
    if (declaration.type === "VariableDeclaration") {
        return nodesAt(declaration, "declarations")
            .filter((d) => isType(nodeAt(d, "id"), "Identifier"))
            .map((d) => ({ name: nameOf(nodeAt(d, "id")), target: d }));
    }
    const id = nodeAt(declaration, "id");
    if (NAMED_DECLARATIONS.has(declaration.type) && isType(id, "Identifier")) {
        return [{ name: nameOf(id), target: declaration }];
    }
    return [];
};

export const declaredAtTop = function declaredAtTop(program: AstNode): ReadonlySet<string> {
    return new Set(
        nodesAt(program, "body").flatMap((statement) => {
            const inner = isType(statement, "ExportNamedDeclaration") ? nodeAt(statement, "declaration") : statement;
            return declaredNames(inner).map((entry) => entry.name);
        }),
    );
};

export const importedNamesOf = function importedNamesOf(program: AstNode): ReadonlySet<string> {
    return new Set(
        nodesAt(program, "body")
            .filter((statement) => isType(statement, "ImportDeclaration"))
            .flatMap((statement) => nodesAt(statement, "specifiers").map((spec) => nameOf(nodeAt(spec, "local")))),
    );
};

export const exportedNamesOf = function exportedNamesOf(node: AstNode): ExportedName[] {
    const names = declaredNames(nodeAt(node, "declaration"));
    for (const spec of nodesAt(node, "specifiers")) {
        const exported = nodeAt(spec, "exported");
        if (isType(exported, "Identifier")) {
            names.push({ name: nameOf(exported), target: spec });
        }
    }
    return names;
};

const BACK_REFERENCES = new Set(["parent"]);

const membersOf = function membersOf(value: unknown): AstNode[] {
    if (Array.isArray(value)) {
        return value.filter(isAstNode);
    }
    const child = asNode(value);
    return child === null ? [] : [child];
};

const childrenOf = function childrenOf(node: AstNode): AstNode[] {
    return Object.entries(node)
        .filter(([key]) => !BACK_REFERENCES.has(key))
        .flatMap(([, value]) => membersOf(value));
};

export const walk = function walk(root: AstNode, visit: (node: AstNode) => void): void {
    const seen = new Set<AstNode>();
    const stack: AstNode[] = [root];
    while (stack.length > 0) {
        const node = stack.pop();
        if (node === undefined || seen.has(node)) {
            continue;
        }
        seen.add(node);
        visit(node);
        stack.push(...childrenOf(node));
    }
};