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,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview indexOf não deve ser comparado com números positivos
|
|
3
|
+
* Comparar indexOf > 0 ignora o primeiro elemento. Use >= 0 ou includes().
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "indexOf não deve ser comparado com números positivos",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noPositiveIndexOf:
|
|
13
|
+
'Comparar indexOf com > 0 ignora o primeiro elemento (índice 0). Use ">= 0" ou ".includes()" ao invés.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
/**
|
|
18
|
+
* Verifica se o nó é uma chamada de indexOf/lastIndexOf
|
|
19
|
+
*/
|
|
20
|
+
function isIndexOfCall(node) {
|
|
21
|
+
if (node.type !== "CallExpression") return false;
|
|
22
|
+
if (node.callee.type !== "MemberExpression") return false;
|
|
23
|
+
|
|
24
|
+
const methodName = node.callee.property.name;
|
|
25
|
+
return methodName === "indexOf" || methodName === "lastIndexOf";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Verifica se é um literal numérico positivo (> 0)
|
|
30
|
+
*/
|
|
31
|
+
function isPositiveNumber(node) {
|
|
32
|
+
return (
|
|
33
|
+
node.type === "Literal" &&
|
|
34
|
+
typeof node.value === "number" &&
|
|
35
|
+
node.value > 0
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Verifica se é o número 0
|
|
41
|
+
*/
|
|
42
|
+
function isZero(node) {
|
|
43
|
+
return (
|
|
44
|
+
node.type === "Literal" &&
|
|
45
|
+
typeof node.value === "number" &&
|
|
46
|
+
node.value === 0
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
BinaryExpression(node) {
|
|
52
|
+
const { operator, left, right } = node;
|
|
53
|
+
|
|
54
|
+
// indexOf(...) > 0 ou indexOf(...) >= 1
|
|
55
|
+
if (isIndexOfCall(left)) {
|
|
56
|
+
if (
|
|
57
|
+
(operator === ">" && isZero(right)) ||
|
|
58
|
+
(operator === ">=" && isPositiveNumber(right))
|
|
59
|
+
) {
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
messageId: "noPositiveIndexOf",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 0 < indexOf(...) ou 1 <= indexOf(...)
|
|
68
|
+
if (isIndexOfCall(right)) {
|
|
69
|
+
if (
|
|
70
|
+
(operator === "<" && isZero(left)) ||
|
|
71
|
+
(operator === "<=" && isPositiveNumber(left))
|
|
72
|
+
) {
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId: "noPositiveIndexOf",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Checks booleanos não devem ser invertidos desnecessariamente
|
|
3
|
+
* !(a === b) deveria ser a !== b
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Checks booleanos não devem ser invertidos desnecessariamente",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
invertedCheck:
|
|
13
|
+
"Negação desnecessária. Use '{{suggestion}}' ao invés de '!({{original}})'.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
18
|
+
|
|
19
|
+
const opposites = {
|
|
20
|
+
"===": "!==",
|
|
21
|
+
"!==": "===",
|
|
22
|
+
"==": "!=",
|
|
23
|
+
"!=": "==",
|
|
24
|
+
"<": ">=",
|
|
25
|
+
">": "<=",
|
|
26
|
+
"<=": ">",
|
|
27
|
+
">=": "<",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
UnaryExpression(node) {
|
|
32
|
+
if (node.operator !== "!") return;
|
|
33
|
+
|
|
34
|
+
const arg = node.argument;
|
|
35
|
+
|
|
36
|
+
// !(a === b) -> a !== b
|
|
37
|
+
if (arg.type === "BinaryExpression" && opposites[arg.operator]) {
|
|
38
|
+
const left = sourceCode.getText(arg.left);
|
|
39
|
+
const right = sourceCode.getText(arg.right);
|
|
40
|
+
const original = `${left} ${arg.operator} ${right}`;
|
|
41
|
+
const suggestion = `${left} ${opposites[arg.operator]} ${right}`;
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "invertedCheck",
|
|
46
|
+
data: { original, suggestion },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Switch não deve conter labels que não são case
|
|
3
|
+
* Misturar labels e cases no switch é confuso e pode ser erro de digitação.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Switch não deve conter labels que não são case",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noLabelInSwitch:
|
|
13
|
+
'Label "{{label}}" dentro do switch é confuso. Use apenas "case" e "default".',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
SwitchStatement(node) {
|
|
19
|
+
// Para cada case do switch
|
|
20
|
+
for (const switchCase of node.cases) {
|
|
21
|
+
// Verifica os statements dentro do case
|
|
22
|
+
for (const statement of switchCase.consequent) {
|
|
23
|
+
checkForLabels(statement, context);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function checkForLabels(node, ctx) {
|
|
30
|
+
if (!node) return;
|
|
31
|
+
|
|
32
|
+
// Se é um labeled statement
|
|
33
|
+
if (node.type === "LabeledStatement") {
|
|
34
|
+
ctx.report({
|
|
35
|
+
node,
|
|
36
|
+
messageId: "noLabelInSwitch",
|
|
37
|
+
data: { label: node.label.name },
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Verifica statements aninhados em blocos
|
|
42
|
+
if (node.type === "BlockStatement") {
|
|
43
|
+
for (const stmt of node.body) {
|
|
44
|
+
checkForLabels(stmt, ctx);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Verifica if/else
|
|
49
|
+
if (node.type === "IfStatement") {
|
|
50
|
+
checkForLabels(node.consequent, ctx);
|
|
51
|
+
checkForLabels(node.alternate, ctx);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Métodos que mutam arrays não devem ser usados de forma enganosa
|
|
3
|
+
* const reversed = arr.reverse() é problemático pois arr também é mutado
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Métodos que mutam arrays não devem ser usados de forma enganosa",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
misleadingMutation:
|
|
13
|
+
"O método '{{method}}' muta o array original. Atribuir o resultado a outra variável é enganoso. Use '.slice().{{method}}()' ou '.toSorted()'/'toReversed()'.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
// Métodos que mutam o array E retornam o próprio array
|
|
18
|
+
// sort é tratado por require-sort-compare, então não incluímos aqui
|
|
19
|
+
const mutatingMethods = new Set([
|
|
20
|
+
"reverse",
|
|
21
|
+
"fill",
|
|
22
|
+
"copyWithin",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
28
|
+
if (node.callee.property.type !== "Identifier") return;
|
|
29
|
+
|
|
30
|
+
const methodName = node.callee.property.name;
|
|
31
|
+
if (!mutatingMethods.has(methodName)) return;
|
|
32
|
+
|
|
33
|
+
const parent = node.parent;
|
|
34
|
+
|
|
35
|
+
// Caso: const reversed = arr.reverse() (atribuindo a variável diferente)
|
|
36
|
+
if (
|
|
37
|
+
parent.type === "VariableDeclarator" &&
|
|
38
|
+
parent.init === node &&
|
|
39
|
+
node.callee.object.type === "Identifier" &&
|
|
40
|
+
parent.id.type === "Identifier"
|
|
41
|
+
) {
|
|
42
|
+
// Se o nome da variável destino é diferente do array original, é enganoso
|
|
43
|
+
if (node.callee.object.name !== parent.id.name) {
|
|
44
|
+
context.report({
|
|
45
|
+
node,
|
|
46
|
+
messageId: "misleadingMutation",
|
|
47
|
+
data: { method: methodName },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regra: Proíbe ternários aninhados
|
|
3
|
+
*
|
|
4
|
+
* Ternários aninhados tornam o código difícil de ler e manter.
|
|
5
|
+
* Use if/else ou extraia a lógica para funções separadas.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
9
|
+
export default {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Proíbe ternários aninhados",
|
|
14
|
+
},
|
|
15
|
+
messages: {
|
|
16
|
+
nested:
|
|
17
|
+
"Ternário aninhado encontrado. Evite ternários dentro de ternários.",
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
return {
|
|
24
|
+
ConditionalExpression(node) {
|
|
25
|
+
const { consequent, alternate } = node;
|
|
26
|
+
|
|
27
|
+
if (consequent && consequent.type === "ConditionalExpression") {
|
|
28
|
+
context.report({
|
|
29
|
+
node: consequent,
|
|
30
|
+
messageId: "nested",
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (alternate && alternate.type === "ConditionalExpression") {
|
|
35
|
+
context.report({
|
|
36
|
+
node: alternate,
|
|
37
|
+
messageId: "nested",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Índices de arrays devem ser numéricos
|
|
3
|
+
* arr["foo"] é problemático, deve usar objeto ao invés de array
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Índices de arrays devem ser numéricos",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
nonNumericIndex:
|
|
13
|
+
"Índice de array deve ser numérico. Use um objeto se precisar de chaves string.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
// Rastreia variáveis que são arrays
|
|
18
|
+
const arrayVariables = new Set();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
// Detecta declarações de array: const arr = [] ou const arr = [1,2,3]
|
|
22
|
+
VariableDeclarator(node) {
|
|
23
|
+
if (
|
|
24
|
+
node.init &&
|
|
25
|
+
node.init.type === "ArrayExpression" &&
|
|
26
|
+
node.id.type === "Identifier"
|
|
27
|
+
) {
|
|
28
|
+
arrayVariables.add(node.id.name);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// Detecta acesso com índice não-numérico: arr["foo"] ou arr[variable]
|
|
33
|
+
MemberExpression(node) {
|
|
34
|
+
// Só verifica se é computed (arr[x]) e não arr.x
|
|
35
|
+
if (!node.computed) return;
|
|
36
|
+
|
|
37
|
+
// Verifica se o objeto é um array conhecido
|
|
38
|
+
if (node.object.type !== "Identifier") return;
|
|
39
|
+
if (!arrayVariables.has(node.object.name)) return;
|
|
40
|
+
|
|
41
|
+
const property = node.property;
|
|
42
|
+
|
|
43
|
+
// Se é um literal string que não é número
|
|
44
|
+
if (property.type === "Literal" && typeof property.value === "string") {
|
|
45
|
+
// Verifica se a string não é um número
|
|
46
|
+
if (isNaN(Number(property.value))) {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: "nonNumericIndex",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Condições booleanas não devem ser redundantes
|
|
3
|
+
* if (a) { if (a) { ... } } - o segundo if é redundante
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Condições booleanas não devem ser redundantes dentro de blocos condicionais",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
redundantCondition:
|
|
13
|
+
"Condição redundante: '{{condition}}' já foi verificada no bloco externo.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
18
|
+
|
|
19
|
+
function getConditionText(node) {
|
|
20
|
+
return sourceCode.getText(node);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Coleta todas as condições de um if/else if chain que garantem verdadeiro
|
|
24
|
+
function collectTruthyConditions(node, conditions = new Set()) {
|
|
25
|
+
if (!node) return conditions;
|
|
26
|
+
|
|
27
|
+
if (node.type === "IfStatement") {
|
|
28
|
+
conditions.add(getConditionText(node.test));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return conditions;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Verifica se uma condição ou parte dela é redundante
|
|
35
|
+
function isRedundant(testNode, truthyConditions) {
|
|
36
|
+
const testText = getConditionText(testNode);
|
|
37
|
+
|
|
38
|
+
// Verifica se a condição exata já foi verificada
|
|
39
|
+
if (truthyConditions.has(testText)) {
|
|
40
|
+
return testText;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Verifica partes de uma expressão &&
|
|
44
|
+
// Ex: if (a && b) { if (a) } - a é redundante
|
|
45
|
+
if (testNode.type === "Identifier" || testNode.type === "MemberExpression") {
|
|
46
|
+
for (const cond of truthyConditions) {
|
|
47
|
+
// Se a condição externa contém && com esta variável
|
|
48
|
+
if (cond.includes(testText + " &&") || cond.includes("&& " + testText)) {
|
|
49
|
+
return testText;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkNestedConditions(node, truthyConditions) {
|
|
58
|
+
if (!node) return;
|
|
59
|
+
|
|
60
|
+
if (node.type === "IfStatement") {
|
|
61
|
+
const redundant = isRedundant(node.test, truthyConditions);
|
|
62
|
+
if (redundant) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: node.test,
|
|
65
|
+
messageId: "redundantCondition",
|
|
66
|
+
data: { condition: redundant },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Adiciona a condição atual e continua verificando dentro do consequent
|
|
71
|
+
const newConditions = new Set(truthyConditions);
|
|
72
|
+
newConditions.add(getConditionText(node.test));
|
|
73
|
+
checkBlock(node.consequent, newConditions);
|
|
74
|
+
|
|
75
|
+
// No else, a condição original é falsa, então não adiciona
|
|
76
|
+
if (node.alternate) {
|
|
77
|
+
checkBlock(node.alternate, truthyConditions);
|
|
78
|
+
}
|
|
79
|
+
} else if (node.type === "BlockStatement") {
|
|
80
|
+
checkBlock(node, truthyConditions);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkBlock(block, truthyConditions) {
|
|
85
|
+
if (!block) return;
|
|
86
|
+
|
|
87
|
+
if (block.type === "BlockStatement") {
|
|
88
|
+
for (const stmt of block.body) {
|
|
89
|
+
checkNestedConditions(stmt, truthyConditions);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
checkNestedConditions(block, truthyConditions);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
IfStatement(node) {
|
|
98
|
+
// Só processa if's de nível superior (não aninhados)
|
|
99
|
+
// Os aninhados serão processados recursivamente
|
|
100
|
+
const parent = node.parent;
|
|
101
|
+
if (
|
|
102
|
+
parent.type === "IfStatement" &&
|
|
103
|
+
(parent.consequent === node || parent.alternate === node)
|
|
104
|
+
) {
|
|
105
|
+
return; // Será processado pelo pai
|
|
106
|
+
}
|
|
107
|
+
if (parent.type === "BlockStatement") {
|
|
108
|
+
const grandparent = parent.parent;
|
|
109
|
+
if (grandparent && grandparent.type === "IfStatement") {
|
|
110
|
+
return; // Será processado pelo avô
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Processa este if e seus aninhados
|
|
115
|
+
const conditions = new Set();
|
|
116
|
+
conditions.add(getConditionText(node.test));
|
|
117
|
+
checkBlock(node.consequent, conditions);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Chamadas .call() e .apply() não devem ser redundantes
|
|
3
|
+
* fn.call(undefined, a, b) deveria ser fn(a, b)
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Chamadas .call() e .apply() não devem ser redundantes",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
redundantCall:
|
|
13
|
+
"Chamada redundante de '.call()'. Use '{{name}}({{args}})' diretamente.",
|
|
14
|
+
redundantApply:
|
|
15
|
+
"Chamada redundante de '.apply()'. Use '{{name}}({{args}})' ou spread operator.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
20
|
+
|
|
21
|
+
function isNullOrUndefined(node) {
|
|
22
|
+
if (!node) return true;
|
|
23
|
+
if (node.type === "Identifier" && node.name === "undefined") return true;
|
|
24
|
+
if (node.type === "Literal" && node.value === null) return true;
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node) {
|
|
30
|
+
if (node.callee.type !== "MemberExpression") return;
|
|
31
|
+
if (node.callee.property.type !== "Identifier") return;
|
|
32
|
+
|
|
33
|
+
const methodName = node.callee.property.name;
|
|
34
|
+
const object = node.callee.object;
|
|
35
|
+
|
|
36
|
+
// fn.call(undefined, a, b) ou fn.call(null, a, b)
|
|
37
|
+
if (methodName === "call") {
|
|
38
|
+
const args = node.arguments;
|
|
39
|
+
|
|
40
|
+
// Se o primeiro argumento é undefined/null, é redundante
|
|
41
|
+
if (args.length === 0 || isNullOrUndefined(args[0])) {
|
|
42
|
+
// Pega o nome da função
|
|
43
|
+
let fnName = sourceCode.getText(object);
|
|
44
|
+
let restArgs = args.slice(1).map(a => sourceCode.getText(a)).join(", ");
|
|
45
|
+
|
|
46
|
+
context.report({
|
|
47
|
+
node,
|
|
48
|
+
messageId: "redundantCall",
|
|
49
|
+
data: { name: fnName, args: restArgs },
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// fn.apply(undefined, [a, b]) ou fn.apply(null, [a, b])
|
|
55
|
+
if (methodName === "apply") {
|
|
56
|
+
const args = node.arguments;
|
|
57
|
+
|
|
58
|
+
// Se o primeiro argumento é undefined/null
|
|
59
|
+
if (args.length <= 1 || isNullOrUndefined(args[0])) {
|
|
60
|
+
let fnName = sourceCode.getText(object);
|
|
61
|
+
let argsText = args[1] ? `...${sourceCode.getText(args[1])}` : "";
|
|
62
|
+
|
|
63
|
+
context.report({
|
|
64
|
+
node,
|
|
65
|
+
messageId: "redundantApply",
|
|
66
|
+
data: { name: fnName, args: argsText },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Não usar "this" em componentes funcionais React
|
|
3
|
+
* Componentes funcionais não têm instância, então "this" não faz sentido.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Não usar "this" em componentes funcionais React',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noThisInFunctional:
|
|
13
|
+
'Não use "this" em componentes funcionais. Use props diretamente ou destructuring.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
// Stack para rastrear funções
|
|
18
|
+
const functionStack = [];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Verifica se o nome parece ser um componente React (PascalCase)
|
|
22
|
+
*/
|
|
23
|
+
function isPascalCase(name) {
|
|
24
|
+
return name && /^[A-Z]/.test(name);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Obtém o nome da função
|
|
29
|
+
*/
|
|
30
|
+
function getFunctionName(node) {
|
|
31
|
+
// function Foo() {}
|
|
32
|
+
if (node.type === "FunctionDeclaration" && node.id) {
|
|
33
|
+
return node.id.name;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// const Foo = function() {} ou const Foo = () => {}
|
|
37
|
+
if (node.parent.type === "VariableDeclarator" && node.parent.id) {
|
|
38
|
+
return node.parent.id.name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function enterFunction(node) {
|
|
45
|
+
const name = getFunctionName(node);
|
|
46
|
+
functionStack.push({
|
|
47
|
+
node,
|
|
48
|
+
name,
|
|
49
|
+
isPascalCase: isPascalCase(name),
|
|
50
|
+
isClassMethod: false,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function exitFunction() {
|
|
55
|
+
functionStack.pop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
// Entra em uma função
|
|
60
|
+
FunctionDeclaration: enterFunction,
|
|
61
|
+
FunctionExpression: enterFunction,
|
|
62
|
+
ArrowFunctionExpression: enterFunction,
|
|
63
|
+
|
|
64
|
+
// Sai da função
|
|
65
|
+
"FunctionDeclaration:exit": exitFunction,
|
|
66
|
+
"FunctionExpression:exit": exitFunction,
|
|
67
|
+
"ArrowFunctionExpression:exit": exitFunction,
|
|
68
|
+
|
|
69
|
+
// Entra em um método de classe
|
|
70
|
+
MethodDefinition() {
|
|
71
|
+
functionStack.push({
|
|
72
|
+
node: null,
|
|
73
|
+
name: null,
|
|
74
|
+
isPascalCase: false,
|
|
75
|
+
isClassMethod: true,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
"MethodDefinition:exit"() {
|
|
80
|
+
functionStack.pop();
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Verifica uso de this
|
|
84
|
+
ThisExpression(node) {
|
|
85
|
+
if (functionStack.length === 0) return;
|
|
86
|
+
|
|
87
|
+
const current = functionStack[functionStack.length - 1];
|
|
88
|
+
|
|
89
|
+
// Se estamos em um método de classe, this é permitido
|
|
90
|
+
if (current.isClassMethod) return;
|
|
91
|
+
|
|
92
|
+
// Se estamos em uma função com nome PascalCase (provável componente), this é erro
|
|
93
|
+
if (current.isPascalCase) {
|
|
94
|
+
context.report({
|
|
95
|
+
node,
|
|
96
|
+
messageId: "noThisInFunctional",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview window.open() deve usar 'noopener' para segurança
|
|
3
|
+
* Sem noopener, a janela aberta pode acessar window.opener
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "window.open() deve incluir 'noopener' nas features",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
missingNoopener:
|
|
13
|
+
"window.open() deve incluir 'noopener' no terceiro argumento para prevenir acesso via window.opener.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
CallExpression(node) {
|
|
19
|
+
// Verifica se é window.open()
|
|
20
|
+
if (
|
|
21
|
+
node.callee.type === "MemberExpression" &&
|
|
22
|
+
node.callee.object.name === "window" &&
|
|
23
|
+
node.callee.property.name === "open"
|
|
24
|
+
) {
|
|
25
|
+
const args = node.arguments;
|
|
26
|
+
|
|
27
|
+
// Se não tem terceiro argumento (features), é problema
|
|
28
|
+
if (args.length < 3) {
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: "missingNoopener",
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Se tem terceiro argumento, verifica se inclui noopener
|
|
37
|
+
const featuresArg = args[2];
|
|
38
|
+
if (
|
|
39
|
+
featuresArg.type === "Literal" &&
|
|
40
|
+
typeof featuresArg.value === "string"
|
|
41
|
+
) {
|
|
42
|
+
if (!featuresArg.value.includes("noopener")) {
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "missingNoopener",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
};
|