UNPKG

frontend-standards-checker

Version:

A comprehensive frontend standards validation tool with TypeScript support

306 lines (305 loc) 13.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.isConfigOrConstantsFile = void 0; exports.findPascalCaseDirectory = findPascalCaseDirectory; exports.shouldProcessFile = shouldProcessFile; exports.findFunctionMatch = findFunctionMatch; exports.validateFunctionName = validateFunctionName; exports.createNameMismatchError = createNameMismatchError; exports.createNoFunctionError = createNoFunctionError; exports.shouldSkipLine = shouldSkipLine; exports.detectFunctionDeclaration = detectFunctionDeclaration; exports.getFunctionName = getFunctionName; exports.shouldSkipFunction = shouldSkipFunction; exports.analyzeFunctionComplexity = analyzeFunctionComplexity; exports.hasProperComments = hasProperComments; exports.createCommentError = createCommentError; const path_1 = __importDefault(require("path")); const isConfigOrConstantsFile = (filePath) => { return /config|constants|enums/i.test(filePath) && filePath.endsWith('.ts'); }; exports.isConfigOrConstantsFile = isConfigOrConstantsFile; /** * Find the first PascalCase directory from the file path upwards * Used for component naming validation to handle nested component structures */ function findPascalCaseDirectory(filePath) { const dirs = filePath.split(path_1.default.sep); // Remove the file name dirs.pop(); for (let i = dirs.length - 1; i >= 0; i--) { const currentDir = dirs[i]; if (typeof currentDir === 'string' && /^[A-Z][a-zA-Z0-9]*$/.test(currentDir)) { return currentDir; } } // Fallback to immediate parent directory if no PascalCase found return path_1.default.basename(path_1.default.dirname(filePath)); } function shouldProcessFile(filePath) { const fileName = path_1.default.basename(filePath); return fileName === 'index.tsx' && (filePath.includes('/components/') || filePath.includes('/containers/') || filePath.includes('/screens/')); } function findFunctionMatch(content) { // Check for direct export default arrow function without name (should be allowed) if (/export\s+default\s+\([^)]*\)\s*=>\s*/.test(content) || /export\s+default\s+<[^>]*>\s*\([^)]*\)\s*=>\s*/.test(content)) { // This is a valid anonymous export, use the folder name as the expected function name return null; // Let the validation pass for anonymous exports } // First, look for export default to find the main function name const exportDefaultMatch = /export\s+default\s+(\w+)/g.exec(content); if (exportDefaultMatch && exportDefaultMatch[1]) { const exportedName = exportDefaultMatch[1]; // Find the declaration of the exported function const functionPatterns = [ // Function declaration: function CalendarPicker() { ... } new RegExp(`function\\s+${exportedName}\\s*\\(`, 'g'), // Const arrow function with optional return type: const CalendarPicker = (): ReactType => { ... } new RegExp(`const\\s+${exportedName}\\s*(?::\\s*[^=]*)?=\\s*\\([^)]*\\)\\s*(?::\\s*[^=]+)?\\s*=>`, 'g'), // Const with React.FC: const CalendarPicker: React.FC = () => { ... } new RegExp(`const\\s+${exportedName}\\s*:\\s*React\\.?FC`, 'g'), // Generic components: const Component = <T>() => { ... } new RegExp(`const\\s+${exportedName}\\s*=\\s*<[^>]*>\\s*\\([^)]*\\)\\s*(?::\\s*[^=]+)?\\s*=>`, 'g'), ]; for (const pattern of functionPatterns) { const match = pattern.exec(content); if (match) { return { functionName: exportedName, lineNumber: content.substring(0, match.index).split('\n').length, }; } } } // Fallback to original patterns if no export default found const fallbackPatterns = [ // Direct export default function: export default function StoriesList() { ... } /export\s+default\s+function\s+(\w+)\s*\(/g, // Direct export const with arrow and optional return type: export const LoadingSkeleton = (): React.JSX.Element => { ... } /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]{0,39})\s*=\s*\([^)]*\)\s*(?::\s*[^=]+)?\s*=>\s*/g, // Direct export const with generics: export const Component = <T>() => { ... } /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]{0,39})\s*=\s*<[^>]*>\s*\([^)]*\)\s*(?::\s*[^=]+)?\s*=>\s*/g, // ForwardRef: const Component = forwardRef(() => { ... }) /const\s+([a-zA-Z_$][a-zA-Z0-9_$]{0,39})\s*=\s*forwardRef/g, // Memo: const Component = memo(() => { ... }) /const\s+([a-zA-Z_$][a-zA-Z0-9_$]{0,39})\s*=\s*memo/g, // React.FC with name: const StoriesList: React.FC = () => { ... } /const\s+(\w+)\s*:\s*React\.?FC/g, // TypeScript FC: const StoriesList: FC<Props> = () => { ... } /const\s+(\w+)\s*:\s*FC</g, ]; for (const pattern of fallbackPatterns) { const match = pattern.exec(content); if (match?.[1]) { return { functionName: match[1], lineNumber: content.substring(0, match.index).split('\n').length, }; } } // Final fallback: look for top-level declarations that might be the main component // This tries to find the main component function while avoiding internal functions // Split content into lines to analyze structure const lines = content.split('\n'); let bracketDepth = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (!line) continue; // Skip undefined lines const trimmedLine = line.trim(); // Skip empty lines and comments if (!trimmedLine || trimmedLine.startsWith('//') || trimmedLine.startsWith('*')) { continue; } // Track bracket depth to know if we're inside a function const openBrackets = (line.match(/{/g) || []).length; const closeBrackets = (line.match(/}/g) || []).length; bracketDepth += openBrackets - closeBrackets; // If we're at the top level (bracketDepth 0 or 1), look for main functions if (bracketDepth <= 1) { // Look for const declarations that might be components const constMatch = /^\s*const\s+([a-zA-Z_$][a-zA-Z0-9_$]{0,39})\s*=/.exec(line); if (constMatch?.[1]) { // Check if this looks like a component (starts with uppercase) const functionName = constMatch[1]; if (/^[A-Z]/.test(functionName)) { return { functionName, lineNumber: i + 1, }; } } // Look for function declarations at top level const functionMatch = /^\s*function\s+([A-Z][a-zA-Z0-9_$]*)\s*\(/.exec(line); if (functionMatch?.[1]) { return { functionName: functionMatch[1], lineNumber: i + 1, }; } } } return null; } function validateFunctionName(functionName, dirName, isPascalCase, filePath, lineNumber) { if (!isPascalCase) { if (functionName.toLowerCase() !== dirName.toLowerCase()) { return createNameMismatchError(functionName, dirName, filePath, lineNumber); } } else if (functionName !== dirName) { return createNameMismatchError(functionName, dirName, filePath, lineNumber); } return null; } function createNameMismatchError(functionName, dirName, filePath, lineNumber) { return { rule: 'Component function name match', message: `The function '${functionName}' (line ${lineNumber}) must have the same name as its containing folder '${dirName}'. ${dirName === dirName.toLowerCase() ? 'The folder must follow PascalCase and the function must have exactly the same name.' : `Found: function='${functionName}', folder='${dirName}'.`}`, filePath: filePath, line: lineNumber, severity: 'error', category: 'naming', }; } function createNoFunctionError(dirName, filePath) { return { rule: 'Component function name match', message: `No main exported function found in index.tsx. The folder '${dirName}' must contain a function with the same name.`, filePath: filePath, line: 1, severity: 'error', category: 'naming', }; } function shouldSkipLine(trimmedLine) { return (!trimmedLine || trimmedLine.startsWith('//') || trimmedLine.startsWith('*') || trimmedLine.startsWith('/*')); } function detectFunctionDeclaration(trimmedLine) { // Split the complex regex into simpler ones const patterns = [ /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*=\s*\(.*\)\s*=>/, /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*=\s*async\s*\(.*\)\s*=>/, /const\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*=\s*async\s*\(.*\)\s*=>/, /export\s+function\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*\(/, /const\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*=\s*function\s*\(/, /function\s+([a-zA-Z_$][a-zA-Z0-9_$]{1,39})\s*\(/, ]; for (const pattern of patterns) { const match = pattern.exec(trimmedLine); if (match) { return match; } } return null; } function getFunctionName(functionMatch) { return functionMatch[1] ?? null; } function shouldSkipFunction(trimmedLine, _functionName) { if (trimmedLine.includes('interface ') || trimmedLine.includes('type ')) { return true; } return (trimmedLine.includes('=>') && trimmedLine.length < 80 && !trimmedLine.includes('async')); } function analyzeFunctionComplexity(lines, startIndex, content) { let complexityScore = 0; let braceCount = 0; let inFunction = false; let linesInFunction = 0; const functionStartLine = lines[startIndex] ?? ''; // Fallback to empty string if undefined for (let j = startIndex; j < Math.min(startIndex + 30, lines.length); j++) { const bodyLine = lines[j]; if (!bodyLine) continue; braceCount = updateBraceCount(bodyLine, braceCount); inFunction = inFunction || braceCount > 0; if (inFunction) { linesInFunction++; complexityScore += calculateLineComplexity(bodyLine); } if (inFunction && braceCount === 0) { break; } } const functionContent = content.indexOf(functionStartLine) >= 0 ? content.substring(content.indexOf(functionStartLine)) : ''; const isComplex = determineComplexity(complexityScore, linesInFunction, functionContent); return { complexityScore, linesInFunction, isComplex }; } function updateBraceCount(line, currentCount) { const openBraces = (line.match(/\{/g) || []).length; const closeBraces = (line.match(/\}/g) || []).length; return currentCount + openBraces - closeBraces; } function calculateLineComplexity(line) { let score = 0; if (/\b(if|else if|switch|case)\b/.test(line)) score += 1; if (/\b(for|while|do)\b/.test(line)) score += 2; if (/\b(try|catch|finally)\b/.test(line)) score += 2; if (/\b(async|await|Promise\.all|Promise\.resolve|Promise\.reject|\.then|\.catch)\b/.test(line)) score += 2; if (/\.(map|filter|reduce|forEach|find|some|every)\s*\(/.test(line)) score += 1; if (/\?\s*[a-zA-Z0-9_$,\s=[]{}:.<>]{0,100}\s*:/.test(line)) score += 1; // Ternary operators if (/&&|\|\|/.test(line)) score += 0.5; // Logical operators return score; } function determineComplexity(score, lines, functionContent) { return (score >= 3 || lines > 8 || (score >= 2 && /async|await|Promise/.test(functionContent))); } function hasProperComments(lines, functionLineIndex, _content) { for (let k = Math.max(0, functionLineIndex - 15); k < functionLineIndex; k++) { const commentLine = lines[k]; if (!commentLine) continue; const trimmedCommentLine = commentLine.trim(); if (isValidComment(trimmedCommentLine)) { return true; } } return false; } function isValidComment(commentLine) { return (commentLine.includes('/**') || commentLine.includes('*/') || (commentLine.startsWith('*') && commentLine.length > 5) || commentLine.includes('/*') || (commentLine.startsWith('//') && commentLine.length > 15 && !/^\s*\/\/\s*(TODO|FIXME|NOTE|HACK)/.test(commentLine))); } function createCommentError(functionName, analysis, filePath, lineNumber) { return { rule: 'Missing comment in complex function', message: `Complex function '${functionName}' (complexity: ${analysis.complexityScore.toFixed(1)}, lines: ${analysis.linesInFunction}) must have comments explaining its behavior.`, filePath: `${filePath}:${lineNumber + 1}`, line: lineNumber + 1, severity: 'warning', category: 'documentation', }; }