Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions scripts/analyze_presenters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env ts-node

/**
* Presenter VM Property Analyzer
*
* This script analyzes presenter files (*.presenter.tsx or *.presenter.ts) to identify
* properties in the VM method that are simple repeats of other properties or props.
*
* Usage:
* npx ts-node scripts/analyze_presenters.ts [directory]
*
* If no directory is specified, it will search in the current directory.
*/

import * as fs from "fs";
import * as path from "path";
import * as glob from "glob";
import * as ts from "typescript";

interface PropertyMapping {
name: string;
value: string;
location: {
file: string;
line: number;
column: number;
};
isRepeat: boolean;
repeatedFrom?: string;
}

// Use a more efficient glob pattern with ignore patterns
function findPresenterFiles(directory: string): string[] {
// Add common directories to ignore
const ignorePatterns = [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.git/**",
];

return glob.sync(`${directory}/**/*.presenter.{ts,tsx}`, {
ignore: ignorePatterns,
});
}

function analyzeVmMethod(
sourceFile: ts.SourceFile,
filePath: string,
): PropertyMapping[] {
const propertyMappings: PropertyMapping[] = [];
const propNames = new Set<string>();

// Optimize by combining the two passes into a single traversal
function visit(node: ts.Node) {
// Collect props from parameters
if (
ts.isParameter(node) &&
node.name &&
ts.isObjectBindingPattern(node.name)
) {
node.name.elements.forEach((element) => {
if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
propNames.add(element.name.text);
}
});
}

// Find VM method and analyze
if (
ts.isMethodDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.name.text === "vm" &&
node.body
) {
// Process the VM method body
ts.forEachChild(node.body, findReturnStatement);
// Skip further traversal for this branch
return;
}

// Continue traversal
ts.forEachChild(node, visit);
}

function findReturnStatement(node: ts.Node) {
if (
ts.isReturnStatement(node) &&
node.expression &&
ts.isObjectLiteralExpression(node.expression)
) {
analyzeReturnedObject(node.expression);
} else {
ts.forEachChild(node, findReturnStatement);
}
}

function analyzeReturnedObject(objectLiteral: ts.ObjectLiteralExpression) {
const properties = objectLiteral.properties;
const valueMap = new Map<string, string>();

// Process all properties in a single pass
for (const prop of properties) {
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
const propName = prop.name.text;
const propValue = prop.initializer.getText();
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
prop.getStart(),
);

valueMap.set(propName, propValue);

const mapping: PropertyMapping = {
name: propName,
value: propValue,
location: {
file: filePath,
line: line + 1,
column: character + 1,
},
isRepeat: false,
};

// Check if this property is a simple repeat of a prop
if (propNames.has(propValue)) {
mapping.isRepeat = true;
mapping.repeatedFrom = propValue;
}

propertyMappings.push(mapping);
}
}

// Second pass to find properties that repeat other properties
// This can't be combined with the first pass because we need all properties first
for (const mapping of propertyMappings) {
if (!mapping.isRepeat && valueMap.has(mapping.value)) {
mapping.isRepeat = true;
mapping.repeatedFrom = mapping.value;
}
}
}

// Start analysis with a single traversal
visit(sourceFile);

return propertyMappings;
}

function analyzeFile(filePath: string): PropertyMapping[] {
// Cache file content to avoid multiple reads
const fileContent = fs.readFileSync(filePath, "utf-8");

// Use a more efficient parsing strategy
const sourceFile = ts.createSourceFile(
filePath,
fileContent,
ts.ScriptTarget.Latest,
true,
);

return analyzeVmMethod(sourceFile, filePath);
}

function formatResults(
results: { file: string; properties: PropertyMapping[] }[],
): string {
let output = "";

results.forEach((result) => {
const repeatedProps = result.properties.filter((prop) => prop.isRepeat);

if (repeatedProps.length > 0) {
output += `\nFile: ${result.file}\n`;
output += "=".repeat(result.file.length + 6) + "\n";

repeatedProps.forEach((prop) => {
output += ` - ${prop.name}: ${prop.value} (line ${prop.location.line})\n`;
output += ` Repeats: ${prop.repeatedFrom}\n`;
});
}
});

if (output === "") {
output = "No repeated properties found in any presenter files.";
}

return output;
}

function main() {
const directory = process.argv[2] || ".";
console.log(`Analyzing presenter files in: ${directory}`);

// Find presenter files
const presenterFiles = findPresenterFiles(directory);

if (presenterFiles.length === 0) {
console.log("No presenter files found.");
return;
}

console.log(`Found ${presenterFiles.length} presenter files.`);

// Process files in batches to avoid memory pressure
const batchSize = 50;
const results: { file: string; properties: PropertyMapping[] }[] = [];

for (let i = 0; i < presenterFiles.length; i += batchSize) {
const batch = presenterFiles.slice(i, i + batchSize);
const batchResults = batch.map((file) => {
const properties = analyzeFile(file);
return { file, properties };
});
results.push(...batchResults);
}

const formattedResults = formatResults(results);
console.log("\nResults:");
console.log(formattedResults);

// Count statistics
const totalRepeatedProps = results.reduce(
(sum, result) =>
sum + result.properties.filter((prop) => prop.isRepeat).length,
0,
);

const filesWithRepeats = results.filter((result) =>
result.properties.some((prop) => prop.isRepeat),
).length;

console.log("\nSummary:");
console.log(`Total presenter files analyzed: ${presenterFiles.length}`);
console.log(`Files with repeated properties: ${filesWithRepeats}`);
console.log(`Total repeated properties found: ${totalRepeatedProps}`);
}

main();
49 changes: 49 additions & 0 deletions scripts/examples/sample.presenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from "react";

interface SampleProps {
title: string;
description: string;
isActive: boolean;
onClick: () => void;
data: any;
}

export class SamplePresenter {
constructor(private props: SampleProps) {}

vm() {
const { title, description, isActive, onClick, data } = this.props;

return {
// Simple repeats of props
heading: title,
content: description,
active: isActive,

// Non-repeats (transformed props)
upperTitle: title.toUpperCase(),

// Repeats of other properties
displayTitle: "heading",

// Function handlers
handleClick: onClick,

// Complex properties
formattedData: JSON.stringify(data),
};
}

render() {
const vm = this.vm();

return (
<div className={vm.active ? "active" : ""}>
<h1>{vm.heading}</h1>
<p>{vm.content}</p>
<button onClick={vm.handleClick}>Click me</button>
<pre>{vm.formattedData}</pre>
</div>
);
}
}
22 changes: 22 additions & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "presenter-vm-analyzer",
"version": "1.0.0",
"description": "Analyzes presenter files to identify redundant VM properties",
"main": "analyze_presenters.ts",
"scripts": {
"analyze": "ts-node analyze_presenters.ts",
"test": "ts-node analyze_presenters.ts ./examples"
},
"keywords": ["typescript", "presenter", "analysis", "react"],
"author": "Codegen",
"license": "MIT",
"dependencies": {
"glob": "^8.0.3",
"typescript": "^4.9.5"
},
"devDependencies": {
"@types/glob": "^8.0.0",
"@types/node": "^18.11.18",
"ts-node": "^10.9.1"
}
}