@netoalmanca/advpl-sensei 1.1.6 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/registry-quality-check.js +3 -0
- package/dist/commands/migrate.js +164 -0
- package/dist/commands/review.js +82 -0
- package/dist/function-registry.js +58 -755
- package/dist/index.js +90 -259
- package/dist/tdn-function-validator.js +13 -108
- package/dist/tdn-scraper.js +4 -549
- package/package.json +1 -1
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* Registry Quality Check - Ferranha para verificar qualidade do Function Registry
|
|
3
3
|
*/
|
|
4
4
|
import { RegistryAnalyzer } from "../registry-analyzer.js";
|
|
5
|
+
import { loadFunctionRegistry } from "../function-registry.js";
|
|
5
6
|
export async function checkRegistryQuality() {
|
|
6
7
|
console.log("\n🔍 Analisando Function Registry...\n");
|
|
8
|
+
// Garante que o registry está carregado do JSON
|
|
9
|
+
await loadFunctionRegistry();
|
|
7
10
|
const analysis = RegistryAnalyzer.analyze();
|
|
8
11
|
const report = RegistryAnalyzer.generateReport(analysis);
|
|
9
12
|
console.log(report);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: advpl_migrate
|
|
3
|
+
* Converte código ADVPL procedural (.prw) para TLPP OO (.tlpp)
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
export class AdvplMigrator {
|
|
8
|
+
/**
|
|
9
|
+
* Executa a migração de um arquivo
|
|
10
|
+
*/
|
|
11
|
+
static async migrate(options) {
|
|
12
|
+
const { target, output, namespace = "advpl.sensei.migrated", className: optClassName, wrapper = true } = options;
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(target, "utf-8");
|
|
15
|
+
const filename = path.basename(target);
|
|
16
|
+
const className = optClassName || filename.replace(".prw", "").replace(".tlpp", "");
|
|
17
|
+
const lines = content.split(/\r?\n/);
|
|
18
|
+
const migratedLines = [];
|
|
19
|
+
const functionsFound = [];
|
|
20
|
+
// 1. Headers e Namespace
|
|
21
|
+
migratedLines.push("#include 'tlpp-core.th'");
|
|
22
|
+
migratedLines.push("#include 'tlpp-rest.th'");
|
|
23
|
+
migratedLines.push("");
|
|
24
|
+
migratedLines.push(`namespace ${namespace}`);
|
|
25
|
+
migratedLines.push("");
|
|
26
|
+
migratedLines.push(`/**`);
|
|
27
|
+
migratedLines.push(` * Classe ${className} - Migrada automaticamente pelo Advpl Sensei`);
|
|
28
|
+
migratedLines.push(` */`);
|
|
29
|
+
migratedLines.push(`class ${className}`);
|
|
30
|
+
const methodDeclarations = [];
|
|
31
|
+
const methodImplementations = [];
|
|
32
|
+
let currentMethod = null;
|
|
33
|
+
let isStatic = false;
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
const upper = trimmed.toUpperCase();
|
|
38
|
+
// Ignora includes antigos
|
|
39
|
+
if (upper.startsWith("#INCLUDE")) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// Detecta funções
|
|
43
|
+
const funcMatch = trimmed.match(/^(USER\s+FUNCTION|STATIC\s+FUNCTION|FUNCTION)\s+([A-Za-z0-9_]+)\s*\((.*)\)/i);
|
|
44
|
+
if (funcMatch) {
|
|
45
|
+
const type = funcMatch[1].toUpperCase();
|
|
46
|
+
const name = funcMatch[2];
|
|
47
|
+
const params = funcMatch[3];
|
|
48
|
+
isStatic = type.includes("STATIC");
|
|
49
|
+
const access = isStatic ? "private" : "public";
|
|
50
|
+
functionsFound.push(name);
|
|
51
|
+
currentMethod = name;
|
|
52
|
+
// Adiciona declaração na classe
|
|
53
|
+
methodDeclarations.push(` ${access} method ${name}(${this.typeParams(params)})`);
|
|
54
|
+
// Inicia implementação do método
|
|
55
|
+
methodImplementations.push("");
|
|
56
|
+
methodImplementations.push(`/**`);
|
|
57
|
+
methodImplementations.push(` * Método ${name}`);
|
|
58
|
+
methodImplementations.push(` */`);
|
|
59
|
+
methodImplementations.push(`method ${name}(${this.typeParams(params)}) class ${className}`);
|
|
60
|
+
}
|
|
61
|
+
else if (currentMethod) {
|
|
62
|
+
// Conteúdo do método
|
|
63
|
+
if (upper.startsWith("RETURN")) {
|
|
64
|
+
methodImplementations.push(this.transformLine(line));
|
|
65
|
+
currentMethod = null;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
methodImplementations.push(this.transformLine(line));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Monta a classe final
|
|
73
|
+
migratedLines.push(...methodDeclarations);
|
|
74
|
+
migratedLines.push("endclass");
|
|
75
|
+
migratedLines.push("");
|
|
76
|
+
migratedLines.push(...methodImplementations);
|
|
77
|
+
const migratedContent = migratedLines.join("\n");
|
|
78
|
+
const outFilename = output || target.replace(".prw", ".tlpp");
|
|
79
|
+
let report = `🚀 Migração concluída com sucesso!\n\n`;
|
|
80
|
+
report += `📦 **Classe:** \`${className}\`\n`;
|
|
81
|
+
report += `🌐 **Namespace:** \`${namespace}\`\n`;
|
|
82
|
+
report += `🛠️ **Funções Migradas:** ${functionsFound.length}\n`;
|
|
83
|
+
functionsFound.forEach(f => report += ` - \`${f}\` -> \`oObj:${f}()\`\n`);
|
|
84
|
+
return {
|
|
85
|
+
success: true,
|
|
86
|
+
content: migratedContent,
|
|
87
|
+
filename: outFilename,
|
|
88
|
+
originalFunctions: functionsFound,
|
|
89
|
+
report
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
content: "",
|
|
96
|
+
filename: "",
|
|
97
|
+
originalFunctions: [],
|
|
98
|
+
report: `Erro na migração: ${e.message}`
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Tenta tipar parâmetros baseados em notação húngara
|
|
104
|
+
*/
|
|
105
|
+
static typeParams(params) {
|
|
106
|
+
if (!params.trim())
|
|
107
|
+
return "";
|
|
108
|
+
return params.split(",").map(p => {
|
|
109
|
+
const param = p.trim();
|
|
110
|
+
if (param.startsWith("c"))
|
|
111
|
+
return `${param} as string`;
|
|
112
|
+
if (param.startsWith("n"))
|
|
113
|
+
return `${param} as numeric`;
|
|
114
|
+
if (param.startsWith("l"))
|
|
115
|
+
return `${param} as logical`;
|
|
116
|
+
if (param.startsWith("d"))
|
|
117
|
+
return `${param} as date`;
|
|
118
|
+
if (param.startsWith("a"))
|
|
119
|
+
return `${param} as array`;
|
|
120
|
+
if (param.startsWith("o"))
|
|
121
|
+
return `${param} as object`;
|
|
122
|
+
return param;
|
|
123
|
+
}).join(", ");
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Transforma linhas de código (ex: Local -> tipado)
|
|
127
|
+
*/
|
|
128
|
+
static transformLine(line) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
const upper = trimmed.toUpperCase();
|
|
131
|
+
// Transforma Local cVar := "" para Local cVar as string := ""
|
|
132
|
+
if (upper.startsWith("LOCAL ")) {
|
|
133
|
+
const rest = line.substring(line.toUpperCase().indexOf("LOCAL ") + 6);
|
|
134
|
+
const parts = rest.split(",");
|
|
135
|
+
const typedParts = parts.map(p => {
|
|
136
|
+
const part = p.trim();
|
|
137
|
+
const varNameMatch = part.match(/^([a-z][A-Za-z0-9_]*)/);
|
|
138
|
+
if (varNameMatch) {
|
|
139
|
+
const varName = varNameMatch[1];
|
|
140
|
+
const type = this.getTlppType(varName);
|
|
141
|
+
if (type && !part.includes(" as ")) {
|
|
142
|
+
return part.replace(varName, `${varName} as ${type}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return p;
|
|
146
|
+
});
|
|
147
|
+
return line.substring(0, line.toUpperCase().indexOf("LOCAL ") + 6) + typedParts.join(", ");
|
|
148
|
+
}
|
|
149
|
+
return line;
|
|
150
|
+
}
|
|
151
|
+
static getTlppType(varName) {
|
|
152
|
+
const prefix = varName[0];
|
|
153
|
+
switch (prefix) {
|
|
154
|
+
case 'c': return 'string';
|
|
155
|
+
case 'n': return 'numeric';
|
|
156
|
+
case 'l': return 'logical';
|
|
157
|
+
case 'd': return 'date';
|
|
158
|
+
case 'a': return 'array';
|
|
159
|
+
case 'o': return 'object';
|
|
160
|
+
case 'b': return 'codeblock';
|
|
161
|
+
default: return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: advpl_review
|
|
3
|
+
* Executa revisão completa de código (Lint + Arquitetura)
|
|
4
|
+
*/
|
|
5
|
+
import { SenseiLinter } from "../linter.js";
|
|
6
|
+
export class AdvplReviewer {
|
|
7
|
+
/**
|
|
8
|
+
* Realiza o review de um arquivo
|
|
9
|
+
*/
|
|
10
|
+
static review(content, filename) {
|
|
11
|
+
// 1. Lint Básico
|
|
12
|
+
const lintResult = SenseiLinter.lint(content, filename);
|
|
13
|
+
// 2. Análise de Arquitetura
|
|
14
|
+
const archIssues = [];
|
|
15
|
+
const recommendations = [];
|
|
16
|
+
const upperContent = content.toUpperCase();
|
|
17
|
+
// Check para Area Handling
|
|
18
|
+
if (upperContent.includes("DBSELECTAREA") && !upperContent.includes("GETAREA")) {
|
|
19
|
+
archIssues.push("Uso de DbSelectArea sem salvamento de área (GetArea/RestArea).");
|
|
20
|
+
recommendations.push("Sempre utilize GetArea() e RestArea() ao manipular tabelas para evitar efeitos colaterais em outras rotinas.");
|
|
21
|
+
}
|
|
22
|
+
// Check para Error Handling
|
|
23
|
+
if (!upperContent.includes("BEGIN SEQUENCE") && content.length > 500) {
|
|
24
|
+
archIssues.push("Código extenso sem tratamento de erros (Begin Sequence).");
|
|
25
|
+
recommendations.push("Implemente blocos Begin Sequence / Recover para tornar o código mais resiliente a falhas de banco ou runtime.");
|
|
26
|
+
}
|
|
27
|
+
// Check para MVC (heurística simples)
|
|
28
|
+
if (upperContent.includes("FWMBROWSE") && !upperContent.includes("MODELDEF")) {
|
|
29
|
+
archIssues.push("Uso de FWMBrowse sem estrutura MVC clara.");
|
|
30
|
+
recommendations.push("Considere migrar para o padrão MVC completo (ModelDef, ViewDef, MenuDef) para melhor manutenibilidade.");
|
|
31
|
+
}
|
|
32
|
+
// Cálculo de Score
|
|
33
|
+
const totalIssues = lintResult.issues.length + archIssues.length;
|
|
34
|
+
let score = 100 - (totalIssues * 5);
|
|
35
|
+
if (score < 0)
|
|
36
|
+
score = 0;
|
|
37
|
+
// Sumário
|
|
38
|
+
let summary = `📊 Review de \`${filename}\` concluído.\n`;
|
|
39
|
+
summary += `🎯 **Score:** ${score}/100\n`;
|
|
40
|
+
summary += `⚠️ **Problemas de Lint:** ${lintResult.issues.length}\n`;
|
|
41
|
+
summary += `🏛️ **Problemas de Arquitetura:** ${archIssues.length}\n`;
|
|
42
|
+
return {
|
|
43
|
+
filename,
|
|
44
|
+
score,
|
|
45
|
+
lint: lintResult,
|
|
46
|
+
architectureIssues: archIssues,
|
|
47
|
+
recommendations,
|
|
48
|
+
summary
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Formata o resultado para exibição no MCP
|
|
53
|
+
*/
|
|
54
|
+
static formatResult(result) {
|
|
55
|
+
let output = result.summary + "\n";
|
|
56
|
+
if (result.lint.issues.length > 0) {
|
|
57
|
+
output += `### 🔍 Problemas de Estilo/Lint\n`;
|
|
58
|
+
result.lint.issues.forEach(issue => {
|
|
59
|
+
const icon = issue.severity === "error" ? "🔴" : "🟡";
|
|
60
|
+
output += `- ${icon} [${issue.code}] Linha ${issue.line}: ${issue.message}\n`;
|
|
61
|
+
});
|
|
62
|
+
output += "\n";
|
|
63
|
+
}
|
|
64
|
+
if (result.architectureIssues.length > 0) {
|
|
65
|
+
output += `### 🏛️ Problemas de Arquitetura\n`;
|
|
66
|
+
result.architectureIssues.forEach(issue => {
|
|
67
|
+
output += `- ❌ ${issue}\n`;
|
|
68
|
+
});
|
|
69
|
+
output += "\n";
|
|
70
|
+
}
|
|
71
|
+
if (result.recommendations.length > 0) {
|
|
72
|
+
output += `### 💡 Recomendações do Sensei\n`;
|
|
73
|
+
result.recommendations.forEach(rec => {
|
|
74
|
+
output += `- ${rec}\n`;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (result.score >= 90) {
|
|
78
|
+
output += `\n✅ **Parabéns!** O código segue as melhores práticas do ecossistema Protheus.`;
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
82
|
+
}
|