codemods/analyzers/code-point.analyzer.ts
codemods/analyzers/code-point.analyzer.ts is a file in GovLab Extension Host. 81 lines of code and 19 definitions.
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;
};