Skip to content
Permalink
e163ea39a8
Go to file
 
 
Cannot retrieve contributors at this time
127 lines (109 sloc) 3.91 KB
import ts from 'typescript';
import { Plugin } from 'ts-migrate-server';
import {
isReactClassComponent,
getReactComponentHeritageType,
getNumComponentsInSourceFile,
} from './utils/react';
import { collectIdentifiers } from './utils/identifiers';
import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText';
type Options = { anyAlias?: string };
const reactClassStatePlugin: Plugin<Options> = {
name: 'react-class-state',
async run({ fileName, sourceFile, options }) {
if (!fileName.endsWith('.tsx')) return undefined;
const updates: SourceTextUpdate[] = [];
const printer = ts.createPrinter();
const reactClassDeclarations = sourceFile.statements
.filter(ts.isClassDeclaration)
.filter(isReactClassComponent);
if (reactClassDeclarations.length === 0) return undefined;
const numComponentsInFile = getNumComponentsInSourceFile(sourceFile);
const usedIdentifiers = collectIdentifiers(sourceFile);
reactClassDeclarations.forEach((classDeclaration) => {
const componentName = (classDeclaration.name && classDeclaration.name.text) || 'Component';
const heritageType = getReactComponentHeritageType(classDeclaration)!;
const heritageTypeArgs = heritageType.typeArguments || [];
const propsType = heritageTypeArgs[0];
const stateType = heritageTypeArgs[1];
const getStateTypeName = () => {
let name = '';
if (propsType && ts.isTypeReferenceNode(propsType) && ts.isIdentifier(propsType.typeName)) {
name = propsType.typeName.text.replace('Props', 'State');
} else if (numComponentsInFile > 1) {
name = `${componentName}State`;
} else {
name = 'State';
}
if (!usedIdentifiers.has(name)) {
return name;
}
// Ensure name is unused.
let i = 1;
while (usedIdentifiers.has(name + i)) {
i += 1;
}
return name + i;
};
if (!stateType && usesState(classDeclaration)) {
const stateTypeName = getStateTypeName();
const anyType =
options.anyAlias != null
? ts.createTypeReferenceNode(options.anyAlias, undefined)
: ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
const newStateType = ts.createTypeAliasDeclaration(
undefined,
undefined,
stateTypeName,
undefined,
anyType,
);
updates.push({
kind: 'insert',
index: classDeclaration.pos,
text: `\n\n${printer.printNode(ts.EmitHint.Unspecified, newStateType, sourceFile)}`,
});
updates.push({
kind: 'replace',
index: heritageType.pos,
length: heritageType.end - heritageType.pos,
text: ` ${printer.printNode(
ts.EmitHint.Unspecified,
ts.updateExpressionWithTypeArguments(
heritageType,
[
propsType || ts.createTypeLiteralNode([]),
ts.createTypeReferenceNode(stateTypeName, undefined),
],
heritageType.expression,
),
sourceFile,
)}`,
});
}
});
return updateSourceText(sourceFile.text, updates);
},
};
export default reactClassStatePlugin;
function usesState(classDeclaration: ts.ClassDeclaration): boolean {
const visitor = (node: ts.Node): boolean | undefined => {
if (
ts.isPropertyAccessExpression(node) &&
node.expression.kind === ts.SyntaxKind.ThisKeyword &&
node.name.text === 'state'
) {
return true;
}
if (
ts.isCallExpression(node) &&
ts.isPropertyAccessExpression(node.expression) &&
node.expression.expression.kind === ts.SyntaxKind.ThisKeyword &&
node.expression.name.text === 'setState'
) {
return true;
}
return ts.forEachChild(node, visitor);
};
return !!ts.forEachChild(classDeclaration, visitor);
}
You can’t perform that action at this time.