scripts/findUnusedStyles.ts
Rama main · réplica de lectura del repositorio original.
import * as ts from '@typescript/typescript6';
import * as fs from 'fs';
import {globSync} from 'glob';
import * as path from 'path';
import TSCompilerUtils from './utils/TSCompilerUtils';
type StyleDefinition = {
key: string;
file: string;
line: number;
column: number;
};
// Static patterns and constants - created once
const STYLE_FILE_EXTENSIONS = '**/*.{ts,tsx}';
const STYLE_KEY_SKIP_PATTERNS = new Set(['default', 'exports', 'module', 'require', 'import', 'from', 'theme', 'colors', 'variables', 'CONST', 'Platform', 'StyleSheet']);
// Use EXCLUDED_STYLE_DIRS to clarify which directories are being ignored
const EXCLUDED_STYLE_DIRS = ['src/styles/utils/**', 'src/styles/generators/**', 'src/styles/theme/**'];
class ComprehensiveStylesFinder {
private rootDir: string;
private styleDefinitions = new Map<string, StyleDefinition>();
private fileContents = new Map<string, string>();
constructor(rootDir: string) {
this.rootDir = rootDir;
}
public findUnusedStyles(): StyleDefinition[] {
console.log('🔍 Step 1: Finding all style definitions...');
this.findAllStyleDefinitions();
console.log(`📊 Found ${this.styleDefinitions.size} style definitions`);
console.log('🔍 Step 2: Loading all file contents...');
this.loadAllFileContents();
console.log('🔍 Step 3: Comprehensive style usage analysis...');
this.findStyleUsagesComprehensive();
console.log('📊 Step 4: Identifying unused styles...');
return this.getUnusedStyles();
}
private findAllStyleDefinitions(): void {
const styleFiles = globSync(`src/styles/${STYLE_FILE_EXTENSIONS}`, {
cwd: this.rootDir,
ignore: EXCLUDED_STYLE_DIRS,
});
console.log(`Scanning ${styleFiles.length} main style files (excluding utils/generators/themes)...`);
const styleFileContents = new Map<string, string>();
for (const file of styleFiles) {
const fullPath = path.join(this.rootDir, file);
try {
const fileContent = fs.readFileSync(fullPath, 'utf8');
const sourceFile = ts.createSourceFile(fullPath, fileContent, ts.ScriptTarget.Latest, true);
styleFileContents.set(file, fileContent);
this.extractStyleKeysFromFile(sourceFile, file);
} catch (error) {
console.warn(`Warning: Could not read style file ${file}:`, error);
}
}
// Spread crediting must run after every style file has registered its definitions, or glob order decides which spreads count.
const indexContent = styleFileContents.get('src/styles/index.ts');
if (indexContent) {
this.checkSpreadPatternsInStylesFile(indexContent, 'src/styles/index.ts');
}
}
private extractStyleKeysFromFile(sourceFile: ts.SourceFile, file: string): void {
const visit = (node: ts.Node) => {
// For styles/index.ts, only process styles that are inside the main styles function
if (file === 'src/styles/index.ts') {
// Look for the main styles function (arrow function assigned to 'styles' variable)
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === 'styles' && node.initializer && ts.isArrowFunction(node.initializer)) {
// Process only the body of the styles function
if (node.initializer.body) {
let returnObject: ts.Expression | undefined;
if (ts.isParenthesizedExpression(node.initializer.body)) {
returnObject = node.initializer.body.expression;
} else if (ts.isObjectLiteralExpression(node.initializer.body)) {
returnObject = node.initializer.body;
} else if (ts.isSatisfiesExpression(node.initializer.body)) {
const satisfiesExpr = node.initializer.body.expression;
if (ts.isParenthesizedExpression(satisfiesExpr)) {
returnObject = satisfiesExpr.expression;
} else {
returnObject = satisfiesExpr;
}
}
if (returnObject && ts.isObjectLiteralExpression(returnObject)) {
// Process each property in the returned object
this.extractStyleDefinitionsFromObject(returnObject, sourceFile, file);
}
}
return; // Don't continue traversing for styles function
}
// Skip processing top-level constants and other functions for styles/index.ts
if (ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node)) {
return;
}
} else {
// For other files, process all style-like nodes normally
this.extractStyleDefinitionFromNode(node, sourceFile, file);
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
/**
* Extracts style definitions from a TypeScript object literal expression.
*
* This function scans through all properties in an object literal (typically the main styles object)
* and identifies valid style definitions. It captures both:
* - Property assignments with object literal values (e.g., `myStyle: { color: 'red' }`)
* - Method declarations that return styles (e.g., `myStyle: () => ({ color: 'red' })`)
*
* Valid style definitions are added to the styleDefinitions map with their source location
* for later unused style detection.
*
* @param objectLiteral - The TypeScript object literal expression to scan for style definitions
* @param sourceFile - The TypeScript source file containing the object literal
* @param file - The relative file path for tracking style definition locations
*/
private extractStyleDefinitionsFromObject(objectLiteral: ts.ObjectLiteralExpression, sourceFile: ts.SourceFile, file: string): void {
for (const property of objectLiteral.properties) {
if (ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property)) {
const key = TSCompilerUtils.extractKeyFromPropertyNode(property);
if (key && this.isStyleKey(key)) {
try {
let shouldCapture = false;
if (ts.isPropertyAssignment(property)) {
// For PropertyAssignment, check if the value is an object literal
if (ts.isObjectLiteralExpression(property.initializer)) {
shouldCapture = true;
}
} else if (ts.isMethodDeclaration(property)) {
// For MethodDeclaration, it's a function that returns styles
shouldCapture = true;
}
if (shouldCapture) {
const {line, character} = sourceFile.getLineAndCharacterOfPosition(property.getStart());
this.styleDefinitions.set(key, {
key,
file,
line: line + 1,
column: character + 1,
});
}
} catch (error) {
console.warn(`Warning: Could not get position for style '${key}' in ${file}`);
}
}
}
}
}
/**
* Extracts a style definition from an individual TypeScript AST node.
*
* This function processes individual AST nodes (typically during tree traversal) to identify
* style definitions in non-main style files. It handles three types of style definitions:
* - Property assignments with object literal values (e.g., `myStyle: { color: 'red' }`)
* - Method declarations that return styles (e.g., `myStyle: () => ({ color: 'red' })`)
* - Variable declarations that define styles (e.g., `const myStyle = { color: 'red' }`)
*
* @param node - The TypeScript AST node to check for style definitions
* @param sourceFile - The TypeScript source file containing the node
* @param file - The relative file path for tracking style definition locations
*/
private extractStyleDefinitionFromNode(node: ts.Node, sourceFile: ts.SourceFile, file: string): void {
// Look for object literal properties (style definitions)
if (ts.isPropertyAssignment(node) || ts.isMethodDeclaration(node)) {
const key = TSCompilerUtils.extractKeyFromPropertyNode(node);
if (key && this.isStyleKey(key)) {
try {
let shouldCapture = false;
if (ts.isPropertyAssignment(node)) {
// For PropertyAssignment, check if the value is an object literal
if (ts.isObjectLiteralExpression(node.initializer)) {
shouldCapture = true;
}
} else if (ts.isMethodDeclaration(node)) {
// For MethodDeclaration, it's a function that returns styles
shouldCapture = true;
}
if (shouldCapture) {
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
this.styleDefinitions.set(key, {
key,
file,
line: line + 1,
column: character + 1,
});
}
} catch (error) {
console.warn(`Warning: Could not get position for style '${key}' in ${file}`);
}
}
}
// Also look for variable declarations that might be styles
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
const key = node.name.text;
if (this.isStyleKey(key)) {
try {
const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart());
this.styleDefinitions.set(key, {
key,
file,
line: line + 1,
column: character + 1,
});
} catch (error) {
console.warn(`Warning: Could not get position for style '${key}' in ${file}`);
}
}
}
}
private isStyleKey(key: string): boolean {
// Skip certain patterns that are likely not style keys
if (STYLE_KEY_SKIP_PATTERNS.has(key)) {
return false;
}
return true;
}
private loadAllFileContents(): void {
// Use excludeMainStyleFiles to exclude only the main style definition files
// Keep utils/generators/themes for usage checking
const excludeMainStyleFiles = ['src/styles/index.ts', 'src/styles/variables.ts'];
const sourceFiles = globSync(`src/**/${STYLE_FILE_EXTENSIONS}`, {
cwd: this.rootDir,
ignore: excludeMainStyleFiles,
});
console.log(`Loading ${sourceFiles.length} source files (including utils/generators/themes for usage checking)...`);
for (const file of sourceFiles) {
const fullPath = path.join(this.rootDir, file);
try {
// Check if it's actually a file (not a directory)
const stat = fs.lstatSync(fullPath);
if (!stat.isFile()) {
console.warn(`Skipping ${file}: not a file`);
continue;
}
const fileContent = fs.readFileSync(fullPath, 'utf8');
this.fileContents.set(file, fileContent);
} catch (error) {
console.warn(`Warning: Could not read source file ${file}:`, error);
}
}
}
private findStyleUsagesComprehensive(): void {
const allFiles = Array.from(this.fileContents.entries());
console.log(`Analyzing usage patterns for ${this.styleDefinitions.size} style keys across ${allFiles.length} files...`);
let processedFiles = 0;
for (const [file, content] of allFiles) {
this.analyzeFileForStyleUsage(file, content);
processedFiles++;
// Early termination: if all styles are found, stop processing
if (this.styleDefinitions.size === 0) {
console.log(` ✅ All styles found as used!`);
break;
}
// Show progress every 100 files
if (processedFiles % 100 === 0) {
console.log(` ${this.styleDefinitions.size} styles remaining...`);
}
}
console.log(` Completed analysis.`);
}
private analyzeFileForStyleUsage(file: string, content: string): void {
// Use TypeScript AST to get content without comments
try {
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
// Use getText() to get content without comments
const cleanContent = sourceFile.getText();
// Check individual style usage patterns - iterate over current definitions
const keysToCheck = Array.from(this.styleDefinitions.keys());
for (const key of keysToCheck) {
if (this.isStyleUsedInContent(key, cleanContent, file)) {
// Remove the key from definitions since it's used
this.styleDefinitions.delete(key);
}
}
} catch (error) {
// Fallback: use original content if AST parsing fails
console.warn(`Warning: Could not parse ${file} with AST, using original content`);
const keysToCheck = Array.from(this.styleDefinitions.keys());
for (const key of keysToCheck) {
if (this.isStyleUsedInContent(key, content, file)) {
this.styleDefinitions.delete(key);
}
}
}
}
private checkSpreadPatternsInStylesFile(content: string, file: string): void {
try {
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
const visit = (node: ts.Node) => {
// Handle object spread assignments (in object literals)
if (ts.isSpreadAssignment(node)) {
this.handleSpreadExpression(node.expression);
}
// Handle array/function spread elements
else if (ts.isSpreadElement(node)) {
this.handleSpreadExpression(node.expression);
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
} catch (error) {
console.warn(`Warning: Could not parse ${file} for spread detection:`, error);
}
}
private handleSpreadExpression(expression: ts.Expression): void {
// Simple spreads like ...baseStyle
if (ts.isIdentifier(expression)) {
const objectName = expression.text;
if (this.styleDefinitions.has(objectName)) {
this.styleDefinitions.delete(objectName);
}
}
// Property access spreads like ...positioning.pFixed
else if (ts.isPropertyAccessExpression(expression)) {
if (ts.isIdentifier(expression.expression) && ts.isIdentifier(expression.name)) {
const objectName = expression.expression.text;
const propertyName = expression.name.text;
// Mark BOTH the object name and property name as used
if (this.styleDefinitions.has(objectName)) {
this.styleDefinitions.delete(objectName);
}
if (this.styleDefinitions.has(propertyName)) {
this.styleDefinitions.delete(propertyName);
}
}
// For complex property access (e.g., ...theme.styles.button)
else {
this.extractIdentifiersFromExpression(expression);
}
}
// Element access spreads like ...styles['key']
else if (ts.isElementAccessExpression(expression)) {
if (ts.isIdentifier(expression.expression)) {
const objectName = expression.expression.text;
if (this.styleDefinitions.has(objectName)) {
this.styleDefinitions.delete(objectName);
}
}
// Also check if the key is a string literal
if (ts.isStringLiteral(expression.argumentExpression)) {
const key = expression.argumentExpression.text;
if (this.styleDefinitions.has(key)) {
this.styleDefinitions.delete(key);
}
}
}
// For all other complex expressions, extract any identifiers
else {
this.extractIdentifiersFromExpression(expression);
}
}
private extractIdentifiersFromExpression(expression: ts.Expression): void {
const visit = (node: ts.Node) => {
if (ts.isIdentifier(node)) {
const name = node.text;
if (this.styleDefinitions.has(name)) {
this.styleDefinitions.delete(name);
}
}
ts.forEachChild(node, visit);
};
visit(expression);
}
private isStyleUsedInContent(key: string, content: string, file: string): boolean {
// Fast check: if the key doesn't appear at all in the content, it's not used
if (!content.includes(key)) {
return false;
}
try {
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
let found = false;
const visit = (node: ts.Node) => {
if (found) {
return; // Early termination for performance
}
// Property access: obj.key, styles.key, theme.key
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) && node.name.text === key) {
found = true;
return;
}
// Identifier: key (in destructuring, assignments, etc.)
if (ts.isIdentifier(node) && node.text === key) {
// Exclude property names in object literals (e.g., {key: value} where key is the property name)
const parent = node.parent;
if (ts.isPropertyAssignment(parent) && parent.name === node) {
// This is a property name, not a usage
ts.forEachChild(node, visit);
return;
}
if (ts.isMethodDeclaration(parent) && parent.name === node) {
// This is a method name, not a usage
ts.forEachChild(node, visit);
return;
}
if (ts.isGetAccessorDeclaration(parent) && parent.name === node) {
// This is a getter name, not a usage
ts.forEachChild(node, visit);
return;
}
if (ts.isSetAccessorDeclaration(parent) && parent.name === node) {
// This is a setter name, not a usage
ts.forEachChild(node, visit);
return;
}
found = true;
return;
}
// Element access: obj["key"], obj['key']
if (ts.isElementAccessExpression(node) && ts.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === key) {
found = true;
return;
}
// Template literal usage: `${key}` or `text-${key}-more`
if (ts.isTemplateExpression(node)) {
for (const span of node.templateSpans) {
if (ts.isIdentifier(span.expression) && span.expression.text === key) {
found = true;
return;
}
// Also check for property access in template spans
if (ts.isPropertyAccessExpression(span.expression) && ts.isIdentifier(span.expression.name) && span.expression.name.text === key) {
found = true;
return;
}
}
}
// Tagged template literals: css`${key}`
if (ts.isTaggedTemplateExpression(node) && ts.isTemplateExpression(node.template)) {
for (const span of node.template.templateSpans) {
if (ts.isIdentifier(span.expression) && span.expression.text === key) {
found = true;
return;
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return found;
} catch (error) {
// Fallback to simple text search if AST parsing fails
console.warn(`Warning: Could not parse ${file} for usage detection, falling back to text search`);
return content.includes(key);
}
}
private getUnusedStyles(): StyleDefinition[] {
// Whatever remains in styleDefinitions are the unused styles
const unused = Array.from(this.styleDefinitions.values());
return unused.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
}
}
// CLI interface
function main() {
const rootDir = process.cwd();
try {
const finder = new ComprehensiveStylesFinder(rootDir);
const unusedStyles = finder.findUnusedStyles();
if (unusedStyles.length === 0) {
console.log('✅ No unused styles found!');
process.exit(0);
}
console.error(`Found ${unusedStyles.length} unused styles:`);
console.log('');
const groupedByFile: Record<string, StyleDefinition[]> = {};
for (const style of unusedStyles) {
if (!groupedByFile[style.file]) {
groupedByFile[style.file] = [];
}
groupedByFile[style.file].push(style);
}
for (const [file, styles] of Object.entries(groupedByFile)) {
console.log(`📁 ${file}:`);
for (const style of styles) {
console.log(` - ${style.key} (line ${style.line}:${style.column})`);
}
console.log('');
}
process.exit(1);
} catch (error) {
console.error('❌ Error analyzing styles:', error);
process.exit(1);
}
}
if (require.main === module) {
main();
}
export {ComprehensiveStylesFinder};
export type {StyleDefinition};