# test.rules/shared/analyzers/rule.analyzer.test.ts

> 91 lines of code and 0 definitions.

Tree: Codebase Testing
Language: typescript
Canonical: https://banes-lab.com/anatomy/testing#file-testing-test-rules-shared-analyzers-rule-analyzer-test-ts
Source text: https://banes-lab.com/source/testing/test.rules/shared/analyzers/rule.analyzer.test.ts.txt

Listed in [test.rules/shared/analyzers](https://banes-lab.com/api/source/testing/test.rules/shared/analyzers.md), after [test.rules/shared/analyzers/location.analyzer.test.ts](https://banes-lab.com/source/testing/test.rules/shared/analyzers/location.analyzer.test.ts.md) and before [test.rules/shared/analyzers/segment.analyzer.test.ts](https://banes-lab.com/source/testing/test.rules/shared/analyzers/segment.analyzer.test.ts.md).

## Contained in

- [test.rules/shared/analyzers](https://banes-lab.com/anatomy/testing/folder-testing-test-rules-shared-analyzers.md)

## Source

```typescript
import { asNode, nodeAt, walk } from "@ssot/govlab/shared/selectors/syntax.selector.ts";
import {
    collectRuleFile,
    collectRuleObject,
    declaredIds,
    isNonEmptyString,
    propertiesOf,
    propertyNamed,
    reportedIds,
    textOf,
} from "@ssot/govlab/shared/analyzers/rule.analyzer.ts";
import { describe, expect, it } from "vitest";
import type { AstNode } from "@ssot/govlab/types/syntax.types.ts";
import { Linter } from "eslint";
import { listener } from "@ssot/govlab/shared/factories/listener.factory.ts";

const linter = new Linter();

const programOf = function programOf(code: string): AstNode {
    let program: unknown = null;
    const capture = {
        create() {
            return listener({
                program(_view, node) {
                    program = node;
                },
            });
        },
        meta: { messages: {}, schema: [], type: "problem" as const },
    };
    linter.verify(
        code,
        [
            {
                files: ["**/*.ts"],
                languageOptions: { ecmaVersion: 2025 as const, sourceType: "module" as const },
                plugins: { t: { rules: { r: capture } } },
                rules: { "t/r": "error" as const },
            },
        ],
        "probe.ts",
    );
    const node = asNode(program);
    if (node === null) {
        throw new Error("the capture rule did not observe a Program node");
    }
    return node;
};

const RULE = [
    "export default {",
    "    create(context) { context.report({ messageId: 'used', node: null }); return {}; },",
    "    meta: { docs: { description: 'states the shape' }, messages: { used: 'u', unused: 'x' }, schema: [], type: 'problem' },",
    "};",
].join("\n");

describe("collectRuleFile and collectRuleObject", () => {
    const program = programOf(RULE);
    const file = collectRuleFile(program);

    it("finds the object carrying both meta and create and neither any nor a guard import", () => {
        expect(file.ruleObjects).toHaveLength(1);
        expect(file.anyNodes).toHaveLength(0);
        expect(file.guards).toHaveLength(0);
    });

    it("resolves the meta property and its value", () => {
        const [ruleObject] = file.ruleObjects;
        const shape = collectRuleObject(ruleObject ?? program);
        expect(shape.metaProperty).not.toBeNull();
        expect(shape.meta?.type).toBe("ObjectExpression");
    });

    it("pairs the reported ids against the declared ones", () => {
        expect([...reportedIds(program)]).toStrictEqual(["used"]);
        expect([...declaredIds(file.ruleObjects)].sort((a, b) => a.localeCompare(b))).toStrictEqual(["unused", "used"]);
    });
});

describe("propertiesOf, propertyNamed, textOf and isNonEmptyString", () => {
    const program = programOf(`const o = { a: "x", b: \`\`, c: "" + "y" };`);
    let init: AstNode | null = null;
    walk(program, (node) => {
        if (node.type === "ObjectExpression") {
            init = node;
        }
    });

    const slotOf = function slotOf(name: string, slot: string): AstNode | null {
        return nodeAt(propertyNamed(init, name), slot);
    };

    it("reads properties by name and reports whether their text is non-empty", () => {
        expect(propertiesOf(init)).toHaveLength(3);
        expect(textOf(slotOf("a", "key"))).toBe("a");
        expect(isNonEmptyString(slotOf("a", "value"))).toBe(true);
        expect(isNonEmptyString(slotOf("b", "value"))).toBe(false);
        expect(isNonEmptyString(slotOf("c", "value"))).toBe(true);
        expect(propertyNamed(init, "missing")).toBeNull();
    });
});
```
