frontend-standards-checker
Version:
A comprehensive frontend standards validation tool with TypeScript support
306 lines (305 loc) • 13.4 kB
JavaScript
;
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',
};
}