# core/quality/eslint/no-sequential-effect-block.eslint.rule.ts

> 117 lines of code and 28 definitions.

Tree: GovLab Quality
Language: typescript
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-quality-eslint-no-sequential-effect-block-eslint-rule-ts
Source text: https://banes-lab.com/source/quality/core/quality/eslint/no-sequential-effect-block.eslint.rule.ts.txt

Listed in [core/quality/eslint](https://banes-lab.com/api/source/quality/core/quality/eslint.md), after [core/quality/eslint/no-regex.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/no-regex.eslint.rule.ts.md) and before [core/quality/eslint/no-shell-argument-injection.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/no-shell-argument-injection.eslint.rule.ts.md).

## Definitions

- `isRecord` (lexical_declaration, line 17)
- `isAstNode` (lexical_declaration, line 19)
- `isRuleNode` (lexical_declaration, line 23)
- `subjectOf` (lexical_declaration, line 49)
- `extendRun` (lexical_declaration, line 54)
- `scanBody` (lexical_declaration, line 78)
- `onHit` (lexical_declaration, line 93, exported)
- `firstArgSubject` (lexical_declaration, line 38)
- `asNode` (lexical_declaration, line 21)
- `runSubject` (lexical_declaration, line 66)
- `visit` (lexical_declaration, line 102, exported)
- `AstNode` (interface_declaration, line 4)
- `OnHit` (type_alias_declaration, line 13)
- `DEFAULT_THRESHOLD` (lexical_declaration, line 15)
- `resolveThreshold` (lexical_declaration, line 25)
- `awaitedCallOf` (lexical_declaration, line 30)
- `first` (lexical_declaration, line 39)
- `call` (lexical_declaration, line 50)
- `end` (lexical_declaration, line 55)
- `next` (lexical_declaration, line 57)
- `subject` (lexical_declaration, line 71)
- `start` (lexical_declaration, line 79)
- `run` (lexical_declaration, line 81)
- `head` (lexical_declaration, line 82)
- `create` (method_definition, line 91, exported)
- `threshold` (lexical_declaration, line 92, exported)
- `body` (lexical_declaration, line 103, exported)
- `handlers` (lexical_declaration, line 108, exported)

## Contained in

- [core/quality/eslint](https://banes-lab.com/anatomy/quality/folder-quality-core-quality-eslint.md)

## Linked from

- [core/quality/eslint](https://banes-lab.com/anatomy/quality/folder-quality-core-quality-eslint.md)

## Source

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

interface AstNode {
    type: string;
    name?: string;
    argument?: AstNode;
    expression?: AstNode;
    arguments?: AstNode[];
    body?: AstNode[];
}

type OnHit = (node: AstNode, subject: string, count: number) => void;

const DEFAULT_THRESHOLD = 3;

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;

const isAstNode = (value: unknown): value is AstNode => isRecord(value) && "type" in value;

const asNode = (value: unknown): AstNode | null => (isAstNode(value) ? value : null);

const isRuleNode = (value: unknown): value is Rule.Node => isRecord(value) && "type" in value;

const resolveThreshold = (raw: unknown): number => {
    const threshold = isRecord(raw) ? raw["threshold"] : DEFAULT_THRESHOLD;
    return typeof threshold === "number" ? threshold : DEFAULT_THRESHOLD;
};

const awaitedCallOf = (stmt: AstNode): AstNode | null => {
    if (stmt.type !== "ExpressionStatement" || stmt.expression?.type !== "AwaitExpression") {
        return null;
    }
    const call = stmt.expression.argument;
    return call?.type === "CallExpression" ? call : null;
};

const firstArgSubject = (call: AstNode): string | null => {
    const first = (call.arguments ?? []).at(0);
    if (!first) {
        return null;
    }
    if (first.type === "Identifier" && typeof first.name === "string") {
        return first.name;
    }
    return first.type === "ThisExpression" ? "this" : null;
};

const subjectOf = (stmt: AstNode): string | null => {
    const call = awaitedCallOf(stmt);
    return call === null ? null : firstArgSubject(call);
};

const extendRun = (body: AstNode[], start: number, subject: string): number => {
    let end = start;
    while (end + 1 < body.length) {
        const next = body[end + 1];
        if (!next || subjectOf(next) !== subject) {
            break;
        }
        end += 1;
    }
    return end;
};

const runSubject = (body: AstNode[], start: number): { end: number; subject: string | null } => {
    const head = body[start];
    if (!head) {
        return { end: start, subject: null };
    }
    const subject = subjectOf(head);
    if (subject === null) {
        return { end: start, subject: null };
    }
    return { end: extendRun(body, start, subject), subject };
};

const scanBody = (body: AstNode[], threshold: number, onHit: OnHit): void => {
    let start = 0;
    while (start < body.length) {
        const run = runSubject(body, start);
        const head = body[start];
        if (head && run.subject !== null && run.end - start + 1 >= threshold) {
            onHit(head, run.subject, threshold);
        }
        start = run.subject === null ? start + 1 : run.end + 1;
    }
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const threshold = resolveThreshold(context.options[0]);
        const onHit: OnHit = (node, subject, count) => {
            if (isRuleNode(node)) {
                context.report({
                    data: { count: String(count), subject, threshold: String(threshold) },
                    messageId: "sequentialEffectBlock",
                    node,
                });
            }
        };
        const visit = (node: Rule.Node): void => {
            const body = asNode(node)?.body;
            if (Array.isArray(body)) {
                scanBody(body, threshold, onHit);
            }
        };
        const handlers: [string, (node: Rule.Node) => void][] = [
            ["BlockStatement", visit],
            ["Program", visit],
            ["StaticBlock", visit],
        ];
        return Object.fromEntries(handlers);
    },
    meta: govlabMeta({
        canonical: ["open-closed"],
        description:
            "Disallow a flat sequential block of effect calls threading a shared subject — mandate a self-registering composable effect pipeline",
        messages: {
            sequentialEffectBlock:
                "This function threads '{{subject}}' through {{count}}+ consecutive awaited effect calls — a flat, hardcoded async pipeline that grows one line per concern and offers no composition, filtering, grouping, instrumentation, or dynamic registration. Replace it with a self-registering effect pipeline: define each effect once (the condition under which it applies, its order, its run), register it, and let a runner compose/filter/order/instrument the registered effects (the defineContextFragment/composeContext + defineTurnField pattern). Adding a concern then means registering an effect, not editing this function.",
        },
        ruleId: "sequential_effect_block",
        schema: [
            {
                additionalProperties: false,
                properties: { threshold: { maximum: 12, minimum: 2, type: "integer" } },
                type: "object",
            },
        ],
    }),
} satisfies Rule.RuleModule;
```
