# codemods/analyzers/specifier.analyzer.ts

> 129 lines of code and 28 definitions.

Tree: Governance tree
Language: typescript
Layer: processing
Canonical: https://banes-lab.com/anatomy/governance#file-governance-codemods-analyzers-specifier-analyzer-ts
Source text: https://banes-lab.com/source/governance/codemods/analyzers/specifier.analyzer.ts.txt

Listed in [codemods/analyzers](https://banes-lab.com/api/source/governance/codemods/analyzers.md), after [codemods/analyzers/reference.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/reference.analyzer.ts.md) and before [codemods/analyzers/specifier.target.analyzer.ts](https://banes-lab.com/source/governance/codemods/analyzers/specifier.target.analyzer.ts.md).

## Definitions

- `findingFor` (lexical_declaration, line 84)
- `exactMatch` (lexical_declaration, line 40)
- `patternMatch` (lexical_declaration, line 55)
- `findingsInProgram` (lexical_declaration, line 124)
- `publishedSpecifier` (lexical_declaration, line 73)
- `findingsInSource` (lexical_declaration, line 118)
- `manifestOf` (lexical_declaration, line 19)
- `buildMembers` (lexical_declaration, line 28)
- `ROOT_POSIX` (lexical_declaration, line 17)
- `{ exports, name }` (lexical_declaration, line 20)
- `MEMBERS` (lexical_declaration, line 34)
- `memberOf` (lexical_declaration, line 36, exported)
- `hit` (lexical_declaration, line 45)
- `[key]` (lexical_declaration, line 51)
- `targetPrefix` (lexical_declaration, line 64)
- `keyPrefix` (lexical_declaration, line 66)
- `entries` (lexical_declaration, line 80)
- `specifier` (lexical_declaration, line 85)
- `fileRel` (lexical_declaration, line 89)
- `from` (lexical_declaration, line 90)
- `absolute` (lexical_declaration, line 94)
- `targetRel` (lexical_declaration, line 98)
- `to` (lexical_declaration, line 99)
- `published` (lexical_declaration, line 103)
- `collectSpecifierFindings` (lexical_declaration, line 130, exported)
- `seen` (lexical_declaration, line 131, exported)
- `isFirstAtSite` (lexical_declaration, line 132, exported)
- `key` (lexical_declaration, line 133, exported)

## Uses

- [codemods/selectors/program.selector.ts](https://banes-lab.com/source/governance/codemods/selectors/program.selector.ts.md)
- [shared/loaders/manifest.loader.ts](https://banes-lab.com/source/governance/shared/loaders/manifest.loader.ts.md)
- [shared/selectors/specifier.selector.ts](https://banes-lab.com/source/governance/shared/selectors/specifier.selector.ts.md)

## Source

```typescript
import {
    CODEMOD_TSCONFIGS,
    inRepo,
    lineOf,
    programFor,
    relPath,
    repoSourceFiles,
    toPosix,
} from "../selectors/program.selector.ts";
import type { SpecifierFinding, WorkspaceMember } from "../../types/analyzer.types.ts";
import { isManifestRecord, manifestAt, memberRels } from "../../shared/loaders/manifest.loader.ts";
import { ROOT } from "@ssot/paths";
import path from "node:path";
import { specifierNodes } from "../../shared/selectors/specifier.selector.ts";
import type ts from "typescript";

const ROOT_POSIX = toPosix(ROOT);

const manifestOf = function manifestOf(memberRel: string): WorkspaceMember {
    const { exports, name } = manifestAt(path.join(ROOT, memberRel));
    return {
        exports: isManifestRecord(exports) ? exports : null,
        name: typeof name === "string" ? name : "",
        rel: memberRel,
    };
};

const buildMembers = function buildMembers(): WorkspaceMember[] {
    return memberRels()
        .map(manifestOf)
        .toSorted((a, b) => b.rel.length - a.rel.length);
};

const MEMBERS = buildMembers();

export const memberOf = function memberOf(relative: string): WorkspaceMember | null {
    return MEMBERS.find((member) => relative === member.rel || relative.startsWith(`${member.rel}/`)) ?? null;
};

const exactMatch = function exactMatch(
    member: WorkspaceMember,
    entries: [string, unknown][],
    subpath: string,
): string | null {
    const hit = entries.find(
        ([key, target]) => typeof target === "string" && !key.includes("*") && target === `./${subpath}`,
    );
    if (hit === undefined) {
        return null;
    }
    const [key] = hit;
    return key === "." ? member.name : `${member.name}${key.slice(1)}`;
};

const patternMatch = function patternMatch(
    member: WorkspaceMember,
    entries: [string, unknown][],
    subpath: string,
): string | null {
    for (const [key, target] of entries) {
        if (typeof target !== "string" || !key.includes("*")) {
            continue;
        }
        const targetPrefix = target.slice(2, target.indexOf("*"));
        if (subpath.startsWith(targetPrefix)) {
            const keyPrefix = key.slice(2, key.indexOf("*"));
            return `${member.name}/${keyPrefix}${subpath.slice(targetPrefix.length)}`;
        }
    }
    return null;
};

const publishedSpecifier = function publishedSpecifier(member: WorkspaceMember, subpath: string): string | null {
    if (member.name.length === 0) {
        return null;
    }
    if (member.exports === null) {
        return `${member.name}/${subpath}`;
    }
    const entries = Object.entries(member.exports);
    return exactMatch(member, entries, subpath) ?? patternMatch(member, entries, subpath);
};

const findingFor = function findingFor(source: ts.SourceFile, literal: ts.StringLiteral): SpecifierFinding | null {
    const specifier = literal.text;
    if (!specifier.startsWith(".")) {
        return null;
    }
    const fileRel = relPath(source.fileName);
    const from = memberOf(fileRel);
    if (from === null) {
        return null;
    }
    const absolute = toPosix(path.resolve(path.dirname(source.fileName), specifier));
    if (!absolute.startsWith(`${ROOT_POSIX}/`)) {
        return null;
    }
    const targetRel = absolute.slice(ROOT_POSIX.length + 1);
    const to = memberOf(targetRel);
    if (to === null || to.rel === from.rel) {
        return null;
    }
    const published = publishedSpecifier(to, targetRel.slice(to.rel.length + 1));
    return {
        end: literal.getEnd() - 1,
        file: fileRel,
        from: specifier,
        line: lineOf(source, literal),
        reason:
            published === null
                ? `'${to.name.length > 0 ? to.name : to.rel}' publishes no subpath covering '${targetRel.slice(to.rel.length + 1)}' — add the pattern to its exports map`
                : null,
        start: literal.getStart(source) + 1,
        to: published ?? specifier,
    };
};

const findingsInSource = function findingsInSource(source: ts.SourceFile): SpecifierFinding[] {
    return specifierNodes(source)
        .map((literal) => findingFor(source, literal))
        .filter((finding): finding is SpecifierFinding => finding !== null);
};

const findingsInProgram = function findingsInProgram(tsconfig: string): SpecifierFinding[] {
    return repoSourceFiles(programFor(tsconfig))
        .filter((source) => inRepo(source.fileName))
        .flatMap(findingsInSource);
};

export const collectSpecifierFindings = function collectSpecifierFindings(): SpecifierFinding[] {
    const seen = new Set<string>();
    const isFirstAtSite = function isFirstAtSite(finding: SpecifierFinding): boolean {
        const key = `${finding.file}:${String(finding.start)}`;
        if (seen.has(key)) {
            return false;
        }
        seen.add(key);
        return true;
    };
    return CODEMOD_TSCONFIGS.flatMap(findingsInProgram).filter(isFirstAtSite);
};
```
