# codemods/analyzers/specifier.target.analyzer.ts

> 99 lines of code and 26 definitions.

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

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

## Definitions

- `filePartOf` (lexical_declaration, line 14, exported)
- `findingFor` (lexical_declaration, line 59)
- `specifierTargetExists` (lexical_declaration, line 48)
- `relocatedSpecifier` (lexical_declaration, line 28, exported)
- `reasonFor` (lexical_declaration, line 41)
- `findingsInProgram` (lexical_declaration, line 89)
- `CURRENT` (lexical_declaration, line 10)
- `SEPARATOR` (lexical_declaration, line 11)
- `QUERY` (lexical_declaration, line 12)
- `query` (lexical_declaration, line 15, exported)
- `targetCandidates` (lexical_declaration, line 19, exported)
- `name` (lexical_declaration, line 20, exported)
- `bare` (lexical_declaration, line 21, exported)
- `candidate` (lexical_declaration, line 23, exported)
- `folder` (lexical_declaration, line 33, exported)
- `prefix` (lexical_declaration, line 37, exported)
- `specifier` (lexical_declaration, line 64)
- `member` (lexical_declaration, line 68)
- `inMember` (lexical_declaration, line 72)
- `candidates` (lexical_declaration, line 75)
- `[only]` (lexical_declaration, line 76)
- `reason` (lexical_declaration, line 77)
- `program` (lexical_declaration, line 90)
- `collectTargetFindings` (lexical_declaration, line 98, exported)
- `seen` (lexical_declaration, line 99, exported)
- `key` (lexical_declaration, line 101, exported)

## Uses

- [codemods/selectors/program.selector.ts](https://banes-lab.com/source/governance/codemods/selectors/program.selector.ts.md)
- [codemods/strings/codemod.strings.ts](https://banes-lab.com/source/governance/codemods/strings/codemod.strings.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 { ambiguousTarget, missingTarget } from "../strings/codemod.strings.ts";
import { basename, dirname, extname, relative, resolve } from "node:path";
import type { SpecifierFinding } from "../../types/analyzer.types.ts";
import { existsSync } from "node:fs";
import { memberOf } from "./specifier.analyzer.ts";
import { specifierNodes } from "../../shared/selectors/specifier.selector.ts";
import ts from "typescript";

const CURRENT = ".";
const SEPARATOR = "/";
const QUERY = "?";

export const filePartOf = function filePartOf(specifier: string): string {
    const query = specifier.indexOf(QUERY);
    return query === -1 ? specifier : specifier.slice(0, query);
};

export const targetCandidates = function targetCandidates(specifier: string, files: readonly string[]): string[] {
    const name = basename(filePartOf(specifier));
    const bare = extname(name).length === 0;
    return files.filter((file) => {
        const candidate = basename(file);
        return candidate === name || (bare && basename(candidate, extname(candidate)) === name);
    });
};

export const relocatedSpecifier = function relocatedSpecifier(
    fromFile: string,
    specifier: string,
    target: string,
): string {
    const folder = toPosix(relative(dirname(fromFile), dirname(target)));
    if (folder.length === 0) {
        return `${CURRENT}${SEPARATOR}${basename(specifier)}`;
    }
    const prefix = folder.startsWith(CURRENT) ? folder : `${CURRENT}${SEPARATOR}${folder}`;
    return `${prefix}${SEPARATOR}${basename(specifier)}`;
};

const reasonFor = function reasonFor(specifier: string, candidates: readonly string[]): string | null {
    if (candidates.length === 0) {
        return missingTarget(specifier, basename(filePartOf(specifier)));
    }
    return candidates.length > 1 ? ambiguousTarget(specifier, candidates.map(relPath)) : null;
};

const specifierTargetExists = function specifierTargetExists(
    source: ts.SourceFile,
    specifier: string,
    options: ts.CompilerOptions,
): boolean {
    if (existsSync(resolve(dirname(source.fileName), filePartOf(specifier)))) {
        return true;
    }
    return ts.resolveModuleName(specifier, source.fileName, options, ts.sys).resolvedModule !== undefined;
};

const findingFor = function findingFor(
    program: ts.Program,
    source: ts.SourceFile,
    literal: ts.StringLiteral,
): SpecifierFinding | null {
    const specifier = literal.text;
    if (!specifier.startsWith(CURRENT) || specifierTargetExists(source, specifier, program.getCompilerOptions())) {
        return null;
    }
    const member = memberOf(relPath(source.fileName));
    if (member === null) {
        return null;
    }
    const inMember = program
        .getRootFileNames()
        .filter((file) => inRepo(file) && memberOf(relPath(file))?.rel === member.rel);
    const candidates = targetCandidates(specifier, inMember);
    const [only] = candidates;
    const reason = reasonFor(specifier, candidates);
    return {
        end: literal.getEnd() - 1,
        file: relPath(source.fileName),
        from: specifier,
        line: lineOf(source, literal),
        reason,
        start: literal.getStart(source) + 1,
        to: only === undefined || reason !== null ? specifier : relocatedSpecifier(source.fileName, specifier, only),
    };
};

const findingsInProgram = function findingsInProgram(tsconfig: string): SpecifierFinding[] {
    const program = programFor(tsconfig);
    return repoSourceFiles(program).flatMap((source) =>
        specifierNodes(source)
            .map((literal) => findingFor(program, source, literal))
            .filter((finding): finding is SpecifierFinding => finding !== null),
    );
};

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