sicua
Version:
A tool for analyzing project structure and dependencies
384 lines (383 loc) • 15.5 kB
JavaScript
;
/**
* Main accessibility analyzer class that orchestrates the entire analysis process
* Updated to work with enhanced ScanResult and project structure detection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AccessibilityAnalyzer = void 0;
const a11yRules_1 = require("./rules/a11yRules");
const jsxAnalysisUtils_1 = require("./utils/jsxAnalysisUtils");
const ruleValidators_1 = require("./validators/ruleValidators");
class AccessibilityAnalyzer {
constructor(scanResult, components) {
this.componentA11yInfo = new Map();
this.scanResult = scanResult;
this.components = components;
}
/**
* Main analysis method that produces complete accessibility analysis
*/
async analyze() {
// Process all components for accessibility issues
await this.analyzeComponents();
// Generate summary metrics
const summary = this.generateSummary();
// Generate rule-based breakdown
const ruleViolations = this.generateRuleViolations();
// Generate component-level details
const componentViolations = this.generateComponentViolations();
// Generate patterns and insights
const patterns = this.generatePatterns();
return {
summary,
ruleViolations,
componentViolations,
patterns,
};
}
/**
* Analyze all components for accessibility violations
*/
async analyzeComponents() {
for (const component of this.components) {
const componentInfo = await this.analyzeComponent(component);
this.componentA11yInfo.set(component.name, componentInfo);
}
}
/**
* Analyze a single component for accessibility issues using enhanced validators
*/
async analyzeComponent(component) {
const context = {
componentName: component.name,
componentPath: component.fullPath,
content: component.content || "",
jsxStructure: component.jsxStructure,
};
// Extract JSX elements from component using enhanced extraction
const elements = jsxAnalysisUtils_1.JSXAnalysisUtils.extractJSXElements(component, this.scanResult);
// Run enhanced individual element validations
const violations = [];
// Use enhanced validators directly for better context support
this.runEnhancedValidations(elements, violations);
// Run multi-element validations
const multiElementViolations = this.runMultiElementValidations(elements);
violations.push(...multiElementViolations);
// Calculate component accessibility score
const accessibilityScore = this.calculateComponentScore(elements, violations);
return {
componentName: component.name,
componentPath: component.fullPath,
elements,
violations,
accessibilityScore,
};
}
/**
* Run enhanced validations with proper context support
*/
runEnhancedValidations(elements, violations) {
for (const element of elements) {
// Image validations
if (element.tagName === "img") {
const violation = ruleValidators_1.RuleValidators.validateImageAlt(element);
if (violation)
violations.push(violation);
}
// Input validations (with context)
if (element.tagName === "input") {
const violation = ruleValidators_1.RuleValidators.validateInputLabel(element, elements);
if (violation)
violations.push(violation);
}
// Button validations
if (element.tagName === "button") {
const violation = ruleValidators_1.RuleValidators.validateButtonText(element);
if (violation)
violations.push(violation);
}
// Link validations
if (element.tagName === "a") {
const violation = ruleValidators_1.RuleValidators.validateLinkText(element);
if (violation)
violations.push(violation);
}
// ARIA role validations
if (jsxAnalysisUtils_1.JSXAnalysisUtils.hasProp(element, "role")) {
const violation = ruleValidators_1.RuleValidators.validateAriaRole(element);
if (violation)
violations.push(violation);
}
// Interactive role validations
const hasInteractiveHandlers = [
"onClick",
"onPress",
"onTap",
"onKeyDown",
"onKeyPress",
"onKeyUp",
].some((handler) => jsxAnalysisUtils_1.JSXAnalysisUtils.hasProp(element, handler));
if (hasInteractiveHandlers) {
const violation = ruleValidators_1.RuleValidators.validateInteractiveRole(element);
if (violation)
violations.push(violation);
}
// ARIA attributes validations
const hasAriaProps = Object.keys(element.props).some((prop) => prop.startsWith("aria-"));
if (hasAriaProps) {
const violation = ruleValidators_1.RuleValidators.validateAriaAttributes(element);
if (violation)
violations.push(violation);
}
// Form structure validations
if (element.tagName === "fieldset") {
const violation = ruleValidators_1.RuleValidators.validateFormStructure(element);
if (violation)
violations.push(violation);
}
// HTML lang validation
if (element.tagName === "html") {
const violation = ruleValidators_1.RuleValidators.validateLangAttribute(element);
if (violation)
violations.push(violation);
}
}
}
/**
* Run validations that require multiple elements
*/
runMultiElementValidations(elements) {
const violations = [];
// Heading hierarchy validation
const headingViolations = ruleValidators_1.RuleValidators.validateHeadingHierarchy(elements);
violations.push(...headingViolations);
// Unique ID validation
const idViolations = ruleValidators_1.RuleValidators.validateUniqueIds(elements);
violations.push(...idViolations);
return violations;
}
/**
* Calculate accessibility score for a component (0-100)
*/
calculateComponentScore(elements, violations) {
if (elements.length === 0) {
return 100; // No elements, no issues
}
// Weight violations by severity
const severityWeights = {
error: 10,
warning: 5,
info: 1,
};
const totalPenalty = violations.reduce((penalty, violation) => {
return penalty + severityWeights[violation.severity];
}, 0);
// Calculate base score (elements without violations get full points)
const elementsWithViolations = new Set(violations.map((v) => `${v.element}-${v.elementLocation?.line || 0}`)).size;
const cleanElements = elements.length - elementsWithViolations;
const baseScore = elements.length > 0 ? (cleanElements / elements.length) * 100 : 100;
// Apply penalty reduction
const penaltyReduction = Math.min(totalPenalty * 2, 50); // Cap penalty at 50 points
return Math.max(0, Math.round(baseScore - penaltyReduction));
}
/**
* Generate summary metrics for the analysis
*/
generateSummary() {
const allViolations = this.getAllViolations();
const componentInfos = Array.from(this.componentA11yInfo.values());
const errorCount = allViolations.filter((v) => v.severity === "error").length;
const warningCount = allViolations.filter((v) => v.severity === "warning").length;
const infoCount = allViolations.filter((v) => v.severity === "info").length;
const componentsWithIssues = componentInfos.filter((info) => info.violations.length > 0).length;
const totalComponentsAnalyzed = componentInfos.length;
// Calculate overall score as weighted average
const totalScore = componentInfos.reduce((sum, info) => sum + info.accessibilityScore, 0);
const overallScore = totalComponentsAnalyzed > 0
? Math.round(totalScore / totalComponentsAnalyzed)
: 100;
return {
totalViolations: allViolations.length,
errorCount,
warningCount,
infoCount,
componentsWithIssues,
totalComponentsAnalyzed,
overallScore,
};
}
/**
* Generate rule-based breakdown of violations
*/
generateRuleViolations() {
const ruleViolations = {};
const allViolations = this.getAllViolations();
// Group violations by rule ID
const violationsByRule = new Map();
for (const violation of allViolations) {
if (!violationsByRule.has(violation.ruleId)) {
violationsByRule.set(violation.ruleId, []);
}
violationsByRule.get(violation.ruleId).push(violation);
}
// Build rule violation summary
for (const [ruleId, violations] of violationsByRule) {
const rule = a11yRules_1.A11Y_RULES[ruleId];
if (!rule)
continue;
const affectedComponents = new Set(violations.map((v) => this.getComponentNameForViolation(v)));
ruleViolations[ruleId] = {
ruleName: rule.name,
severity: rule.severity,
description: rule.description,
violationCount: violations.length,
affectedComponents: Array.from(affectedComponents),
wcagLevel: rule.wcagLevel,
wcagCriterion: rule.wcagCriterion,
};
}
return ruleViolations;
}
/**
* Generate component-level violation details
*/
generateComponentViolations() {
const componentViolations = {};
for (const [componentName, info] of this.componentA11yInfo) {
if (info.violations.length > 0) {
componentViolations[componentName] = {
componentPath: info.componentPath,
violationCount: info.violations.length,
accessibilityScore: info.accessibilityScore,
violations: info.violations,
};
}
}
return componentViolations;
}
/**
* Generate patterns and insights from the analysis
*/
generatePatterns() {
const allViolations = this.getAllViolations();
// Most common violations
const violationCounts = new Map();
for (const violation of allViolations) {
violationCounts.set(violation.ruleId, (violationCounts.get(violation.ruleId) || 0) + 1);
}
const mostCommonViolations = Array.from(violationCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ruleId, count]) => ({
ruleId,
count,
percentage: Math.round((count / allViolations.length) * 100),
}));
// Violations by directory
const violationsByDirectory = {};
for (const [componentName, info] of this.componentA11yInfo) {
const directory = this.getDirectoryFromPath(info.componentPath);
violationsByDirectory[directory] =
(violationsByDirectory[directory] || 0) + info.violations.length;
}
// Violations by severity
const violationsBySeverity = {
error: allViolations.filter((v) => v.severity === "error").length,
warning: allViolations.filter((v) => v.severity === "warning").length,
info: allViolations.filter((v) => v.severity === "info").length,
};
// WCAG compliance level assessment
const wcagComplianceLevel = this.assessWCAGCompliance(allViolations);
return {
mostCommonViolations,
violationsByDirectory,
violationsBySeverity,
wcagComplianceLevel,
};
}
/**
* Get all violations from all components
*/
getAllViolations() {
const allViolations = [];
for (const info of this.componentA11yInfo.values()) {
allViolations.push(...info.violations);
}
return allViolations;
}
/**
* Get component name for a violation (for tracking purposes)
*/
getComponentNameForViolation(violation) {
// Find component that contains this violation
for (const [componentName, info] of this.componentA11yInfo) {
if (info.violations.includes(violation)) {
return componentName;
}
}
return "unknown";
}
/**
* Extract directory from file path
*/
getDirectoryFromPath(filePath) {
const parts = filePath.split("/");
return parts.slice(0, -1).join("/") || "/";
}
/**
* Assess WCAG compliance level based on violations
*/
assessWCAGCompliance(violations) {
const errorViolations = violations.filter((v) => v.severity === "error");
if (errorViolations.length === 0) {
// Check for AA level compliance
const aaViolations = violations.filter((v) => {
const rule = a11yRules_1.A11Y_RULES[v.ruleId];
return rule && rule.wcagLevel === "AA" && v.severity === "warning";
});
if (aaViolations.length === 0) {
return "AAA"; // No errors or AA warnings
}
return "AA"; // No errors but has AA warnings
}
// Check if only AA/AAA level errors exist
const aLevelErrors = errorViolations.filter((v) => {
const rule = a11yRules_1.A11Y_RULES[v.ruleId];
return rule && rule.wcagLevel === "A";
});
if (aLevelErrors.length === 0) {
return "A"; // No A-level errors
}
return "none"; // Has A-level errors
}
/**
* Get detailed component information for a specific component
*/
getComponentInfo(componentName) {
return this.componentA11yInfo.get(componentName);
}
/**
* Get violations for a specific rule across all components
*/
getViolationsForRule(ruleId) {
const violations = [];
for (const info of this.componentA11yInfo.values()) {
const ruleViolations = info.violations.filter((v) => v.ruleId === ruleId);
violations.push(...ruleViolations);
}
return violations;
}
/**
* Get components that have no accessibility violations
*/
getCleanComponents() {
const cleanComponents = [];
for (const [componentName, info] of this.componentA11yInfo) {
if (info.violations.length === 0) {
cleanComponents.push(componentName);
}
}
return cleanComponents;
}
}
exports.AccessibilityAnalyzer = AccessibilityAnalyzer;