UNPKG

prprompts-flutter-generator

Version:

AI-powered Flutter development with full automation + official extension support - Generate 32 security-audited guides & auto-implement in 2-3 hours. NEW v5.1: Official Claude Code plugin with hooks, Gemini TOML commands, Qwen MCP settings. Features: Comp

336 lines (287 loc) 9.47 kB
/** * AI-Powered Accessibility Checker * Validates accessibility compliance and suggests improvements */ class AccessibilityChecker { constructor(aiClient, logger = console) { this.aiClient = aiClient; this.logger = logger; } /** * Check accessibility of generated code * @param {string} code - Flutter code to check * @param {Object} options - Check options * @returns {Promise<Object>} Accessibility report */ async check(code, options = {}) { try { this.logger.log('♿ Checking accessibility...'); // Run local checks first const localIssues = this._checkLocally(code); // Enhance with AI const aiResult = await this._checkWithAI(code, options); // Merge issues const allIssues = this._mergeIssues(localIssues, aiResult.issues || []); // Calculate score const score = this._calculateScore(allIssues); this.logger.log(`✓ Accessibility score: ${(score * 100).toFixed(0)}% (${allIssues.length} issues)`); return { score, issues: allIssues, summary: this._generateSummary(score, allIssues), recommendations: this._generateRecommendations(allIssues) }; } catch (error) { this.logger.error(`❌ Accessibility check failed: ${error.message}`); return { score: 0.5, issues: [], summary: `Accessibility check failed: ${error.message}`, recommendations: [] }; } } /** * Local accessibility checks * @private */ _checkLocally(code) { const issues = []; // Check for Semantics widgets if (!code.includes('Semantics(')) { issues.push({ severity: 'warning', category: 'semantics', description: 'No Semantics widgets found', suggestion: 'Add Semantics widgets for screen reader support', line: null }); } // Check for screen reader labels on interactive widgets const interactiveWidgets = [ 'IconButton', 'FloatingActionButton', 'GestureDetector', 'InkWell' ]; interactiveWidgets.forEach(widget => { const pattern = new RegExp(`${widget}\\s*\\(`, 'g'); let match; while ((match = pattern.exec(code)) !== null) { const snippet = code.substring(match.index, match.index + 200); if (!snippet.includes('semanticLabel:') && !snippet.includes('Semantics(')) { issues.push({ severity: 'error', category: 'labels', description: `${widget} missing semantic label`, suggestion: `Add semanticLabel: 'description' to ${widget}`, line: this._getLineNumber(code, match.index) }); } } }); // Check for text contrast (heuristic) if (code.includes('Colors.grey') && code.includes('Colors.white')) { issues.push({ severity: 'warning', category: 'contrast', description: 'Potential low contrast between grey and white', suggestion: 'Ensure text has sufficient contrast ratio (WCAG AA: 4.5:1)', line: null }); } // Check for non-accessible form fields if (code.includes('TextField') || code.includes('TextFormField')) { const fieldPattern = /(?:TextField|TextFormField)\s*\(/g; let match; while ((match = fieldPattern.exec(code)) !== null) { const snippet = code.substring(match.index, match.index + 300); if (!snippet.includes('decoration:') || !snippet.includes('labelText:')) { issues.push({ severity: 'warning', category: 'forms', description: 'Text field missing label', suggestion: 'Add labelText to TextField decoration for accessibility', line: this._getLineNumber(code, match.index) }); } } } // Check for tap target size if (code.includes('GestureDetector') || code.includes('InkWell')) { issues.push({ severity: 'info', category: 'tap_targets', description: 'Verify tap targets are at least 48x48 pixels', suggestion: 'Use Material Design minimum touch target size (48dp)', line: null }); } return issues; } /** * AI-powered accessibility check * @private */ async _checkWithAI(code, options) { try { const result = await this.aiClient.validate(code, { checkAccessibility: true, checkPerformance: false, checkSecurity: false }); // Filter for accessibility issues only const accessibilityIssues = (result.issues || []).filter(issue => issue.category === 'accessibility' ); return { issues: accessibilityIssues, score: result.score || 0.7 }; } catch (error) { this.logger.warn(`⚠️ AI accessibility check failed: ${error.message}`); return { issues: [], score: 0.5 }; } } /** * Merge local and AI issues * @private */ _mergeIssues(local, ai) { const merged = [...local]; // Add AI issues that aren't duplicates ai.forEach(aiIssue => { const duplicate = merged.some(localIssue => localIssue.description === aiIssue.description ); if (!duplicate) { merged.push(aiIssue); } }); return merged; } /** * Calculate accessibility score (0-100) * @private */ _calculateScore(issues) { if (issues.length === 0) return 1.0; let score = 1.0; issues.forEach(issue => { switch (issue.severity) { case 'error': score -= 0.15; break; case 'warning': score -= 0.08; break; case 'info': score -= 0.03; break; } }); return Math.max(0, Math.min(1, score)); } /** * Generate summary * @private */ _generateSummary(score, issues) { if (score >= 0.9) { return 'Excellent accessibility - minor improvements possible'; } else if (score >= 0.7) { return 'Good accessibility - some improvements recommended'; } else if (score >= 0.5) { return 'Fair accessibility - several issues need attention'; } else { return 'Poor accessibility - significant improvements required'; } } /** * Generate recommendations * @private */ _generateRecommendations(issues) { const recommendations = []; const errorCount = issues.filter(i => i.severity === 'error').length; const warningCount = issues.filter(i => i.severity === 'warning').length; if (errorCount > 0) { recommendations.push(`Fix ${errorCount} critical accessibility error(s)`); } if (warningCount > 0) { recommendations.push(`Address ${warningCount} accessibility warning(s)`); } // Category-specific recommendations const categories = {}; issues.forEach(issue => { categories[issue.category] = (categories[issue.category] || 0) + 1; }); if (categories.semantics) { recommendations.push('Add Semantics widgets for screen reader support'); } if (categories.labels) { recommendations.push('Add semantic labels to all interactive elements'); } if (categories.contrast) { recommendations.push('Verify color contrast meets WCAG AA standards (4.5:1)'); } if (categories.forms) { recommendations.push('Ensure all form fields have proper labels'); } return recommendations; } /** * Get line number from index * @private */ _getLineNumber(code, index) { const lines = code.substring(0, index).split('\n'); return lines.length; } /** * Format accessibility report */ formatReport(report) { let output = `\n♿ Accessibility Report\n`; output += `Score: ${(report.score * 100).toFixed(0)}%\n`; output += `Summary: ${report.summary}\n\n`; if (report.issues.length === 0) { output += '✓ No accessibility issues found\n'; return output; } const byCategory = {}; report.issues.forEach(issue => { if (!byCategory[issue.category]) { byCategory[issue.category] = []; } byCategory[issue.category].push(issue); }); Object.entries(byCategory).forEach(([category, issues]) => { output += `📋 ${this._capitalizeFirst(category)} (${issues.length}):\n`; issues.forEach(issue => { const icon = issue.severity === 'error' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️'; output += ` ${icon} ${issue.description}\n`; output += ` ${issue.suggestion}\n`; if (issue.line) { output += ` Line: ${issue.line}\n`; } }); output += '\n'; }); if (report.recommendations.length > 0) { output += '💡 Recommendations:\n'; report.recommendations.forEach(rec => { output += ` • ${rec}\n`; }); } return output; } /** * Capitalize first letter * @private */ _capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1).replace(/_/g, ' '); } } module.exports = AccessibilityChecker;