# codemods/analyzers/identifier.analyzer.ts

> 149 lines of code and 36 definitions.

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

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

## Definitions

- `isUpperAlpha` (lexical_declaration, line 11)
- `visit` (lexical_declaration, line 83)
- `isGenerated` (lexical_declaration, line 37)
- `unaliased` (lexical_declaration, line 61)
- `findingsIn` (lexical_declaration, line 129)
- `isConstantCase` (lexical_declaration, line 15)
- `isConstDeclaration` (lexical_declaration, line 41)
- `shorthandBlocked` (lexical_declaration, line 76)
- `nameTakenInFile` (lexical_declaration, line 114)
- `blockedReason` (lexical_declaration, line 119)
- `isPascalCase` (lexical_declaration, line 26)
- `targetSymbol` (lexical_declaration, line 68)
- `scanProgram` (lexical_declaration, line 159, exported)
- `declarationsOf` (lexical_declaration, line 49)
- `identifiersIn` (lexical_declaration, line 81)
- `collectReferences` (lexical_declaration, line 93)
- `UPPER_A` (lexical_declaration, line 5)
- `UPPER_Z` (lexical_declaration, line 6)
- `DIGIT_0` (lexical_declaration, line 7)
- `DIGIT_9` (lexical_declaration, line 8)
- `UNDERSCORE` (lexical_declaration, line 9)
- `i` (lexical_declaration, line 16)
- `code` (lexical_declaration, line 17)
- `allowed` (lexical_declaration, line 18)
- `camelised` (lexical_declaration, line 33)
- `list` (lexical_declaration, line 42)
- `{ parent }` (lexical_declaration, line 77)
- `referencing` (lexical_declaration, line 99)
- `locations` (lexical_declaration, line 104)
- `locals` (lexical_declaration, line 115)
- `checker` (lexical_declaration, line 134)
- `out` (lexical_declaration, line 135)
- `renameable` (lexical_declaration, line 136)
- `declared` (lexical_declaration, line 140)
- `to` (lexical_declaration, line 144)
- `{ blocked, locations }` (lexical_declaration, line 145)

## Uses

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

## Used by

- [rules/eslint/closure-register-needs-consumer.eslint.rule.ts](https://banes-lab.com/source/governance/rules/eslint/closure-register-needs-consumer.eslint.rule.ts.md)
- [shared/manifests/verb.manifest.ts](https://banes-lab.com/source/governance/shared/manifests/verb.manifest.ts.md)

## Source

```typescript
import type { IdentifierFinding, RenameLocation } from "../../types/analyzer.types.ts";
import { inRepo, lineOf, relPath, toPosix } from "../selectors/program.selector.ts";
import ts from "typescript";

const UPPER_A = 65;
const UPPER_Z = 90;
const DIGIT_0 = 48;
const DIGIT_9 = 57;
const UNDERSCORE = 95;

const isUpperAlpha = function isUpperAlpha(code: number): boolean {
    return code >= UPPER_A && code <= UPPER_Z;
};

const isConstantCase = function isConstantCase(name: string): boolean {
    for (let i = 0; i < name.length; i += 1) {
        const code = name.codePointAt(i) ?? 0;
        const allowed = isUpperAlpha(code) || code === UNDERSCORE || (code >= DIGIT_0 && code <= DIGIT_9);
        if (!allowed) {
            return false;
        }
    }
    return true;
};

const isPascalCase = function isPascalCase(name: string): boolean {
    if (name.length === 0 || !isUpperAlpha(name.codePointAt(0) ?? 0)) {
        return false;
    }
    return !isConstantCase(name);
};

const camelised = function camelised(name: string): string {
    return name.charAt(0).toLowerCase() + name.slice(1);
};

const isGenerated = function isGenerated(fileName: string): boolean {
    return toPosix(fileName).includes(".generated.");
};

const isConstDeclaration = function isConstDeclaration(node: ts.VariableDeclaration): boolean {
    const list = node.parent;
    if (!ts.isVariableDeclarationList(list)) {
        return false;
    }
    return (list.flags & ts.NodeFlags.Const) !== 0;
};

const declarationsOf = function declarationsOf(sourceFile: ts.SourceFile): ts.VariableDeclaration[] {
    const out: ts.VariableDeclaration[] = [];
    const visit = function visit(node: ts.Node): void {
        if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && isConstDeclaration(node)) {
            out.push(node);
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return out;
};

const unaliased = function unaliased(checker: ts.TypeChecker, symbol: ts.Symbol | undefined): ts.Symbol | undefined {
    if (!symbol) {
        return undefined;
    }
    return (symbol.flags & ts.SymbolFlags.Alias) === 0 ? symbol : checker.getAliasedSymbol(symbol);
};

const targetSymbol = function targetSymbol(checker: ts.TypeChecker, node: ts.Identifier): ts.Symbol | undefined {
    const { parent } = node;
    if (ts.isShorthandPropertyAssignment(parent)) {
        return unaliased(checker, checker.getShorthandAssignmentValueSymbol(parent));
    }
    return unaliased(checker, checker.getSymbolAtLocation(node));
};

const shorthandBlocked = function shorthandBlocked(node: ts.Identifier): boolean {
    const { parent } = node;
    return ts.isShorthandPropertyAssignment(parent) || ts.isBindingElement(parent);
};

const identifiersIn = function identifiersIn(sourceFile: ts.SourceFile): ts.Identifier[] {
    const out: ts.Identifier[] = [];
    const visit = function visit(node: ts.Node): void {
        if (ts.isIdentifier(node)) {
            out.push(node);
        }
        ts.forEachChild(node, visit);
    };
    visit(sourceFile);
    return out;
};

const collectReferences = function collectReferences(
    program: ts.Program,
    files: readonly ts.SourceFile[],
    declared: ts.Symbol,
): { blocked: boolean; locations: RenameLocation[] } {
    const checker = program.getTypeChecker();
    const referencing = files.flatMap((sourceFile) =>
        identifiersIn(sourceFile)
            .filter((node) => targetSymbol(checker, node) === declared)
            .map((node) => ({ node, sourceFile })),
    );
    const locations = referencing
        .filter((hit) => !shorthandBlocked(hit.node))
        .map((hit) => ({
            end: hit.node.getEnd(),
            fileName: hit.sourceFile.fileName,
            start: hit.node.getStart(hit.sourceFile),
        }));
    return { blocked: referencing.some((hit) => shorthandBlocked(hit.node)), locations };
};

const nameTakenInFile = function nameTakenInFile(sourceFile: ts.SourceFile, name: string): boolean {
    const locals: unknown = Reflect.get(sourceFile, "locals");
    return locals instanceof Map && locals.has(name);
};

const blockedReason = function blockedReason(blocked: boolean, collides: boolean, to: string): string | null {
    if (collides) {
        return `the file already declares '${to}', so renaming would shadow or collide with an existing binding`;
    }
    if (blocked) {
        return "the name is used in a shorthand property or binding pattern, where renaming would silently change the property key";
    }
    return null;
};

const findingsIn = function findingsIn(
    program: ts.Program,
    files: readonly ts.SourceFile[],
    sourceFile: ts.SourceFile,
): IdentifierFinding[] {
    const checker = program.getTypeChecker();
    const out: IdentifierFinding[] = [];
    const renameable = declarationsOf(sourceFile)
        .map((declaration) => declaration.name)
        .filter((name) => ts.isIdentifier(name) && isPascalCase(name.text));
    for (const name of renameable) {
        const declared = ts.isIdentifier(name) ? targetSymbol(checker, name) : undefined;
        if (declared === undefined || !ts.isIdentifier(name)) {
            continue;
        }
        const to = camelised(name.text);
        const { blocked, locations } = collectReferences(program, files, declared);
        out.push({
            file: relPath(sourceFile.fileName),
            fileName: sourceFile.fileName,
            from: name.text,
            line: lineOf(sourceFile, name),
            locations: locations.filter((location) => inRepo(location.fileName) && !isGenerated(location.fileName)),
            reason: blockedReason(blocked, nameTakenInFile(sourceFile, to), to),
            to,
        });
    }
    return out;
};

export const scanProgram = function scanProgram(
    program: ts.Program,
    files: readonly ts.SourceFile[],
): IdentifierFinding[] {
    return files
        .filter((sourceFile) => !isGenerated(sourceFile.fileName))
        .flatMap((sourceFile) => findingsIn(program, files, sourceFile));
};
```
