# codemods/analyzers/code-point.analyzer.ts

> 81 lines of code and 19 definitions.

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

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

## Definitions

- `receiverReason` (lexical_declaration, line 24)
- `shapeReason` (lexical_declaration, line 38)
- `findingFor` (lexical_declaration, line 48)
- `SOURCE_METHOD` (lexical_declaration, line 5)
- `TARGET_METHOD` (lexical_declaration, line 6)
- `STRING_TYPE` (lexical_declaration, line 7)
- `UNTYPED` (lexical_declaration, line 9)
- `typeNameOf` (lexical_declaration, line 11)
- `type` (lexical_declaration, line 12)
- `accessOf` (lexical_declaration, line 16)
- `{ expression }` (lexical_declaration, line 17)
- `typeName` (lexical_declaration, line 28)
- `access` (lexical_declaration, line 53)
- `[indexArg]` (lexical_declaration, line 57)
- `replacementText` (lexical_declaration, line 70, exported)
- `scanSourceFile` (lexical_declaration, line 74, exported)
- `out` (lexical_declaration, line 78, exported)
- `visit` (lexical_declaration, line 79, exported)
- `finding` (lexical_declaration, line 81, exported)

## Uses

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

## Source

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

const SOURCE_METHOD = "charCodeAt";
const TARGET_METHOD = "codePointAt";
const STRING_TYPE = "string";

const UNTYPED = new Set(["any", "unknown", "error"]);

const typeNameOf = function typeNameOf(checker: ts.TypeChecker, node: ts.Expression): string {
    const type = checker.getTypeAtLocation(node);
    return type.isStringLiteral() ? STRING_TYPE : checker.typeToString(type);
};

const accessOf = function accessOf(call: ts.CallExpression): ts.PropertyAccessExpression | null {
    const { expression } = call;
    if (!ts.isPropertyAccessExpression(expression)) {
        return null;
    }
    return expression.name.text === SOURCE_METHOD ? expression : null;
};

const receiverReason = function receiverReason(
    checker: ts.TypeChecker,
    access: ts.PropertyAccessExpression,
): string | null {
    const typeName = typeNameOf(checker, access.expression);
    if (typeName === STRING_TYPE) {
        return null;
    }
    if (UNTYPED.has(typeName)) {
        return `receiver is typed '${typeName}' — type it as a string before converting`;
    }
    return `receiver is '${typeName}', so '${SOURCE_METHOD}' here is not the String method`;
};

const shapeReason = function shapeReason(call: ts.CallExpression, access: ts.PropertyAccessExpression): string | null {
    if (call.arguments.length !== 1) {
        return `expected exactly one index argument, found ${String(call.arguments.length)}`;
    }
    if (call.questionDotToken ?? access.questionDotToken) {
        return "optional call — the receiver may be nullish";
    }
    return null;
};

const findingFor = function findingFor(
    checker: ts.TypeChecker,
    sourceFile: ts.SourceFile,
    call: ts.CallExpression,
): CodePointFinding | null {
    const access = accessOf(call);
    if (!access) {
        return null;
    }
    const [indexArg] = call.arguments;
    return {
        args: indexArg ? indexArg.getText(sourceFile) : "",
        end: call.getEnd(),
        file: relPath(sourceFile.fileName),
        fileName: sourceFile.fileName,
        line: lineOf(sourceFile, call),
        reason: shapeReason(call, access) ?? receiverReason(checker, access),
        receiver: access.expression.getText(sourceFile),
        start: call.getStart(sourceFile),
    };
};

export const replacementText = function replacementText(finding: CodePointFinding): string {
    return `(${finding.receiver}.${TARGET_METHOD}(${finding.args}) ?? 0)`;
};

export const scanSourceFile = function scanSourceFile(
    checker: ts.TypeChecker,
    sourceFile: ts.SourceFile,
): CodePointFinding[] {
    const out: CodePointFinding[] = [];
    const visit = function visit(node: ts.Node): void {
        if (ts.isCallExpression(node)) {
            const finding = findingFor(checker, sourceFile, node);
            if (finding) {
                out.push(finding);
            }
        }
        ts.forEachChild(node, visit);
    };
    ts.forEachChild(sourceFile, visit);
    return out;
};
```
