# rules/eslint/closure-show-flag-polarity.eslint.rule.ts

> 69 lines of code and 13 definitions.

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

Listed in [rules/eslint](https://banes-lab.com/api/source/governance/rules/eslint.md), after [rules/eslint/closure-rule-shape.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-rule-shape.eslint.rule.ts.md) and before [rules/eslint/closure-standalone-script-imported.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-standalone-script-imported.eslint.rule.ts.md).

## Definitions

- `isLiteralValue` (lexical_declaration, line 19)
- `isExemptFile` (lexical_declaration, line 14)
- `binaryExpression` (method_definition, line 41, exported)
- `logicalExpression` (method_definition, line 50, exported)
- `flaggedName` (lexical_declaration, line 23)
- `create` (method_definition, line 36, exported)
- `EXEMPT_BASENAME_SUFFIXES` (lexical_declaration, line 9)
- `SHOW_VERBS` (lexical_declaration, line 10)
- `NEGATED_OPERATORS` (lexical_declaration, line 11)
- `DEFAULTING_OPERATORS` (lexical_declaration, line 12)
- `basename` (lexical_declaration, line 15)
- `property` (lexical_declaration, line 27)
- `name` (lexical_declaration, line 54, 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

- [Principle of Least Surprise](https://banes-lab.com/records/architecture/principle-of-least-surprise.md)

## Source

```typescript
import type { LocalRule, RuleContext, RuleListener } from "../../types/rule.types.ts";
import { basenameOf, normalizePath } from "../../shared/resolvers/anchor.resolver.ts";
import { isType, nameOf, nodeAt, stringAt } from "../../shared/selectors/syntax.selector.ts";
import type { AstNode } from "../../types/syntax.types.ts";
import { defineCheck } from "@govlab/context/check";
import { listener } from "../../shared/factories/listener.factory.ts";
import { opensWith } from "../../shared/manifests/verb.manifest.ts";

const EXEMPT_BASENAME_SUFFIXES = [".test.ts", ".spec.ts"];
const SHOW_VERBS = ["show"];
const NEGATED_OPERATORS = new Set(["!==", "!="]);
const DEFAULTING_OPERATORS = new Set(["??", "||"]);

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

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

const flaggedName = function flaggedName(left: AstNode | null): string {
    if (!isType(left, "MemberExpression")) {
        return "";
    }
    const property = nodeAt(left, "property");
    if (!isType(property, "Identifier")) {
        return "";
    }
    const name = nameOf(property);
    return opensWith(name, SHOW_VERBS) ? name : "";
};

export default {
    create(context: RuleContext): RuleListener {
        if (isExemptFile(context.filename)) {
            return {};
        }
        return listener({
            binaryExpression(view, node) {
                if (!NEGATED_OPERATORS.has(stringAt(view, "operator"))) {
                    return;
                }
                const name = flaggedName(nodeAt(view, "left"));
                if (name !== "" && isLiteralValue(nodeAt(view, "right"), false)) {
                    context.report({ data: { name }, messageId: "inversePolarity", node });
                }
            },
            logicalExpression(view, node) {
                if (!DEFAULTING_OPERATORS.has(stringAt(view, "operator"))) {
                    return;
                }
                const name = flaggedName(nodeAt(view, "left"));
                if (name !== "" && isLiteralValue(nodeAt(view, "right"), true)) {
                    context.report({ data: { name }, messageId: "inversePolarity", node });
                }
            },
        });
    },
    meta: {
        docs: {
            checks: defineCheck({ detects: [], enforces: ["architecture:principle-of-least-surprise"] }),
            description:
                "`show*` flags must default to HIDE (reader uses `=== true`). Inverse polarity (`!== false`) defaults to SHOW, which is unsafe — adding a new field starts showing across every existing config that doesnt declare it. Either rename to `hide*` (matching the inverse polarity) or invert the reader to `=== true`.",
        },
        messages: {
            inversePolarity:
                "Reader for `{{ name }}` uses `!== false` (defaults to SHOW when undefined). `show*` flags must default to HIDE: use `=== true` (and treat undefined as `not set` = hide). If the field should default to SHOW, rename to `hide*` to match the polarity.",
        },
        schema: [],
        type: "problem",
    },
} satisfies LocalRule;
```
