UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

642 lines (641 loc) 23.7 kB
"use strict"; /** * Utility functions for analyzing JSX structure from ComponentRelation data * Enhanced with better text extraction and accessibility analysis helpers * Updated to work with enhanced ScanResult and ProcessedContent */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.JSXAnalysisUtils = void 0; const ts = __importStar(require("typescript")); const ASTUtils_1 = require("../../../utils/ast/ASTUtils"); const traversal_1 = require("../../../utils/ast/traversal"); const constants_1 = require("../constants"); class JSXAnalysisUtils { /** * Extracts JSX elements from component content for accessibility analysis * Updated to work with enhanced ScanResult and ProcessedContent */ static extractJSXElements(component, scanResult) { const elements = []; // First try to use existing jsxStructure if available if (component.jsxStructure) { elements.push(this.convertJSXStructureToElementInfo(component.jsxStructure)); } // Get enhanced content from ScanResult const enhancedContent = this.getEnhancedContent(component, scanResult); if (enhancedContent) { const contentElements = this.parseJSXFromContent(enhancedContent); elements.push(...contentElements); } // Fallback to original content if available if (component.content && elements.length === 0) { const fallbackElements = this.parseJSXFromContent(component.content); elements.push(...fallbackElements); } return this.deduplicateElements(elements); } /** * Gets enhanced content from ScanResult fileContents map */ static getEnhancedContent(component, scanResult) { // Try to get content from ScanResult first (this is the most up-to-date) const scanContent = scanResult.fileContents.get(component.fullPath); if (scanContent) { return scanContent; } // Fallback to component content if not in scan result return component.content || null; } /** * Converts existing JSXStructure to JSXElementInfo format */ static convertJSXStructureToElementInfo(jsxStructure) { const props = {}; // Convert props from JSXStructure format jsxStructure.props.forEach((prop) => { props[prop.name] = { type: this.inferPropType(prop.type), value: this.parsePropValue(prop.type), rawValue: prop.type, }; }); // Recursively convert children const children = jsxStructure.children.map((child) => this.convertJSXStructureToElementInfo(child)); return { tagName: jsxStructure.tagName.toLowerCase(), props, children, textContent: this.extractTextContent(children), }; } /** * Parses JSX elements directly from component content string * Enhanced to handle TypeScript and modern JSX patterns */ static parseJSXFromContent(content) { const elements = []; try { // Create a TypeScript source file for AST parsing const sourceFile = ts.createSourceFile("temp.tsx", content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); // Traverse AST to find JSX elements (0, traversal_1.traverseAST)(sourceFile, (node) => { if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) { const elementInfo = this.convertTSNodeToElementInfo(node, sourceFile); if (elementInfo) { elements.push(elementInfo); } } }); } catch (error) { // Silently handle parsing errors - some content may not be valid JSX // This is expected for files that might contain partial JSX or complex patterns } return elements; } /** * Converts TypeScript JSX node to JSXElementInfo * Enhanced with better error handling and context extraction */ static convertTSNodeToElementInfo(node, sourceFile) { try { const tagName = this.getJSXTagName(node); const props = this.extractPropsFromNode(node); const location = ASTUtils_1.ASTUtils.getNodeLocation(node, sourceFile); const context = this.getNodeContext(node, sourceFile); let children = []; let textContent; // Handle children for regular JSX elements (not self-closing) if (ts.isJsxElement(node)) { const childElements = node.children .filter((child) => ts.isJsxElement(child) || ts.isJsxSelfClosingElement(child)) .map((child) => this.convertTSNodeToElementInfo(child, sourceFile)) .filter((child) => child !== null); children = childElements; textContent = this.extractTextFromJSXChildren(node.children); } return { tagName: tagName.toLowerCase(), props, children, textContent, location, context, }; } catch (error) { return null; } } /** * Extracts tag name from JSX node * Fixed to handle TypeScript type narrowing properly */ static getJSXTagName(node) { const openingElement = ts.isJsxElement(node) ? node.openingElement : node; const tagNameNode = openingElement.tagName; if (ts.isIdentifier(tagNameNode)) { return tagNameNode.text; } else if (ts.isPropertyAccessExpression(tagNameNode)) { // Handle cases like React.Fragment, styled.div, or motion.div const objectName = ts.isIdentifier(tagNameNode.expression) ? tagNameNode.expression.text : tagNameNode.expression.getText(); const propertyName = tagNameNode.name.text; return `${objectName}.${propertyName}`; } else if (ts.isJsxNamespacedName(tagNameNode)) { // Handle namespace cases like svg:circle return `${tagNameNode.namespace.text}:${tagNameNode.name.text}`; } return "unknown"; } /** * Extracts props from JSX node * Enhanced with better prop value extraction */ static extractPropsFromNode(node) { const props = {}; const openingElement = ts.isJsxElement(node) ? node.openingElement : node; if (openingElement.attributes) { openingElement.attributes.properties.forEach((attr) => { if (ts.isJsxAttribute(attr) && attr.name) { const propName = this.getAttributeName(attr.name); const propValue = this.extractPropValue(attr); props[propName] = propValue; } else if (ts.isJsxSpreadAttribute(attr)) { // Handle spread attributes like {...props} const spreadExpression = attr.expression.getText(); props[`...${spreadExpression}`] = { type: "expression", value: undefined, rawValue: `{...${spreadExpression}}`, }; } }); } return props; } /** * Safely extracts attribute name from different JSX attribute name types */ static getAttributeName(name) { if (ts.isIdentifier(name)) { return name.text; } else if (ts.isJsxNamespacedName(name)) { // Handle namespaced attributes like xml:lang return `${name.namespace.text}:${name.name.text}`; } return "unknown"; } /** * Extracts value from JSX attribute * Enhanced with better expression handling */ static extractPropValue(attr) { if (!attr.initializer) { // Boolean prop with no value (e.g., <input disabled />) return { type: "boolean", value: true, }; } if (ts.isStringLiteral(attr.initializer)) { return { type: "string", value: attr.initializer.text, }; } if (ts.isJsxExpression(attr.initializer) && attr.initializer.expression) { const expr = attr.initializer.expression; if (ts.isStringLiteral(expr)) { return { type: "string", value: expr.text, }; } if (ts.isNumericLiteral(expr)) { return { type: "number", value: parseFloat(expr.text), }; } if (expr.kind === ts.SyntaxKind.TrueKeyword) { return { type: "boolean", value: true, }; } if (expr.kind === ts.SyntaxKind.FalseKeyword) { return { type: "boolean", value: false, }; } if (expr.kind === ts.SyntaxKind.NullKeyword) { return { type: "undefined", value: undefined, rawValue: "null", }; } if (expr.kind === ts.SyntaxKind.UndefinedKeyword) { return { type: "undefined", value: undefined, rawValue: "undefined", }; } // For complex expressions, store the raw text return { type: "expression", value: undefined, rawValue: expr.getText(), }; } return { type: "undefined", value: undefined, }; } /** * Extracts text content from JSX children * Enhanced to handle more text patterns */ static extractTextFromJSXChildren(children) { const textParts = []; children.forEach((child) => { if (ts.isJsxText(child)) { const text = child.text.trim(); if (text) { textParts.push(text); } } else if (ts.isJsxExpression(child) && child.expression) { // Try to extract static text from expressions if (ts.isStringLiteral(child.expression)) { textParts.push(child.expression.text); } else if (ts.isTemplateExpression(child.expression)) { // Handle template literals const templateText = child.expression.head.text; if (templateText.trim()) { textParts.push(templateText); } } } }); return textParts.length > 0 ? textParts.join(" ") : undefined; } /** * Gets surrounding context for better error reporting * Enhanced with better context boundaries */ static getNodeContext(node, sourceFile) { try { const start = Math.max(0, node.getStart(sourceFile) - 100); const end = Math.min(sourceFile.getFullText().length, node.getEnd() + 100); const context = sourceFile.getFullText().substring(start, end); // Clean up context to remove excessive whitespace return context.replace(/\s+/g, " ").trim(); } catch (error) { return ""; } } /** * Extracts text content from child elements */ static extractTextContent(children) { const textParts = children .map((child) => child.textContent) .filter((text) => Boolean(text)); return textParts.length > 0 ? textParts.join(" ") : undefined; } /** * Infers prop type from string representation */ static inferPropType(typeStr) { if (typeStr === "string" || typeStr.startsWith('"') || typeStr.startsWith("'") || typeStr.startsWith("`")) { return "string"; } if (typeStr === "number" || !isNaN(Number(typeStr))) { return "number"; } if (typeStr === "boolean" || typeStr === "true" || typeStr === "false") { return "boolean"; } if (typeStr === "undefined" || typeStr === "null") { return "undefined"; } return "expression"; } /** * Parses prop value from string representation */ static parsePropValue(typeStr) { if (typeStr.startsWith('"') || typeStr.startsWith("'")) { return typeStr.slice(1, -1); // Remove quotes } if (typeStr.startsWith("`") && typeStr.endsWith("`")) { return typeStr.slice(1, -1); // Remove backticks for template literals } if (!isNaN(Number(typeStr))) { return Number(typeStr); } if (typeStr === "true") { return true; } if (typeStr === "false") { return false; } return undefined; } /** * Removes duplicate elements based on tag name, props, and content * Enhanced deduplication logic */ static deduplicateElements(elements) { const seen = new Set(); return elements.filter((element) => { // Create a more comprehensive key for deduplication const propsKey = Object.entries(element.props) .sort(([a], [b]) => a.localeCompare(b)) .map(([key, value]) => `${key}:${value.type}:${value.value}`) .join(";"); const key = `${element.tagName}-${propsKey}-${element.textContent || ""}-${element.location?.line || 0}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } /** * Checks if an element has a specific prop */ static hasProp(element, propName) { return propName in element.props; } /** * Gets prop value safely */ static getPropValue(element, propName) { return element.props[propName]; } /** * Checks if element has any of the specified props */ static hasAnyProp(element, propNames) { return propNames.some((propName) => this.hasProp(element, propName)); } /** * Gets string value of a prop, handling different value types */ static getPropStringValue(element, propName) { const prop = this.getPropValue(element, propName); if (!prop) return undefined; if (prop.type === "string") { return prop.value; } if (prop.type === "number" || prop.type === "boolean") { return String(prop.value); } if (prop.type === "expression" && prop.rawValue) { return prop.rawValue; } return undefined; } /** * Checks if element is an HTML element using the comprehensive HTML_TAGS constant */ static isHTMLElement(element) { return constants_1.HTML_TAGS.includes(element.tagName); } /** * Checks if element is a React component (capitalized tag name) */ static isReactComponent(element) { return (element.tagName.charAt(0) === element.tagName.charAt(0).toUpperCase()); } /** * Enhanced helper: Checks if element is hidden via CSS classes */ static isHiddenByClass(element) { const className = this.getPropStringValue(element, "className"); if (!className) return false; return constants_1.HIDDEN_ELEMENT_PATTERNS.some((pattern) => pattern.test(className)); } /** * Enhanced helper: Checks if element is screen reader only */ static isScreenReaderOnly(element) { const className = this.getPropStringValue(element, "className"); if (!className) return false; return constants_1.SCREEN_READER_ONLY_PATTERNS.some((pattern) => pattern.test(className)); } /** * Enhanced helper: Checks if element is hidden via inline styles */ static isHiddenByStyle(element) { const style = this.getPropStringValue(element, "style"); if (!style) return false; return constants_1.HIDDEN_STYLE_PATTERNS.some((pattern) => pattern.test(style)); } /** * Enhanced helper: Checks if element is completely hidden */ static isHiddenElement(element) { // Check type="hidden" for inputs if (element.tagName === "input") { const inputType = this.getPropStringValue(element, "type"); if (inputType === "hidden") return true; } // Check aria-hidden="true" const ariaHidden = this.getPropStringValue(element, "aria-hidden"); if (ariaHidden === "true") return true; // Check CSS-based hiding return this.isHiddenByClass(element) || this.isHiddenByStyle(element); } /** * Enhanced helper: Checks if element is likely an icon component */ static isIconComponent(element) { const tagName = element.tagName; // Check component name patterns if (constants_1.ICON_COMPONENT_PATTERNS.some((pattern) => pattern.test(tagName))) { return true; } // Check CSS class patterns const className = this.getPropStringValue(element, "className"); if (className && constants_1.ICON_CLASS_PATTERNS.some((pattern) => pattern.test(className))) { return true; } return false; } /** * Enhanced helper: Checks if element is decorative */ static isDecorativeElement(element) { const tagName = element.tagName.toLowerCase(); // Check tag name if (constants_1.DECORATIVE_ELEMENTS.includes(tagName)) { // For images, check if they have empty alt text (decorative) if (tagName === "img") { const alt = this.getPropStringValue(element, "alt"); return alt === ""; } return true; } // Check for decorative role const role = this.getPropStringValue(element, "role"); return role === "presentation" || role === "none"; } /** * Enhanced helper: Checks if variable name suggests text content */ static isTextVariable(variableName) { return constants_1.TEXT_VARIABLE_PATTERNS.some((pattern) => pattern.test(variableName)); } /** * Enhanced helper: Checks if element has labeling props */ static hasLabelingProps(element) { return constants_1.LABELING_PROP_NAMES.some((propName) => this.hasProp(element, propName)); } /** * Enhanced helper: Checks if element has spread props */ static hasSpreadProps(element) { // Check for common spread prop patterns in prop names const propNames = Object.keys(element.props); if (constants_1.SPREAD_PROP_PATTERNS.some((pattern) => propNames.some((prop) => prop.includes(pattern)))) { return true; } // Check for spread syntax in prop values return Object.values(element.props).some((propValue) => propValue.type === "expression" && propValue.rawValue && constants_1.SPREAD_PROP_PATTERNS.some((pattern) => propValue.rawValue.includes(`...${pattern}`))); } /** * Enhanced helper: Extracts variable names from expression */ static extractVariableNames(expression) { const variableNames = []; // Simple regex to extract identifiers const identifierRegex = /\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g; let match; while ((match = identifierRegex.exec(expression)) !== null) { const identifier = match[0]; // Filter out keywords and common non-variable identifiers if (!this.isJavaScriptKeyword(identifier)) { variableNames.push(identifier); } } return [...new Set(variableNames)]; // Remove duplicates } /** * Enhanced helper: Checks if a string is a JavaScript keyword */ static isJavaScriptKeyword(word) { const keywords = [ "true", "false", "null", "undefined", "if", "else", "for", "while", "function", "return", "var", "let", "const", "class", "extends", "import", "export", "default", "from", "as", "typeof", "instanceof", ]; return keywords.includes(word); } /** * Enhanced helper: Checks if expression likely contains text */ static expressionLikelyContainsText(expression) { const variables = this.extractVariableNames(expression); return variables.some((variable) => this.isTextVariable(variable)); } /** * Enhanced helper: Gets all accessible text sources from element */ static getAccessibleTextSources(element) { const sources = []; // Direct text content if (element.textContent && element.textContent.trim()) { sources.push(`text: "${element.textContent.trim()}"`); } // ARIA labeling const ariaLabel = this.getPropStringValue(element, "aria-label"); if (ariaLabel) { sources.push(`aria-label: "${ariaLabel}"`); } const ariaLabelledby = this.getPropStringValue(element, "aria-labelledby"); if (ariaLabelledby) { sources.push(`aria-labelledby: "${ariaLabelledby}"`); } // Title attribute const title = this.getPropStringValue(element, "title"); if (title) { sources.push(`title: "${title}"`); } // Children text const childrenText = this.extractTextContent(element.children); if (childrenText && childrenText.trim()) { sources.push(`children: "${childrenText.trim()}"`); } return sources; } } exports.JSXAnalysisUtils = JSXAnalysisUtils;