# shared/analyzers/specifier.analyzer.ts

> 190 lines of code and 52 definitions.

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

Listed in [shared/analyzers](https://banes-lab.com/api/source/governance/shared/analyzers.md), after [shared/analyzers/sink.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/sink.analyzer.ts.md) and before [shared/analyzers/taxonomy.analyzer.ts](https://banes-lab.com/source/governance/shared/analyzers/taxonomy.analyzer.ts.md).

## Definitions

- `absoluteOf` (lexical_declaration, line 102)
- `applicationEdges` (lexical_declaration, line 136)
- `subpathTarget` (lexical_declaration, line 93)
- `resolve` (lexical_declaration, line 116, exported)
- `sourceFilesUnder` (lexical_declaration, line 120)
- `specifierTarget` (lexical_declaration, line 106, exported)
- `scriptTargets` (lexical_declaration, line 181, exported)
- `gateTargets` (lexical_declaration, line 197, exported)
- `exportPairsOf` (lexical_declaration, line 19)
- `packageTargets` (lexical_declaration, line 38)
- `subpathImportsOf` (lexical_declaration, line 64)
- `applicationImporters` (lexical_declaration, line 148, exported)
- `SOURCE_EXTENSION` (lexical_declaration, line 10)
- `RELATIVE_MARK` (lexical_declaration, line 11)
- `WILDCARD` (lexical_declaration, line 12)
- `SUBPATH_MARK` (lexical_declaration, line 13)
- `TOKEN_SEPARATOR` (lexical_declaration, line 14)
- `IMPORT_MARK` (lexical_declaration, line 15)
- `APPLICATION_ROOT` (lexical_declaration, line 17)
- `{ exports }` (lexical_declaration, line 22)
- `manifest` (lexical_declaration, line 41)
- `{ name }` (lexical_declaration, line 42)
- `pairs` (lexical_declaration, line 46)
- `pair` (lexical_declaration, line 48)
- `PACKAGES` (lexical_declaration, line 56)
- `SubpathImport` (interface_declaration, line 58)
- `{ imports }` (lexical_declaration, line 65)
- `star` (lexical_declaration, line 73)
- `MEMBER_IMPORTS` (lexical_declaration, line 84)
- `memberOf` (lexical_declaration, line 88)
- `owners` (lexical_declaration, line 89)
- `member` (lexical_declaration, line 94)
- `entry` (lexical_declaration, line 95)
- `folder` (lexical_declaration, line 108, exported)
- `parts` (lexical_declaration, line 114, exported)
- `name` (lexical_declaration, line 115, exported)
- `full` (lexical_declaration, line 125)
- `target` (lexical_declaration, line 140)
- `importers` (lexical_declaration, line 149, exported)
- `reachedUnder` (lexical_declaration, line 158, exported)
- `edges` (lexical_declaration, line 159, exported)
- `inside` (lexical_declaration, line 160, exported)
- `start` (lexical_declaration, line 161, exported)
- `reached` (lexical_declaration, line 165, exported)
- `queue` (lexical_declaration, line 166, exported)
- `file` (lexical_declaration, line 168, exported)
- `scriptTokens` (lexical_declaration, line 177)
- `targets` (lexical_declaration, line 182, exported)
- `{ scripts }` (lexical_declaration, line 184, exported)
- `EMPTY_OPTION` (lexical_declaration, line 195)
- `{ stages }` (lexical_declaration, line 198, exported)
- `commands` (lexical_declaration, line 204, exported)

## Uses

- [shared/loaders/manifest.loader.ts](https://banes-lab.com/source/governance/shared/loaders/manifest.loader.ts.md)
- [shared/resolvers/anchor.resolver.ts](https://banes-lab.com/source/governance/shared/resolvers/anchor.resolver.ts.md)

## Source

```typescript
import { PROJECT_ROOT, collapsePath, normalizePath } from "../resolvers/anchor.resolver.ts";
import { WORKSPACE_POSIX, isManifestRecord, manifestAt, memberDirs } from "../loaders/manifest.loader.ts";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { MASTER_EXCLUDE_MARKERS } from "../generated/exclusions.generated.ts";
import { isExcludedPath } from "@govlab/quality/core/matchers/exclusions.matcher.ts";
import path from "node:path";
import { specifiersOf } from "../selectors/specifier.selector.ts";
import { stagesFor } from "@govlab/pipeline/core/factories/plan.factory.ts";

const SOURCE_EXTENSION = ".ts";
const RELATIVE_MARK = ".";
const WILDCARD = "*";
const SUBPATH_MARK = "./";
const TOKEN_SEPARATOR = " ";
const IMPORT_MARK = "#";

const APPLICATION_ROOT = normalizePath(PROJECT_ROOT);

const exportPairsOf = function exportPairsOf(
    manifest: Record<string, unknown>,
): readonly (readonly [string, string])[] {
    const { exports } = manifest;
    if (!isManifestRecord(exports)) {
        return [];
    }
    return Object.entries(exports).flatMap(([key, target]) =>
        typeof target === "string" && key.endsWith(WILDCARD) && target.endsWith(WILDCARD)
            ? [
                  [
                      key.slice(SUBPATH_MARK.length, -WILDCARD.length),
                      target.slice(SUBPATH_MARK.length, -WILDCARD.length),
                  ] as const,
              ]
            : [],
    );
};

const packageTargets = function packageTargets(): ReadonlyMap<string, (sub: string) => string | null> {
    return new Map(
        memberDirs().flatMap((dir) => {
            const manifest = manifestAt(dir);
            const { name } = manifest;
            if (typeof name !== "string") {
                return [];
            }
            const pairs = exportPairsOf(manifest);
            const resolve = (sub: string): string | null => {
                const pair = pairs.find(([key]) => sub.startsWith(key));
                return pair === undefined ? null : `${dir}/${pair[1]}${sub.slice(pair[0].length)}`;
            };
            return [[name, resolve] as const];
        }),
    );
};

const PACKAGES = packageTargets();

interface SubpathImport {
    readonly key: string;
    readonly prefix: string;
    readonly suffix: string;
}

const subpathImportsOf = function subpathImportsOf(manifest: Record<string, unknown>): readonly SubpathImport[] {
    const { imports } = manifest;
    if (!isManifestRecord(imports)) {
        return [];
    }
    return Object.entries(imports).flatMap(([key, target]) => {
        if (typeof target !== "string" || !key.endsWith(WILDCARD) || !target.includes(WILDCARD)) {
            return [];
        }
        const star = target.indexOf(WILDCARD);
        return [
            {
                key: key.slice(0, -WILDCARD.length),
                prefix: target.slice(SUBPATH_MARK.length, star),
                suffix: target.slice(star + WILDCARD.length),
            },
        ];
    });
};

const MEMBER_IMPORTS: ReadonlyMap<string, readonly SubpathImport[]> = new Map(
    memberDirs().map((dir) => [dir, subpathImportsOf(manifestAt(dir))] as const),
);

const memberOf = function memberOf(file: string): string | null {
    const owners = [...MEMBER_IMPORTS.keys()].filter((dir) => file.startsWith(`${dir}/`));
    return owners.reduce<string | null>((best, dir) => (best === null || dir.length > best.length ? dir : best), null);
};

const subpathTarget = function subpathTarget(importer: string, specifier: string): string | null {
    const member = memberOf(importer);
    const entry =
        member === null ? undefined : MEMBER_IMPORTS.get(member)?.find((held) => specifier.startsWith(held.key));
    return member === null || entry === undefined
        ? null
        : `${member}/${entry.prefix}${specifier.slice(entry.key.length)}${entry.suffix}`;
};

const absoluteOf = function absoluteOf(collapsed: string): string {
    return collapsed.startsWith("/") || collapsed.includes(":") ? collapsed : `/${collapsed}`;
};

export const specifierTarget = function specifierTarget(importer: string, specifier: string): string | null {
    if (specifier.startsWith(RELATIVE_MARK)) {
        const folder = importer.slice(0, importer.lastIndexOf("/"));
        return absoluteOf(collapsePath(`${folder}/${specifier}`));
    }
    if (specifier.startsWith(IMPORT_MARK)) {
        return subpathTarget(importer, specifier);
    }
    const parts = specifier.split("/");
    const name = specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? "");
    const resolve = PACKAGES.get(name);
    return resolve === undefined ? null : resolve(specifier.slice(name.length + 1));
};

const sourceFilesUnder = function sourceFilesUnder(dir: string): string[] {
    if (!existsSync(dir)) {
        return [];
    }
    return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
        const full = normalizePath(path.join(dir, entry.name));
        if (isExcludedPath(full.slice(WORKSPACE_POSIX.length + 1), MASTER_EXCLUDE_MARKERS)) {
            return [];
        }
        if (entry.isDirectory()) {
            return sourceFilesUnder(full);
        }
        return entry.name.endsWith(SOURCE_EXTENSION) ? [full] : [];
    });
};

const applicationEdges = function applicationEdges(): ReadonlyMap<string, readonly string[]> {
    const edges = new Map<string, string[]>();
    for (const file of sourceFilesUnder(APPLICATION_ROOT)) {
        const targets = specifiersOf(file, readFileSync(file, "utf8")).flatMap((specifier) => {
            const target = specifierTarget(file, specifier);
            return target === null || target === file ? [] : [target];
        });
        edges.set(file, targets);
    }
    return edges;
};

export const applicationImporters = function applicationImporters(): ReadonlyMap<string, readonly string[]> {
    const importers = new Map<string, string[]>();
    for (const [file, targets] of applicationEdges()) {
        for (const target of targets) {
            importers.set(target, [...(importers.get(target) ?? []), file.slice(APPLICATION_ROOT.length + 1)]);
        }
    }
    return importers;
};

export const reachedUnder = function reachedUnder(root: string, seeds: ReadonlySet<string>): ReadonlySet<string> {
    const edges = applicationEdges();
    const inside = (file: string): boolean => file.startsWith(root);
    const start = [
        ...[...edges].filter(([file]) => !inside(file)).flatMap(([, targets]) => targets.filter(inside)),
        ...[...seeds].filter(inside),
    ];
    const reached = new Set<string>();
    const queue = [...start];
    while (queue.length > 0) {
        const file = queue.pop();
        if (file !== undefined && !reached.has(file)) {
            reached.add(file);
            queue.push(...(edges.get(file) ?? []).filter(inside));
        }
    }
    return reached;
};

const scriptTokens = function scriptTokens(command: string): readonly string[] {
    return command.split(TOKEN_SEPARATOR).filter((token) => token.endsWith(SOURCE_EXTENSION));
};

export const scriptTargets = function scriptTargets(): ReadonlySet<string> {
    const targets = new Set<string>();
    for (const dir of [WORKSPACE_POSIX, ...memberDirs()]) {
        const { scripts } = manifestAt(dir);
        const commands = isManifestRecord(scripts)
            ? Object.values(scripts).filter((value) => typeof value === "string")
            : [];
        for (const token of commands.flatMap(scriptTokens)) {
            targets.add(absoluteOf(collapsePath(`${dir}/${token}`)));
        }
    }
    return targets;
};

const EMPTY_OPTION = "";

export const gateTargets = function gateTargets(): ReadonlySet<string> {
    const { stages } = stagesFor({
        cleanCommentsIgnore: EMPTY_OPTION,
        hexIgnore: EMPTY_OPTION,
        qualityRoot: EMPTY_OPTION,
        scope: new Set<string>(),
    });
    const commands = stages.flatMap((stage) =>
        stage.steps.flatMap((step) => (step.parallel ? step.parallel.map((sub) => sub.run) : [step.run ?? ""])),
    );
    return new Set(
        commands.flatMap(scriptTokens).map((token) => absoluteOf(collapsePath(`${WORKSPACE_POSIX}/${token}`))),
    );
};
```
