eslint-plugin-sonar-config 0.1.1
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 +66 -0
- package/index.js +116 -0
- package/package.json +24 -0
- package/rules/array-method-missing-args.js +44 -0
- package/rules/await-only-promises.js +84 -0
- package/rules/for-loop-update-counter.js +103 -0
- package/rules/max-switch-cases.js +39 -0
- package/rules/no-array-delete.js +69 -0
- package/rules/no-bitwise-in-boolean.js +57 -0
- package/rules/no-constant-binary-expression.js +166 -0
- package/rules/no-empty-regex-alternatives.js +102 -0
- package/rules/no-example-rule.js +56 -0
- package/rules/no-gratuitous-boolean.js +121 -0
- package/rules/no-identical-branches.js +93 -0
- package/rules/no-in-array.js +103 -0
- package/rules/no-in-with-primitives.js +56 -0
- package/rules/no-index-of-compare-positive.js +82 -0
- package/rules/no-inverted-boolean-check.js +52 -0
- package/rules/no-label-in-switch.js +55 -0
- package/rules/no-misleading-array-mutation.js +54 -0
- package/rules/no-nested-ternary.js +43 -0
- package/rules/no-non-numeric-array-index.js +56 -0
- package/rules/no-redundant-boolean-condition.js +121 -0
- package/rules/no-redundant-call-apply.js +73 -0
- package/rules/no-this-in-functional-component.js +102 -0
- package/rules/no-unsafe-window-open.js +53 -0
- package/rules/prefer-for-of.js +119 -0
- package/rules/prefer-regex-exec.js +72 -0
- package/rules/prefer-while.js +28 -0
- package/rules/regex-anchor-with-alternation.js +125 -0
- package/rules/require-reduce-initial-value.js +43 -0
- package/rules/require-sort-compare.js +43 -0
- package/rules/use-state-callback.js +121 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Preferir for...of ao invés de for tradicional com índice
|
|
3
|
+
* Quando o índice é usado apenas para acessar elementos do array, for...of é mais legível.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Preferir for...of ao invés de for tradicional com índice",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
preferForOf:
|
|
13
|
+
"Use for...of ao invés de for com índice para iterar arrays. É mais legível.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
ForStatement(node) {
|
|
19
|
+
// Verifica se tem inicialização com let i = 0
|
|
20
|
+
const init = node.init;
|
|
21
|
+
if (!init || init.type !== "VariableDeclaration") return;
|
|
22
|
+
if (init.declarations.length !== 1) return;
|
|
23
|
+
|
|
24
|
+
const declaration = init.declarations[0];
|
|
25
|
+
if (declaration.id.type !== "Identifier") return;
|
|
26
|
+
|
|
27
|
+
const counterName = declaration.id.name;
|
|
28
|
+
|
|
29
|
+
// Verifica se inicializa com 0
|
|
30
|
+
if (
|
|
31
|
+
!declaration.init ||
|
|
32
|
+
declaration.init.type !== "Literal" ||
|
|
33
|
+
declaration.init.value !== 0
|
|
34
|
+
) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verifica se a condição é i < arr.length
|
|
39
|
+
const test = node.test;
|
|
40
|
+
if (!test || test.type !== "BinaryExpression") return;
|
|
41
|
+
if (test.operator !== "<" && test.operator !== "<=") return;
|
|
42
|
+
|
|
43
|
+
// Lado esquerdo deve ser o contador
|
|
44
|
+
if (test.left.type !== "Identifier" || test.left.name !== counterName) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Lado direito deve ser .length
|
|
49
|
+
if (
|
|
50
|
+
test.right.type !== "MemberExpression" ||
|
|
51
|
+
test.right.property.type !== "Identifier" ||
|
|
52
|
+
test.right.property.name !== "length"
|
|
53
|
+
) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const arrayName =
|
|
58
|
+
test.right.object.type === "Identifier"
|
|
59
|
+
? test.right.object.name
|
|
60
|
+
: null;
|
|
61
|
+
|
|
62
|
+
if (!arrayName) return;
|
|
63
|
+
|
|
64
|
+
// Verifica se o update é i++ ou i += 1
|
|
65
|
+
const update = node.update;
|
|
66
|
+
if (!update) return;
|
|
67
|
+
|
|
68
|
+
let isSimpleIncrement = false;
|
|
69
|
+
if (
|
|
70
|
+
update.type === "UpdateExpression" &&
|
|
71
|
+
update.operator === "++" &&
|
|
72
|
+
update.argument.type === "Identifier" &&
|
|
73
|
+
update.argument.name === counterName
|
|
74
|
+
) {
|
|
75
|
+
isSimpleIncrement = true;
|
|
76
|
+
} else if (
|
|
77
|
+
update.type === "AssignmentExpression" &&
|
|
78
|
+
update.operator === "+=" &&
|
|
79
|
+
update.left.type === "Identifier" &&
|
|
80
|
+
update.left.name === counterName &&
|
|
81
|
+
update.right.type === "Literal" &&
|
|
82
|
+
update.right.value === 1
|
|
83
|
+
) {
|
|
84
|
+
isSimpleIncrement = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!isSimpleIncrement) return;
|
|
88
|
+
|
|
89
|
+
// Verifica se o corpo usa apenas arr[i] para ler valores
|
|
90
|
+
// e não usa o índice i diretamente para outras coisas
|
|
91
|
+
const body = node.body;
|
|
92
|
+
const sourceCode = context.getSourceCode();
|
|
93
|
+
const bodyText = sourceCode.getText(body);
|
|
94
|
+
|
|
95
|
+
// Conta usos do índice
|
|
96
|
+
const indexPattern = new RegExp(`\\b${counterName}\\b`, "g");
|
|
97
|
+
const indexMatches = bodyText.match(indexPattern) || [];
|
|
98
|
+
|
|
99
|
+
// Conta usos como arr[i]
|
|
100
|
+
const arrayAccessPattern = new RegExp(
|
|
101
|
+
`\\b${arrayName}\\s*\\[\\s*${counterName}\\s*\\]`,
|
|
102
|
+
"g"
|
|
103
|
+
);
|
|
104
|
+
const arrayAccessMatches = bodyText.match(arrayAccessPattern) || [];
|
|
105
|
+
|
|
106
|
+
// Se o índice é usado apenas para acessar o array, sugere for...of
|
|
107
|
+
if (
|
|
108
|
+
indexMatches.length > 0 &&
|
|
109
|
+
indexMatches.length === arrayAccessMatches.length
|
|
110
|
+
) {
|
|
111
|
+
context.report({
|
|
112
|
+
node,
|
|
113
|
+
messageId: "preferForOf",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Preferir RegExp.exec() ao invés de String.match()
|
|
3
|
+
* RegExp.exec() é mais rápido e produz o mesmo resultado quando não há flag 'g'.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Preferir RegExp.exec() ao invés de String.match()",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
preferRegexExec:
|
|
13
|
+
"Use regex.exec(string) ao invés de string.match(regex). É mais performático.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
// Verifica se é uma chamada de .match()
|
|
20
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
21
|
+
if (node.callee.property.type !== "Identifier") return;
|
|
22
|
+
if (node.callee.property.name !== "match") return;
|
|
23
|
+
|
|
24
|
+
// Verifica se tem exatamente 1 argumento
|
|
25
|
+
if (node.arguments.length !== 1) return;
|
|
26
|
+
|
|
27
|
+
const arg = node.arguments[0];
|
|
28
|
+
|
|
29
|
+
// Se o argumento é um regex literal
|
|
30
|
+
if (arg.type === "Literal" && arg.regex) {
|
|
31
|
+
// Se não tem flag 'g', pode usar exec()
|
|
32
|
+
if (!arg.regex.flags.includes("g")) {
|
|
33
|
+
context.report({
|
|
34
|
+
node,
|
|
35
|
+
messageId: "preferRegexExec",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Se o argumento é new RegExp()
|
|
42
|
+
if (
|
|
43
|
+
arg.type === "NewExpression" &&
|
|
44
|
+
arg.callee.type === "Identifier" &&
|
|
45
|
+
arg.callee.name === "RegExp"
|
|
46
|
+
) {
|
|
47
|
+
// Verifica se as flags não contêm 'g'
|
|
48
|
+
if (arg.arguments.length < 2) {
|
|
49
|
+
// Sem flags, pode usar exec()
|
|
50
|
+
context.report({
|
|
51
|
+
node,
|
|
52
|
+
messageId: "preferRegexExec",
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const flagsArg = arg.arguments[1];
|
|
58
|
+
if (
|
|
59
|
+
flagsArg.type === "Literal" &&
|
|
60
|
+
typeof flagsArg.value === "string" &&
|
|
61
|
+
!flagsArg.value.includes("g")
|
|
62
|
+
) {
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: "preferRegexExec",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Use "while" ao invés de "for" quando não há init e update
|
|
3
|
+
* for (; condition; ) {} → while (condition) {}
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Use "while" ao invés de "for" quando não há inicialização e incremento',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
preferWhile: 'Use "while" ao invés de "for" quando não há inicialização e incremento.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
ForStatement(node) {
|
|
18
|
+
// Se não tem init E não tem update, deveria ser while
|
|
19
|
+
if (!node.init && !node.update) {
|
|
20
|
+
context.report({
|
|
21
|
+
node,
|
|
22
|
+
messageId: "preferWhile",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Alternativas em regex devem ser agrupadas quando usadas com anchors (^ ou $)
|
|
3
|
+
* /^a|b/ deveria ser /^(a|b)/ ou /^a|^b/
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Alternativas em regex devem ser agrupadas quando usadas com anchors",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
anchorWithAlternation:
|
|
13
|
+
"Anchor (^ ou $) com alternativas não agrupadas. Use /^(a|b)/ ao invés de /^a|b/.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
function checkRegexPattern(pattern, node) {
|
|
18
|
+
// Ignora se não tem alternação
|
|
19
|
+
if (!pattern.includes("|")) return false;
|
|
20
|
+
|
|
21
|
+
// Ignora se já está todo dentro de um grupo: ^(...|...)$
|
|
22
|
+
if (/^\^?\([^)]+\)\$?$/.test(pattern)) return false;
|
|
23
|
+
|
|
24
|
+
// Verifica se tem ^ no início sem agrupar todas as alternativas
|
|
25
|
+
// /^a|b/ é problemático, mas /^a|^b/ é ok
|
|
26
|
+
if (pattern.startsWith("^")) {
|
|
27
|
+
// Divide por | no nível raiz (não dentro de grupos)
|
|
28
|
+
const alternatives = splitByPipe(pattern.slice(1));
|
|
29
|
+
|
|
30
|
+
// Se tem mais de uma alternativa e nem todas começam com ^
|
|
31
|
+
if (alternatives.length > 1) {
|
|
32
|
+
const allHaveStart = alternatives.every(alt => alt.startsWith("^") || alternatives.indexOf(alt) === 0);
|
|
33
|
+
if (!allHaveStart) {
|
|
34
|
+
// Verifica se a primeira alternativa termina antes do |
|
|
35
|
+
// e a segunda não tem ^ próprio
|
|
36
|
+
const secondAlt = alternatives[1];
|
|
37
|
+
if (secondAlt && !secondAlt.startsWith("^")) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verifica se tem $ no final sem agrupar todas as alternativas
|
|
45
|
+
// /a|b$/ é problemático, mas /a$|b$/ é ok
|
|
46
|
+
if (pattern.endsWith("$") && !pattern.endsWith("\\$")) {
|
|
47
|
+
const alternatives = splitByPipe(pattern.slice(0, -1));
|
|
48
|
+
|
|
49
|
+
if (alternatives.length > 1) {
|
|
50
|
+
const allHaveEnd = alternatives.every(alt => alt.endsWith("$") || alternatives.indexOf(alt) === alternatives.length - 1);
|
|
51
|
+
if (!allHaveEnd) {
|
|
52
|
+
const secondToLast = alternatives[alternatives.length - 2];
|
|
53
|
+
if (secondToLast && !secondToLast.endsWith("$")) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Divide string por | mas não dentro de grupos () ou []
|
|
64
|
+
function splitByPipe(str) {
|
|
65
|
+
const result = [];
|
|
66
|
+
let current = "";
|
|
67
|
+
let parenDepth = 0;
|
|
68
|
+
let bracketDepth = 0;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < str.length; i++) {
|
|
71
|
+
const char = str[i];
|
|
72
|
+
const prevChar = i > 0 ? str[i - 1] : "";
|
|
73
|
+
|
|
74
|
+
if (prevChar === "\\") {
|
|
75
|
+
current += char;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (char === "(") parenDepth++;
|
|
80
|
+
else if (char === ")") parenDepth--;
|
|
81
|
+
else if (char === "[") bracketDepth++;
|
|
82
|
+
else if (char === "]") bracketDepth--;
|
|
83
|
+
else if (char === "|" && parenDepth === 0 && bracketDepth === 0) {
|
|
84
|
+
result.push(current);
|
|
85
|
+
current = "";
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
current += char;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (current) result.push(current);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
Literal(node) {
|
|
98
|
+
if (node.regex) {
|
|
99
|
+
if (checkRegexPattern(node.regex.pattern, node)) {
|
|
100
|
+
context.report({
|
|
101
|
+
node,
|
|
102
|
+
messageId: "anchorWithAlternation",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
NewExpression(node) {
|
|
109
|
+
if (
|
|
110
|
+
node.callee.name === "RegExp" &&
|
|
111
|
+
node.arguments.length > 0 &&
|
|
112
|
+
node.arguments[0].type === "Literal" &&
|
|
113
|
+
typeof node.arguments[0].value === "string"
|
|
114
|
+
) {
|
|
115
|
+
if (checkRegexPattern(node.arguments[0].value, node)) {
|
|
116
|
+
context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: "anchorWithAlternation",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regra: Array.reduce() deve incluir um valor inicial
|
|
3
|
+
*
|
|
4
|
+
* Sonar: javascript:S6959
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
"Array.reduce() e Array.reduceRight() devem ter um valor inicial",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
requireInitialValue:
|
|
17
|
+
'"{{method}}" deve ter um valor inicial para evitar TypeError em arrays vazios.',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node) {
|
|
24
|
+
// Verifica se é uma chamada de método
|
|
25
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
26
|
+
|
|
27
|
+
const methodName = node.callee.property.name;
|
|
28
|
+
|
|
29
|
+
// Verifica se é .reduce() ou .reduceRight()
|
|
30
|
+
if (methodName !== "reduce" && methodName !== "reduceRight") return;
|
|
31
|
+
|
|
32
|
+
// Verifica se tem apenas 1 argumento (callback sem valor inicial)
|
|
33
|
+
if (node.arguments.length === 1) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "requireInitialValue",
|
|
37
|
+
data: { method: methodName },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regra: Array.sort() e Array.toSorted() devem usar função de comparação
|
|
3
|
+
*
|
|
4
|
+
* Sonar: javascript:S2871
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
"Array.sort() e Array.toSorted() devem ter uma função de comparação",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
requireCompare:
|
|
17
|
+
'Forneça uma função de comparação para "{{method}}". Sem ela, a ordenação é lexicográfica (ex: [1, 10, 2] em vez de [1, 2, 10]).',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
return {
|
|
23
|
+
CallExpression(node) {
|
|
24
|
+
// Verifica se é uma chamada de método
|
|
25
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
26
|
+
|
|
27
|
+
const methodName = node.callee.property.name;
|
|
28
|
+
|
|
29
|
+
// Verifica se é .sort() ou .toSorted()
|
|
30
|
+
if (methodName !== "sort" && methodName !== "toSorted") return;
|
|
31
|
+
|
|
32
|
+
// Verifica se não tem argumentos (sem função de comparação)
|
|
33
|
+
if (node.arguments.length === 0) {
|
|
34
|
+
context.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "requireCompare",
|
|
37
|
+
data: { method: methodName },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview useState setter deve usar callback quando referencia o estado anterior
|
|
3
|
+
* Evita bugs de state stale devido ao batching do React.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verifica recursivamente se um nó contém uma referência ao identificador
|
|
8
|
+
*/
|
|
9
|
+
function containsIdentifier(node, name) {
|
|
10
|
+
if (!node) return false;
|
|
11
|
+
|
|
12
|
+
if (node.type === "Identifier" && node.name === name) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Verifica todos os filhos do nó
|
|
17
|
+
for (const key of Object.keys(node)) {
|
|
18
|
+
if (key === "parent" || key === "range" || key === "loc") continue;
|
|
19
|
+
|
|
20
|
+
const child = node[key];
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(child)) {
|
|
23
|
+
for (const item of child) {
|
|
24
|
+
if (item && typeof item === "object" && containsIdentifier(item, name)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} else if (child && typeof child === "object" && child.type) {
|
|
29
|
+
if (containsIdentifier(child, name)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
meta: {
|
|
40
|
+
type: "problem",
|
|
41
|
+
docs: {
|
|
42
|
+
description:
|
|
43
|
+
"useState setter deve usar callback quando referencia o estado anterior",
|
|
44
|
+
},
|
|
45
|
+
messages: {
|
|
46
|
+
useCallback:
|
|
47
|
+
'Use callback no setter: "{{setter}}(prev => ...)" ao invés de referenciar "{{state}}" diretamente.',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
create(context) {
|
|
51
|
+
// Map de setters para seus estados: { setCount: "count", setName: "name" }
|
|
52
|
+
const setterToState = new Map();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
// Detecta: const [count, setCount] = useState(...)
|
|
56
|
+
VariableDeclarator(node) {
|
|
57
|
+
if (node.id.type !== "ArrayPattern") return;
|
|
58
|
+
if (node.id.elements.length !== 2) return;
|
|
59
|
+
|
|
60
|
+
const init = node.init;
|
|
61
|
+
if (!init) return;
|
|
62
|
+
|
|
63
|
+
// Verifica se é useState()
|
|
64
|
+
const isUseState =
|
|
65
|
+
(init.type === "CallExpression" &&
|
|
66
|
+
init.callee.type === "Identifier" &&
|
|
67
|
+
init.callee.name === "useState") ||
|
|
68
|
+
(init.type === "CallExpression" &&
|
|
69
|
+
init.callee.type === "MemberExpression" &&
|
|
70
|
+
init.callee.property.name === "useState");
|
|
71
|
+
|
|
72
|
+
if (!isUseState) return;
|
|
73
|
+
|
|
74
|
+
const stateElement = node.id.elements[0];
|
|
75
|
+
const setterElement = node.id.elements[1];
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
stateElement &&
|
|
79
|
+
stateElement.type === "Identifier" &&
|
|
80
|
+
setterElement &&
|
|
81
|
+
setterElement.type === "Identifier"
|
|
82
|
+
) {
|
|
83
|
+
setterToState.set(setterElement.name, stateElement.name);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Detecta: setCount(count + 1) ao invés de setCount(prev => prev + 1)
|
|
88
|
+
CallExpression(node) {
|
|
89
|
+
if (node.callee.type !== "Identifier") return;
|
|
90
|
+
|
|
91
|
+
const setterName = node.callee.name;
|
|
92
|
+
const stateName = setterToState.get(setterName);
|
|
93
|
+
|
|
94
|
+
if (!stateName) return;
|
|
95
|
+
if (node.arguments.length !== 1) return;
|
|
96
|
+
|
|
97
|
+
const arg = node.arguments[0];
|
|
98
|
+
|
|
99
|
+
// Se já é uma função callback, está OK
|
|
100
|
+
if (
|
|
101
|
+
arg.type === "ArrowFunctionExpression" ||
|
|
102
|
+
arg.type === "FunctionExpression"
|
|
103
|
+
) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Verifica se o argumento referencia o estado
|
|
108
|
+
if (containsIdentifier(arg, stateName)) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "useCallback",
|
|
112
|
+
data: {
|
|
113
|
+
setter: setterName,
|
|
114
|
+
state: stateName,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|