@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.
- package/README.md +165 -511
- package/dist/analysts/css/analysts/analyst-css.js +10 -1
- package/dist/analysts/detectors/detector-bugs-ml.js +81 -19
- package/dist/analysts/js-ts/registrar.js +1 -1
- package/dist/analysts/react/analysts/analyst-react-hooks.js +109 -90
- package/dist/analysts/react/detectors/detector-react-best-practices.js +100 -6
- package/dist/cli/commands/command-perf.js +1 -1
- package/dist/cli/commands/command-update.js +3 -2
- package/dist/core/config/auto/auto-fix-config.js +9 -4
- package/dist/core/config/config.js +1 -1
- package/dist/core/messages/en/core/formatters-messages.js +1 -0
- package/dist/core/messages/ja/core/formatters-messages.js +1 -0
- package/dist/core/messages/pt/core/formatters-messages.js +1 -0
- package/dist/core/messages/shared/icons.js +3 -3
- package/dist/core/messages/zh/core/formatters-messages.js +1 -0
- package/dist/core/parsing/langs/json.js +2 -2
- package/dist/core/parsing/langs/typescript.js +33 -7
- package/dist/core/schema/version.js +1 -1
- package/dist/core/utils/exec-safe.js +47 -25
- package/dist/lib/api.js +14 -0
- package/dist/shared/data-processing/occurrences.js +3 -0
- package/dist/shared/formatters/code-min-formatter.js +1 -1
- package/dist/shared/formatters/formatters/commons.js +19 -5
- package/dist/shared/formatters/formatters/shell.js +128 -9
- package/dist/shared/helpers/ast-visitor-utils.js +3 -0
- package/dist/shared/helpers/imports.js +1 -1
- package/dist/shared/helpers/reader-report.js +13 -0
- package/dist/shared/helpers/structure.js +2 -2
- package/dist/types/guardian/result.js +38 -0
- package/dist/types/reports/processing.js +12 -0
- package/dist/types/shared/validation.js +111 -0
- package/dist/vulnerabilities/scanner.js +7 -2
- 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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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,
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
217
|
+
// Limiar maior: só 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 {
|
|
@@ -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
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
+
// Só 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.
|
|
269
|
-
if (parent)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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`,
|