core/quality/eslint/no-timestamp-ordering.eslint.rule.ts

core/quality/eslint/no-timestamp-ordering.eslint.rule.ts is a file in GovLab Quality. 149 lines of code and 26 definitions.

import type { Rule } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";

const ORDERING_OPERATORS = new Set<string>(["<", ">", "<=", ">=", "-"]);
const FUNCTION_TYPES = new Set<string>(["ArrowFunctionExpression", "FunctionExpression"]);
const CURRENT_TIME_NAMES = new Set<string>([
    "now",
    "nowMs",
    "nowTime",
    "currentTime",
    "currentMs",
    "today",
    "clock",
    "at",
    "asOf",
]);

const STORED = "stored";
const CURRENT = "current";

type InstantKind = typeof CURRENT | typeof STORED | null;

interface AstNode {
    type: string;
    name?: string;
    operator?: string;
    callee?: AstNode;
    object?: AstNode;
    property?: AstNode;
    left?: AstNode;
    right?: AstNode;
    init?: AstNode;
}

const isAstNode = function isAstNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object" && "type" in value;
};

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

const inSortComparator = function inSortComparator(ancestors: AstNode[]): boolean {
    for (let i = ancestors.length - 1; i >= 0; i -= 1) {
        const node = ancestors[i];
        if (node && FUNCTION_TYPES.has(node.type)) {
            if (i === 0) {
                return false;
            }
            const parent = ancestors[i - 1];
            return (
                parent?.type === "CallExpression" &&
                parent.callee?.type === "MemberExpression" &&
                parent.callee.property?.name === "sort"
            );
        }
    }
    return false;
};

const isDateNowCall = function isDateNowCall(callee: AstNode): boolean {
    return callee.object?.type === "Identifier" && callee.object.name === "Date" && callee.property?.name === "now";
};

const namesCurrentTime = function namesCurrentTime(receiver: AstNode | undefined): boolean {
    return (
        receiver?.type === "Identifier" && typeof receiver.name === "string" && CURRENT_TIME_NAMES.has(receiver.name)
    );
};

const callInstantKind = function callInstantKind(node: AstNode): InstantKind {
    const { callee } = node;
    if (callee?.type !== "MemberExpression") {
        return null;
    }
    if (isDateNowCall(callee)) {
        return CURRENT;
    }
    if (callee.property?.name !== "getTime") {
        return null;
    }
    return namesCurrentTime(callee.object) ? CURRENT : STORED;
};

const definitionInit = function definitionInit(
    context: Rule.RuleContext,
    node: Rule.Node,
    name: string,
): AstNode | null {
    let scope: ReturnType<Rule.RuleContext["sourceCode"]["getScope"]> | null = context.sourceCode.getScope(node);
    while (scope) {
        const variable = scope.variables.find((candidate) => candidate.name === name);
        if (variable) {
            if (
                variable.defs.length !== 1 ||
                variable.references.filter((reference) => reference.isWrite()).length > 1
            ) {
                return null;
            }
            return asNode(variable.defs[0]?.node)?.init ?? null;
        }
        scope = scope.upper;
    }
    return null;
};

const instantKind = function instantKind(
    context: Rule.RuleContext,
    node: Rule.Node,
    operand: AstNode | undefined,
): InstantKind {
    if (!operand) {
        return null;
    }
    if (operand.type === "CallExpression") {
        return callInstantKind(operand);
    }
    if (operand.type !== "Identifier" || typeof operand.name !== "string") {
        return null;
    }
    if (CURRENT_TIME_NAMES.has(operand.name)) {
        return CURRENT;
    }
    const init = definitionInit(context, node, operand.name);
    return init?.type === "CallExpression" ? callInstantKind(init) : null;
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const onBinary = (node: Rule.Node): void => {
            const binary = asNode(node);
            if (binary === null || typeof binary.operator !== "string" || !ORDERING_OPERATORS.has(binary.operator)) {
                return;
            }
            if (
                instantKind(context, node, binary.left) !== STORED ||
                instantKind(context, node, binary.right) !== STORED
            ) {
                return;
            }
            if (
                binary.operator === "-" &&
                !inSortComparator(context.sourceCode.getAncestors(node).map((a) => asNode(a) ?? { type: "" }))
            ) {
                return;
            }
            context.report({ messageId: "timestampOrdering", node });
        };
        const handlers: [string, (node: Rule.Node) => void][] = [["BinaryExpression", onBinary]];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["ordinal-time"],
        description:
            "Disallow ordering two recorded instants against each other — mandate a logical ordinal sequence instead",
        messages: {
            timestampOrdering:
                "This orders two recorded instants against each other — order that depends on physical clocks (skew, non-monotonicity) rather than causality. Assign a monotonic ordinal (a logical sequence number) when the event is recorded and order by that. Comparing a stored deadline against the current time is a different concern and is not reported.",
        },
        ruleId: "timestamp_ordering",
    }),
} satisfies Rule.RuleModule;