diff --git a/scripts/analyze_presenters.ts b/scripts/analyze_presenters.ts new file mode 100644 index 0000000..fcf9cce --- /dev/null +++ b/scripts/analyze_presenters.ts @@ -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(); + + // 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(); + + // 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(); diff --git a/scripts/examples/sample.presenter.tsx b/scripts/examples/sample.presenter.tsx new file mode 100644 index 0000000..84677d6 --- /dev/null +++ b/scripts/examples/sample.presenter.tsx @@ -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 ( +
+

{vm.heading}

+

{vm.content}

+ +
{vm.formattedData}
+
+ ); + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..43c84b2 --- /dev/null +++ b/scripts/package.json @@ -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" + } +}