# rules/eslint/no-redundant-type-shim.eslint.rule.ts

> 151 lines of code and 42 definitions.

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

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/no-raw-file-write.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-raw-file-write.eslint.rule.ts.md) and before [rules/eslint/no-string-literal-ids.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/no-string-literal-ids.eslint.rule.ts.md).

## Definitions

- `isRecord` (lexical_declaration, line 37)
- `declaresValue` (lexical_declaration, line 114)
- `declaresSurface` (lexical_declaration, line 125)
- `typeOfNode` (lexical_declaration, line 106)
- `isNode` (lexical_declaration, line 25)
- `isExternal` (lexical_declaration, line 44)
- `shipsOwnTypes` (lexical_declaration, line 73)
- `onModule` (lexical_declaration, line 137)
- `asNode` (lexical_declaration, line 29)
- `realTypesSource` (lexical_declaration, line 84)
- `AstNode` (interface_declaration, line 10)
- `BlockNode` (interface_declaration, line 18)
- `MESSAGE` (lexical_declaration, line 22)
- `normalize` (lexical_declaration, line 33)
- `LOCAL_PREFIXES` (lexical_declaration, line 41)
- `SPECIFIER_BREAKERS` (lexical_declaration, line 42)
- `packageRoot` (lexical_declaration, line 51)
- `firstSlash` (lexical_declaration, line 53)
- `secondSlash` (lexical_declaration, line 57)
- `slash` (lexical_declaration, line 60)
- `typesPackageName` (lexical_declaration, line 64)
- `PACKAGE_TYPES_ENTRY_HINTS` (lexical_declaration, line 71)
- `pkgJson` (lexical_declaration, line 74)
- `parsed` (lexical_declaration, line 76)
- `dir` (lexical_declaration, line 85)
- `typesPkg` (lexical_declaration, line 86)
- `nm` (lexical_declaration, line 88)
- `parent` (lexical_declaration, line 95)
- `VALUE_DECLS` (lexical_declaration, line 103)
- `SURFACE_EXPORTS` (lexical_declaration, line 104)
- `kind` (lexical_declaration, line 115)
- `block` (lexical_declaration, line 126)
- `statements` (lexical_declaration, line 130)
- `noRedundantTypeShim` (lexical_declaration, line 134)
- `create` (method_definition, line 135)
- `fileDir` (lexical_declaration, line 136)
- `decl` (lexical_declaration, line 138)
- `{ id }` (lexical_declaration, line 142)
- `spec` (lexical_declaration, line 146)
- `pkg` (lexical_declaration, line 150)
- `source` (lexical_declaration, line 151)
- `handlers` (lexical_declaration, line 156)

## Records this file checks

- [Single Source of Truth](https://banes-lab.com/records/architecture/single-source-of-truth.md)

## Source

```typescript
import type { Rule } from "eslint";
import { defineCheck } from "@govlab/context/check";

defineCheck({ detects: [], enforces: ["architecture:single-source-of-truth"] });

import fs from "node:fs";
import { jsonRecordAt } from "../../shared/loaders/manifest.loader.ts";
import path from "node:path";

interface AstNode {
    type: string;
    value?: unknown;
    id?: AstNode;
    body?: BlockNode;
    declaration?: AstNode;
}

interface BlockNode {
    body?: AstNode[];
}

const MESSAGE =
    "'{{pkg}}' is faked by a hand-written `declare module` shim, but real types are available ({{source}}). Delete this declare module: install @types/{{pkg}} if needed, then rely on the package's own typings. A genuinely untyped package may keep a minimal shim; this one is redundant.";

const isNode = function isNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object" && "type" in value;
};

const asNode = function asNode(value: unknown): AstNode | null {
    return isNode(value) ? value : null;
};

const normalize = function normalize(filename: string): string {
    return filename.split("\\").join("/");
};

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

const LOCAL_PREFIXES = new Set([".", "/", "#"]);
const SPECIFIER_BREAKERS = ["*", ":"];

const isExternal = function isExternal(specifier: string): boolean {
    if (specifier.length === 0 || LOCAL_PREFIXES.has(specifier.slice(0, 1))) {
        return false;
    }
    return !SPECIFIER_BREAKERS.some((breaker) => specifier.includes(breaker));
};

const packageRoot = function packageRoot(specifier: string): string {
    if (specifier.startsWith("@")) {
        const firstSlash = specifier.indexOf("/");
        if (firstSlash === -1) {
            return specifier;
        }
        const secondSlash = specifier.indexOf("/", firstSlash + 1);
        return secondSlash === -1 ? specifier : specifier.slice(0, secondSlash);
    }
    const slash = specifier.indexOf("/");
    return slash === -1 ? specifier : specifier.slice(0, slash);
};

const typesPackageName = function typesPackageName(pkg: string): string {
    if (pkg.startsWith("@")) {
        return `@types/${pkg.slice(1).split("/").join("__")}`;
    }
    return `@types/${pkg}`;
};

const PACKAGE_TYPES_ENTRY_HINTS = ["index.d.ts"];

const shipsOwnTypes = function shipsOwnTypes(pkgDir: string): boolean {
    const pkgJson = path.join(pkgDir, "package.json");
    if (fs.existsSync(pkgJson)) {
        const parsed = jsonRecordAt(pkgJson);
        if (typeof parsed["types"] === "string" || typeof parsed["typings"] === "string") {
            return true;
        }
    }
    return PACKAGE_TYPES_ENTRY_HINTS.some((entry) => fs.existsSync(path.join(pkgDir, entry)));
};

const realTypesSource = function realTypesSource(pkg: string, startDir: string): string | null {
    let dir = startDir;
    const typesPkg = typesPackageName(pkg);
    for (;;) {
        const nm = path.join(dir, "node_modules");
        if (shipsOwnTypes(path.join(nm, ...pkg.split("/")))) {
            return `${pkg} ships its own types`;
        }
        if (fs.existsSync(path.join(nm, ...typesPkg.split("/")))) {
            return `${typesPkg} is installed`;
        }
        const parent = path.dirname(dir);
        if (parent === dir) {
            return null;
        }
        dir = parent;
    }
};

const VALUE_DECLS = new Set(["FunctionDeclaration", "TSDeclareFunction", "ClassDeclaration", "VariableDeclaration"]);
const SURFACE_EXPORTS = new Set(["ExportDefaultDeclaration", "TSExportAssignment"]);

const typeOfNode = function typeOfNode(value: unknown): string {
    if (!isRecord(value)) {
        return "";
    }
    const kind = value["type"];
    return typeof kind === "string" ? kind : "";
};

const declaresValue = function declaresValue(statement: unknown): boolean {
    const kind = typeOfNode(statement);
    if (SURFACE_EXPORTS.has(kind) || VALUE_DECLS.has(kind)) {
        return true;
    }
    if (kind !== "ExportNamedDeclaration" || !isRecord(statement)) {
        return false;
    }
    return VALUE_DECLS.has(typeOfNode(statement["declaration"]));
};

const declaresSurface = function declaresSurface(node: AstNode): boolean {
    const block = node.body;
    if (!isRecord(block)) {
        return false;
    }
    const statements = block["body"];
    return Array.isArray(statements) && statements.some((statement: unknown) => declaresValue(statement));
};

const noRedundantTypeShim: Rule.RuleModule = {
    create(context: Rule.RuleContext): Rule.RuleListener {
        const fileDir = path.dirname(normalize(context.filename));
        const onModule = function onModule(node: Rule.Node): void {
            const decl = asNode(node);
            if (decl === null) {
                return;
            }
            const { id } = decl;
            if (!isRecord(id) || id.type !== "Literal" || typeof id.value !== "string") {
                return;
            }
            const spec = id.value;
            if (!isExternal(spec) || !declaresSurface(decl)) {
                return;
            }
            const pkg = packageRoot(spec);
            const source = realTypesSource(pkg, fileDir);
            if (source !== null) {
                context.report({ data: { pkg, source }, messageId: "redundant", node });
            }
        };
        const handlers: [string, (node: Rule.Node) => void][] = [["TSModuleDeclaration", onModule]];
        return Object.fromEntries(handlers);
    },
    meta: {
        docs: {
            description:
                "An ambient module declaration for a package that already ships its own types is deleted, not kept. A package with no available types may keep a minimal shim.",
        },
        messages: { redundant: MESSAGE },
        schema: [],
        type: "problem",
    },
};

export default {
    plugins: { "govlab-deps": { rules: { "no-redundant-type-shim": noRedundantTypeShim } } },
    tool: "eslint",
};
```
