graphql-lint-clint-platform
Version:
GraphQL unused fields linter for Clint platform - Custom patterns and actions.graphql support
252 lines (217 loc) • 7.58 kB
text/typescript
import { GraphQLQuery, GraphQLField } from "@graphql-lint/core";
import { Project, SourceFile, Node, SyntaxKind } from "ts-morph";
import { parse as babelParse } from "@babel/parser";
import traverse from "@babel/traverse";
import * as t from "@babel/types";
export class ClintUsageAnalyzer {
private project: Project;
private clintPatternUsages: Map<string, Array<{
file: string;
line: number;
column: number;
pattern: string;
}>> = new Map();
constructor() {
this.project = new Project({
useInMemoryFileSystem: true,
compilerOptions: {
allowJs: true,
allowSyntheticDefaultImports: true,
esModuleInterop: true,
},
});
}
/**
* Analisa uso de padrões Clint específicos: clint.entity.method
*/
async analyzeClintUsagePatterns(
actionsQueries: GraphQLQuery[],
fileContents: Map<string, string>
): Promise<Map<string, any[]>> {
console.log(`🔍 Analisando padrões Clint para ${actionsQueries.length} actions em ${fileContents.size} arquivos`);
// Para cada action, procurar o padrão correspondente no código
for (const action of actionsQueries) {
const clintPattern = this.actionToClintPattern(action.name);
if (clintPattern) {
await this.findClintPatternUsages(clintPattern, action.name, fileContents);
}
}
return this.clintPatternUsages;
}
/**
* Procura usos de um padrão Clint específico no código
* Ex: clint.owner.getName
*/
private async findClintPatternUsages(
clintPattern: string,
actionName: string,
fileContents: Map<string, string>
): Promise<void> {
for (const [filePath, content] of fileContents) {
if (filePath.includes('node_modules') || filePath.includes('.test.') || filePath.includes('.spec.')) {
continue;
}
try {
if (filePath.endsWith('.ts') || filePath.endsWith('.tsx')) {
await this.analyzeTypeScriptClintPattern(filePath, content, clintPattern, actionName);
} else if (filePath.endsWith('.js') || filePath.endsWith('.jsx')) {
await this.analyzeJavaScriptClintPattern(filePath, content, clintPattern, actionName);
}
} catch (error) {
console.warn(`⚠️ Erro ao analisar padrão Clint em ${filePath}:`, error);
}
}
}
/**
* Analisa padrões Clint em arquivos TypeScript
*/
private async analyzeTypeScriptClintPattern(
filePath: string,
content: string,
clintPattern: string,
actionName: string
): Promise<void> {
const sourceFile = this.project.createSourceFile(filePath, content, { overwrite: true });
// Procurar por member expressions que correspondem ao padrão Clint
// Ex: clint.owner.getName
sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression).forEach(propAccess => {
const fullText = propAccess.getText();
if (fullText === clintPattern) {
this.recordClintPatternUsage(actionName, {
file: filePath,
line: propAccess.getStartLineNumber(),
column: propAccess.getStart(),
pattern: clintPattern
});
}
});
// Procurar por call expressions do padrão
// Ex: clint.owner.getName() ou clint.owner.getName(id)
sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(callExpr => {
const expression = callExpr.getExpression();
if (Node.isPropertyAccessExpression(expression)) {
const fullText = expression.getText();
if (fullText === clintPattern) {
this.recordClintPatternUsage(actionName, {
file: filePath,
line: callExpr.getStartLineNumber(),
column: callExpr.getStart(),
pattern: `${clintPattern}()`
});
}
}
});
}
/**
* Analisa padrões Clint em arquivos JavaScript
*/
private async analyzeJavaScriptClintPattern(
filePath: string,
content: string,
clintPattern: string,
actionName: string
): Promise<void> {
try {
const ast = babelParse(content, {
sourceType: "module",
allowImportExportEverywhere: true,
plugins: ["jsx", "typescript", "decorators-legacy"],
});
traverse(ast, {
MemberExpression: (path) => {
const memberChain = this.getMemberExpressionChain(path.node);
const fullPattern = memberChain.join('.');
if (fullPattern === clintPattern) {
this.recordClintPatternUsage(actionName, {
file: filePath,
line: path.node.loc?.start.line || 0,
column: path.node.loc?.start.column || 0,
pattern: clintPattern
});
}
},
CallExpression: (path) => {
if (t.isMemberExpression(path.node.callee)) {
const memberChain = this.getMemberExpressionChain(path.node.callee);
const fullPattern = memberChain.join('.');
if (fullPattern === clintPattern) {
this.recordClintPatternUsage(actionName, {
file: filePath,
line: path.node.loc?.start.line || 0,
column: path.node.loc?.start.column || 0,
pattern: `${clintPattern}()`
});
}
}
}
});
} catch (error) {
console.warn(`⚠️ Erro ao parsear JavaScript para padrão Clint em ${filePath}:`, error);
}
}
/**
* Constrói cadeia de member expressions
* clint.owner.getName -> ['clint', 'owner', 'getName']
*/
private getMemberExpressionChain(memberExpr: any): string[] {
const chain: string[] = [];
let current = memberExpr;
while (t.isMemberExpression(current)) {
if (t.isIdentifier(current.property)) {
chain.unshift(current.property.name);
}
current = current.object;
}
if (t.isIdentifier(current)) {
chain.unshift(current.name);
}
return chain;
}
/**
* Registra uso de padrão Clint
*/
private recordClintPatternUsage(actionName: string, usage: {
file: string;
line: number;
column: number;
pattern: string;
}): void {
if (!this.clintPatternUsages.has(actionName)) {
this.clintPatternUsages.set(actionName, []);
}
this.clintPatternUsages.get(actionName)!.push(usage);
}
/**
* Converte nome de action para padrão Clint
* owner_get_name -> clint.owner.getName
*/
private actionToClintPattern(actionName: string): string | null {
const match = actionName.match(/^(\w+)_(.+)$/);
if (!match) return null;
const [, entity, action] = match;
// Converter action para camelCase
const camelAction = action.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
return `clint.${entity}.${camelAction}`;
}
/**
* Retorna estatísticas de uso dos padrões Clint
*/
getUsageStats(): {
totalActions: number;
usedActions: number;
unusedActions: string[];
usageDetails: Map<string, any[]>;
} {
const totalActions = this.clintPatternUsages.size;
const usedActions = Array.from(this.clintPatternUsages.values()).filter(usages => usages.length > 0).length;
const unusedActions = Array.from(this.clintPatternUsages.entries())
.filter(([, usages]) => usages.length === 0)
.map(([actionName]) => actionName);
return {
totalActions,
usedActions,
unusedActions,
usageDetails: this.clintPatternUsages
};
}
}