codemods/analyzers/increment.analyzer.ts

codemods/analyzers/increment.analyzer.ts is a file in GovLab Extension Host. 71 lines of code and 12 definitions.

import { lineOf, relPath } from "../selectors/program.selector.ts";
import type { IncrementFinding } from "../../types/analyzer.types.ts";
import ts from "typescript";

const STEP = "1";

const OPERATORS: ReadonlyMap<ts.SyntaxKind, string> = new Map([
    [ts.SyntaxKind.PlusPlusToken, "+="],
    [ts.SyntaxKind.MinusMinusToken, "-="],
]);

type UpdateExpression = ts.PostfixUnaryExpression | ts.PrefixUnaryExpression;

const isUpdateExpression = function isUpdateExpression(node: ts.Node): node is UpdateExpression {
    if (!ts.isPostfixUnaryExpression(node) && !ts.isPrefixUnaryExpression(node)) {
        return false;
    }
    return OPERATORS.has(node.operator);
};

const discardsValue = function discardsValue(node: UpdateExpression, parent: ts.Node | null): boolean {
    if (!parent) {
        return false;
    }
    if (ts.isExpressionStatement(parent)) {
        return true;
    }
    return ts.isForStatement(parent) && parent.incrementor === node;
};

const positionReason = function positionReason(node: UpdateExpression, parent: ts.Node | null): string | null {
    if (discardsValue(node, parent)) {
        return null;
    }
    return "the increment's value is read, so '+= 1' would change what the expression evaluates to";
};

const operandReason = function operandReason(operand: ts.Expression): string | null {
    if (ts.isIdentifier(operand) || ts.isPropertyAccessExpression(operand)) {
        return null;
    }
    if (ts.isElementAccessExpression(operand)) {
        return "operand is an element access — rewriting would evaluate the index expression twice";
    }
    return "operand is not a simple reference";
};

const findingFor = function findingFor(
    sourceFile: ts.SourceFile,
    node: UpdateExpression,
    parent: ts.Node | null,
): IncrementFinding {
    return {
        end: node.getEnd(),
        file: relPath(sourceFile.fileName),
        fileName: sourceFile.fileName,
        line: lineOf(sourceFile, node),
        operand: node.operand.getText(sourceFile),
        operator: OPERATORS.get(node.operator) ?? "+=",
        reason: positionReason(node, parent) ?? operandReason(node.operand),
        start: node.getStart(sourceFile),
    };
};

export const replacementText = function replacementText(finding: IncrementFinding): string {
    return `${finding.operand} ${finding.operator} ${STEP}`;
};

export const scanSourceFile = function scanSourceFile(sourceFile: ts.SourceFile): IncrementFinding[] {
    const out: IncrementFinding[] = [];
    const visit = function visit(node: ts.Node, parent: ts.Node | null): void {
        if (isUpdateExpression(node)) {
            out.push(findingFor(sourceFile, node, parent));
        }
        ts.forEachChild(node, (child) => {
            visit(child, node);
        });
    };
    visit(sourceFile, null);
    return out;
};