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

> 60 lines of code and 0 definitions.

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

Listed in [test.rules/codemods/analyzers](https://banes-lab.com/api/source/testing/test.rules/codemods/analyzers.md), before [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).

## Contained in

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

## Source

```typescript
import { collectOverrides, scanSourceFile } from "@ssot/govlab/codemods/analyzers/binding.analyzer.ts";
import { describe, expect, it } from "vitest";
import type { BindingFinding as Finding } from "@ssot/govlab/types/analyzer.types.ts";
import { sep } from "node:path";
import ts from "typescript";

const FILE = `${process.cwd().split(sep).join("/")}/probe.ts`;

interface Analysis {
    findings: Finding[];
    overrides: Map<ts.Symbol, Set<string>>;
}

const analyze = function analyze(code: string): Analysis {
    const source = ts.createSourceFile(FILE, code, ts.ScriptTarget.Latest, true);
    const host: ts.CompilerHost = {
        fileExists: (name) => name === FILE,
        getCanonicalFileName: (name) => name,
        getCurrentDirectory: () => process.cwd(),
        getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
        getNewLine: () => "\n",
        getSourceFile: (name) => (name === FILE ? source : undefined),
        readFile: (name) => (name === FILE ? code : undefined),
        useCaseSensitiveFileNames: () => true,
        writeFile: () => {},
    };
    const program = ts.createProgram({ host, options: {}, rootNames: [FILE] });
    const checker = program.getTypeChecker();
    const resolved = program.getSourceFile(FILE) ?? source;
    const overrides = collectOverrides(checker, [resolved]);
    return { findings: scanSourceFile(checker, resolved, overrides), overrides };
};

const CALLABLE_CONTRACT = "interface Runner { run: () => void; }";

describe("scanSourceFile", () => {
    it("finds a callable contract field satisfied by a method", () => {
        const { findings } = analyze(`${CALLABLE_CONTRACT}\nclass Job implements Runner { run() {} }`);
        expect(findings).toHaveLength(1);
        expect(findings[0]?.member).toBe("run");
    });

    it("accepts a class that already binds the field as a property", () => {
        const code = `${CALLABLE_CONTRACT}\nclass Job implements Runner { run = () => {}; }`;
        expect(analyze(code).findings).toStrictEqual([]);
    });

    it("ignores a contract field that is not callable", () => {
        const code = "interface Named { id: string; }\nclass Job implements Named { id = 'a'; }";
        expect(analyze(code).findings).toStrictEqual([]);
    });

    it("ignores a class that implements nothing", () => {
        expect(analyze("class Job { run() {} }").findings).toStrictEqual([]);
    });

    it("finds nothing in source with no class", () => {
        expect(analyze("export const a = 1;").findings).toStrictEqual([]);
    });
});

describe("collectOverrides", () => {
    it("returns a map, empty where no class overrides an inherited member", () => {
        expect(analyze("class Job { run() {} }").overrides.size).toBe(0);
    });

    it("records a member a subclass overrides, which blocks a naive rewrite of the base", () => {
        const code = "class Base { run() {} }\nclass Leaf extends Base { run() {} }";
        expect(analyze(code).overrides.size).toBeGreaterThan(0);
    });
});
```
