# core/quality/eslint/require-canonical-concept.eslint.rule.ts

> 72 lines of code and 14 definitions.

Tree: GovLab Quality
Language: typescript
Canonical: https://banes-lab.com/anatomy/quality#file-quality-core-quality-eslint-require-canonical-concept-eslint-rule-ts
Source text: https://banes-lab.com/source/quality/core/quality/eslint/require-canonical-concept.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-wildcard-exports.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/no-wildcard-exports.eslint.rule.ts.md) and before [core/quality/eslint/require-central-config.eslint.rule.ts](https://banes-lab.com/source/quality/core/quality/eslint/require-central-config.eslint.rule.ts.md).

## Definitions

- `propNamed` (lexical_declaration, line 31)
- `reportMissing` (lexical_declaration, line 44)
- `onCall` (lexical_declaration, line 53)
- `hasNonEmptyArray` (lexical_declaration, line 40)
- `create` (method_definition, line 66, exported)
- `MESSAGE` (lexical_declaration, line 3)
- `SELECTORS` (lexical_declaration, line 8)
- `PropValue` (interface_declaration, line 10)
- `PropNode` (interface_declaration, line 15)
- `CallNode` (interface_declaration, line 21)
- `isCallNode` (lexical_declaration, line 27)
- `call` (lexical_declaration, line 54)
- `arg` (lexical_declaration, line 58)
- `listeners` (lexical_declaration, line 67, 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";

const MESSAGE =
    "govlabMeta() is missing '{{field}}'. Every @govlab/* rule must declare meta.docs.description and a non-empty " +
    "meta.docs.canonical (its shared quality concept) so the quality-relations ingest registers it with a concept — " +
    "a concept-less rule breaks the SSOT completeness gate. [require_canonical_concept]";

const SELECTORS = ["CallExpression"];

interface PropValue {
    type?: string;
    elements?: unknown[];
}

interface PropNode {
    type: string;
    key?: { type?: string; name?: string };
    value?: PropValue;
}

interface CallNode {
    type: string;
    callee?: { type?: string; name?: string };
    arguments?: { type?: string; properties?: PropNode[] }[];
}

const isCallNode = function isCallNode(node: unknown): node is CallNode {
    return node !== null && typeof node === "object" && "type" in node;
};

const propNamed = function propNamed(properties: PropNode[], name: string): PropNode | null {
    for (const prop of properties) {
        if (prop.type === "Property" && prop.key?.type === "Identifier" && prop.key.name === name) {
            return prop;
        }
    }
    return null;
};

const hasNonEmptyArray = function hasNonEmptyArray(prop: PropNode | null): boolean {
    return prop?.value?.type === "ArrayExpression" && (prop.value.elements?.length ?? 0) > 0;
};

const reportMissing = function reportMissing(context: Rule.RuleContext, node: Rule.Node, properties: PropNode[]): void {
    if (propNamed(properties, "description") === null) {
        context.report({ data: { field: "description" }, messageId: "missing", node });
    }
    if (!hasNonEmptyArray(propNamed(properties, "canonical"))) {
        context.report({ data: { field: "canonical" }, messageId: "missing", node });
    }
};

const onCall = function onCall(context: Rule.RuleContext, node: Rule.Node): void {
    const call: CallNode | null = isCallNode(node) ? node : null;
    if (call?.callee?.type !== "Identifier" || call.callee.name !== "govlabMeta") {
        return;
    }
    const arg = call.arguments?.[0];
    if (arg?.type !== "ObjectExpression") {
        return;
    }
    reportMissing(context, node, arg.properties ?? []);
};

export default {
    create(context): Rule.RuleListener {
        const listeners: Rule.RuleListener = {};
        for (const selector of SELECTORS) {
            listeners[selector] = (node: Rule.Node): void => {
                onCall(context, node);
            };
        }
        return listeners;
    },
    meta: {
        docs: {
            description: "Require meta.docs.description and a non-empty meta.docs.canonical on every govlabMeta rule",
        },
        messages: { missing: MESSAGE },
        schema: [],
        type: "problem",
    },
} satisfies Rule.RuleModule;
```
