core/visitors/block.visitor.ts

core/visitors/block.visitor.ts is a file in GovLab Quality. 74 lines of code and 18 definitions.

import type { LineRange } from "#types/edit.types";
import path from "node:path";
import { readFileSync } from "node:fs";
import { safeStat } from "#core/loaders/source.loader";
import ts from "typescript";

const LINE_OFFSET = 1;

const declaredFunctionName = function declaredFunctionName(node: ts.VariableStatement): string | null {
    const [decl] = node.declarationList.declarations;
    if (!decl || !ts.isIdentifier(decl.name)) {
        return null;
    }
    const init = decl.initializer;
    const initializesFunction = init !== undefined && (ts.isArrowFunction(init) || ts.isFunctionExpression(init));
    return initializesFunction ? decl.name.text : null;
};

const functionNameOf = function functionNameOf(node: ts.Node): string | null {
    if (ts.isFunctionDeclaration(node)) {
        return node.name ? node.name.text : null;
    }
    if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name)) {
        return node.name.text;
    }
    return ts.isVariableStatement(node) ? declaredFunctionName(node) : null;
};

const visitFunctions = function visitFunctions(
    source: ts.SourceFile,
    onFunction: (name: string, node: ts.Node) => void,
): void {
    const visit = (node: ts.Node): void => {
        const name = functionNameOf(node);
        if (name !== null) {
            onFunction(name, node);
        }
        ts.forEachChild(node, visit);
    };
    visit(source);
};

const sourceAt = function sourceAt(root: string, file: string): ts.SourceFile | null {
    const full = path.join(root, file);
    if (safeStat(full) === null) {
        return null;
    }
    return ts.createSourceFile(file, readFileSync(full, "utf8"), ts.ScriptTarget.Latest, true);
};

export const functionNamesIn = function functionNamesIn(root: string, file: string): Set<string> | null {
    const source = sourceAt(root, file);
    if (source === null) {
        return null;
    }
    const names = new Set<string>();
    visitFunctions(source, (name) => {
        names.add(name);
    });
    return names;
};

export const functionRangesIn = function functionRangesIn(
    root: string,
    file: string,
    wanted: ReadonlySet<string>,
): LineRange[] {
    const source = sourceAt(root, file);
    const ranges: LineRange[] = [];
    if (source === null) {
        return ranges;
    }
    visitFunctions(source, (name, node) => {
        if (wanted.has(name)) {
            const start = source.getLineAndCharacterOfPosition(node.getStart(source)).line + LINE_OFFSET;
            const end = source.getLineAndCharacterOfPosition(node.getEnd()).line + LINE_OFFSET;
            ranges.push({ end, start });
        }
    });
    return ranges;
};