# rules/eslint/closure-no-null-managed-instance.eslint.rule.ts

> 84 lines of code and 17 definitions.

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

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-no-module-singleton.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-module-singleton.eslint.rule.ts.md) and before [rules/eslint/closure-no-platform-capability-redefine.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-no-platform-capability-redefine.eslint.rule.ts.md).

## Definitions

- `isNullLiteral` (lexical_declaration, line 45)
- `isExemptFile` (lexical_declaration, line 25)
- `endsWithManagedSuffix` (lexical_declaration, line 30)
- `nullCastTarget` (lexical_declaration, line 49)
- `typeNameOf` (lexical_declaration, line 34)
- `create` (method_definition, line 68, exported)
- `pascal` (lexical_declaration, line 8)
- `MANAGED_TYPE_SUFFIXES` (lexical_declaration, line 15)
- `EXEMPT_BASENAME_SUFFIXES` (lexical_declaration, line 17)
- `basenameOf` (lexical_declaration, line 19)
- `norm` (lexical_declaration, line 20)
- `idx` (lexical_declaration, line 21)
- `basename` (lexical_declaration, line 26)
- `name` (lexical_declaration, line 38)
- `inner` (lexical_declaration, line 57)
- `tSAsExpression` (method_definition, line 73, exported)
- `typeName` (lexical_declaration, line 74, exported)

## Uses

- [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

- [Type Safety](https://banes-lab.com/records/architecture/type-safety.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { isType, nameOf, nodeAt } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { concernTags } from "../../shared/manifests/taxonomy.manifest.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";

const pascal = function pascal(tag: string): string {
    return tag
        .split("-")
        .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
        .join("");
};

const MANAGED_TYPE_SUFFIXES = concernTags().map(pascal);

const EXEMPT_BASENAME_SUFFIXES = [".test.ts", ".spec.ts"];

const basenameOf = function basenameOf(path: string): string {
    const norm = path.split("\\").join("/");
    const idx = norm.lastIndexOf("/");
    return idx === -1 ? norm : norm.slice(idx + 1);
};

const isExemptFile = function isExemptFile(filename: string): boolean {
    const basename = basenameOf(filename);
    return EXEMPT_BASENAME_SUFFIXES.some((suffix) => basename.endsWith(suffix));
};

const endsWithManagedSuffix = function endsWithManagedSuffix(name: string): boolean {
    return MANAGED_TYPE_SUFFIXES.some((suffix) => name.endsWith(suffix));
};

const typeNameOf = function typeNameOf(typeAnnotation: AstNode | null): string {
    if (!isType(typeAnnotation, "TSTypeReference")) {
        return "";
    }
    const name = nodeAt(typeAnnotation, "typeName");
    if (isType(name, "Identifier")) {
        return nameOf(name);
    }
    return isType(name, "TSQualifiedName") ? nameOf(nodeAt(name, "right")) : "";
};

const isNullLiteral = function isNullLiteral(node: AstNode | null): boolean {
    return node?.type === "Literal" && node["value"] === null;
};

const nullCastTarget = function nullCastTarget(node: AstNode): string {
    if (node.type !== "TSAsExpression") {
        return "";
    }
    const typeName = typeNameOf(nodeAt(node, "typeAnnotation"));
    if (typeName === "" || !endsWithManagedSuffix(typeName)) {
        return "";
    }
    const inner = nodeAt(node, "expression");
    if (isNullLiteral(inner)) {
        return typeName;
    }
    if (!isType(inner, "TSAsExpression") || !isType(nodeAt(inner, "typeAnnotation"), "TSUnknownKeyword")) {
        return "";
    }
    return isNullLiteral(nodeAt(inner, "expression")) ? typeName : "";
};

export default {
    create(context: RuleContext): RuleListener {
        if (isExemptFile(context.filename)) {
            return {};
        }
        return listener({
            tSAsExpression(view, node) {
                const typeName = nullCastTarget(view);
                if (typeName !== "") {
                    context.report({ data: { typeName }, messageId: "nullManaged", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:type-safety"] }),
            description:
                "Bans `null as <ManagedType>` and `null as unknown as <ManagedType>` injection. Null in place of a stateful collaborator guarantees a runtime crash the moment any method is called. Type the field as `<Type> | null` and check before access. Which type names count as managed is DERIVED from the declared concern tags, pascal-cased, so a concern added to the taxonomy is covered without a rule edit.",
        },
        messages: {
            nullManaged:
                "Injecting `null` typed as `{{ typeName }}` (a managed/stateful type) bypasses type safety AND will crash at first method call. Type the field as `{{ typeName }} | null` and check before access.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
