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
text/typescript
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 };