/** * cli:eslint-validator — execute.ts * 5 regles d'analyse statique par regex sur fichiers .tsx/.ts * Ne valide rien — recoit des donnees deja validees */ import { readdirSync, readFileSync, statSync } from 'node:fs' import { join, relative, basename, sep, posix } from 'node:path' import type { ESLintInput, CheckResult, ESLintReport } from './types' // ─── Helpers ─── /** Recursively collect files matching extensions */ function collectFiles(dir: string, extensions: string[]): string[] { const results: string[] = [] let entries: string[] try { entries = readdirSync(dir) } catch { return results } for (const entry of entries) { const fullPath = join(dir, entry) let stat try { stat = statSync(fullPath) } catch { continue } if (stat.isDirectory()) { results.push(...collectFiles(fullPath, extensions)) } else if (extensions.some((ext) => entry.endsWith(ext))) { results.push(fullPath) } } return results } /** Collect files under a specific subfolder name (e.g. 'components', 'pages') */ function collectFilesInSubfolder(srcDir: string, folderName: string, extensions: string[]): string[] { const results: string[] = [] const allFiles = collectFiles(srcDir, extensions) // Normalize path separators for comparison const normalizedFolder = folderName.replace(/\\/g, '/') for (const file of allFiles) { const relPath = relative(srcDir, file).replace(/\\/g, '/') // Check if path contains the folder as a directory segment const segments = relPath.split('/') if (segments.includes(normalizedFolder)) { results.push(file) } } return results } /** Read file content safely */ function readFileSafe(filePath: string): string { try { return readFileSync(filePath, 'utf-8') } catch { return '' } } /** Get 1-based line number for a character index */ function getLineNumber(content: string, charIndex: number): number { let line = 1 for (let i = 0; i < charIndex && i < content.length; i++) { if (content[i] === '\n') line++ } return line } // ─── Rule 1: ss/no-direct-fetch ─── function checkNoDirectFetch(srcDir: string): CheckResult[] { const results: CheckResult[] = [] const extensions = ['.tsx', '.ts'] // Collect files in components/ and pages/ subfolders const targetFiles = [ ...collectFilesInSubfolder(srcDir, 'components', extensions), ...collectFilesInSubfolder(srcDir, 'pages', extensions), ] // Patterns to detect direct fetch/axios usage // NOTE: single regex per concern to avoid double-matching on the same line const patterns = [ { regex: /import\s+.*['"]axios['"]/g, label: 'import axios' }, { regex: /\baxios\s*\.\s*(get|post|put|patch|delete|request)\s*\(/g, label: 'axios call' }, { regex: /\bfetch\s*\(/g, label: 'fetch call' }, ] for (const file of targetFiles) { const content = readFileSafe(file) if (!content) continue const relFile = relative(srcDir, file).replace(/\\/g, '/') for (const { regex, label } of patterns) { // Reset regex state regex.lastIndex = 0 let match: RegExpExecArray | null while ((match = regex.exec(content)) !== null) { const line = getLineNumber(content, match.index) results.push({ code: 'ss/no-direct-fetch', status: 'error', file: relFile, line, message: `Direct ${label} in component/page — use service API instead`, }) } } } // If no violations, report ok if (results.length === 0) { results.push({ code: 'ss/no-direct-fetch', status: 'ok', count: 0 }) } return results } // ─── Rule 2: ss/permission-guard ─── function checkPermissionGuard(srcDir: string): CheckResult[] { const results: CheckResult[] = [] const extensions = ['.tsx'] // Check components and pages const targetFiles = [ ...collectFilesInSubfolder(srcDir, 'components', extensions), ...collectFilesInSubfolder(srcDir, 'pages', extensions), ] // Sensitive action keywords in button/action context const sensitiveActions = ['delete', 'remove', 'approve', 'reject', 'execute', 'destroy'] // Build regex: button or onClick with sensitive text nearby // Look for patterns like: