# rules/eslint/closure-rule-shape.eslint.rule.ts

> 151 lines of code and 24 definitions.

Tree: Governance tree
Language: typescript
Canonical: https://banes-lab.com/anatomy/governance#file-governance-rules-eslint-closure-rule-shape-eslint-rule-ts
Source text: https://banes-lab.com/source/governance/rules/eslint/closure-rule-shape.eslint.rule.ts.txt

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-rule-declarations-resolve.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-rule-declarations-resolve.eslint.rule.ts.md) and before [rules/eslint/closure-show-flag-polarity.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-show-flag-polarity.eslint.rule.ts.md).

## Definitions

- `reportMetaDefects` (lexical_declaration, line 56)
- `reportShapeDefects` (lexical_declaration, line 27)
- `isRuleFile` (lexical_declaration, line 22)
- `declaresInDocs` (lexical_declaration, line 51)
- `reportMessageDefects` (lexical_declaration, line 80)
- `program` (method_definition, line 107, exported)
- `declaresAtModule` (lexical_declaration, line 39)
- `create` (method_definition, line 102, exported)
- `docsField` (lexical_declaration, line 46)
- `RULE_TAG` (lexical_declaration, line 18)
- `REQUIRED_META_KEYS` (lexical_declaration, line 19)
- `SEVERITY_KEY` (lexical_declaration, line 20)
- `posix` (lexical_declaration, line 23)
- `payload` (lexical_declaration, line 32)
- `EXPRESSION_STATEMENT` (lexical_declaration, line 37)
- `field` (lexical_declaration, line 47)
- `declaring` (lexical_declaration, line 52)
- `docs` (lexical_declaration, line 67)
- `description` (lexical_declaration, line 68)
- `file` (lexical_declaration, line 108, exported)
- `[first]` (lexical_declaration, line 114, exported)
- `declared` (lexical_declaration, line 115, exported)
- `shape` (lexical_declaration, line 117, exported)
- `anchor` (lexical_declaration, line 126, exported)

## Uses

- [shared/analyzers/rule.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/rule.analyzer.ts.md)
- [shared/factories/listener.factory.ts](https://banes-lab.com/source/governance/shared/factories/listener.factory.ts.md)
- [shared/selectors/syntax.selector.ts](https://banes-lab.com/source/governance/shared/selectors/syntax.selector.ts.md)

## Records this file checks

- [Policy as Code](https://banes-lab.com/records/architecture/policy-as-code.md)

## Source

```typescript
import type { AstNode, SourceLoc } from "../../types/syntax.types.ts";
import type { LocalRule, RuleContext, RuleFileShape, RuleListener } from "../../types/rule.types.ts";
import { RULE_HOST, normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { calleeName, locOf, nodeAt, nodesAt } from "../../shared/selectors/syntax.selector.ts";
import {
    collectRuleFile,
    collectRuleObject,
    declaredIds,
    isNonEmptyString,
    propertyNamed,
    reportedIds,
    textOf,
} from "../../shared/analyzers/rule.analyzer.ts";
import { DECLARING_CALLEE } from "../../shared/analyzers/check.analyzer.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

const RULE_TAG = ".eslint.rule.ts";
const REQUIRED_META_KEYS = ["type", "schema", "messages"];
const SEVERITY_KEY = "severity";

const isRuleFile = function isRuleFile(filename: string): boolean {
    const posix = normalizePath(filename);
    return posix.startsWith(normalizePath(RULE_HOST)) && posix.endsWith(RULE_TAG);
};

const reportShapeDefects = function reportShapeDefects(context: RuleContext, shape: RuleFileShape): void {
    for (const offender of shape.anyNodes) {
        context.report({ loc: locOf(offender), messageId: "explicitAny" });
    }
    for (const guard of shape.guards) {
        const payload = { name: textOf(nodeAt(guard, "imported")) };
        context.report({ data: payload, loc: locOf(guard), messageId: "projectGuard" });
    }
};

const EXPRESSION_STATEMENT = "ExpressionStatement";

const declaresAtModule = function declaresAtModule(program: AstNode): boolean {
    return nodesAt(program, "body").some(
        (statement) =>
            statement.type === EXPRESSION_STATEMENT && calleeName(nodeAt(statement, "expression")) === DECLARING_CALLEE,
    );
};

const docsField = function docsField(docs: AstNode | null, key: string): AstNode | null {
    const field = docs === null ? null : propertyNamed(nodeAt(docs, "value"), key);
    return field === null ? null : nodeAt(field, "value");
};

const declaresInDocs = function declaresInDocs(docs: AstNode | null): boolean {
    const declaring = docsField(docs, "checks");
    return declaring !== null && calleeName(declaring) === DECLARING_CALLEE;
};

const reportMetaDefects = function reportMetaDefects(
    context: RuleContext,
    meta: AstNode,
    anchor: SourceLoc,
    declared: boolean,
): void {
    for (const key of REQUIRED_META_KEYS) {
        if (propertyNamed(meta, key) === null) {
            context.report({ data: { key }, loc: anchor, messageId: "missingMetaKey" });
        }
    }
    const docs = propertyNamed(meta, "docs");
    const description = docsField(docs, "description");
    if (description === null || !isNonEmptyString(description)) {
        context.report({ loc: anchor, messageId: "missingDescription" });
    }
    if (!declared && !declaresInDocs(docs)) {
        context.report({ data: { callee: DECLARING_CALLEE }, loc: anchor, messageId: "missingChecks" });
    }
    if (propertyNamed(meta, SEVERITY_KEY) !== null) {
        context.report({ loc: anchor, messageId: "severityDeclared" });
    }
};

const reportMessageDefects = function reportMessageDefects(
    context: RuleContext,
    declared: ReadonlySet<string>,
    reported: ReadonlySet<string>,
    anchor: SourceLoc,
): void {
    if (declared.size === 0) {
        context.report({ loc: anchor, messageId: "missingMessages" });
    }
    for (const id of reported) {
        if (!declared.has(id)) {
            context.report({ data: { id }, loc: anchor, messageId: "unknownMessageId" });
        }
    }
    for (const id of declared) {
        if (!reported.has(id)) {
            context.report({ data: { id }, loc: anchor, messageId: "unusedMessage" });
        }
    }
};

export default {
    create(context: RuleContext): RuleListener {
        if (!isRuleFile(context.filename)) {
            return {};
        }
        return listener({
            program(view, node) {
                const file = collectRuleFile(view);
                reportShapeDefects(context, file);
                if (file.ruleObjects.length === 0) {
                    context.report({ messageId: "missingRuleObject", node });
                    return;
                }
                const [first] = file.ruleObjects;
                const declared = declaresAtModule(view);
                for (const ruleObject of file.ruleObjects) {
                    const shape = collectRuleObject(ruleObject);
                    const anchor = locOf(shape.metaProperty ?? ruleObject);
                    if (shape.meta === null) {
                        context.report({ loc: anchor, messageId: "missingMeta" });
                        continue;
                    }
                    reportMetaDefects(context, shape.meta, anchor, declared);
                }
                if (first !== undefined) {
                    const anchor = locOf(collectRuleObject(first).metaProperty ?? first);
                    reportMessageDefects(context, declaredIds(file.ruleObjects), reportedIds(view), anchor);
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:policy-as-code"] }),
            description:
                "A rule file declares the whole contract the loader and the generator depend on: a meta block carrying type, schema, messages and a description, plus a create function. Every messageId reported must be declared and every declared message must be reported, so a rule can neither ship a message it never emits nor emit one it never declared. Severity is the generator's to set, scope is the loader's to apply, and an untyped node defeats the typed rule surface — each is refused here rather than left to review.",
            workspaceWide: true,
        },
        messages: {
            explicitAny:
                "`any` in a rule file defeats the typed rule surface. Type the node through the shared rule types, or narrow it where it is read.",
            missingChecks:
                "The rule declares no check: `meta.docs.checks` is missing or not built by `{{ callee }}`, and no module-level `{{ callee }}(...)` statement stands in for it. Declare the ontology records the rule enforces and the anti-patterns it detects, so the ontology can count this rule as their check.",
            missingDescription:
                "`meta.docs.description` is missing or empty. It states the shape the rule enforces, and is the only place that contract is written.",
            missingMessages: "`meta.messages` declares no message. A rule that can report nothing enforces nothing.",
            missingMeta: "This rule declares a `create` function but no `meta` object to describe or register it.",
            missingMetaKey: "`meta.{{ key }}` is missing.",
            missingRuleObject:
                "A rule file must declare at least one object carrying both `meta` and `create`. Neither half enforces anything alone — `meta` with no `create` never runs, and `create` with no `meta` cannot be registered or described.",
            projectGuard:
                "A rule must not import `{{ name }}`. Scope is applied once by the loader, so a guard inside a rule duplicates it and hides the rule from every member the loader would have shown it.",
            severityDeclared:
                "A rule must not declare its own severity. The generator emits it, and every rule registers at error.",
            unknownMessageId: "`{{ id }}` is reported but not declared in `meta.messages`.",
            unusedMessage: "`{{ id }}` is declared in `meta.messages` but never reported.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
