Skip to content

Instantly share code, notes, and snippets.

@kgrz
Last active August 5, 2019 07:29
Show Gist options
  • Save kgrz/fe9c62c43601098ac1862b65dc687bb5 to your computer and use it in GitHub Desktop.
Save kgrz/fe9c62c43601098ac1862b65dc687bb5 to your computer and use it in GitHub Desktop.
// wip
import * as ts from 'typescript';
import { readFileSync, stat } from 'fs';
import { promisify } from 'util';
const statp = promisify(stat);
const filelist = process.argv;
if (!filelist || !filelist.slice(2).length) {
console.error('need atleast one file.');
process.exit(255);
}
type Ctx = {
keys: Array<string>;
filename: string;
};
const traverseFnBody = (body: ts.Block | ts.ConciseBody, ctx: Ctx) => {
function traverseReturnStatementExpression(node?: ts.Node) {
if (!node) return;
// TODO: handle the cases where shorthand property assignment and
// object spread operations are used. In those cases the
// node.properties[0].name won't be available.
if (ts.isObjectLiteralExpression(node)) {
const propsToList = node.properties
.map(prop => {
if (prop.name && 'text' in prop.name) {
return prop.name.text;
} else {
console.error('computed property: ', prop);
}
})
.join(',');
console.log(propsToList);
ctx.keys.push(propsToList);
}
ts.forEachChild(node, traverseReturnStatementExpression);
}
function traverseNode(node: ts.Node) {
if (ts.isReturnStatement(node)) {
traverseReturnStatementExpression(node.expression);
}
ts.forEachChild(node, traverseNode);
}
traverseNode(body);
};
const functionLabel = 'mapStateToProps';
const traverse = (sourceFile: ts.SourceFile, ctx: Ctx) => {
traverseNode(sourceFile);
function traverseNode(node: ts.Node) {
let body: ts.Block | ts.ConciseBody | undefined;
if (ts.isVariableDeclaration(node)) {
if (node.name && ts.isIdentifier(node.name) && node.name.text === functionLabel) {
if (node.initializer && (ts.isFunctionExpression(node.initializer) || ts.isArrowFunction(node.initializer))) {
body = node.initializer.body;
}
}
}
if (ts.isFunctionDeclaration(node)) {
if (node.name && ts.isIdentifier(node.name) && node.name.text === functionLabel) {
body = node.body;
}
}
if (body) {
traverseFnBody(body, ctx);
}
ts.forEachChild(node, traverseNode);
}
};
const promises = filelist.slice(2).map(filename => {
return statp(filename).then((stats: any) => {
if (stats.isDirectory()) return;
const source = ts.createSourceFile(filename, readFileSync(filename).toString(), ts.ScriptTarget.ES2015, true);
const ctx: Ctx = { filename, keys: [] };
traverse(source, ctx);
return ctx;
});
});
Promise.all(promises).then(list => console.log(JSON.stringify(list)));
import * as ts from 'typescript';
import { readFileSync, stat } from 'fs';
import { promisify } from 'util';
const statp = promisify(stat);
const filelist = process.argv;
if (!filelist || !filelist.slice(2).length) {
console.error('need atleast one file.');
process.exit(255);
}
const caseLabel = (node: ts.CaseClause) => {
let labels: string[] = [];
function traverseAccessNode(node: ts.PropertyAccessExpression | ts.ElementAccessExpression) {
const { expression } = node;
if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
traverseAccessNode(expression);
} else if (ts.isIdentifier(expression)) {
labels.push(expression.text);
}
if (ts.isPropertyAccessExpression(node)) {
const { name } = node;
// this check probably is not required at all.
if (ts.isIdentifier(name)) {
labels.push(name.text);
}
} else {
const { argumentExpression } = node;
if ('text' in argumentExpression) {
labels.push(argumentExpression['text']);
}
}
}
if (ts.isIdentifier(node.expression)) {
labels.push(node.expression.text);
} else if (ts.isPropertyAccessExpression(node.expression)) {
traverseAccessNode(node.expression);
}
return labels.join('.');
};
const AsyncAction = /(REQUEST|SUCCESS|ERROR)$/;
let asyncClauses = 0;
const thresholds = [3];
// TODO: Handle two reducers in a single file. Right now this function counts
// the number of case clauses inside each file, which could be higher if there
// are two simple reducers (3 case clauses) in a single file.
//
// TODO: Handle async-only reducers that have multiple case clauses. This might
// mean there are more than 3 case keywords, but the actual blocks associated
// would just switch on the SUCCESS, LOADING and ERROR enums.
const traverse = (sourceFile: ts.SourceFile, filename: string) => {
let caseClauses = 0;
let keys: Array<string> = [];
traverseNode(sourceFile);
function traverseNode(node: ts.Node) {
if (ts.isCaseClause(node)) {
caseClauses++;
const identifier = caseLabel(node);
if (identifier) keys.push(identifier);
}
ts.forEachChild(node, traverseNode);
}
if (thresholds.indexOf(caseClauses) > -1) {
console.log(`${caseClauses} case clauses in ${filename}`);
asyncClauses += keys.filter(key => AsyncAction.test(key)).length;
console.log('async clauses: ', asyncClauses);
}
};
filelist.slice(2).forEach(filename => {
statp(filename).then(stats => {
if (stats.isDirectory()) return;
const source = ts.createSourceFile(filename, readFileSync(filename).toString(), ts.ScriptTarget.ES2015, true);
traverse(source, filename);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment