# test.rules/codemods/analyzers/field.analyzer.test.ts

> 69 lines of code and 0 definitions.

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

Listed in [test.rules/codemods/analyzers](https://banes-lab.com/api/source/testing/test.rules/codemods/analyzers.md), after [test.rules/codemods/analyzers/code-point.analyzer.test.ts](https://banes-lab.com/source/testing/test.rules/codemods/analyzers/code-point.analyzer.test.ts.md) and before [test.rules/codemods/analyzers/field.reference.analyzer.test.ts](https://banes-lab.com/source/testing/test.rules/codemods/analyzers/field.reference.analyzer.test.ts.md).

## Contained in

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

## Source

```typescript
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { writeCanonicalJson, writeCanonicalText } from "@govlab/canonical-write";
import type { FieldScope } from "@ssot/govlab/types/field.types.ts";
import { join } from "node:path";
import { scopeReach } from "@ssot/govlab/codemods/analyzers/field.analyzer.ts";
import { tmpdir } from "node:os";

const TYPES = [
    "export interface Inner { deep: number; skipped: number }",
    "export interface Carried { kept: string; dropped: string }",
    "export interface Root { read: string; unread: string; nested: Inner[]; spread: Carried }",
    "export interface View { kept: string }",
].join("\n");

const TYPES_FILE = "types.generated.ts";
const READER_FILE = "reader.generated.ts";
const CONFIG_FILE = "tsconfig.generated.json";

const READER = [
    'import type { Root, View } from "./types.generated";',
    "export const use = (root: Root): readonly unknown[] => [",
    "    root.read,",
    "    root.nested.map(({ deep }) => deep),",
    "    ((): View => ({ ...root.spread }))(),",
    "];",
].join("\n");

let root = "";

const scope = function scope(roots: FieldScope["roots"]): FieldScope {
    return {
        label: "probe",
        readerRoots: [join(root, READER_FILE)],
        roots,
        tsconfig: join(root, CONFIG_FILE),
        typeRoots: [join(root, TYPES_FILE)],
    };
};

beforeEach(async () => {
    root = mkdtempSync(join(tmpdir(), "field-"));
    await writeCanonicalText(join(root, TYPES_FILE), TYPES);
    await writeCanonicalText(join(root, READER_FILE), READER);
    await writeCanonicalJson(join(root, CONFIG_FILE), {
        compilerOptions: { module: "esnext", noEmit: true, strict: true, target: "es2022" },
    });
});

afterEach(() => {
    rmSync(root, { force: true, recursive: true });
});

describe("scopeReach", () => {
    it("collects every field reachable from the root and marks the ones a reader reads", () => {
        const reach = scopeReach(scope([{ file: join(root, TYPES_FILE), name: "Root" }]));
        const unread = reach.fields.map((field) => field.key).filter((key) => !reach.read.has(key));
        expect(reach.fields.map((field) => field.key).sort()).toStrictEqual([
            "Carried.dropped",
            "Carried.kept",
            "Inner.deep",
            "Inner.skipped",
            "Root.nested",
            "Root.read",
            "Root.spread",
            "Root.unread",
        ]);
        expect(unread.sort()).toStrictEqual(["Carried.dropped", "Inner.skipped", "Root.unread"]);
        expect(reach.missingRoots).toStrictEqual([]);
    });

    it("names a root the file does not declare, and collects no field from it", () => {
        const gone = { file: join(root, TYPES_FILE), name: "Gone" };
        const reach = scopeReach(scope([gone]));
        expect(reach.missingRoots).toStrictEqual([gone]);
        expect(reach.fields).toStrictEqual([]);
    });
});
```
