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

core/quality/eslint/no-inline-style.eslint.rule.ts is a file in GovLab Quality. 74 lines of code and 16 definitions.

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

interface AstNode {
    type: string;
    name?: string;
    value?: unknown;
    left?: AstNode;
    object?: AstNode;
    property?: AstNode;
    callee?: AstNode;
    arguments?: AstNode[];
}

const STYLE_OWN_PROPS = new Set(["style", "cssText"]);

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 identName = function identName(node: AstNode | undefined): string | null {
    return node?.type === "Identifier" && typeof node.name === "string" ? node.name : null;
};

const assignsStyle = function assignsStyle(left: AstNode | undefined): boolean {
    if (left?.type !== "MemberExpression") {
        return false;
    }
    const prop = identName(left.property);
    if (prop === null) {
        return false;
    }
    if (STYLE_OWN_PROPS.has(prop)) {
        return true;
    }
    return left.object?.type === "MemberExpression" && identName(left.object.property) === "style";
};

const firstArgIsStyle = function firstArgIsStyle(call: AstNode): boolean {
    const first = call.arguments?.[0];
    return first?.type === "Literal" && first.value === "style";
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        if (!inHostScope(context)) {
            return {};
        }
        const onAssign = (node: Rule.Node): void => {
            const assign = asNode(node);
            if (assign !== null && assignsStyle(assign.left)) {
                context.report({ messageId: "inline", node });
            }
        };
        const onCall = (node: Rule.Node): void => {
            const call = asNode(node);
            const callee = call?.callee;
            const prop = callee?.type === "MemberExpression" ? identName(callee.property) : null;
            if (prop === "setProperty" || (prop === "setAttribute" && call !== null && firstArgIsStyle(call))) {
                context.report({ messageId: "inline", node });
            }
        };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["AssignmentExpression", onAssign],
            ["CallExpression", onCall],
        ];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["csp"],
        description: "Forbid scripted inline style (strict CSP + type-cascade runtime-appearance ban)",
        messages: {
            inline: 'Scripted inline style is forbidden (strict CSP). Express appearance via a data-el/data-variant class + a CSS custom property in a stylesheet or constructable sheet — never element.style, .cssText, .setProperty(), or setAttribute("style", ...).',
        },
        ruleId: "no_inline_style",
    }),
} satisfies Rule.RuleModule;