sicua
Version:
A tool for analyzing project structure and dependencies
293 lines (292 loc) • 11.4 kB
JavaScript
"use strict";
/**
* Context analyzer for accessibility validation
* Analyzes surrounding elements and component context for accessibility patterns
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextAnalyzer = void 0;
const jsxAnalysisUtils_1 = require("./jsxAnalysisUtils");
const textContentExtractor_1 = require("./textContentExtractor");
const constants_1 = require("../constants");
class ContextAnalyzer {
/**
* Analyzes if a form input has proper labeling considering context
*/
static hasFormInputLabeling(element, allElements) {
// Skip input types that don't require labels
const inputType = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "type");
if (inputType && constants_1.INPUT_TYPES_WITHOUT_LABELS.includes(inputType)) {
return true;
}
// Check if input is hidden
if (this.isHiddenInput(element)) {
return true;
}
// Check for direct ARIA labeling
if (this.hasDirectLabeling(element)) {
return true;
}
// Check for associated label elements
if (this.hasAssociatedLabel(element, allElements)) {
return true;
}
// Check for parent label element
if (this.hasParentLabel(element)) {
return true;
}
// Check for placeholder as fallback (warning level)
if (this.hasPlaceholderText(element)) {
return true; // Consider as labeled but should generate warning elsewhere
}
// Check for props that might be passed through (common in component libraries)
if (this.hasLabelingProps(element)) {
return true;
}
return false;
}
/**
* Checks if an input is hidden and doesn't need labeling
*/
static isHiddenInput(element) {
// Check for hidden type
const inputType = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "type");
if (inputType === "hidden") {
return true;
}
// Check for hidden class patterns
const className = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "className");
if (className) {
const hiddenPatterns = [
/\bhidden\b/,
/\binvisible\b/,
/\bopacity-0\b/,
/\bsr-only\b/,
/\bscreen-reader-only\b/,
];
return hiddenPatterns.some((pattern) => pattern.test(className));
}
// Check for style-based hiding
const style = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "style");
if (style) {
return /display:\s*none|visibility:\s*hidden|opacity:\s*0/.test(style);
}
return false;
}
/**
* Checks for direct ARIA labeling attributes
*/
static hasDirectLabeling(element) {
for (const attr of constants_1.ARIA_LABELING_ATTRIBUTES) {
const value = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, attr);
if (value && value.trim()) {
return true;
}
}
return false;
}
/**
* Checks for associated label elements using htmlFor/id relationship
*/
static hasAssociatedLabel(element, allElements) {
const elementId = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "id");
if (!elementId) {
return false;
}
// Find label elements that reference this input
return allElements.some((el) => {
if (el.tagName.toLowerCase() !== "label") {
return false;
}
const htmlFor = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(el, "htmlFor") ||
jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(el, "for");
return htmlFor === elementId;
});
}
/**
* Checks if element is wrapped in a label element
*/
static hasParentLabel(element) {
// This would require parent traversal which isn't available in current structure
// For now, we'll check if this is commonly implemented via component patterns
return false; // TODO: Implement when parent traversal is available
}
/**
* Checks for placeholder text as a fallback labeling mechanism
*/
static hasPlaceholderText(element) {
const placeholder = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "placeholder");
return Boolean(placeholder && placeholder.trim());
}
/**
* Checks for labeling props that might be spread or passed through
*/
static hasLabelingProps(element) {
return constants_1.LABELING_PROP_NAMES.some((prop) => jsxAnalysisUtils_1.JSXAnalysisUtils.hasProp(element, prop));
}
/**
* Analyzes interactive elements for proper roles and labeling
*/
static hasInteractiveLabeling(element) {
// Check for accessible text content
if (textContentExtractor_1.TextContentExtractor.hasAccessibleText(element)) {
return true;
}
// Check if element likely has text but we can't extract it
if (textContentExtractor_1.TextContentExtractor.likelyHasText(element)) {
return true;
}
// Check for ARIA labeling
if (this.hasDirectLabeling(element)) {
return true;
}
// For icon-only buttons, require explicit labeling
if (this.isIconOnlyElement(element)) {
return this.hasExplicitLabeling(element);
}
return false;
}
/**
* Checks if element appears to be icon-only
*/
static isIconOnlyElement(element) {
// Check for common icon indicators
const className = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, "className");
if (className) {
const iconPatterns = [
/\bicon\b/,
/\bfa-/,
/\bmaterial-icons\b/,
/\blucide\b/,
/\bfeather\b/,
/\bheroicons\b/,
];
if (iconPatterns.some((pattern) => pattern.test(className))) {
return true;
}
}
// Check for icon-like children
const hasIconChildren = element.children.some((child) => {
const childTag = child.tagName.toLowerCase();
return (childTag === "svg" ||
childTag === "icon" ||
childTag.includes("icon") ||
this.isIconComponent(child));
});
return hasIconChildren;
}
/**
* Checks if a child element is likely an icon component
*/
static isIconComponent(element) {
const tagName = element.tagName;
return constants_1.ICON_COMPONENT_PATTERNS.some((pattern) => pattern.test(tagName));
}
/**
* Checks for explicit labeling required for icon-only elements
*/
static hasExplicitLabeling(element) {
const explicitLabels = ["aria-label", "aria-labelledby", "title"];
return explicitLabels.some((attr) => {
const value = jsxAnalysisUtils_1.JSXAnalysisUtils.getPropStringValue(element, attr);
return Boolean(value && value.trim());
});
}
/**
* Analyzes if a link has descriptive text considering context
*/
static hasDescriptiveLinkText(element) {
const text = textContentExtractor_1.TextContentExtractor.extractAccessibleText(element);
if (!text) {
return false;
}
// Check against non-descriptive patterns
const nonDescriptivePatterns = [
/^(click here|here|this|that|more|read more|learn more|see more|view more|show more|continue|next|previous|prev|back|forward|go|download|open|view|see|watch|listen)$/i,
/^(link|url|website|page|site|document|file|article|post|item|content|details|info|information)$/i,
/^[>\<»«x+\-123]$/,
/^lorem ipsum/i,
/^(placeholder|example|sample|test)$/i,
];
return !nonDescriptivePatterns.some((pattern) => pattern.test(text.trim()));
}
/**
* Checks if an element is within a form context
*/
static isInFormContext(allElements) {
// Check if any parent elements are forms
// This would require parent traversal - for now check for form-related siblings
const formElements = allElements.filter((el) => el.tagName.toLowerCase() === "form" ||
el.tagName.toLowerCase() === "fieldset");
return formElements.length > 0;
}
/**
* Analyzes component props for accessibility patterns
*/
static hasAccessibilityProps(element) {
const a11yProps = Object.keys(element.props).filter((prop) => prop.startsWith("aria-") ||
prop.startsWith("data-testid") ||
["role", "tabIndex", "title"].includes(prop));
return a11yProps.length > 0;
}
/**
* Checks for spread props that might contain accessibility attributes
*/
static hasSpreadProps(element) {
// Look for common spread prop patterns
const spreadPatterns = [
"...props",
"...rest",
"...otherProps",
"...additionalProps",
"...a11yProps",
"...accessibility",
];
return (Object.keys(element.props).some((prop) => spreadPatterns.some((pattern) => prop.includes(pattern.replace("...", "")))) ||
Object.values(element.props).some((propValue) => propValue.type === "expression" &&
propValue.rawValue &&
spreadPatterns.some((pattern) => propValue.rawValue.includes(pattern))));
}
/**
* Determines severity level based on context analysis
*/
static determineSeverity(element, hasIssue, hasPartialSupport) {
if (!hasIssue) {
return null;
}
// Error level: Critical accessibility missing
if (!hasPartialSupport && !this.hasSpreadProps(element)) {
return "error";
}
// Warning level: Some accessibility but not complete
if (hasPartialSupport || this.hasSpreadProps(element)) {
return "warning";
}
// Info level: Minor improvements
return "info";
}
/**
* Provides context-aware suggestions for accessibility improvements
*/
static getSuggestions(element) {
const suggestions = [];
if (element.tagName === "input") {
if (!this.hasDirectLabeling(element)) {
suggestions.push("Add aria-label or associate with a label element");
}
}
if (element.tagName === "button") {
if (this.isIconOnlyElement(element) &&
!this.hasExplicitLabeling(element)) {
suggestions.push("Icon-only buttons should have aria-label");
}
}
if (element.tagName === "a") {
const text = textContentExtractor_1.TextContentExtractor.extractAccessibleText(element);
if (text && !this.hasDescriptiveLinkText(element)) {
suggestions.push("Use more descriptive link text");
}
}
return suggestions;
}
}
exports.ContextAnalyzer = ContextAnalyzer;