# codemods/analyzers/location.analyzer.ts

> 180 lines of code and 42 definitions.

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

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

## Definitions

- `isPathName` (lexical_declaration, line 69)
- `calleeOf` (lexical_declaration, line 74)
- `findingFor` (lexical_declaration, line 130)
- `viaSsot` (lexical_declaration, line 125)
- `stringLiterals` (lexical_declaration, line 159)
- `walk` (lexical_declaration, line 35)
- `visit` (lexical_declaration, line 161)
- `usedAsPath` (lexical_declaration, line 79)
- `findingsInProgram` (lexical_declaration, line 181)
- `findingsInSource` (lexical_declaration, line 175)
- `keyIndex` (lexical_declaration, line 33)
- `SSOT_MODULE` (lexical_declaration, line 16)
- `ROOT_POSIX` (lexical_declaration, line 17)
- `PATH_CALLEES` (lexical_declaration, line 19)
- `PATH_NAME_SUFFIXES` (lexical_declaration, line 31)
- `flat` (lexical_declaration, line 34)
- `KEYS` (lexical_declaration, line 54)
- `matchOf` (lexical_declaration, line 56)
- `posix` (lexical_declaration, line 57)
- `upper` (lexical_declaration, line 70)
- `callee` (lexical_declaration, line 75)
- `index` (lexical_declaration, line 80)
- `node` (lexical_declaration, line 81)
- `isSsotImport` (lexical_declaration, line 101)
- `ssotLocalName` (lexical_declaration, line 108)
- `bindings` (lexical_declaration, line 110)
- `SSOT_CALLEES` (lexical_declaration, line 123)
- `literal` (lexical_declaration, line 131)
- `match` (lexical_declaration, line 132)
- `local` (lexical_declaration, line 136)
- `accessor` (lexical_declaration, line 137)
- `resolves` (lexical_declaration, line 138)
- `positionReason` (lexical_declaration, line 139)
- `blocked` (lexical_declaration, line 142)
- `call` (lexical_declaration, line 145)
- `out` (lexical_declaration, line 160)
- `parent` (lexical_declaration, line 162)
- `isSpecifier` (lexical_declaration, line 163)
- `collectLocationFindings` (lexical_declaration, line 187, exported)
- `seen` (lexical_declaration, line 188, exported)
- `isFirstAtSite` (lexical_declaration, line 189, exported)
- `key` (lexical_declaration, line 190, exported)

## Uses

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

## Source

```typescript
import {
    CODEMOD_TSCONFIGS,
    inRepo,
    lineOf,
    programFor,
    relPath,
    repoSourceFiles,
    toPosix,
} from "../selectors/program.selector.ts";
import type { LiteralSite, LocationFinding } from "../../types/analyzer.types.ts";
import { ROOT, paths } from "@ssot/paths";
import { existsSync } from "node:fs";
import path from "node:path";
import ts from "typescript";

const SSOT_MODULE = "@ssot/paths";
const ROOT_POSIX = toPosix(ROOT);

const PATH_CALLEES = new Set([
    "existsSync",
    "join",
    "mkdirSync",
    "readFileSync",
    "readdirSync",
    "resolve",
    "rmSync",
    "statSync",
    "writeFileSync",
]);

const PATH_NAME_SUFFIXES = ["DIR", "DIRS", "FILE", "FILES", "GLOB", "PATH", "PATHS", "ROOT"];

const keyIndex = function keyIndex(): [string, string][] {
    const flat: [string, string][] = [];
    const walk = function walk(node: unknown, trail: string[]): void {
        if (typeof node === "string") {
            const posix = toPosix(node);
            if (posix.startsWith(`${ROOT_POSIX}/`)) {
                flat.push([trail.join("."), posix.slice(ROOT_POSIX.length + 1)]);
            }
            return;
        }
        if (typeof node !== "object" || node === null) {
            return;
        }
        for (const [key, value] of Object.entries(node)) {
            walk(value, [...trail, key]);
        }
    };
    walk(paths, []);
    return flat.sort((a, b) => b[1].length - a[1].length);
};

const KEYS = keyIndex();

const matchOf = function matchOf(value: string): { key: string; rest: string } | null {
    const posix = toPosix(value);
    for (const [key, token] of KEYS) {
        if (posix === token) {
            return { key, rest: "" };
        }
        if (posix.startsWith(`${token}/`)) {
            return { key, rest: posix.slice(token.length) };
        }
    }
    return null;
};

const isPathName = function isPathName(name: string): boolean {
    const upper = name.toUpperCase();
    return PATH_NAME_SUFFIXES.some((suffix) => upper.endsWith(suffix));
};

const calleeOf = function calleeOf(call: ts.CallExpression): string {
    const callee = call.expression;
    return ts.isPropertyAccessExpression(callee) ? callee.name.text : (ts.isIdentifier(callee) ? callee.text : "");
};

const usedAsPath = function usedAsPath(chain: readonly ts.Node[]): boolean {
    for (let index = chain.length - 1; index >= 0; index -= 1) {
        const node = chain[index];
        if (node === undefined) {
            return false;
        }
        if (ts.isCallExpression(node)) {
            return PATH_CALLEES.has(calleeOf(node));
        }
        if (ts.isVariableDeclaration(node)) {
            return ts.isIdentifier(node.name) && isPathName(node.name.text);
        }
        if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
            return isPathName(node.name.text);
        }
        if (!ts.isArrayLiteralExpression(node) && !ts.isParenthesizedExpression(node)) {
            return false;
        }
    }
    return false;
};

const isSsotImport = function isSsotImport(statement: ts.Statement): statement is ts.ImportDeclaration {
    if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
        return false;
    }
    return statement.moduleSpecifier.text === SSOT_MODULE;
};

const ssotLocalName = function ssotLocalName(source: ts.SourceFile): string | null {
    for (const statement of source.statements.filter(isSsotImport)) {
        const bindings = statement.importClause?.namedBindings;
        if (bindings === undefined || !ts.isNamedImports(bindings)) {
            continue;
        }
        for (const element of bindings.elements) {
            if ((element.propertyName?.text ?? element.name.text) === "rel") {
                return element.name.text;
            }
        }
    }
    return null;
};

const SSOT_CALLEES = new Set(["rel", "resolve", "pathRel"]);

const viaSsot = function viaSsot(chain: readonly ts.Node[]): boolean {
    const parent = chain.at(-1);
    return parent !== undefined && ts.isCallExpression(parent) && SSOT_CALLEES.has(calleeOf(parent));
};

const findingFor = function findingFor(source: ts.SourceFile, site: LiteralSite): LocationFinding | null {
    const literal = site.node;
    const match = matchOf(literal.text);
    if (match === null || viaSsot(site.chain)) {
        return null;
    }
    const local = ssotLocalName(source);
    const accessor = local ?? "rel";
    const resolves = existsSync(path.join(ROOT, literal.text));
    const positionReason = usedAsPath(site.chain)
        ? null
        : `'${literal.text}' spells a workspace location but is not in a path-forming position — confirm whether it is a path, a declaration key, or a fixture before rewriting`;
    const blocked = resolves
        ? positionReason
        : `'${literal.text}' does not exist on disk — it may have been renamed and moved at once, so the intended target must be confirmed by hand`;
    const call = `${accessor}("${match.key}")`;
    return {
        end: literal.getEnd(),
        file: relPath(source.fileName),
        from: literal.text,
        line: lineOf(source, literal),
        localName: accessor,
        needsImport: local === null,
        reason: blocked,
        start: literal.getStart(source),
        to: match.rest.length > 0 ? `\`\${${call}}${match.rest}\`` : call,
    };
};

const stringLiterals = function stringLiterals(source: ts.SourceFile): LiteralSite[] {
    const out: LiteralSite[] = [];
    const visit = function visit(node: ts.Node, chain: ts.Node[]): void {
        const parent = chain.at(-1);
        const isSpecifier = parent !== undefined && (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent));
        if (ts.isStringLiteral(node) && !isSpecifier) {
            out.push({ chain, node });
        }
        ts.forEachChild(node, (child) => {
            visit(child, [...chain, node]);
        });
    };
    visit(source, []);
    return out;
};

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

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

export const collectLocationFindings = function collectLocationFindings(): LocationFinding[] {
    const seen = new Set<string>();
    const isFirstAtSite = function isFirstAtSite(finding: LocationFinding): 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);
};
```
