@mocoto/mahoraga 0.14.2 → 0.14.3

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.
Files changed (105) hide show
  1. package/README.md +0 -34
  2. package/dist/analysts/architects/analyst-structure.js +19 -19
  3. package/dist/analysts/architects/archetypes-custom.js +8 -6
  4. package/dist/analysts/architects/detector-archetypes.js +30 -27
  5. package/dist/analysts/architects/signals-project-advanced.js +24 -26
  6. package/dist/analysts/architects/signals-project.js +1 -1
  7. package/dist/analysts/architects/strategist-structure.js +47 -66
  8. package/dist/analysts/azure-pipelines/analysts/analyst-azure.js +21 -14
  9. package/dist/analysts/azure-pipelines/corrections/correction-azure.js +22 -19
  10. package/dist/analysts/azure-pipelines/detectors/detector-azure-performance.js +21 -18
  11. package/dist/analysts/azure-pipelines/detectors/detector-azure-security.js +25 -20
  12. package/dist/analysts/azure-pipelines/detectors/detector-azure-structure.js +39 -37
  13. package/dist/analysts/azure-pipelines/scorers/scorer-azure.js +10 -7
  14. package/dist/analysts/circleci/analysts/analyst-circleci.js +23 -15
  15. package/dist/analysts/circleci/corrections/correction-circleci.js +22 -20
  16. package/dist/analysts/circleci/detectors/detector-circleci-performance.js +23 -19
  17. package/dist/analysts/circleci/detectors/detector-circleci-security.js +78 -82
  18. package/dist/analysts/circleci/detectors/detector-circleci-structure.js +20 -17
  19. package/dist/analysts/converters/engines/converter-gha-to-gitlab.js +17 -15
  20. package/dist/analysts/corrections/analyst-quick-fixes.js +25 -28
  21. package/dist/analysts/corrections/analyst-scoring.js +24 -21
  22. package/dist/analysts/corrections/corrector-structure.js +35 -35
  23. package/dist/analysts/corrections/fix-alias-imports.js +10 -6
  24. package/dist/analysts/corrections/fix-md-fences.js +23 -23
  25. package/dist/analysts/corrections/map-reversion.js +54 -51
  26. package/dist/analysts/corrections/pruning.js +19 -16
  27. package/dist/analysts/corrections/quick-fixes/fix-any-to-proper-type.js +57 -61
  28. package/dist/analysts/corrections/quick-fixes/fix-unknown-to-specific-type.js +30 -30
  29. package/dist/analysts/corrections/reorganize-barrels.js +10 -10
  30. package/dist/analysts/corrections/scoring-shared.js +19 -18
  31. package/dist/analysts/corrections/type-safety/context-analyzer.js +15 -10
  32. package/dist/analysts/corrections/type-safety/type-validator.js +2 -1
  33. package/dist/analysts/corrections/type-safety/usage-analyzer.js +36 -39
  34. package/dist/analysts/css/analysts/analyst-css.js +16 -15
  35. package/dist/analysts/css/corrections/correction-css.js +19 -11
  36. package/dist/analysts/css/detectors/detector-css-best-practices.js +129 -118
  37. package/dist/analysts/css/scorers/scorer-css.js +23 -18
  38. package/dist/analysts/css-in-js/analysts/analyst-css-in-js.js +24 -17
  39. package/dist/analysts/css-in-js/corrections/correction-css-in-js.js +31 -16
  40. package/dist/analysts/css-in-js/detectors/detector-css-in-js-best-practices.js +20 -15
  41. package/dist/analysts/css-in-js/scorers/scorer-css-in-js.js +79 -54
  42. package/dist/analysts/css-in-js/shared.js +7 -0
  43. package/dist/analysts/detectors/detector-architecture.js +89 -83
  44. package/dist/analysts/detectors/detector-bugs-ml.js +21 -19
  45. package/dist/analysts/detectors/detector-code-fragile.js +30 -26
  46. package/dist/analysts/detectors/detector-duplications.js +21 -18
  47. package/dist/analysts/detectors/detector-monorepo.js +41 -36
  48. package/dist/analysts/detectors/detector-performance.js +22 -16
  49. package/dist/analysts/detectors/detector-recommendation-architecture.js +39 -35
  50. package/dist/analysts/detectors/detector-security.js +43 -42
  51. package/dist/analysts/detectors/detector-structure.js +29 -26
  52. package/dist/analysts/js-ts/detectors/detector-anti-patterns-async.js +4 -1
  53. package/dist/analysts/js-ts/detectors/detector-constructions-syntactic.js +10 -16
  54. package/dist/analysts/plugins/detector-documentation.js +1 -0
  55. package/dist/analysts/react/analysts/analyst-react-hooks.js +4 -2
  56. package/dist/analysts/react/analysts/analyst-react.js +1 -1
  57. package/dist/analysts/react/corrections/correction-react.js +2 -2
  58. package/dist/analysts/react/detectors/detector-react-best-practices.js +1 -1
  59. package/dist/analysts/registry/autodiscovery.js +2 -2
  60. package/dist/analysts/tailwind/corrections/correction-tailwind.js +0 -8
  61. package/dist/analysts/tailwind/scorers/scorer-tailwind.js +1 -2
  62. package/dist/cli/commands/command-analysts.js +3 -2
  63. package/dist/cli/commands/command-formatters.js +1 -1
  64. package/dist/cli/commands/command-github-actions.js +1 -1
  65. package/dist/cli/commands/command-guardian.js +1 -1
  66. package/dist/cli/commands/command-marketplace.js +4 -4
  67. package/dist/cli/diagnostic/filters.js +9 -9
  68. package/dist/core/config/chalk-safe.js +4 -4
  69. package/dist/core/config/config.js +5 -5
  70. package/dist/core/execution/analysis-cache.js +1 -1
  71. package/dist/core/execution/ast-cache.js +1 -3
  72. package/dist/core/execution/executor.js +2 -4
  73. package/dist/core/execution/scanner.js +1 -1
  74. package/dist/core/messages/en/analysts/report-circleci-messages.js +7 -7
  75. package/dist/core/messages/en/caretakers/caretaker-messages.js +1 -7
  76. package/dist/core/messages/ja/analysts/report-circleci-messages.js +7 -7
  77. package/dist/core/messages/ja/caretakers/caretaker-messages.js +1 -7
  78. package/dist/core/messages/pt/analysts/report-circleci-messages.js +7 -7
  79. package/dist/core/messages/pt/caretakers/caretaker-messages.js +1 -7
  80. package/dist/core/messages/zh/analysts/report-circleci-messages.js +7 -7
  81. package/dist/core/messages/zh/caretakers/caretaker-messages.js +1 -7
  82. package/dist/core/parsing/parser.js +2 -3
  83. package/dist/core/parsing/utils.js +1 -0
  84. package/dist/core/workers/worker-pool.js +0 -0
  85. package/dist/guardian/records.js +1 -0
  86. package/dist/guardian/sentinel.js +1 -0
  87. package/dist/guardian/watcher-hidden.js +1 -0
  88. package/dist/licenses/fs-utils.js +2 -1
  89. package/dist/licenses/header-options.js +1 -0
  90. package/dist/licenses/index.js +1 -1
  91. package/dist/reports/generator-report.js +3 -3
  92. package/dist/reports/report-patterns-usage.js +1 -1
  93. package/dist/shared/data-processing/fragment-report.js +1 -1
  94. package/dist/shared/formatters/engines/stylelint.js +40 -34
  95. package/dist/shared/formatters/formatters/markdown.js +1 -1
  96. package/dist/shared/helpers/framework-detector.js +5 -1
  97. package/dist/shared/persistence/persistence.js +13 -5
  98. package/dist/shared/plugins/core-plugin.js +3 -1
  99. package/dist/types/analysts/tailwind-correction.js +2 -0
  100. package/dist/types/index.js +3 -3
  101. package/dist/types/processing/filters.js +5 -4
  102. package/dist/vulnerabilities/npm-audit.js +1 -0
  103. package/dist/vulnerabilities/scanner.js +1 -1
  104. package/dist/vulnerabilities/vulnerabilities.js +1 -0
  105. package/package.json +5 -10
@@ -104,6 +104,35 @@ const visitorFragil = {
104
104
  visitorState.resultados.set('codigo-fragil', fragilidades);
105
105
  }
106
106
  };
107
+ function converterFragilidadesParaOcorrencias(fragilidades, relPath) {
108
+ const ocorrencias = [];
109
+ const porSeveridade = agruparPor(fragilidades, arquivo => arquivo.severidade ?? 'media');
110
+ for (const [severidade, items] of Object.entries(porSeveridade)) {
111
+ if (items.length === 0) {
112
+ continue;
113
+ }
114
+ const nivel = severidade === 'critica' || severidade === 'alta' ? 'erro' : severidade === 'media' ? 'aviso' : 'info';
115
+ const tipos = {};
116
+ for (const it of items) {
117
+ tipos[it.tipo] = (tipos[it.tipo] || 0) + 1;
118
+ }
119
+ const resumo = items.slice(0, 3).map(caminho => caminho.tipo).join(', ');
120
+ const amostra = items.slice(0, 5).map(it => `${it.tipo} (L${it.linha})`);
121
+ ocorrencias.push(criarOcorrencia({
122
+ tipo: 'codigo-fragil',
123
+ nivel,
124
+ mensagem: messages.DetectorCodigoFragilMensagens.fragilidadesResumo(severidade, resumo, {
125
+ severidade,
126
+ total: items.length,
127
+ tipos,
128
+ amostra
129
+ }),
130
+ relPath,
131
+ linha: items[0].linha
132
+ }));
133
+ }
134
+ return ocorrencias;
135
+ }
107
136
  export const analistaCodigoFragil = {
108
137
  nome: 'codigo-fragil',
109
138
  categoria: 'qualidade',
@@ -170,32 +199,7 @@ export const analistaCodigoFragil = {
170
199
  }
171
200
  // Detecções de texto complementares (Heurísticas)
172
201
  detectarProblemasAvancados(src, fragilidades);
173
- // Converter para ocorrências Mahoraga
174
- const ocorrencias = [];
175
- const porSeveridade = agruparPor(fragilidades, arquivo => arquivo.severidade ?? 'media');
176
- for (const [severidade, items] of Object.entries(porSeveridade)) {
177
- if (items.length > 0) {
178
- const nivel = severidade === 'critica' || severidade === 'alta' ? 'erro' : severidade === 'media' ? 'aviso' : 'info';
179
- const resumo = items.slice(0, 3).map(caminho => caminho.tipo).join(', ');
180
- const tipos = {};
181
- for (const it of items) {
182
- tipos[it.tipo] = (tipos[it.tipo] || 0) + 1;
183
- }
184
- const amostra = items.slice(0, 5).map(it => `${it.tipo} (L${it.linha})`);
185
- ocorrencias.push(criarOcorrencia({
186
- tipo: 'codigo-fragil',
187
- nivel,
188
- mensagem: messages.DetectorCodigoFragilMensagens.fragilidadesResumo(severidade, resumo, {
189
- severidade,
190
- total: items.length,
191
- tipos,
192
- amostra
193
- }),
194
- relPath,
195
- linha: items[0].linha
196
- }));
197
- }
198
- }
202
+ const ocorrencias = converterFragilidadesParaOcorrencias(fragilidades, relPath);
199
203
  return filtrarOcorrenciasSuprimidas(ocorrencias, 'codigo-fragil', src);
200
204
  }
201
205
  };
@@ -40,24 +40,7 @@ export const analistaDuplicacoes = {
40
40
  if (duplicacoes.length === 0) {
41
41
  return [];
42
42
  }
43
- // Agrupar por tipo de similaridade
44
- const porTipo = agruparPorTipoSimilaridade(duplicacoes);
45
- const ocorrencias = [];
46
- for (const [tipo, dups] of Object.entries(porTipo)) {
47
- if (dups.length > 0) {
48
- const nivel = tipo === 'identica' ? 'aviso' : 'info';
49
- const primeiraFunc = dups[0]?.funcaoA;
50
- const resumo = dups.map(diretorio => `${diretorio.funcaoA?.nome ?? 'unknown'} ≈ ${diretorio.funcaoB?.nome ?? 'unknown'} (${diretorio.similaridade.toFixed(0)}%)`).slice(0, 3).join(', ');
51
- ocorrencias.push(criarOcorrencia({
52
- tipo: 'codigo_duplicado',
53
- nivel,
54
- mensagem: messages.DetectorAgregadosMensagens.duplicacoesResumo(tipo, resumo, dups.length),
55
- relPath,
56
- linha: primeiraFunc?.inicio ?? 1
57
- }));
58
- }
59
- }
60
- return ocorrencias;
43
+ return agruparEConverterDuplicacoes(duplicacoes, relPath);
61
44
  }
62
45
  catch (erro) {
63
46
  return [criarOcorrencia({
@@ -70,6 +53,26 @@ export const analistaDuplicacoes = {
70
53
  }
71
54
  }
72
55
  };
56
+ function agruparEConverterDuplicacoes(duplicacoes, relPath) {
57
+ const porTipo = agruparPorTipoSimilaridade(duplicacoes);
58
+ const ocorrencias = [];
59
+ for (const [tipo, dups] of Object.entries(porTipo)) {
60
+ if (dups.length === 0) {
61
+ continue;
62
+ }
63
+ const nivel = tipo === 'identica' ? 'aviso' : 'info';
64
+ const primeiraFunc = dups[0]?.funcaoA;
65
+ const resumo = dups.map(diretorio => `${diretorio.funcaoA?.nome ?? 'unknown'} ≈ ${diretorio.funcaoB?.nome ?? 'unknown'} (${diretorio.similaridade.toFixed(0)}%)`).slice(0, 3).join(', ');
66
+ ocorrencias.push(criarOcorrencia({
67
+ tipo: 'codigo_duplicado',
68
+ nivel,
69
+ mensagem: messages.DetectorAgregadosMensagens.duplicacoesResumo(tipo, resumo, dups.length),
70
+ relPath,
71
+ linha: primeiraFunc?.inicio ?? 1
72
+ }));
73
+ }
74
+ return ocorrencias;
75
+ }
73
76
  function extrairFuncoes(ast, caminho, src) {
74
77
  const funcoes = [];
75
78
  const linhas = src.split('\n');
@@ -14,7 +14,7 @@ export function detectarMonorepo(baseDir) {
14
14
  return info;
15
15
  }
16
16
  // Detect package manager
17
- const pm = rootPkg.packageManager || '';
17
+ const pm = rootPkg.packageManager ?? '';
18
18
  if (pm) {
19
19
  info.packageManager = pm;
20
20
  }
@@ -237,47 +237,52 @@ export const detectorMonorepo = {
237
237
  versionMap.set(dep, verMap);
238
238
  }
239
239
  }
240
- for (const [dep, versions] of versionMap) {
241
- if (versions.size > 1) {
242
- const uniqueVersions = new Set(versions.values());
243
- if (uniqueVersions.size > 1) {
244
- ocorrencias.push({
245
- tipo: 'monorepo-versao-mismatch',
246
- nivel: 'aviso',
247
- mensagem: `Dependência ${dep} com versões diferentes entre workspaces: ${Array.from(versions.entries()).map(([w, v]) => `${w}@${v}`).join(', ')}`,
248
- origem: 'detector-monorepo',
249
- relPath: '[monorepo]',
250
- linha: 0,
251
- });
252
- }
240
+ detectarVersionMismatch(versionMap, ocorrencias);
241
+ detectarDependenciasInternasFaltando(info, ocorrencias);
242
+ return ocorrencias;
243
+ },
244
+ };
245
+ function detectarVersionMismatch(versionMap, ocorrencias) {
246
+ for (const [dep, versions] of versionMap) {
247
+ if (versions.size > 1) {
248
+ const uniqueVersions = new Set(versions.values());
249
+ if (uniqueVersions.size > 1) {
250
+ ocorrencias.push({
251
+ tipo: 'monorepo-versao-mismatch',
252
+ nivel: 'aviso',
253
+ mensagem: `Dependência ${dep} com versões diferentes entre workspaces: ${Array.from(versions.entries()).map(([w, v]) => `${w}@${v}`).join(', ')}`,
254
+ origem: 'detector-monorepo',
255
+ relPath: '[monorepo]',
256
+ linha: 0,
257
+ });
253
258
  }
254
259
  }
255
- // Detect missing internal dependencies (workspace A uses workspace B but B isn't in A's deps)
256
- const workspaceNames = new Set(info.workspaces.map(w => w.name));
257
- for (const ws of info.workspaces) {
258
- const imports = coletarImports(ws.dir);
259
- for (const imp of imports) {
260
- for (const wsName of workspaceNames) {
261
- if (imp.includes(wsName) && wsName !== ws.name) {
262
- const isDep = ws.dependencies[wsName] || ws.devDependencies[wsName] || ws.peerDependencies[wsName];
263
- if (!isDep) {
264
- ocorrencias.push({
265
- tipo: 'monorepo-dependencia-interna-faltando',
266
- nivel: 'aviso',
267
- mensagem: `${ws.name} importa ${wsName} mas não o declara como dependência`,
268
- origem: 'detector-monorepo',
269
- relPath: `[${ws.name}]`,
270
- linha: 0,
271
- });
272
- }
273
- break;
260
+ }
261
+ }
262
+ function detectarDependenciasInternasFaltando(info, ocorrencias) {
263
+ const workspaceNames = new Set(info.workspaces.map(w => w.name));
264
+ for (const ws of info.workspaces) {
265
+ const imports = coletarImports(ws.dir);
266
+ for (const imp of imports) {
267
+ for (const wsName of workspaceNames) {
268
+ if (imp.includes(wsName) && wsName !== ws.name) {
269
+ const isDep = ws.dependencies[wsName] || ws.devDependencies[wsName] || ws.peerDependencies[wsName];
270
+ if (!isDep) {
271
+ ocorrencias.push({
272
+ tipo: 'monorepo-dependencia-interna-faltando',
273
+ nivel: 'aviso',
274
+ mensagem: `${ws.name} importa ${wsName} mas não o declara como dependência`,
275
+ origem: 'detector-monorepo',
276
+ relPath: `[${ws.name}]`,
277
+ linha: 0,
278
+ });
274
279
  }
280
+ break;
275
281
  }
276
282
  }
277
283
  }
278
- return ocorrencias;
279
- },
280
- };
284
+ }
285
+ }
281
286
  function coletarImports(dir) {
282
287
  const imports = [];
283
288
  try {
@@ -169,6 +169,27 @@ function detectarPadroesPerformance(src, problemas, relPath) {
169
169
  }
170
170
  });
171
171
  }
172
+ function detectarTimerSemCleanup(path, ast, problemas, relPath) {
173
+ if (path.node.callee.type !== 'Identifier') {
174
+ return;
175
+ }
176
+ if (path.node.callee.name !== 'setTimeout' && path.node.callee.name !== 'setInterval') {
177
+ return;
178
+ }
179
+ const parentFunction = path.getFunctionParent();
180
+ const context = parentFunction ? parentFunction.toString() : ast.toString();
181
+ if (context.includes('clear') || relPath.includes('test')) {
182
+ return;
183
+ }
184
+ problemas.push({
185
+ tipo: 'memory-leak',
186
+ descricao: `${path.node.callee.name} sem cleanup pode causar vazamento`,
187
+ impacto: 'medio',
188
+ linha: path.node.loc?.start.line ?? 0,
189
+ coluna: path.node.loc?.start.column ?? 0,
190
+ sugestao: `Use clear${path.node.callee.name.replace('set', '')} para limpar`
191
+ });
192
+ }
172
193
  function detectarProblemasPerformanceAST(ast, problemas, relPath) {
173
194
  try {
174
195
  const isLoop = (type) => ['ForStatement', 'WhileStatement', 'DoWhileStatement', 'ForInStatement', 'ForOfStatement'].includes(type);
@@ -195,23 +216,8 @@ function detectarProblemasPerformanceAST(ast, problemas, relPath) {
195
216
  DoWhileStatement: checkNesting,
196
217
  ForInStatement: checkNesting,
197
218
  ForOfStatement: checkNesting,
198
- // Detectar função setTimeout/setInterval sem cleanup
199
219
  CallExpression(path) {
200
- if (path.node.callee.type === 'Identifier' && (path.node.callee.name === 'setTimeout' || path.node.callee.name === 'setInterval')) {
201
- // Heurística simples: se não há clear* na mesma função/arquivo
202
- const parentFunction = path.getFunctionParent();
203
- const context = parentFunction ? parentFunction.toString() : ast.toString();
204
- if (!context.includes('clear') && !relPath.includes('test')) {
205
- problemas.push({
206
- tipo: 'memory-leak',
207
- descricao: `${path.node.callee.name} sem cleanup pode causar vazamento`,
208
- impacto: 'medio',
209
- linha: path.node.loc?.start.line ?? 0,
210
- coluna: path.node.loc?.start.column ?? 0,
211
- sugestao: `Use clear${path.node.callee.name.replace('set', '')} para limpar`
212
- });
213
- }
214
- }
220
+ detectarTimerSemCleanup(path, ast, problemas, relPath);
215
221
  }
216
222
  });
217
223
  }
@@ -3,6 +3,43 @@ import { criarOcorrencia } from '../../types/index.js';
3
3
  * Analista global que analisa o projeto completo e recomenda
4
4
  * a melhor arquitetura (Clean Architecture, Microsserviços, MVC, etc.)
5
5
  */
6
+ const CONFIANCA_SIMPLES = 60;
7
+ function adicionarOcorrenciasRecomendacao(ocorrencias, _relPath, sinaisCleanArch, sinaisDistributed, recomendacoes, arquiteturaRecomendada, confianca) {
8
+ ocorrencias.push(criarOcorrencia({
9
+ tipo: 'recomendacao-arquitetura',
10
+ nivel: 'info',
11
+ relPath: _relPath,
12
+ linha: 1,
13
+ mensagem: `[TARGET] Arquitetura recomendada: ${arquiteturaRecomendada} (${confianca}% confiança)`
14
+ }));
15
+ if (sinaisCleanArch.length > 0) {
16
+ ocorrencias.push(criarOcorrencia({
17
+ tipo: 'sinais-clean-arch',
18
+ nivel: 'info',
19
+ relPath: _relPath,
20
+ linha: 1,
21
+ mensagem: `Sinais de Clean Architecture: ${sinaisCleanArch.join(', ')}`
22
+ }));
23
+ }
24
+ if (sinaisDistributed.length > 0) {
25
+ ocorrencias.push(criarOcorrencia({
26
+ tipo: 'sinais-distributed-arch',
27
+ nivel: 'info',
28
+ relPath: _relPath,
29
+ linha: 1,
30
+ mensagem: `Sinais de Arquitetura Distribuída: ${sinaisDistributed.join(', ')}`
31
+ }));
32
+ }
33
+ for (const rec of recomendacoes.slice(0, 5)) {
34
+ ocorrencias.push(criarOcorrencia({
35
+ tipo: 'recomendacao-arquitetura-item',
36
+ nivel: 'info',
37
+ relPath: _relPath,
38
+ linha: 1,
39
+ mensagem: `[IDEA] ${rec}`
40
+ }));
41
+ }
42
+ }
6
43
  export const analistaRecomendacaoArquitetura = {
7
44
  nome: 'recomendacao-arquitetura',
8
45
  categoria: 'arquitetura',
@@ -221,7 +258,7 @@ export const analistaRecomendacaoArquitetura = {
221
258
  if (melhorScore === 0) {
222
259
  if (complexidadeBaixa) {
223
260
  arquiteturaRecomendada = 'Arquitetura Simples / Monolítica';
224
- confianca = 60;
261
+ confianca = CONFIANCA_SIMPLES;
225
262
  recomendacoes.push('Projeto de baixa complexidade - comece com uma estrutura simples');
226
263
  recomendacoes.push('Considere evoluir para Clean Architecture conforme o projeto crescer');
227
264
  }
@@ -243,40 +280,7 @@ export const analistaRecomendacaoArquitetura = {
243
280
  if (totalArquivos > 100 && servicosSet.size === 0) {
244
281
  recomendacoes.push('Projeto grande e monolítico - considere dividir em serviços menores');
245
282
  }
246
- ocorrencias.push(criarOcorrencia({
247
- tipo: 'recomendacao-arquitetura',
248
- nivel: 'info',
249
- relPath: _relPath,
250
- linha: 1,
251
- mensagem: `[TARGET] Arquitetura recomendada: ${arquiteturaRecomendada} (${confianca}% confiança)`
252
- }));
253
- if (sinaisCleanArch.length > 0) {
254
- ocorrencias.push(criarOcorrencia({
255
- tipo: 'sinais-clean-arch',
256
- nivel: 'info',
257
- relPath: _relPath,
258
- linha: 1,
259
- mensagem: `Sinais de Clean Architecture: ${sinaisCleanArch.join(', ')}`
260
- }));
261
- }
262
- if (sinaisDistributed.length > 0) {
263
- ocorrencias.push(criarOcorrencia({
264
- tipo: 'sinais-distributed-arch',
265
- nivel: 'info',
266
- relPath: _relPath,
267
- linha: 1,
268
- mensagem: `Sinais de Arquitetura Distribuída: ${sinaisDistributed.join(', ')}`
269
- }));
270
- }
271
- for (const rec of recomendacoes.slice(0, 5)) {
272
- ocorrencias.push(criarOcorrencia({
273
- tipo: 'recomendacao-arquitetura-item',
274
- nivel: 'info',
275
- relPath: _relPath,
276
- linha: 1,
277
- mensagem: `[IDEA] ${rec}`
278
- }));
279
- }
283
+ adicionarOcorrenciasRecomendacao(ocorrencias, _relPath, sinaisCleanArch, sinaisDistributed, recomendacoes, arquiteturaRecomendada, confianca);
280
284
  return ocorrencias;
281
285
  }
282
286
  };
@@ -322,6 +322,47 @@ function detectarAsyncSemTryCatch(src, problemas) {
322
322
  }
323
323
  }
324
324
  }
325
+ function detectarCookieInseguro(callee, args, path, problemas) {
326
+ if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === 'cookie') {
327
+ const object = callee.object;
328
+ const isRequest = object.type === 'Identifier' && (object.name === 'req' || object.name === 'request');
329
+ if (!isRequest) {
330
+ const options = args[2];
331
+ let hasHttpOnly = false;
332
+ if (options.type === 'ObjectExpression') {
333
+ hasHttpOnly = options.properties.some((prop) => prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'httpOnly' && (prop.value.type === 'BooleanLiteral' ? prop.value.value : false));
334
+ }
335
+ if (!hasHttpOnly) {
336
+ problemas.push({
337
+ tipo: 'insecure-cookie',
338
+ descricao: 'Cookie configurado sem a flag httpOnly',
339
+ severidade: 'media',
340
+ linha: path.node.loc?.start.line ?? 0,
341
+ sugestao: 'Sempre use httpOnly: true para evitar acesso via JavaScript (XSS)'
342
+ });
343
+ }
344
+ }
345
+ }
346
+ }
347
+ function detectarSQLInjectionAST(callee, args, path, problemas) {
348
+ if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && ['query', 'execute', 'raw', 'queryRaw', 'executeRaw'].includes(callee.property.name)) {
349
+ const firstArg = args[0];
350
+ const isUnsafe = firstArg.type === 'TemplateLiteral' && firstArg.expressions.length > 0 || firstArg.type === 'BinaryExpression' && firstArg.operator === '+';
351
+ if (isUnsafe) {
352
+ const code = firstArg.type === 'TemplateLiteral' ? firstArg.quasis.map((delimitadorString) => delimitadorString.value.raw).join('') : '';
353
+ const temParametros = /['"`].*(\?|\$\d|:\w+).*['"`]/.test(code);
354
+ if (!temParametros) {
355
+ problemas.push({
356
+ tipo: 'sql-injection',
357
+ descricao: 'Possível SQL Injection detectada via AST (concatenação no primeiro argumento)',
358
+ severidade: 'critica',
359
+ linha: path.node.loc?.start.line ?? 0,
360
+ sugestao: 'Use queries parametrizadas ao invés de concatenar strings ou template literals com variáveis'
361
+ });
362
+ }
363
+ }
364
+ }
365
+ }
325
366
  function detectarProblemasAST(ast, problemas) {
326
367
  try {
327
368
  traverse(ast.node, {
@@ -351,48 +392,8 @@ function detectarProblemasAST(ast, problemas) {
351
392
  sugestao: 'Use função ao invés de string'
352
393
  });
353
394
  }
354
- // 2. res.cookie() sem httpOnly
355
- if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && callee.property.name === 'cookie') {
356
- const object = callee.object;
357
- const isRequest = object.type === 'Identifier' && (object.name === 'req' || object.name === 'request');
358
- if (!isRequest) {
359
- // Heurística: assume-se que se não é req/request, é provável ser a resposta (res)
360
- const options = args[2];
361
- let hasHttpOnly = false;
362
- if (options.type === 'ObjectExpression') {
363
- hasHttpOnly = options.properties.some(prop => prop.type === 'ObjectProperty' && prop.key.type === 'Identifier' && prop.key.name === 'httpOnly' && (prop.value.type === 'BooleanLiteral' ? prop.value.value : false));
364
- }
365
- if (!hasHttpOnly) {
366
- problemas.push({
367
- tipo: 'insecure-cookie',
368
- descricao: 'Cookie configurado sem a flag httpOnly',
369
- severidade: 'media',
370
- linha: path.node.loc?.start.line ?? 0,
371
- sugestao: 'Sempre use httpOnly: true para evitar acesso via JavaScript (XSS)'
372
- });
373
- }
374
- }
375
- }
376
- // 3. SQL Injection (básico via AST)
377
- if (callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && ['query', 'execute', 'raw', 'queryRaw', 'executeRaw'].includes(callee.property.name)) {
378
- const firstArg = args[0];
379
- // Se o primeiro argumento é uma TemplateLiteral com expressões ou uma BinaryExpression (+)
380
- const isUnsafe = firstArg.type === 'TemplateLiteral' && firstArg.expressions.length > 0 || firstArg.type === 'BinaryExpression' && firstArg.operator === '+';
381
- if (isUnsafe) {
382
- // Verificar se parece usar parâmetros de qualquer forma
383
- const code = firstArg.type === 'TemplateLiteral' ? firstArg.quasis.map(delimitadorString => delimitadorString.value.raw).join('') : '';
384
- const temParametros = /['"`].*(\?|\$\d|:\w+).*['"`]/.test(code);
385
- if (!temParametros) {
386
- problemas.push({
387
- tipo: 'sql-injection',
388
- descricao: 'Possível SQL Injection detectada via AST (concatenação no primeiro argumento)',
389
- severidade: 'critica',
390
- linha: path.node.loc?.start.line ?? 0,
391
- sugestao: 'Use queries parametrizadas ao invés de concatenar strings ou template literals com variáveis'
392
- });
393
- }
394
- }
395
- }
395
+ detectarCookieInseguro(callee, args, path, problemas);
396
+ detectarSQLInjectionAST(callee, args, path, problemas);
396
397
  }
397
398
  });
398
399
  }
@@ -4,6 +4,32 @@ import { config, getAllExpectedDirNames, getDirVariantPattern, getNameConvention
4
4
  import { messages } from '../../core/messages/index.js';
5
5
  import { grafoDependencias } from './detector-dependencies.js';
6
6
  export const sinaisDetectados = {};
7
+ function verificarConvencoesNomesDiretorios(ocorrencias, caminhosNorm) {
8
+ const expectedDirs = getAllExpectedDirNames();
9
+ if (expectedDirs.length === 0) {
10
+ return;
11
+ }
12
+ const topLevelDirs = new Set(caminhosNorm.filter(caminho => caminho.includes('/') && !caminho.startsWith('node_modules/') && !caminho.startsWith('dist/')).map(caminho => caminho.split('/')[0]));
13
+ const configuredNames = new Set(Object.values(getNameConventions()).filter((valor) => typeof valor === 'string' && valor.trim().length > 0).map(valor => valor.trim().toLowerCase()));
14
+ for (const { key, expected } of expectedDirs) {
15
+ const expectedLower = expected.toLowerCase();
16
+ const match = [...topLevelDirs].find(diretorio => diretorio.toLowerCase() === expectedLower);
17
+ if (!match) {
18
+ const variantPattern = getDirVariantPattern(key);
19
+ const foundVariant = [...topLevelDirs].find(diretorio => variantPattern.test(diretorio));
20
+ if (foundVariant && !configuredNames.has(foundVariant.toLowerCase())) {
21
+ ocorrencias.push({
22
+ tipo: 'name-convention',
23
+ nivel: 'aviso',
24
+ mensagem: messages.DetectorEstruturaMensagens.nomeDiretorioNaoConforme(foundVariant, expected),
25
+ origem: 'detector-estrutura',
26
+ relPath: `[raiz do projeto]`,
27
+ linha: 0
28
+ });
29
+ }
30
+ }
31
+ }
32
+ }
7
33
  /**
8
34
  * Analisa a estrutura do projeto e detecta padrões como monorepo, fullstack, mistura de src/packages, etc.
9
35
  * Retorna ocorrências para cada sinal relevante encontrado.
@@ -24,7 +50,7 @@ export const detectorEstrutura = {
24
50
  // Conjuntos para buscas O(1) e deduplicação implícita
25
51
  const setCaminhos = new Set(caminhos);
26
52
  // Detecta contexto geral do projeto para análise inteligente
27
- const arquivoPrincipal = caminhos?.find(caminho => /package\.json$/.test(caminho)) ?? caminhos?.[0];
53
+ const arquivoPrincipal = caminhos?.find(caminho => /package\.json$/.test(caminho)) ?? caminhos[0];
28
54
  const conteudoPrincipal = contexto.arquivos?.find(arquivo => arquivo.relPath === arquivoPrincipal)?.content ?? '';
29
55
  const contextoProjeto = detectarContextoProjeto({
30
56
  arquivo: arquivoPrincipal,
@@ -160,7 +186,7 @@ export const detectorEstrutura = {
160
186
  // Múltiplos entrypoints
161
187
  // Lista de entrypoints potencialmente grande em repositórios com dependências; filtra ignorados
162
188
  const entrypointsAll = caminhosNorm.filter(caminho => /(^|[\\/])(cli|index|main)\.(ts|js)$/.test(caminho));
163
- const ignoredDyn = config.INCLUDE_EXCLUDE_RULES.globalExcludeGlob ?? [];
189
+ const ignoredDyn = config.INCLUDE_EXCLUDE_RULES.globalExcludeGlob;
164
190
  const entrypoints = entrypointsAll.filter(caminho => !micromatch.isMatch(caminho, ignoredDyn)).sort();
165
191
  if (entrypoints.length > 1) {
166
192
  const MAX_LIST = 20;
@@ -220,30 +246,7 @@ export const detectorEstrutura = {
220
246
  linha: 0
221
247
  });
222
248
  }
223
- // Verifica convenções de nomes de diretórios
224
- const expectedDirs = getAllExpectedDirNames();
225
- if (expectedDirs.length > 0) {
226
- const topLevelDirs = new Set(caminhosNorm.filter(caminho => caminho.includes('/') && !caminho.startsWith('node_modules/') && !caminho.startsWith('dist/')).map(caminho => caminho.split('/')[0]));
227
- const configuredNames = new Set(Object.values(getNameConventions()).filter((valor) => typeof valor === 'string' && valor.trim().length > 0).map(valor => valor.trim().toLowerCase()));
228
- for (const { key, expected } of expectedDirs) {
229
- const expectedLower = expected.toLowerCase();
230
- const match = [...topLevelDirs].find(diretorio => diretorio.toLowerCase() === expectedLower);
231
- if (!match) {
232
- const variantPattern = getDirVariantPattern(key);
233
- const foundVariant = [...topLevelDirs].find(diretorio => variantPattern.test(diretorio));
234
- if (foundVariant && !configuredNames.has(foundVariant.toLowerCase())) {
235
- ocorrencias.push({
236
- tipo: 'name-convention',
237
- nivel: 'aviso',
238
- mensagem: messages.DetectorEstruturaMensagens.nomeDiretorioNaoConforme(foundVariant, expected),
239
- origem: 'detector-estrutura',
240
- relPath: `[raiz do projeto]`,
241
- linha: 0
242
- });
243
- }
244
- }
245
- }
246
- }
249
+ verificarConvencoesNomesDiretorios(ocorrencias, caminhosNorm);
247
250
  return Array.isArray(ocorrencias) ? ocorrencias : [];
248
251
  }
249
252
  };
@@ -276,7 +276,10 @@ export const analistaAntiPadroesAsync = {
276
276
  traverse(nodeToTraverse, visitorAsync, undefined, stateObj);
277
277
  // Top-level await detection via text
278
278
  if (/^\s*await\s+/m.test(src)) {
279
- const topLevelAwaitLine = src.match(/^\s*await\s+/m) ? src.substring(0, src.match(/^\s*await\s+/m).index).split('\n').length : 1;
279
+ const topLevelAwaitLine = (() => {
280
+ const m = src.match(/^\s*await\s+/m);
281
+ return m ? src.substring(0, m.index).split('\n').length : 1;
282
+ })();
280
283
  const fnParentLines = new Set();
281
284
  const fnVisitor = {
282
285
  Function(path) {
@@ -208,23 +208,20 @@ export const analistaConstrucoesSintaticas = {
208
208
  });
209
209
  // Text-based detection for ES2025/ES2026 patterns
210
210
  const esPatterns = [
211
- [/\busing\s+\w+\s*=\s*new\s+/g, 'explicit-resource-management', (linha) => pushExtra('explicit-resource-management', 'info', 'Explicit Resource Management with "using" (ES2026)', linha)],
212
- [/\bawait\s+using\s+/g, 'explicit-resource-management-async', (linha) => pushExtra('explicit-resource-management-async', 'info', 'Async Resource Management with "await using" (ES2026)', linha)],
213
- [/\bTemporal\.\w+/g, 'temporal-api-usage', (linha) => pushExtra('temporal-api-usage', 'info', 'Temporal API usage (ES2026)', linha)],
214
- [/Promise\.withResolvers\s*\(/g, 'promise-with-resolvers', (linha) => pushExtra('promise-with-resolvers', 'info', 'Promise.withResolvers() (ES2025)', linha)],
215
- [/Promise\.try\s*\(/g, 'promise-try', (linha) => pushExtra('promise-try', 'info', 'Promise.try() (ES2025)', linha)],
216
- [/RegExp\.escape\s*\(/g, 'regexp-escape', (linha) => pushExtra('regexp-escape', 'info', 'RegExp.escape() (ES2025)', linha)],
217
- [/Error\.isError\s*\(/g, 'error-is-error', (linha) => pushExtra('error-is-error', 'info', 'Error.isError() (ES2026)', linha)],
218
- [/Array\.fromAsync\s*\(/g, 'array-from-async', (linha) => pushExtra('array-from-async', 'info', 'Array.fromAsync() (ES2026)', linha)],
219
- [/\bIterator\.from\s*\(/g, 'iterator-from', (linha) => pushExtra('iterator-from', 'info', 'Iterator.from() (ES2025)', linha)],
211
+ [/\busing\s+\w+\s*=\s*new\s+/g, 'explicit-resource-management', (linha) => { pushExtra('explicit-resource-management', 'info', 'Explicit Resource Management with "using" (ES2026)', linha); }],
212
+ [/\bawait\s+using\s+/g, 'explicit-resource-management-async', (linha) => { pushExtra('explicit-resource-management-async', 'info', 'Async Resource Management with "await using" (ES2026)', linha); }],
213
+ [/\bTemporal\.\w+/g, 'temporal-api-usage', (linha) => { pushExtra('temporal-api-usage', 'info', 'Temporal API usage (ES2026)', linha); }],
214
+ [/Promise\.withResolvers\s*\(/g, 'promise-with-resolvers', (linha) => { pushExtra('promise-with-resolvers', 'info', 'Promise.withResolvers() (ES2025)', linha); }],
215
+ [/Promise\.try\s*\(/g, 'promise-try', (linha) => { pushExtra('promise-try', 'info', 'Promise.try() (ES2025)', linha); }],
216
+ [/RegExp\.escape\s*\(/g, 'regexp-escape', (linha) => { pushExtra('regexp-escape', 'info', 'RegExp.escape() (ES2025)', linha); }],
217
+ [/Error\.isError\s*\(/g, 'error-is-error', (linha) => { pushExtra('error-is-error', 'info', 'Error.isError() (ES2026)', linha); }],
218
+ [/Array\.fromAsync\s*\(/g, 'array-from-async', (linha) => { pushExtra('array-from-async', 'info', 'Array.fromAsync() (ES2026)', linha); }],
219
+ [/\bIterator\.from\s*\(/g, 'iterator-from', (linha) => { pushExtra('iterator-from', 'info', 'Iterator.from() (ES2025)', linha); }],
220
220
  ];
221
221
  for (const [pattern, , fn] of esPatterns) {
222
222
  let m;
223
223
  while ((m = pattern.exec(src)) !== null) {
224
224
  const position = m.index;
225
- if (position == null) {
226
- continue;
227
- }
228
225
  if (!isInStringOrComment(src, position)) {
229
226
  const linha = src.substring(0, position).split('\n').length;
230
227
  fn(linha);
@@ -237,10 +234,7 @@ export const analistaConstrucoesSintaticas = {
237
234
  while ((match = templateLiteralPadrao.exec(src)) !== null) {
238
235
  const content = match[1];
239
236
  if (content && !content.includes('${') && content.length < 10) {
240
- const position = match.index;
241
- if (position == null) {
242
- continue;
243
- }
237
+ const position = match.index ?? 0;
244
238
  if (!isInStringOrComment(src, position)) {
245
239
  const linha = src.substring(0, position).split('\n').length;
246
240
  pushExtra('template-literal-overuse', 'info', messages.DetectorConstrucoesSintaticasMensagens.templateLiteralOveruse, linha);
@@ -173,6 +173,7 @@ function detectarPadroesDocumentacao(src, problemas, relPath) {
173
173
  // 2. Tem exportações públicas E
174
174
  // 3. Não tem nenhum JSDoc E
175
175
  // 4. Não é arquivo de teste/config/setup
176
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- vars mutated inside forEach closure; TS can't track
176
177
  if (isLibraryArquivo && temExportacaoPublica && !temJSDoc && !isInternalArquivo && src.length > 200) {
177
178
  // Arquivo grande o suficiente para justificar documentação
178
179
  problemas.push({
@@ -114,6 +114,7 @@ function collectHookIssues(src, relPath) {
114
114
  for (const match of src.matchAll(/\buse\s*\(/g)) {
115
115
  const line = lineOf(match.index);
116
116
  // Avoid matching "useState", "useEffect", etc.
117
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime safety for match.index === 0
117
118
  if (src[match.index - 1]?.match(/[a-zA-Z]/)) {
118
119
  continue;
119
120
  }
@@ -145,7 +146,7 @@ function parseHooksWithBabel(src, relPath) {
145
146
  if (isJsxLike) {
146
147
  plugins.unshift('jsx');
147
148
  }
148
- const ast = babelParse(src, { sourceType: 'unambiguous', errorRecovery: true, plugins });
149
+ const ast = babelParse(src, { sourceType: 'unambiguous', errorRecovery: true, plugins: plugins });
149
150
  const out = [];
150
151
  const seen = new Set();
151
152
  const pushOnce = (m) => {
@@ -161,8 +162,9 @@ function parseHooksWithBabel(src, relPath) {
161
162
  if (t.isIdentifier(callee)) {
162
163
  const name = callee.name;
163
164
  // React 19: use() is a special case (not prefixed with use but still a hook)
164
- if (name === 'use')
165
+ if (name === 'use') {
165
166
  return { name };
167
+ }
166
168
  return /^use[A-Z0-9_]/.test(name) ? { name } : null;
167
169
  }
168
170
  if (t.isMemberExpression(callee)) {