core/quality/eslint/no-inline-events.eslint.rule.ts

core/quality/eslint/no-inline-events.eslint.rule.ts is a file in GovLab Quality. 66 lines of code and 12 definitions.

import { asNode, identName } from "#core/selectors/syntax.selector";
import type { Rule } from "eslint";
import type { SyntaxNode } from "#types/syntax.types";
import { containsAny } from "#core/predicates/filename.predicate";
import { govlabMeta } from "#core/factories/eslint.factory";
import { inHostScope } from "#core/predicates/eslint.predicate";
import { isAlpha } from "@govlab/constants";

const EXCLUDED_FRAGMENTS = ["style-property-helpers.ts"] as const;
const ON_PREFIX_LENGTH = 2;
const ON_PREFIX = "on";

const isOnEventAttribute = function isOnEventAttribute(value: string): boolean {
    if (value.length <= ON_PREFIX_LENGTH) {
        return false;
    }
    if (value.slice(0, ON_PREFIX_LENGTH).toLowerCase() !== ON_PREFIX) {
        return false;
    }
    for (const ch of value.slice(ON_PREFIX_LENGTH)) {
        if (!isAlpha(ch)) {
            return false;
        }
    }
    return true;
};

const setAttributeName = function setAttributeName(call: SyntaxNode): string | null {
    const { callee } = call;
    if (callee?.type !== "MemberExpression" || identName(callee.property) !== "setAttribute") {
        return null;
    }
    const first = call.arguments?.[0];
    return first?.type === "Literal" && typeof first.value === "string" ? first.value : null;
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        if (!inHostScope(context) || containsAny(context.filename, EXCLUDED_FRAGMENTS)) {
            return {};
        }
        const onCall = (node: Rule.Node): void => {
            const call = asNode(node);
            if (call === null) {
                return;
            }
            if (identName(call.callee) === "eval") {
                context.report({ messageId: "unsafeEval", node });
                return;
            }
            const attr = setAttributeName(call);
            if (attr !== null && isOnEventAttribute(attr)) {
                context.report({ data: { attr }, messageId: "inlineHandler", node });
            }
        };
        const handlers: [string, (node: Rule.Node) => void][] = [["CallExpression", onCall]];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["csp"],
        description: 'Forbid inline event handlers (setAttribute("on*", ...)) and eval under strict CSP',
        messages: {
            inlineHandler:
                'Inline event handler detected (setAttribute("{{attr}}", ...)). Bind via EventManager.on() or the DOMFactory Rich API — inline handlers violate strict CSP script-src.',
            unsafeEval:
                "unsafe-eval usage detected (eval(...)). Remove eval and use a safer construct — strict CSP forbids eval.",
        },
        ruleId: "no_inline_events",
    }),
} satisfies Rule.RuleModule;