ts-transformer-properties-rename
Version:
A TypeScript custom transformer which renames internal properties of the project
416 lines (415 loc) • 21.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = propertiesRenameTransformer;
const ts = require("typescript");
const exports_symbol_tree_1 = require("./exports-symbol-tree");
const typescript_helpers_1 = require("./typescript-helpers");
const defaultOptions = {
entrySourceFiles: [],
privatePrefix: '_private_',
internalPrefix: '_internal_',
publicJSDocTag: 'public',
ignoreDecorated: false,
};
// eslint-disable-next-line import/no-default-export
function propertiesRenameTransformer(program, config) {
return createTransformerFactory(program, config);
}
function createTransformerFactory(program, options) {
const fullOptions = { ...defaultOptions, ...options };
const typeChecker = program.getTypeChecker();
const exportsSymbolTree = new exports_symbol_tree_1.ExportsSymbolTree(program, fullOptions.entrySourceFiles);
const cache = new Map();
function putToCache(nodeSymbol, val) {
cache.set(nodeSymbol, val);
return val;
}
return (context) => {
function transformNodeAndChildren(node, ctx) {
return ts.visitEachChild(transformNode(node), (childNode) => transformNodeAndChildren(childNode, ctx), ctx);
}
// eslint-disable-next-line complexity
function transformNode(node) {
// const a = { node }
if (ts.isShorthandPropertyAssignment(node)) {
return handleShorthandPropertyAssignment(node);
}
// const { node } = obj;
if (ts.isBindingElement(node) && node.propertyName === undefined && ts.isObjectBindingPattern(node.parent)) {
return handleShorthandObjectBindingElement(node);
}
// 'node' in obj
if (ts.isStringLiteral(node) && ts.isBinaryExpression(node.parent) && node.parent.operatorToken.kind === ts.SyntaxKind.InKeyword) {
return handleInKeyword(node);
}
if (ts.isIdentifier(node) && node.parent) {
// obj.node
if (ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) {
return handlePropertyAccessIdentifier(node);
}
// private node
// public node()
if ((0, typescript_helpers_1.isClassMember)(node.parent) && node.parent.name === node) {
return handleClassMember(node);
}
// enum Enum { node }
if (ts.isEnumMember(node.parent) && node.parent.name === node) {
return handleEnumMember(node);
}
// const a = { node: 123 }
if (ts.isPropertyAssignment(node.parent) && node.parent.name === node) {
return handlePropertyAssignment(node);
}
// const { node: localName } = obj;
if (ts.isBindingElement(node.parent) && node.parent.propertyName === node) {
return handleBindingElement(node);
}
// <Comp node={...} />
if (ts.isJsxAttribute(node.parent) && node.parent.name === node) {
return handleJsxAttributeName(node);
}
// constructor(public node: string) { // <--- this
// console.log(node); // <--- and this
// }
if ((0, typescript_helpers_1.isConstructorParameter)(node.parent) && node.parent.name === node
|| isConstructorParameterReference(node)) {
return handleCtorParameter(node);
}
}
// obj['fooBar']
if (ts.isStringLiteral(node)
&& ts.isElementAccessExpression(node.parent)
&& node.parent.argumentExpression === node) {
return handleElementAccessExpression(node);
}
return node;
}
function handleInKeyword(node) {
const parent = node.parent;
const nodeType = typeChecker.getTypeAtLocation(node);
if (!nodeType.isStringLiteral()) {
throw new Error(`Can't get type for left expression in ${node.parent.getText()}`);
}
const propertyName = nodeType.value;
if (isTypePropertyExternal(typeChecker.getTypeAtLocation(parent.right), propertyName)) {
return node;
}
return createNewNode(propertyName, 0 /* VisibilityType.Internal */, context.factory.createStringLiteral);
}
// obj.node
function handlePropertyAccessIdentifier(node) {
return createNewIdentifier(node);
}
// obj['fooBar']
function handleElementAccessExpression(node) {
const visibilityType = getNodeVisibilityType(node);
if (visibilityType === 2 /* VisibilityType.External */) {
return node;
}
return createNewNodeFromProperty(node, visibilityType, context.factory.createStringLiteral);
}
// private node
// public node()
function handleClassMember(node) {
return createNewIdentifier(node);
}
// enum Enum { node }
function handleEnumMember(node) {
return createNewIdentifier(node);
}
// const { node: localName } = obj;
function handleBindingElement(node) {
return createNewIdentifier(node);
}
// <Comp node={...} />
function handleJsxAttributeName(node) {
return createNewIdentifier(node);
}
// const a = { node: 123 }
function handlePropertyAssignment(node) {
return createNewIdentifier(node);
}
// const a = { node }
function handleShorthandPropertyAssignment(node) {
const visibilityType = getNodeVisibilityType(node.name);
if (visibilityType === 2 /* VisibilityType.External */) {
return node;
}
return createNewNodeFromProperty(node.name, visibilityType, (newName) => {
return context.factory.createPropertyAssignment(newName, node.name);
});
}
// const { node } = obj;
function handleShorthandObjectBindingElement(node) {
if (!ts.isIdentifier(node.name)) {
return node;
}
const visibilityType = getNodeVisibilityType(node);
if (visibilityType === 2 /* VisibilityType.External */) {
return node;
}
return createNewNodeFromProperty(node.name, visibilityType, (newName) => {
return context.factory.createBindingElement(node.dotDotDotToken, newName, node.name, node.initializer);
});
}
// constructor(public node: string) { // <--- this
// console.log(node); // <--- and this
// }
function handleCtorParameter(node) {
return createNewIdentifier(node);
}
function createNewIdentifier(oldIdentifier) {
const visibilityType = getNodeVisibilityType(oldIdentifier);
if (visibilityType === 2 /* VisibilityType.External */) {
return oldIdentifier;
}
return createNewNodeFromProperty(oldIdentifier, visibilityType, context.factory.createIdentifier);
}
function createNewNodeFromProperty(oldProperty, type, createNode) {
const symbol = typeChecker.getSymbolAtLocation(oldProperty);
if (symbol === undefined) {
throw new Error(`Cannot get symbol for node "${oldProperty.getText()}"`);
}
const oldPropertyName = ts.unescapeLeadingUnderscores(symbol.escapedName);
return createNewNode(oldPropertyName, type, createNode);
}
function createNewNode(oldPropertyName, type, createNode) {
const newPropertyName = getNewName(oldPropertyName, type);
return createNode(newPropertyName);
}
function getNewName(originalName, type) {
return `${type === 1 /* VisibilityType.Private */ ? fullOptions.privatePrefix : fullOptions.internalPrefix}${originalName}`;
}
function getActualSymbol(symbol) {
if (symbol.flags & ts.SymbolFlags.Alias) {
symbol = typeChecker.getAliasedSymbol(symbol);
}
return symbol;
}
function getNodeSymbol(node) {
const symbol = typeChecker.getSymbolAtLocation(node);
if (symbol === undefined) {
return null;
}
return getActualSymbol(symbol);
}
// eslint-disable-next-line complexity
function isTypePropertyExternal(type, typePropertyName) {
// if a type is unknown or any - they should be interpret as a public ones
if (type.flags & ts.TypeFlags.Unknown || type.flags & ts.TypeFlags.Any) {
return true;
}
if (type.flags & ts.TypeFlags.IndexedAccess) {
return isTypePropertyExternal(typeChecker.getApparentType(type), typePropertyName);
}
const symbol = type.getSymbol();
const propertySymbol = typeChecker.getPropertyOfType(type, typePropertyName);
if (type.flags & ts.TypeFlags.Object) {
const objectType = type;
// treat any tuple property as "external"
if (objectType.objectFlags & ts.ObjectFlags.Tuple) {
return true;
}
if (objectType.objectFlags & ts.ObjectFlags.Reference) {
const target = objectType.target;
if (target !== objectType && isTypePropertyExternal(target, typePropertyName)) {
return true;
}
}
// in case when we can't get where a property come from in mapped types
// let's check the whole type explicitly
// thus in case of when property doesn't have a declaration let's treat any property of a mapped type as "external" if its parent type is external
// e.g. Readonly<Foo>.field will look for `field` in _Foo_ type (not in Readonly<Foo>), but { [K in 'foo' | 'bar']: any } won't
// perhaps it would be awesome to handle exactly property we have, but ¯\_(ツ)_/¯
const propertyHasDeclarations = propertySymbol !== undefined ? (0, typescript_helpers_1.getDeclarationsForSymbol)(propertySymbol).length !== 0 : false;
if ((objectType.objectFlags & ts.ObjectFlags.Mapped) && symbol !== undefined && !propertyHasDeclarations && getSymbolVisibilityType(symbol) === 2 /* VisibilityType.External */) {
return true;
}
}
if (type.isUnionOrIntersection()) {
const hasExternalSubType = type.types.some((t) => isTypePropertyExternal(t, typePropertyName));
if (hasExternalSubType) {
return hasExternalSubType;
}
}
if (symbol !== undefined) {
const declarations = (0, typescript_helpers_1.getDeclarationsForSymbol)(symbol);
for (const declaration of declarations) {
if ((ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && declaration.heritageClauses !== undefined) {
const hasHeritageClausesExternals = declaration.heritageClauses.some((clause) => {
return clause.types.some((expr) => {
return isTypePropertyExternal(typeChecker.getTypeAtLocation(expr), typePropertyName);
});
});
if (hasHeritageClausesExternals) {
return true;
}
}
}
}
if (propertySymbol === undefined) {
return false;
}
return [propertySymbol, ...(0, typescript_helpers_1.splitTransientSymbol)(propertySymbol, typeChecker)]
.some((sym) => getSymbolVisibilityType(sym) === 2 /* VisibilityType.External */);
}
// eslint-disable-next-line complexity
function getNodeVisibilityType(node) {
if (ts.isPropertyAssignment(node.parent) || ts.isShorthandPropertyAssignment(node.parent)) {
let expressionToGetTypeFrom = node.parent.parent;
let lastKnownExpression = node.parent.parent;
let currentNode = node.parent.parent.parent;
while (ts.isParenthesizedExpression(currentNode) || ts.isAsExpression(currentNode)) {
if (ts.isAsExpression(currentNode)) {
expressionToGetTypeFrom = lastKnownExpression;
lastKnownExpression = currentNode;
}
currentNode = currentNode.parent;
}
// to get correct contextual type we need to provide the previous last expression rather than the last one
const type = typeChecker.getContextualType(expressionToGetTypeFrom);
if (type !== undefined && isTypePropertyExternal(type, node.getText())) {
return 2 /* VisibilityType.External */;
}
}
if (ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent)) {
if (ts.isIdentifier(node.parent.expression)) {
const expressionSymbol = typeChecker.getSymbolAtLocation(node.parent.expression);
if (expressionSymbol !== undefined) {
// import * as foo from '...';
// foo.node;
// foo['node'];
// or
// namespace Foo { ... }
// Foo.node
// Foo['node'];
const isModuleOrStarImport = (0, typescript_helpers_1.getDeclarationsForSymbol)(expressionSymbol)
.some((decl) => ts.isNamespaceImport(decl) || ts.isModuleDeclaration(decl));
if (isModuleOrStarImport) {
// treat accessing to import-star or namespace as external one
// because we can't rename them yet
return 2 /* VisibilityType.External */;
}
}
}
if (isTypePropertyExternal(typeChecker.getTypeAtLocation(node.parent.expression), node.getText())) {
return 2 /* VisibilityType.External */;
}
}
// shorthand binding element
// const { node } = obj;
if (ts.isBindingElement(node) && isTypePropertyExternal(typeChecker.getTypeAtLocation(node.parent), node.getText())) {
return 2 /* VisibilityType.External */;
}
// full binding element
// const { node: propName } = obj;
if (ts.isBindingElement(node.parent) && isTypePropertyExternal(typeChecker.getTypeAtLocation(node.parent.parent), node.getText())) {
return 2 /* VisibilityType.External */;
}
// <Comp node={...} />
if (ts.isJsxAttribute(node.parent)) {
const jsxTagSymbol = typeChecker.getSymbolAtLocation(node.parent.parent.parent.tagName);
if (jsxTagSymbol !== undefined && jsxTagSymbol.valueDeclaration !== undefined) {
const jsxPropsType = typeChecker.getTypeOfSymbolAtLocation(jsxTagSymbol, jsxTagSymbol.valueDeclaration);
if (isTypePropertyExternal(jsxPropsType, node.getText())) {
return 2 /* VisibilityType.External */;
}
}
}
const nodeSymbol = getNodeSymbol(node);
const classOfMember = nodeSymbol !== null ? (0, typescript_helpers_1.getClassOfMemberSymbol)(nodeSymbol) : null;
if (classOfMember !== null && isTypePropertyExternal(typeChecker.getTypeAtLocation(classOfMember), node.getText())) {
return 2 /* VisibilityType.External */;
}
const symbol = ts.isBindingElement(node) ? getShorthandObjectBindingElementSymbol(node) : nodeSymbol;
if (symbol === null) {
return 2 /* VisibilityType.External */;
}
return getSymbolVisibilityType(symbol);
}
// eslint-disable-next-line complexity
function getSymbolVisibilityType(nodeSymbol) {
nodeSymbol = getActualSymbol(nodeSymbol);
const cachedValue = cache.get(nodeSymbol);
if (cachedValue !== undefined) {
return cachedValue;
}
const symbolDeclarations = (0, typescript_helpers_1.getDeclarationsForSymbol)(nodeSymbol);
if (symbolDeclarations.some(isDeclarationFromExternals)) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
if (symbolDeclarations.some((decl) => (0, typescript_helpers_1.hasModifier)(decl, ts.SyntaxKind.DeclareKeyword))) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
if (nodeSymbol.escapedName === 'prototype') {
// accessing to prototype
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
if (fullOptions.publicJSDocTag.length !== 0 || fullOptions.ignoreDecorated) {
for (const declaration of symbolDeclarations) {
let currentNode = declaration;
while (!ts.isSourceFile(currentNode)) {
if (fullOptions.publicJSDocTag.length !== 0
&& (0, typescript_helpers_1.getNodeJSDocComment)(currentNode).includes(`@${fullOptions.publicJSDocTag}`)) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
if (fullOptions.ignoreDecorated && (0, typescript_helpers_1.hasDecorators)(currentNode)) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
currentNode = currentNode.parent;
}
}
}
if ((0, typescript_helpers_1.isPrivateClassMember)(nodeSymbol)) {
return putToCache(nodeSymbol, 1 /* VisibilityType.Private */);
}
if (exportsSymbolTree.isSymbolAccessibleFromExports(nodeSymbol)) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
for (const declaration of symbolDeclarations) {
if (!(0, typescript_helpers_1.isNodeNamedDeclaration)(declaration.parent) || declaration.parent.name === undefined) {
continue;
}
const parentSymbol = getNodeSymbol(declaration.parent.name);
if (parentSymbol === null) {
continue;
}
if (getSymbolVisibilityType(parentSymbol) === 2 /* VisibilityType.External */) {
return putToCache(nodeSymbol, 2 /* VisibilityType.External */);
}
}
return putToCache(nodeSymbol, 0 /* VisibilityType.Internal */);
}
function getShorthandObjectBindingElementSymbol(element) {
if (element.propertyName !== undefined) {
throw new Error(`Cannot handle binding element with property name: ${element.getText()} in ${element.getSourceFile().fileName}`);
}
if (!ts.isIdentifier(element.name)) {
return null;
}
// if no property name is set (const { a } = foo)
// then node.propertyName is undefined and we need to find this property ourselves
// so let's use go-to-definition algorithm from TSServer
// see https://github.com/microsoft/TypeScript/blob/672b0e3e16ad18b422dbe0cec5a98fce49881b76/src/services/goToDefinition.ts#L58-L77
const type = typeChecker.getTypeAtLocation(element.parent);
if (type.isUnion()) {
return null;
}
return type.getProperty(ts.idText(element.name)) || null;
}
function isDeclarationFromExternals(declaration) {
const sourceFile = declaration.getSourceFile();
// all declarations from declaration source files are external by default
return sourceFile.isDeclarationFile
|| program.isSourceFileDefaultLibrary(sourceFile)
|| /[\\/]node_modules[\\/]/.test(sourceFile.fileName);
}
function isConstructorParameterReference(node) {
if (!ts.isIdentifier(node)) {
return false;
}
return (0, typescript_helpers_1.isSymbolClassMember)(typeChecker.getSymbolAtLocation(node));
}
return (sourceFile) => transformNodeAndChildren(sourceFile, context);
};
}