""" Quick-Win Finder Identifies easy improvement opportunities in a codebase: - Inline styles that can be extracted - Duplicate values that should be tokens - Unused styles - Naming inconsistencies - Accessibility issues """ import re from pathlib import Path from typing import List, Dict, Any, Optional from dataclasses import dataclass from .base import ( QuickWin, QuickWinType, QuickWinPriority, Location, ProjectAnalysis, ) from .styles import StyleAnalyzer from .react import ReactAnalyzer class QuickWinFinder: """ Finds quick improvement opportunities in a project. Categories: - INLINE_STYLE: Inline styles that can be extracted to CSS/tokens - DUPLICATE_VALUE: Repeated values that should be tokens - UNUSED_STYLE: CSS that's defined but not used - HARDCODED_VALUE: Magic numbers/colors that should be tokens - NAMING_INCONSISTENCY: Inconsistent naming patterns - DEPRECATED_PATTERN: Outdated styling approaches - ACCESSIBILITY: A11y improvements """ def __init__(self, root_path: str): self.root = Path(root_path).resolve() self.style_analyzer = StyleAnalyzer(root_path) self.react_analyzer = ReactAnalyzer(root_path) async def find_all(self) -> List[QuickWin]: """ Find all quick-win opportunities. Returns: List of QuickWin objects sorted by priority """ quick_wins = [] # Find inline styles inline_wins = await self._find_inline_style_wins() quick_wins.extend(inline_wins) # Find duplicate values duplicate_wins = await self._find_duplicate_value_wins() quick_wins.extend(duplicate_wins) # Find unused styles unused_wins = await self._find_unused_style_wins() quick_wins.extend(unused_wins) # Find hardcoded values hardcoded_wins = await self._find_hardcoded_value_wins() quick_wins.extend(hardcoded_wins) # Find naming inconsistencies naming_wins = await self._find_naming_inconsistency_wins() quick_wins.extend(naming_wins) # Find accessibility issues a11y_wins = await self._find_accessibility_wins() quick_wins.extend(a11y_wins) # Sort by priority priority_order = { QuickWinPriority.CRITICAL: 0, QuickWinPriority.HIGH: 1, QuickWinPriority.MEDIUM: 2, QuickWinPriority.LOW: 3, } quick_wins.sort(key=lambda x: priority_order[x.priority]) return quick_wins async def _find_inline_style_wins(self) -> List[QuickWin]: """Find inline styles that should be extracted.""" wins = [] inline_styles = await self.react_analyzer.find_inline_styles() if not inline_styles: return wins # Group by file by_file = {} for style in inline_styles: file_path = style['file'] if file_path not in by_file: by_file[file_path] = [] by_file[file_path].append(style) # Create quick-wins for files with multiple inline styles for file_path, styles in by_file.items(): if len(styles) >= 3: # Only flag if 3+ inline styles wins.append(QuickWin( type=QuickWinType.INLINE_STYLE, priority=QuickWinPriority.HIGH, title=f"Extract {len(styles)} inline styles", description=f"File {file_path} has {len(styles)} inline style declarations that could be extracted to CSS classes or design tokens.", location=Location(file_path, styles[0]['line']), affected_files=[file_path], estimated_impact=f"Reduce inline styles, improve maintainability", fix_suggestion="Extract repeated style properties to CSS classes or design tokens. Use className instead of style prop.", auto_fixable=True, )) # Create summary if many files have inline styles total_inline = len(inline_styles) if total_inline >= 10: wins.insert(0, QuickWin( type=QuickWinType.INLINE_STYLE, priority=QuickWinPriority.HIGH, title=f"Project has {total_inline} inline styles", description=f"Found {total_inline} inline style declarations across {len(by_file)} files. Consider migrating to CSS classes or design tokens.", affected_files=list(by_file.keys())[:10], estimated_impact=f"Improve code maintainability and bundle size", fix_suggestion="Run 'dss migrate inline-styles' to preview migration options.", auto_fixable=True, )) return wins async def _find_duplicate_value_wins(self) -> List[QuickWin]: """Find duplicate values that should be tokens.""" wins = [] analysis = await self.style_analyzer.analyze() duplicates = analysis.get('duplicates', []) # Find high-occurrence duplicates for dup in duplicates[:10]: # Top 10 duplicates if dup['count'] >= 5: # Only if used 5+ times priority = QuickWinPriority.HIGH if dup['count'] >= 10 else QuickWinPriority.MEDIUM wins.append(QuickWin( type=QuickWinType.DUPLICATE_VALUE, priority=priority, title=f"Duplicate value '{dup['value']}' used {dup['count']} times", description=f"The value '{dup['value']}' appears {dup['count']} times across {len(dup['files'])} files. This should be a design token.", affected_files=dup['files'], estimated_impact=f"Create single source of truth, easier theme updates", fix_suggestion=f"Create token for this value and replace all occurrences.", auto_fixable=True, )) return wins async def _find_unused_style_wins(self) -> List[QuickWin]: """Find unused CSS styles.""" wins = [] unused = await self.style_analyzer.find_unused_styles() if len(unused) >= 5: wins.append(QuickWin( type=QuickWinType.UNUSED_STYLE, priority=QuickWinPriority.MEDIUM, title=f"Found {len(unused)} potentially unused CSS classes", description=f"These CSS classes are defined but don't appear to be used in the codebase. Review and remove if confirmed unused.", affected_files=list(set(u['file'] for u in unused))[:10], estimated_impact=f"Reduce CSS bundle size by removing dead code", fix_suggestion="Review each class and remove if unused. Some may be dynamically generated.", auto_fixable=False, # Needs human review )) return wins async def _find_hardcoded_value_wins(self) -> List[QuickWin]: """Find hardcoded magic values.""" wins = [] analysis = await self.style_analyzer.analyze() candidates = analysis.get('token_candidates', []) # Find high-confidence candidates high_confidence = [c for c in candidates if c.confidence >= 0.7] if high_confidence: wins.append(QuickWin( type=QuickWinType.HARDCODED_VALUE, priority=QuickWinPriority.MEDIUM, title=f"Found {len(high_confidence)} values that should be tokens", description="These hardcoded values appear multiple times and should be extracted as design tokens for consistency.", estimated_impact="Improve theme consistency and make updates easier", fix_suggestion="Use 'dss extract-tokens' to create tokens from these values.", auto_fixable=True, )) # Add specific wins for top candidates for candidate in high_confidence[:5]: wins.append(QuickWin( type=QuickWinType.HARDCODED_VALUE, priority=QuickWinPriority.LOW, title=f"Extract '{candidate.value}' as token", description=f"Value '{candidate.value}' appears {candidate.occurrences} times. Suggested token: {candidate.suggested_name}", location=candidate.locations[0] if candidate.locations else None, affected_files=[loc.file_path for loc in candidate.locations[:5]], estimated_impact=f"Single source of truth for this value", fix_suggestion=f"Create token '{candidate.suggested_name}' with value '{candidate.value}'", auto_fixable=True, )) return wins async def _find_naming_inconsistency_wins(self) -> List[QuickWin]: """Find naming inconsistencies.""" wins = [] naming = await self.style_analyzer.analyze_naming_consistency() if naming.get('inconsistencies'): primary = naming.get('primary_pattern', 'unknown') inconsistent_count = len(naming['inconsistencies']) wins.append(QuickWin( type=QuickWinType.NAMING_INCONSISTENCY, priority=QuickWinPriority.LOW, title=f"Found {inconsistent_count} naming inconsistencies", description=f"The project primarily uses {primary} naming, but {inconsistent_count} classes use different conventions.", affected_files=list(set(i['file'] for i in naming['inconsistencies']))[:10], estimated_impact="Improve code consistency and readability", fix_suggestion=f"Standardize all class names to use {primary} convention.", auto_fixable=True, )) return wins async def _find_accessibility_wins(self) -> List[QuickWin]: """Find accessibility issues.""" wins = [] skip_dirs = {'node_modules', '.git', 'dist', 'build'} a11y_issues = [] for ext in ['*.jsx', '*.tsx']: for file_path in self.root.rglob(ext): if any(skip in file_path.parts for skip in skip_dirs): continue try: content = file_path.read_text(encoding='utf-8', errors='ignore') rel_path = str(file_path.relative_to(self.root)) # Check for images without alt img_no_alt = re.findall(r']+(?]*>', content) if img_no_alt: for match in img_no_alt[:3]: if 'alt=' not in match: line = content[:content.find(match)].count('\n') + 1 a11y_issues.append({ 'type': 'img-no-alt', 'file': rel_path, 'line': line, }) # Check for buttons without accessible text icon_only_buttons = re.findall( r']*>\s*<(?:svg|Icon|img)[^>]*/?>\s*', content, re.IGNORECASE ) if icon_only_buttons: a11y_issues.append({ 'type': 'icon-button-no-label', 'file': rel_path, }) # Check for click handlers on non-interactive elements div_onclick = re.findall(r']+onClick', content) if div_onclick: a11y_issues.append({ 'type': 'div-click-handler', 'file': rel_path, 'count': len(div_onclick), }) except Exception: continue # Group issues by type if a11y_issues: img_issues = [i for i in a11y_issues if i['type'] == 'img-no-alt'] if img_issues: wins.append(QuickWin( type=QuickWinType.ACCESSIBILITY, priority=QuickWinPriority.HIGH, title=f"Found {len(img_issues)} images without alt text", description="Images should have alt attributes for screen readers. Empty alt='' is acceptable for decorative images.", affected_files=list(set(i['file'] for i in img_issues))[:10], estimated_impact="Improve accessibility for screen reader users", fix_suggestion="Add descriptive alt text to images or alt='' for decorative images.", auto_fixable=False, )) div_issues = [i for i in a11y_issues if i['type'] == 'div-click-handler'] if div_issues: wins.append(QuickWin( type=QuickWinType.ACCESSIBILITY, priority=QuickWinPriority.MEDIUM, title=f"Found click handlers on div elements", description="Using onClick on div elements makes them inaccessible to keyboard users. Use button or add proper ARIA attributes.", affected_files=list(set(i['file'] for i in div_issues))[:10], estimated_impact="Improve keyboard navigation accessibility", fix_suggestion="Replace
with