@mocoto/mahoraga 0.14.6 → 0.14.8

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 (33) hide show
  1. package/README.md +165 -511
  2. package/dist/analysts/css/analysts/analyst-css.js +10 -1
  3. package/dist/analysts/detectors/detector-bugs-ml.js +81 -19
  4. package/dist/analysts/js-ts/registrar.js +1 -1
  5. package/dist/analysts/react/analysts/analyst-react-hooks.js +109 -90
  6. package/dist/analysts/react/detectors/detector-react-best-practices.js +100 -6
  7. package/dist/cli/commands/command-perf.js +1 -1
  8. package/dist/cli/commands/command-update.js +3 -2
  9. package/dist/core/config/auto/auto-fix-config.js +9 -4
  10. package/dist/core/config/config.js +1 -1
  11. package/dist/core/messages/en/core/formatters-messages.js +1 -0
  12. package/dist/core/messages/ja/core/formatters-messages.js +1 -0
  13. package/dist/core/messages/pt/core/formatters-messages.js +1 -0
  14. package/dist/core/messages/shared/icons.js +3 -3
  15. package/dist/core/messages/zh/core/formatters-messages.js +1 -0
  16. package/dist/core/parsing/langs/json.js +2 -2
  17. package/dist/core/parsing/langs/typescript.js +33 -7
  18. package/dist/core/schema/version.js +1 -1
  19. package/dist/core/utils/exec-safe.js +47 -25
  20. package/dist/lib/api.js +14 -0
  21. package/dist/shared/data-processing/occurrences.js +3 -0
  22. package/dist/shared/formatters/code-min-formatter.js +1 -1
  23. package/dist/shared/formatters/formatters/commons.js +19 -5
  24. package/dist/shared/formatters/formatters/shell.js +128 -9
  25. package/dist/shared/helpers/ast-visitor-utils.js +3 -0
  26. package/dist/shared/helpers/imports.js +1 -1
  27. package/dist/shared/helpers/reader-report.js +13 -0
  28. package/dist/shared/helpers/structure.js +2 -2
  29. package/dist/types/guardian/result.js +38 -0
  30. package/dist/types/reports/processing.js +12 -0
  31. package/dist/types/shared/validation.js +111 -0
  32. package/dist/vulnerabilities/scanner.js +7 -2
  33. package/package.json +11 -18
@@ -179,7 +179,16 @@ const CONHECIDAS_CSS_PROPRIEDADES = new Set([
179
179
  // CSS3 e modernas
180
180
  'transform-origin', 'inset', 'background-clip', 'max-width', 'max-height', 'min-height', 'animation-delay', 'pointer-events', 'scroll-behavior', 'background-attachment', 'text-shadow', 'inset-inline', 'inset-block', 'inset-inline-start', 'inset-inline-end', 'inset-block-start', 'inset-block-end',
181
181
  // Outros comuns
182
- 'cursor', 'opacity', 'transform', 'transition', 'animation', 'list-style', 'vertical-align', 'text-overflow', 'box-shadow', 'border-radius', 'outline', 'content', 'counter-reset', 'counter-increment'
182
+ 'cursor', 'opacity', 'transform', 'transition', 'animation', 'list-style', 'vertical-align', 'text-overflow', 'box-shadow', 'border-radius', 'outline', 'content', 'counter-reset', 'counter-increment',
183
+ // CSS modernas (propriedades válidas e amplamente suportadas — ver feedback.md 1.4)
184
+ 'clip-path', 'backdrop-filter', 'aspect-ratio', 'accent-color', 'appearance', 'user-select',
185
+ 'object-fit', 'object-position', 'mix-blend-mode', 'filter', 'backdrop', 'will-change',
186
+ 'place-items', 'place-content', 'place-self', 'grid-auto-flow', 'grid-auto-columns', 'grid-auto-rows',
187
+ 'scroll-snap-type', 'scroll-snap-align', 'scroll-snap-stop', 'scroll-behavior', 'scroll-padding',
188
+ 'text-wrap', 'overflow-wrap', 'word-break', 'hyphens', 'line-break', 'paint-order',
189
+ 'touch-action', 'user-zoom', 'zoom', 'field-sizing', 'd', 'fill', 'stroke', 'stroke-width',
190
+ 'mask-image', 'mask-size', 'mask-position', 'mask-repeat', 'clip-rule', 'fill-rule',
191
+ 'isolation', 'contain', 'contain-intrinsic-size', 'content-visibility', 'rotate', 'scale', 'translate'
183
192
  ]);
184
193
  function isValidCssProperty(prop) {
185
194
  const normalized = prop.toLowerCase();
@@ -1,6 +1,6 @@
1
- // SPDX-License-Identifier: MIT
2
- // @mahoraga-disable ml-deep-nesting
3
- // Justificativa: O detector de aninhamento excessivo analisa o próprio código gerando falso positivo em auto-análise. O padrão deep-nesting é necessário para detecção de bugs.
1
+ import { parse as babelParse } from '@babel/parser';
2
+ import traverse from '@babel/traverse';
3
+ import * as t from '@babel/types';
4
4
  import { getMessages } from '../../core/messages/index.js';
5
5
  const MAX_MESSAGE_PREVIEW_LENGTH = 60;
6
6
  // Statistical bug patterns with ML-like scoring
@@ -180,27 +180,42 @@ const BUG_PATTERNS = [
180
180
  severity: 'aviso',
181
181
  category: 'logic',
182
182
  weight: 0.7,
183
- detect(content, _relPath) {
183
+ detect(content, relPath) {
184
+ // Contagem ingênua de todos os `()[]{}` globais gera falso positivo em
185
+ // bases modernas: aninhamento de JSX (divs aninhadas) e parênteses/colchetes
186
+ // de expressões inflam a profundidade sem aumentar a complexidade de
187
+ // decisão. Seguindo feedback.md seção 1.5, contamos apenas o aninhamento
188
+ // de BLOCOS DE LÓGICA (BlockStatement) via AST — JSX, literais de objeto/
189
+ // array e parênteses de expressão são ignorados. Arquivos não analisáveis
190
+ // mantêm o fallback conservador por contagem de colchetes.
191
+ const logic = computeLogicNestingDepth(content, relPath);
184
192
  let maxDepth = 0;
185
- let currentDepth = 0;
186
193
  let depthLine = 0;
187
- let lineCount = 1;
188
- for (const char of content) {
189
- if (char === '\n') {
190
- lineCount++;
191
- }
192
- if (char === '{' || char === '(' || char === '[') {
193
- currentDepth++;
194
- if (currentDepth > maxDepth) {
195
- maxDepth = currentDepth;
196
- depthLine = lineCount;
194
+ if (logic !== null) {
195
+ maxDepth = logic.depth;
196
+ depthLine = logic.line;
197
+ }
198
+ else {
199
+ let currentDepth = 0;
200
+ let lineCount = 1;
201
+ for (const char of content) {
202
+ if (char === '\n') {
203
+ lineCount++;
204
+ }
205
+ if (char === '{' || char === '(' || char === '[') {
206
+ currentDepth++;
207
+ if (currentDepth > maxDepth) {
208
+ maxDepth = currentDepth;
209
+ depthLine = lineCount;
210
+ }
211
+ }
212
+ else if (char === '}' || char === ')' || char === ']') {
213
+ currentDepth = Math.max(0, currentDepth - 1);
197
214
  }
198
- }
199
- else if (char === '}' || char === ')' || char === ']') {
200
- currentDepth = Math.max(0, currentDepth - 1);
201
215
  }
202
216
  }
203
- if (maxDepth > 6) {
217
+ // Limiar maior: sinaliza aninhamento de lógica genuinamente profundo.
218
+ if (maxDepth > 7) {
204
219
  return {
205
220
  confidence: Math.min(0.95, 0.5 + maxDepth * 0.06),
206
221
  line: depthLine,
@@ -447,6 +462,53 @@ function hasValidation(content, _line) {
447
462
  function contarLinhasAtePos(content, pos) {
448
463
  return content.slice(0, pos).split('\n').length;
449
464
  }
465
+ const traverseFn = traverse;
466
+ /**
467
+ * Calcula a profundidade de aninhamento de BLOCOS DE LÓGICA (BlockStatement)
468
+ * via AST, ignorando JSX, literais de objeto/array e parênteses de expressões
469
+ * — que a contagem ingênua de colchetes contava indevidamente (feedback.md 1.5).
470
+ * Retorna `null` quando o arquivo não é JS/TS ou não pôde ser parseado.
471
+ */
472
+ function computeLogicNestingDepth(content, relPath) {
473
+ if (!/\.(tsx?|jsx?|mjs|cjs)$/i.test(relPath)) {
474
+ return null;
475
+ }
476
+ try {
477
+ const lower = relPath.toLowerCase();
478
+ const isTs = lower.endsWith('.ts') || lower.endsWith('.tsx');
479
+ const isJsxLike = lower.endsWith('.tsx') || lower.endsWith('.jsx');
480
+ const plugins = ['decorators-legacy', 'classProperties', 'classPrivateProperties', 'classPrivateMethods', 'optionalChaining', 'nullishCoalescingOperator', 'topLevelAwait', 'importAttributes'];
481
+ if (isTs) {
482
+ plugins.unshift('typescript');
483
+ }
484
+ if (isJsxLike) {
485
+ plugins.unshift('jsx');
486
+ }
487
+ const ast = babelParse(content, { sourceType: 'unambiguous', errorRecovery: true, plugins: plugins });
488
+ let depth = 0;
489
+ let max = 0;
490
+ let maxLine = 0;
491
+ const visitor = {
492
+ BlockStatement: {
493
+ enter(path) {
494
+ depth++;
495
+ if (depth > max) {
496
+ max = depth;
497
+ maxLine = path.node.loc?.start.line ?? 0;
498
+ }
499
+ },
500
+ exit() {
501
+ depth--;
502
+ }
503
+ }
504
+ };
505
+ traverseFn(ast, visitor);
506
+ return { depth: max, line: maxLine };
507
+ }
508
+ catch {
509
+ return null;
510
+ }
511
+ }
450
512
  function detectarPadroesBug(src, relPath, ocorrencias) {
451
513
  for (const pattern of BUG_PATTERNS) {
452
514
  try {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  const detectoresJsTs = [];
6
6
  export function registrarDetectorJsTs(detector) {
7
- if (!detector.nome) {
7
+ if (!detector || !detector.nome) {
8
8
  return;
9
9
  }
10
10
  const exists = detectoresJsTs.some(d => d.nome === detector.nome);
@@ -106,73 +106,80 @@ function extractHookCallContent(src, startIndex) {
106
106
  }
107
107
  return content;
108
108
  }
109
- function collectHookIssues(src, relPath) {
109
+ function collectHookIssues(src, relPath, opts = {}) {
110
110
  const ocorrencias = [];
111
111
  const lineOf = createLineLookup(src).lineAt;
112
- // Check useEffect/useLayoutEffect for deps
113
- const effectMatches = [...src.matchAll(/use(Layout)?Effect\s*\(/g)];
114
- effectMatches.forEach(m => {
115
- const hookInicio = m.index;
116
- const fullCall = extractHookCallContent(src, hookInicio + m[0].length - 1);
117
- // More robust deps detection: find the last comma followed by an array or identifier before the closing paren
118
- const hasDepsArg = (() => {
119
- // Find the last ')' (closing paren of the full call)
120
- const lastClosing = fullCall.lastIndexOf(')');
121
- if (lastClosing === -1)
112
+ // As verificações estruturais (useEffect/useMemo/useCallback sem deps e hook
113
+ // em condicional) são imprecisas quando feitas por regex/linha e geram falsos
114
+ // positivos. Quando a análise por AST (`parseHooksWithBabel`) está disponível,
115
+ // ela é a fonte autoritativa e estas verificações devem ser ignoradas.
116
+ const skipStructural = opts.skipStructural === true;
117
+ if (!skipStructural) {
118
+ // Check useEffect/useLayoutEffect for deps
119
+ const effectMatches = [...src.matchAll(/use(Layout)?Effect\s*\(/g)];
120
+ effectMatches.forEach(m => {
121
+ const hookInicio = m.index;
122
+ const fullCall = extractHookCallContent(src, hookInicio + m[0].length - 1);
123
+ // More robust deps detection: find the last comma followed by an array or identifier before the closing paren
124
+ const hasDepsArg = (() => {
125
+ // Find the last ')' (closing paren of the full call)
126
+ const lastClosing = fullCall.lastIndexOf(')');
127
+ if (lastClosing === -1)
128
+ return false;
129
+ const beforeClosing = fullCall.slice(0, lastClosing);
130
+ // Find the last '[' or identifier before the closing paren
131
+ const arrayMatch = beforeClosing.match(/,\s*(\[[\s\S]*?\])\s*$/);
132
+ if (arrayMatch)
133
+ return true;
134
+ const identMatch = beforeClosing.match(/,\s*(\w+)\s*$/);
135
+ if (identMatch)
136
+ return true;
122
137
  return false;
123
- const beforeClosing = fullCall.slice(0, lastClosing);
124
- // Find the last '[' or identifier before the closing paren
125
- const arrayMatch = beforeClosing.match(/,\s*(\[[\s\S]*?\])\s*$/);
126
- if (arrayMatch)
127
- return true;
128
- const identMatch = beforeClosing.match(/,\s*(\w+)\s*$/);
129
- if (identMatch)
130
- return true;
131
- return false;
132
- })();
133
- const skip = /no-deps-ok|eslint-disable-next-line\s+react-hooks\/exhaustive-deps/i.test(fullCall);
134
- if (!hasDepsArg && !skip) {
135
- const line = lineOf(hookInicio);
136
- ocorrencias.push(warn(messages.ReactHooksMensagens.useEffectNoDeps, relPath, line));
137
- }
138
- if (hasDepsArg && /\[\s*\]/.test(fullCall)) {
139
- const bodyWithRefs = fullCall.match(/^[\s\S]*?(?=,\s*\[)/);
140
- if (bodyWithRefs) {
141
- const varRefs = bodyWithRefs[0].match(/\b([a-z]\w+)\b/g) ?? [];
142
- const nonTrivialRefs = varRefs.filter(v => !/^(set|use)/.test(v) && v.length > 1 && !['undefined', 'null', 'true', 'false'].includes(v));
143
- if (nonTrivialRefs.length > 0) {
144
- const line = lineOf(hookInicio);
145
- ocorrencias.push(warn('Empty deps array but effect references component variables', relPath, line));
138
+ })();
139
+ const skip = /no-deps-ok|eslint-disable-next-line\s+react-hooks\/exhaustive-deps/i.test(fullCall);
140
+ if (!hasDepsArg && !skip) {
141
+ const line = lineOf(hookInicio);
142
+ ocorrencias.push(warn(messages.ReactHooksMensagens.useEffectNoDeps, relPath, line));
143
+ }
144
+ if (hasDepsArg && /\[\s*\]/.test(fullCall)) {
145
+ const bodyWithRefs = fullCall.match(/^[\s\S]*?(?=,\s*\[)/);
146
+ if (bodyWithRefs) {
147
+ const varRefs = bodyWithRefs[0].match(/\b([a-z]\w+)\b/g) ?? [];
148
+ const nonTrivialRefs = varRefs.filter(v => !/^(set|use)/.test(v) && v.length > 1 && !['undefined', 'null', 'true', 'false'].includes(v));
149
+ if (nonTrivialRefs.length > 0) {
150
+ const line = lineOf(hookInicio);
151
+ ocorrencias.push(warn('Empty deps array but effect references component variables', relPath, line));
152
+ }
146
153
  }
147
154
  }
148
- }
149
- });
150
- // useMemo/useCallback deps check
151
- const memoMatches = [...src.matchAll(/use(Memo|Callback)\s*\(/g)];
152
- memoMatches.forEach(m => {
153
- const hookInicio = m.index;
154
- const fullCall = extractHookCallContent(src, hookInicio + m[0].length - 1);
155
- const hasDepsArg = /,\s*(\[[\s\S]*?\]|\w+)\s*\)$/.test(fullCall);
156
- const skip = /no-deps-ok|eslint-disable-next-line\s+react-hooks\/exhaustive-deps/i.test(fullCall);
157
- if (!hasDepsArg && !skip) {
158
- const line = lineOf(hookInicio);
159
- ocorrencias.push(warn(messages.ReactHooksMensagens.memoCallbackNoDeps, relPath, line));
160
- }
161
- });
162
- // Conditional hooks only flag when a hook call is directly inside a conditional block,
163
- // not when the conditional is inside a hook callback (e.g. cleanup inside useEffect).
164
- // The regex matches: if/for/while (...) { ... useXxx ... } but limits the match scope
165
- // to avoid matching conditionals that appear inside hook callbacks.
166
- const conditionalHooks = [...src.matchAll(/(?:^|(?<=[\n;]))\s*(if|for|while)\s*\([^)]*\)\s*\{[\s\S]{0,160}?use[A-Z][A-Za-z0-9_]*/g)];
167
- conditionalHooks.forEach(m => {
168
- const line = lineOf(m.index || 0);
169
- // Heuristic: skip if the 'if' keyword is preceded by a closing paren or brace at the same indentation,
170
- // which suggests it's inside a callback body rather than at the component top level.
171
- const pre = src.slice(Math.max(0, (m.index ?? 0) - 80), m.index ?? 0);
172
- if (/=>\s*$|\)\s*=>\s*$|\)\s*\{[\s\S]{0,40}$/.test(pre))
173
- return;
174
- ocorrencias.push(warn(messages.ReactHooksMensagens.hookInConditional, relPath, line));
175
- });
155
+ });
156
+ // useMemo/useCallback deps check
157
+ const memoMatches = [...src.matchAll(/use(Memo|Callback)\s*\(/g)];
158
+ memoMatches.forEach(m => {
159
+ const hookInicio = m.index;
160
+ const fullCall = extractHookCallContent(src, hookInicio + m[0].length - 1);
161
+ const hasDepsArg = /,\s*(\[[\s\S]*?\]|\w+)\s*\)$/.test(fullCall);
162
+ const skip = /no-deps-ok|eslint-disable-next-line\s+react-hooks\/exhaustive-deps/i.test(fullCall);
163
+ if (!hasDepsArg && !skip) {
164
+ const line = lineOf(hookInicio);
165
+ ocorrencias.push(warn(messages.ReactHooksMensagens.memoCallbackNoDeps, relPath, line));
166
+ }
167
+ });
168
+ // Conditional hooks — only flag when a hook call is directly inside a conditional block,
169
+ // not when the conditional is inside a hook callback (e.g. cleanup inside useEffect).
170
+ // The regex matches: if/for/while (...) { ... useXxx ... } but limits the match scope
171
+ // to avoid matching conditionals that appear inside hook callbacks.
172
+ const conditionalHooks = [...src.matchAll(/(?:^|(?<=[\n;]))\s*(if|for|while)\s*\([^)]*\)\s*\{[\s\S]{0,160}?use[A-Z][A-Za-z0-9_]*/g)];
173
+ conditionalHooks.forEach(m => {
174
+ const line = lineOf(m.index || 0);
175
+ // Heuristic: skip if the 'if' keyword is preceded by a closing paren or brace at the same indentation,
176
+ // which suggests it's inside a callback body rather than at the component top level.
177
+ const pre = src.slice(Math.max(0, (m.index ?? 0) - 80), m.index ?? 0);
178
+ if (/=>\s*$|\)\s*=>\s*$|\)\s*\{[\s\S]{0,40}$/.test(pre))
179
+ return;
180
+ ocorrencias.push(warn(messages.ReactHooksMensagens.hookInConditional, relPath, line));
181
+ });
182
+ } // fim de if (!skipStructural)
176
183
  // React 19: useOptimistic detected
177
184
  for (const match of src.matchAll(/\buseOptimistic\s*\(/g)) {
178
185
  const line = lineOf(match.index);
@@ -188,16 +195,12 @@ function collectHookIssues(src, relPath) {
188
195
  const line = lineOf(match.index);
189
196
  ocorrencias.push(warn('useFormStatus (React 19) detected - access pending form state', relPath, line, messages.SeverityNiveis.info));
190
197
  }
191
- // React 19: use() hook - suspends, must be in Suspense boundary
192
- for (const match of src.matchAll(/\buse\s*\(/g)) {
193
- const line = lineOf(match.index);
194
- // Avoid matching "useState", "useEffect", etc.
195
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime safety for match.index === 0
196
- if (src[match.index - 1]?.match(/[a-zA-Z]/)) {
197
- continue;
198
- }
199
- ocorrencias.push(warn('React 19 use() hook detected - must be wrapped in <Suspense> boundary', relPath, line, messages.SeverityNiveis.error));
200
- }
198
+ // React 19: use() hook - suspends, must be in Suspense boundary.
199
+ // OBS: esta detecção por regex é intencionalmente OMITIDA aqui porque
200
+ // casaria falsamente com comentários/strings (ex.: "terms-of-use (...)") e
201
+ // com identificadores como "reuse(" / "abuse(". O hook use() é detectado de
202
+ // forma precisa via AST em parseHooksWithBabel (que ignora comentários),
203
+ // tornando este regex redundante e gerador de falsos positivos.
201
204
  // Custom hooks naming convention
202
205
  const customHooks = [...src.matchAll(/\buse[A-Z][A-Za-z0-9_]*\s*=\s*\(/g)];
203
206
  customHooks.forEach(m => {
@@ -260,24 +263,33 @@ function parseHooksWithBabel(src, relPath) {
260
263
  const isMemoLike = (name) => name === 'useMemo' || name === 'useCallback';
261
264
  const isAnyHook = (name) => /^use[A-Z0-9_]/.test(name) || name === 'use';
262
265
  const inConditionalOrLoop = (path) => {
263
- // Only consider conditionals/loops in the SAME function scope, not inside nested callbacks.
264
- // Stop climbing at the nearest function boundary to avoid false positives when
265
- // a hook is used inside a callback that happens to be in a conditional context.
266
+ // viola as Rules of Hooks se o hook estiver DENTRO de um
267
+ // if/for/while/do-while/switch/conditional no MESMO escopo de função que o
268
+ // contém. Condicionais em funções pais (ex.: um `if` no componente que define
269
+ // um hook customizado como useMusicPlayer) são válidas e NÃO devem gerar falso
270
+ // positivo (feedback.md seção 1.2). Callbacks aninhados (outra função) também
271
+ // delimitam o escopo — um condicional dentro de um callback não conta para o
272
+ // hook de fora.
266
273
  let current = path;
267
274
  while (current) {
268
- const parent = current.findParent(p => p.isIfStatement() || p.isForStatement() || p.isWhileStatement() || p.isDoWhileStatement() || p.isSwitchStatement() || p.isConditionalExpression());
269
- if (parent)
270
- return true;
271
- // Climb to the nearest function boundary; stop if we hit one
272
- const fnBoundary = current.findParent(p => p.isFunction());
273
- if (!fnBoundary)
275
+ const parent = current.parentPath;
276
+ if (!parent) {
277
+ break;
278
+ }
279
+ // Função que contém o hook: condicionais acima dela estão em escopo diferente.
280
+ if (parent.isFunction()) {
274
281
  break;
275
- // If the fnBoundary is the component function itself, we've exhausted this scope
276
- // Check if there's a conditional ABOVE this function boundary
277
- const aboveFn = fnBoundary.findParent(p => p.isIfStatement() || p.isForStatement() || p.isWhileStatement() || p.isDoWhileStatement() || p.isSwitchStatement() || p.isConditionalExpression());
278
- if (aboveFn)
282
+ }
283
+ if (parent.isIfStatement()
284
+ || parent.isForStatement()
285
+ || parent.isWhileStatement()
286
+ || parent.isDoWhileStatement()
287
+ || parent.isSwitchStatement()
288
+ || parent.isSwitchCase()
289
+ || parent.isConditionalExpression()) {
279
290
  return true;
280
- break;
291
+ }
292
+ current = parent;
281
293
  }
282
294
  return false;
283
295
  };
@@ -388,12 +400,14 @@ function parseHooksWithBabel(src, relPath) {
388
400
  return !depsSet.has(r);
389
401
  });
390
402
  if (maybeMissing.length > 1) {
403
+ // Apenas useMemo/useCallback com deps faltando são "sem array de
404
+ // dependências". useEffect/useLayoutEffect QUE JÁ POSSUEM um array
405
+ // de deps (mesmo que incompleto) NÃO devem ser reportados como
406
+ // "sem array" — ver feedback.md seção 1.1. O aviso de stale closure
407
+ // para esses casos já é emitido por outro analisador.
391
408
  if (isMemoLike(name)) {
392
409
  pushOnce(warn(messages.ReactHooksMensagens.memoCallbackNoDeps, relPath, locLine));
393
410
  }
394
- else {
395
- pushOnce(warn(messages.ReactHooksMensagens.useEffectNoDeps, relPath, locLine));
396
- }
397
411
  }
398
412
  if (deps.elements.length === 0 && refsInCb.size > 0) {
399
413
  const nonTrivialRefs = [...refsInCb].filter(r => {
@@ -459,7 +473,12 @@ export const analistaReactHooks = criarAnalista({
459
473
  }
460
474
  const astMsgs = parseHooksWithBabel(src, relPath);
461
475
  if (astMsgs !== null) {
462
- const regexMsgs = collectHookIssues(src, relPath);
476
+ // O AST é a fonte autoritativa para verificações estruturais (deps de
477
+ // useEffect/useMemo e hooks em condicional). Quando disponível, o regex
478
+ // estrutural é ignorado para evitar os falsos positivos de análise por
479
+ // texto (ver feedback.md seção 1.1 e 1.2). As verificações informativas
480
+ // do regex permanecem como rede de segurança.
481
+ const regexMsgs = collectHookIssues(src, relPath, { skipStructural: true });
463
482
  const all = [...astMsgs, ...regexMsgs];
464
483
  const seen = new Set();
465
484
  const deduped = all.filter(m => {
@@ -9,6 +9,68 @@ const D = messages.DetectorReactMensagens;
9
9
  function warn(tipo, mensagem, relPath, linha, sugestao, nivel = 'aviso') {
10
10
  return criarOcorrencia({ tipo, nivel, mensagem, relPath, linha, sugestao });
11
11
  }
12
+ /**
13
+ * Decide se o valor passado a `dangerouslySetInnerHTML` é estático/controlado
14
+ * e, portanto, seguro o suficiente para NÃO gerar falso positivo.
15
+ * (ver feedback.md seção 1.3 — JSON-LD, string interna, conteúdo sanitizado).
16
+ *
17
+ * Consideramos controlado quando o valor é:
18
+ * - uma string literal (estática);
19
+ * - resultado de `JSON.stringify(...)` (ex.: JSON-LD / structured data);
20
+ * - resultado de uma chamada de sanitização/escape (`sanitize`, `escape`,
21
+ * `encode`, `secure`, etc.);
22
+ * - um template literal (conteúdo estático na prática).
23
+ */
24
+ function isJsonStringifyCall(node) {
25
+ if (!t.isCallExpression(node)) {
26
+ return false;
27
+ }
28
+ const callee = node.callee;
29
+ return t.isMemberExpression(callee)
30
+ && t.isIdentifier(callee.object)
31
+ && callee.object.name === 'JSON'
32
+ && t.isIdentifier(callee.property)
33
+ && callee.property.name === 'stringify';
34
+ }
35
+ function calleeName(callee) {
36
+ if (t.isIdentifier(callee)) {
37
+ return callee.name;
38
+ }
39
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
40
+ return callee.property.name;
41
+ }
42
+ return '';
43
+ }
44
+ function isControlledDangerousHtml(expr) {
45
+ if (!expr || t.isJSXEmptyExpression(expr)) {
46
+ return true;
47
+ }
48
+ if (t.isStringLiteral(expr)) {
49
+ return true;
50
+ }
51
+ if (t.isTemplateLiteral(expr)) {
52
+ return true;
53
+ }
54
+ if (t.isCallExpression(expr)) {
55
+ if (isJsonStringifyCall(expr)) {
56
+ return true;
57
+ }
58
+ // JSON.stringify(data).replace(...) / .trim() — o objeto da chamada é JSON.stringify
59
+ if (t.isMemberExpression(expr.callee) && isJsonStringifyCall(expr.callee.object)) {
60
+ return true;
61
+ }
62
+ const name = calleeName(expr.callee);
63
+ if (/sanit|escape|encod|secure|trust/i.test(name)) {
64
+ return true;
65
+ }
66
+ }
67
+ // JSON.stringify(...).replace(...) — o objeto da MemberExpression é a chamada
68
+ // JSON.stringify, então o conteúdo é estruturado/controlado (ex.: JSON-LD).
69
+ if (t.isMemberExpression(expr) && isJsonStringifyCall(expr.object)) {
70
+ return true;
71
+ }
72
+ return false;
73
+ }
12
74
  export const detectorReactBestPractices = {
13
75
  nome: 'detector-react-best-practices',
14
76
  categoria: 'framework',
@@ -22,12 +84,6 @@ export const detectorReactBestPractices = {
22
84
  const linhas = splitLines(src);
23
85
  linhas.forEach((linha, index) => {
24
86
  const numeroLinha = index + 1;
25
- if (/dangerouslySetInnerHTML/.test(linha)) {
26
- // Skip <script type="speculationrules"> — dangerouslySetInnerHTML is required
27
- if (/speculationrules/i.test(linha))
28
- return;
29
- ocorrencias.push(warn('react-dangerous-html', D.dangerousHtml, relPath, numeroLinha, D.dangerousHtmlSugestao, 'erro'));
30
- }
31
87
  if (/onClick=|onSubmit=|onChange=|onMouseOver=/.test(linha) && /{[^}]*=>/.test(linha)) {
32
88
  ocorrencias.push(warn('react-inline-handler', D.inlineHandler, relPath, numeroLinha, D.inlineHandlerSugestao, 'info'));
33
89
  }
@@ -62,6 +118,21 @@ export const detectorReactBestPractices = {
62
118
  }
63
119
  }
64
120
  }
121
+ // Fallback conservador: quando o parse por AST falha, ainda detectamos
122
+ // dangerouslySetInnerHTML de forma limitada, alertando apenas quando o
123
+ // valor claramente vem de fonte externa/não controlada (props, params,
124
+ // localStorage, fetch, location, etc). Conteúdo estático/controlado é
125
+ // ignorado (ver feedback.md seção 1.3 - ex.: JSON-LD).
126
+ if (!deeper) {
127
+ linhas.forEach((linha, index) => {
128
+ if (/dangerouslySetInnerHTML/.test(linha) && !/speculationrules/i.test(linha)) {
129
+ const numeroLinha = index + 1;
130
+ if (/\b(props|params|query|searchParams|localStorage|sessionStorage|fetch\(|\blocation\b|window\.|document\.cookie|req\.|res\.)/.test(linha)) {
131
+ ocorrencias.push(warn('react-dangerous-html', D.dangerousHtml, relPath, numeroLinha, D.dangerousHtmlSugestao, 'aviso'));
132
+ }
133
+ }
134
+ });
135
+ }
65
136
  return ocorrencias;
66
137
  }
67
138
  };
@@ -326,6 +397,29 @@ function detectWithBabel(src, relPath) {
326
397
  pushOnce(warn('react-key-index', D.keyIndexUsage, relPath, locLine, D.keyIndexUsageSugestao, 'aviso'));
327
398
  }
328
399
  }
400
+ // dangerouslySetInnerHTML: só alerta quando o valor NÃO é estático/
401
+ // controlado (ex.: vem de props, params, fetch, localStorage). Conteúdo
402
+ // como JSON-LD (JSON.stringify) ou string interna é seguro e não deve
403
+ // gerar ruído (ver feedback.md seção 1.3).
404
+ if (name === 'dangerouslySetInnerHTML') {
405
+ const value = path.node.value;
406
+ let htmlExpr;
407
+ if (t.isJSXExpressionContainer(value)) {
408
+ const inner = value.expression;
409
+ if (t.isObjectExpression(inner)) {
410
+ const htmlProp = inner.properties?.find(p => t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === '__html');
411
+ if (htmlProp && t.isObjectProperty(htmlProp) && !t.isPrivateName(htmlProp.value) && !t.isJSXEmptyExpression(htmlProp.value)) {
412
+ htmlExpr = htmlProp.value;
413
+ }
414
+ }
415
+ else if (t.isExpression(inner)) {
416
+ htmlExpr = inner;
417
+ }
418
+ }
419
+ if (!isControlledDangerousHtml(htmlExpr)) {
420
+ pushOnce(warn('react-dangerous-html', D.dangerousHtml, relPath, locLine, D.dangerousHtmlSugestao, 'aviso'));
421
+ }
422
+ }
329
423
  },
330
424
  MemberExpression(path) {
331
425
  const locLine = path.node.loc?.start.line;
@@ -13,7 +13,7 @@ async function obterCommit() {
13
13
  try {
14
14
  // usar helper seguro
15
15
  const { executarShellSeguro } = await import('../../core/utils/index.js');
16
- return executarShellSeguro('git rev-parse --short HEAD', {
16
+ return executarShellSeguro('git', ['rev-parse', '--short', 'HEAD'], {
17
17
  stdio: ['ignore', 'pipe', 'ignore']
18
18
  }).toString().trim();
19
19
  }
@@ -38,9 +38,10 @@ export function comandoAtualizar(aplicarFlagsGlobais) {
38
38
  log.aviso(CliAtualizarExtraMensagens.guardianBaselineAlterado);
39
39
  log.info(CliAtualizarExtraMensagens.recomendadoGuardianDiff);
40
40
  }
41
- const cmd = opts.global ? 'npm install -g mahoraga@latest' : 'npm install mahoraga@latest';
41
+ const cmd = opts.global ? 'npm install -g @mocoto/mahoraga@latest' : 'npm install @mocoto/mahoraga@latest';
42
42
  logSistema.atualizacaoExecutando(cmd);
43
- executarShellSeguro(cmd, {
43
+ const npmArgs = opts.global ? ['install', '-g', '@mocoto/mahoraga@latest'] : ['install', '@mocoto/mahoraga@latest'];
44
+ executarShellSeguro('npm', npmArgs, {
44
45
  stdio: 'inherit'
45
46
  });
46
47
  logSistema.atualizacaoSucesso();
@@ -35,6 +35,9 @@ export const AGRESSIVA_AUTO_CORRECAO_CONFIGURACAO = {
35
35
  // Backwards compat constant name used elsewhere in the codebase
36
36
  export const AUTO_CORRECAO_CONFIGURACAO_PADROES = PADRAO_AUTO_CORRECAO_CONFIGURACAO;
37
37
  export function shouldExcludeFile(fileCaminho, config) {
38
+ if (!config.excludePadroes || !Array.isArray(config.excludePadroes)) {
39
+ return false;
40
+ }
38
41
  return config.excludePadroes.some(pattern => {
39
42
  const regexPadrao = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\?/g, '.');
40
43
  return new RegExp(regexPadrao).test(fileCaminho);
@@ -48,7 +51,10 @@ export function shouldExcludeFunction(functionName, config) {
48
51
  }
49
52
  export function isCategoryAllowed(category, config) {
50
53
  // allowedCategories is an array of known category strings - coerce to string[] for safe includes check
51
- return (config.allowedCategories).includes(category);
54
+ if (!config.allowedCategories || !Array.isArray(config.allowedCategories)) {
55
+ return true;
56
+ }
57
+ return config.allowedCategories.includes(category);
52
58
  }
53
59
  export function hasMinimumConfidence(confidence, config) {
54
60
  if (typeof config.minConfidence !== 'number') {
@@ -63,9 +69,8 @@ export function getAutoFixConfig(mode) {
63
69
  case 'aggressive':
64
70
  return AGRESSIVA_AUTO_CORRECAO_CONFIGURACAO;
65
71
  case 'balanced':
66
- case undefined: {
67
- throw new Error('Not implemented yet: undefined case');
68
- }
72
+ case undefined:
73
+ return PADRAO_AUTO_CORRECAO_CONFIGURACAO;
69
74
  default:
70
75
  return PADRAO_AUTO_CORRECAO_CONFIGURACAO;
71
76
  }
@@ -110,7 +110,7 @@ export const configPadrao = {
110
110
  ANALISE_INCREMENTAL_ENABLED: false,
111
111
  ANALISE_INCREMENTAL_STATE_PATH: path.join(SUKUNA_ESTADO, 'incremental-analise.json'),
112
112
  ANALISE_INCREMENTAL_VERSION: 1,
113
- ANALISE_CACHE_ENABLED: true,
113
+ ANALISE_CACHE_ENABLED: false,
114
114
  ANALISE_CACHE_STATE_PATH: path.join(SUKUNA_ESTADO, 'analysis-cache.json'),
115
115
  ANALISE_CACHE_VERSION: 1,
116
116
  ANALISE_CACHE_TTL_MS: 86400000,
@@ -13,6 +13,7 @@ export const FormattersMensagens = {
13
13
  };
14
14
  export const ExecSafeMensagens = {
15
15
  safeModeBloqueado: 'Command execution disabled in SAFE_MODE. Set SUKUNA_ALLOW_EXEC=1 to allow it.',
16
+ binarioNaoPermitido: (bin) => `Binary '${bin}' is not in the allowed list.`,
16
17
  };
17
18
  export const WorkerExecutorMensagens = {
18
19
  timeoutTecnica: (nome, ms) => `Timeout: technique '${nome}' exceeded ${ms}ms`,
@@ -13,6 +13,7 @@ export const FormattersMensagens = {
13
13
  };
14
14
  export const ExecSafeMensagens = {
15
15
  safeModeBloqueado: 'SAFE_MODEではコマンド実行が無効です。許可するには SUKUNA_ALLOW_EXEC=1 を設定してください。',
16
+ binarioNaoPermitido: (bin) => `バイナリ '${bin}' は許可リストに含まれていません。`,
16
17
  };
17
18
  export const WorkerExecutorMensagens = {
18
19
  timeoutTecnica: (nome, ms) => `タイムアウト: 手法 '${nome}' が ${ms}ms を超えました`,
@@ -21,6 +21,7 @@ export const FormattersMensagens = {
21
21
  };
22
22
  export const ExecSafeMensagens = {
23
23
  safeModeBloqueado: 'Execução de comandos desabilitada em SAFE_MODE. Defina SUKUNA_ALLOW_EXEC=1 para permitir.',
24
+ binarioNaoPermitido: (bin) => `Binário '${bin}' não está na lista de permitidos.`,
24
25
  };
25
26
  export const WorkerExecutorMensagens = {
26
27
  timeoutTecnica: (nome, ms) => `Timeout: tecnica '${nome}' excedeu ${ms}ms`,