UNPKG

graphql-lint-clint-platform

Version:

GraphQL unused fields linter for Clint platform - Custom patterns and actions.graphql support

265 lines (222 loc) 8.51 kB
import { GraphQLQuery, GraphQLField } from "graphql-lint-unused-fields"; import fs from "fs"; import path from "path"; import { glob } from "glob"; interface ClintAction { name: string; pattern: string; // clint.entity.method file: string; line: number; } interface ClintUsageResult { unusedActions: string[]; usedActions: Record<string, Array<{file: string, line: number, code: string}>>; totalActions: number; } export class ClintUsageAnalyzer { private actionPatterns: Record<string, string[]> = {}; private sourceFiles: string[] = []; constructor(actionPatterns: Record<string, string[]>) { this.actionPatterns = actionPatterns; } private async findSourceFiles(sourcePath: string): Promise<string[]> { const tsFiles = await glob('**/*.{ts,tsx,js,jsx}', { cwd: sourcePath, ignore: ['node_modules/**', 'dist/**', 'build/**', '.git/**'], absolute: true }); return tsFiles; } async analyzeUsageInPath( sourcePath: string, verbose: boolean = false ): Promise<ClintUsageResult> { if (verbose) { console.log('📋 Iniciando análise DETALHADA de uso dos actions da Clint...\n'); console.log(`🎯 Total de actions detectadas: ${Object.keys(this.actionPatterns).length}`); } // Encontrar arquivos fonte this.sourceFiles = await this.findSourceFiles(sourcePath); if (verbose) { console.log(`📁 Total de arquivos para análise: ${this.sourceFiles.length}\n`); } const unusedActions: string[] = []; const usedActions: Record<string, Array<{file: string, line: number, code: string}>> = {}; // Buscar por todos os patterns de todas as actions for (const [actionName, patterns] of Object.entries(this.actionPatterns)) { if (verbose) { console.log(`🔍 Analisando action: ${actionName}`); console.log(` 🎨 Padrões gerados (${patterns.length}): ${patterns.join(', ')}`); } let hasUsage = false; const actionUsages: Array<{file: string, line: number, code: string}> = []; // Testar cada padrão com logging detalhado for (const pattern of patterns) { if (verbose) { console.log(` 🔎 Testando padrão: "${pattern}"`); } const usages = await this.findUsageInFiles(pattern); if (usages.length > 0) { hasUsage = true; actionUsages.push(...usages); if (verbose) { console.log(` ✅ ${usages.length} uso(s) encontrado(s) para "${pattern}"`); } } else { if (verbose) { console.log(` ❌ Nenhum uso para "${pattern}"`); } } } if (hasUsage) { usedActions[actionName] = actionUsages; if (verbose) { console.log(` ✅ USADO - Total: ${actionUsages.length} ocorrência(s)\n`); } } else { unusedActions.push(actionName); if (verbose) { console.log(` ❌ NÃO USADO - Nenhuma ocorrência encontrada\n`); } } } if (verbose) { console.log(`📊 RESUMO DA ANÁLISE:`); console.log(` ✅ Actions usadas: ${Object.keys(usedActions).length}`); console.log(` ❌ Actions não usadas: ${unusedActions.length}`); console.log(` 📈 Total: ${Object.keys(this.actionPatterns).length}\n`); } return { unusedActions, usedActions, totalActions: Object.keys(this.actionPatterns).length }; } private async findUsageInFiles(pattern: string): Promise<Array<{file: string, line: number, code: string}>> { const allUsages: Array<{file: string, line: number, code: string}> = []; for (const filePath of this.sourceFiles) { const usages = await this.findUsageInFile(filePath, pattern); allUsages.push(...usages); } return allUsages; } private async findUsageInFile(filePath: string, pattern: string): Promise<Array<{file: string, line: number, code: string}>> { try { const content = fs.readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); const usages: Array<{file: string, line: number, code: string}> = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; // MÚLTIPLAS ESTRATÉGIAS DE BUSCA: // 1. Busca exata pelo padrão if (line.includes(pattern)) { usages.push({ file: filePath, line: lineNumber, code: line.trim() }); continue; } // 2. Busca por regex - padrões com pontos if (pattern.includes('.')) { // Escapar pontos para regex const regexPattern = pattern.replace(/\./g, '\\.'); const regex = new RegExp(`\\b${regexPattern}\\b`); if (regex.test(line)) { usages.push({ file: filePath, line: lineNumber, code: line.trim() }); continue; } } // 3. Busca por partes do padrão (mais flexível) const patternParts = pattern.split('.'); if (patternParts.length >= 2) { // Verificar se todas as partes estão na linha const allPartsPresent = patternParts.every(part => line.toLowerCase().includes(part.toLowerCase()) ); if (allPartsPresent) { // Verificação adicional: deve ser código, não comentário const trimmedLine = line.trim(); if (!trimmedLine.startsWith('//') && !trimmedLine.startsWith('/*') && !trimmedLine.startsWith('*')) { usages.push({ file: filePath, line: lineNumber, code: line.trim() }); } } } // 4. Busca por snake_case original (fallback) const snakeCasePattern = pattern.replace(/\./g, '_').replace(/([A-Z])/g, '_$1').toLowerCase().replace(/^_/, ''); if (line.toLowerCase().includes(snakeCasePattern)) { usages.push({ file: filePath, line: lineNumber, code: line.trim() }); } } if (usages.length > 0) { console.log(` ✅ Padrão "${pattern}" encontrado ${usages.length}x em ${filePath}`); } return usages; } catch (error) { console.warn(`⚠️ Erro ao analisar arquivo ${filePath}:`, error); return []; } } /** * Analisa uso de actions customizadas da Clint no código * Detecta padrões como: clint.owner.getName, clint.user.createAccount */ analyzeClintUsage(sourceCode: string, filePath: string): Array<{pattern: string, line: number, column: number}> { const usages: Array<{pattern: string, line: number, column: number}> = []; const lines = sourceCode.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNumber = i + 1; // Detectar padrões clint.*.* const clintMatches = line.matchAll(/clint\.([a-zA-Z_]+)\.([a-zA-Z_]+)/g); for (const match of clintMatches) { const fullPattern = match[0]; // clint.owner.getName const column = match.index || 0; usages.push({ pattern: fullPattern, line: lineNumber, column: column }); } } return usages; } /** * Mapeia uso de padrões Clint para actions do Hasura */ mapClintUsageToActions(clintUsages: Array<{pattern: string, line: number, column: number}>): Array<{actionName: string, line: number, column: number}> { return clintUsages.map(usage => ({ actionName: this.clintPatternToAction(usage.pattern), line: usage.line, column: usage.column })); } /** * Converte padrão Clint para nome de action do Hasura * Exemplo: clint.owner.getName -> owner_get_name */ private clintPatternToAction(pattern: string): string { const match = pattern.match(/^clint\.([^.]+)\.([^.]+)$/); if (!match) { return pattern.replace('clint.', ''); } const [, entity, method] = match; // Converter camelCase para snake_case const actionPart = method.replace(/([A-Z])/g, '_$1').toLowerCase(); return `${entity}_${actionPart}`; } } export { ClintAction, ClintUsageResult };