codemods/analyzers/binding.analyzer.ts

codemods/analyzers/binding.analyzer.ts is a file in GovLab Extension Host. 128 lines of code and 21 definitions.

import type { BindingContext, BindingFinding } from "../../types/analyzer.types.ts";
import { clausesOfToken, implementsClauses, memberNamesOf } from "../selectors/heritage.selector.ts";
import { lineOf, relPath } from "../selectors/program.selector.ts";
import ts from "typescript";
import { unsafeReason } from "../predicates/binding.predicate.ts";

const callableFieldDecl = function callableFieldDecl(
    checker: ts.TypeChecker,
    member: ts.Symbol,
): ts.PropertySignature | null {
    const decl = member.valueDeclaration ?? member.declarations?.[0] ?? null;
    if (!decl || !ts.isPropertySignature(decl)) {
        return null;
    }
    return checker.getTypeOfSymbolAtLocation(member, decl).getCallSignatures().length > 0 ? decl : null;
};

const methodImplementing = function methodImplementing(
    classNode: ts.ClassDeclaration,
    name: string,
): ts.MethodDeclaration | null {
    for (const classMember of classNode.members) {
        if (ts.isMethodDeclaration(classMember) && classMember.name.getText() === name) {
            return classMember;
        }
    }
    return null;
};

const findingForMember = function findingForMember(
    ctx: BindingContext,
    interfaceName: string,
    member: ts.Symbol,
): BindingFinding | null {
    const decl = callableFieldDecl(ctx.checker, member);
    if (!decl) {
        return null;
    }
    const name = member.getName();
    const implementation = methodImplementing(ctx.classNode, name);
    if (!implementation) {
        return null;
    }
    const memberSource = decl.getSourceFile();
    return {
        end: implementation.getEnd(),
        file: relPath(ctx.sourceFile.fileName),
        fileName: ctx.sourceFile.fileName,
        iface: interfaceName,
        ifaceFile: relPath(memberSource.fileName),
        ifaceLine: lineOf(memberSource, decl),
        line: lineOf(ctx.sourceFile, implementation),
        member: name,
        method: implementation,
        reason: unsafeReason(ctx, implementation, name),
        start: implementation.getStart(ctx.sourceFile),
    };
};

const findingsForImplements = function findingsForImplements(
    ctx: BindingContext,
    typeExpr: ts.ExpressionWithTypeArguments,
): BindingFinding[] {
    const ifaceType = ctx.checker.getTypeAtLocation(typeExpr.expression);
    const iface = ifaceType.getSymbol()?.getName() ?? "<interface>";
    const out: BindingFinding[] = [];
    for (const member of ifaceType.getProperties()) {
        const finding = findingForMember(ctx, iface, member);
        if (finding) {
            out.push(finding);
        }
    }
    return out;
};

const overridesOf = function overridesOf(
    checker: ts.TypeChecker,
    node: ts.ClassLikeDeclaration,
): [ts.Symbol, string[]][] {
    const out: [ts.Symbol, string[]][] = [];
    for (const typeExpr of clausesOfToken(node, ts.SyntaxKind.ExtendsKeyword)) {
        const symbol = checker.getSymbolAtLocation(typeExpr.expression);
        if (symbol) {
            out.push([symbol, memberNamesOf(node)]);
        }
    }
    return out;
};

const classesIn = function classesIn(sourceFile: ts.SourceFile): ts.ClassLikeDeclaration[] {
    const out: ts.ClassLikeDeclaration[] = [];
    const visit = function visit(node: ts.Node): void {
        if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
            out.push(node);
        }
        ts.forEachChild(node, visit);
    };
    ts.forEachChild(sourceFile, visit);
    return out;
};

export const collectOverrides = function collectOverrides(
    checker: ts.TypeChecker,
    sourceFiles: readonly ts.SourceFile[],
): Map<ts.Symbol, Set<string>> {
    const out = new Map<ts.Symbol, Set<string>>();
    const pairs = sourceFiles.flatMap((sourceFile) =>
        classesIn(sourceFile).flatMap((node) => overridesOf(checker, node)),
    );
    for (const [symbol, names] of pairs) {
        out.set(symbol, new Set([...(out.get(symbol) ?? []), ...names]));
    }
    return out;
};

export const scanSourceFile = function scanSourceFile(
    typeChecker: ts.TypeChecker,
    source: ts.SourceFile,
    overrideMap: ReadonlyMap<ts.Symbol, ReadonlySet<string>>,
): BindingFinding[] {
    const out: BindingFinding[] = [];
    const contextFor = function contextFor(declaration: ts.ClassDeclaration): BindingContext {
        return { checker: typeChecker, classNode: declaration, overrides: overrideMap, sourceFile: source };
    };
    const visit = function visit(node: ts.Node): void {
        if (ts.isClassDeclaration(node)) {
            const ctx = contextFor(node);
            for (const typeExpr of implementsClauses(typeChecker, node)) {
                out.push(...findingsForImplements(ctx, typeExpr));
            }
        }
        ts.forEachChild(node, visit);
    };
    ts.forEachChild(source, visit);
    return out;
};