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,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Expressões que sempre retornam o mesmo valor
|
|
3
|
+
* Cobre: a < a, true && x, x === new Object(), if ({})
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Expressões não devem sempre retornar o mesmo valor",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
alwaysSame:
|
|
13
|
+
"Esta expressão sempre retorna o mesmo valor ({{result}}).",
|
|
14
|
+
compareNewObject:
|
|
15
|
+
"Comparação com novo objeto sempre retorna {{result}}. Objetos são comparados por referência.",
|
|
16
|
+
constantLogical:
|
|
17
|
+
"Expressão lógica com '{{value}}' sempre {{behavior}}.",
|
|
18
|
+
alwaysTruthy:
|
|
19
|
+
"{{type}} são sempre truthy em condições.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
create(context) {
|
|
23
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
24
|
+
|
|
25
|
+
function getNodeText(node) {
|
|
26
|
+
return sourceCode.getText(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function areNodesEqual(left, right) {
|
|
30
|
+
return getNodeText(left) === getNodeText(right);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isNewExpression(node) {
|
|
34
|
+
return node.type === "NewExpression";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isLiteralBoolean(node, value) {
|
|
38
|
+
return node.type === "Literal" && node.value === value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isAlwaysTruthyObject(node) {
|
|
42
|
+
return (
|
|
43
|
+
node.type === "ObjectExpression" ||
|
|
44
|
+
node.type === "ArrayExpression" ||
|
|
45
|
+
node.type === "ArrowFunctionExpression" ||
|
|
46
|
+
node.type === "FunctionExpression"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
// Comparações consigo mesmo: a < a, a === a
|
|
52
|
+
BinaryExpression(node) {
|
|
53
|
+
const operator = node.operator;
|
|
54
|
+
|
|
55
|
+
// Comparação com new Object() - sempre false para ===, true para !==
|
|
56
|
+
if (
|
|
57
|
+
(operator === "===" || operator === "==" || operator === "!==" || operator === "!=") &&
|
|
58
|
+
(isNewExpression(node.left) || isNewExpression(node.right))
|
|
59
|
+
) {
|
|
60
|
+
const result = operator === "!==" || operator === "!=" ? "true" : "false";
|
|
61
|
+
context.report({
|
|
62
|
+
node,
|
|
63
|
+
messageId: "compareNewObject",
|
|
64
|
+
data: { result },
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Comparações consigo mesmo
|
|
70
|
+
if (!areNodesEqual(node.left, node.right)) return;
|
|
71
|
+
|
|
72
|
+
let result = null;
|
|
73
|
+
|
|
74
|
+
if (operator === "<" || operator === ">") {
|
|
75
|
+
result = "false";
|
|
76
|
+
} else if (operator === "<=" || operator === ">=" || operator === "===" || operator === "==") {
|
|
77
|
+
result = "true";
|
|
78
|
+
} else if (operator === "!==" || operator === "!=") {
|
|
79
|
+
result = "false";
|
|
80
|
+
} else if (operator === "-") {
|
|
81
|
+
result = "0";
|
|
82
|
+
} else if (operator === "/") {
|
|
83
|
+
result = "1";
|
|
84
|
+
} else if (operator === "%") {
|
|
85
|
+
result = "0";
|
|
86
|
+
} else if (operator === "^") {
|
|
87
|
+
result = "0";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (result !== null) {
|
|
91
|
+
context.report({
|
|
92
|
+
node,
|
|
93
|
+
messageId: "alwaysSame",
|
|
94
|
+
data: { result },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Logical expressions: true && x, false || x
|
|
100
|
+
LogicalExpression(node) {
|
|
101
|
+
const operator = node.operator;
|
|
102
|
+
|
|
103
|
+
// true && x -> sempre executa x (redundante)
|
|
104
|
+
// false && x -> nunca executa x
|
|
105
|
+
// true || x -> nunca executa x
|
|
106
|
+
// false || x -> sempre executa x (redundante)
|
|
107
|
+
if (operator === "&&") {
|
|
108
|
+
if (isLiteralBoolean(node.left, true)) {
|
|
109
|
+
context.report({
|
|
110
|
+
node,
|
|
111
|
+
messageId: "constantLogical",
|
|
112
|
+
data: { value: "true", behavior: "executa o lado direito (redundante)" },
|
|
113
|
+
});
|
|
114
|
+
} else if (isLiteralBoolean(node.left, false)) {
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
messageId: "constantLogical",
|
|
118
|
+
data: { value: "false", behavior: "retorna false (lado direito nunca executa)" },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} else if (operator === "||") {
|
|
122
|
+
if (isLiteralBoolean(node.left, true)) {
|
|
123
|
+
context.report({
|
|
124
|
+
node,
|
|
125
|
+
messageId: "constantLogical",
|
|
126
|
+
data: { value: "true", behavior: "retorna true (lado direito nunca executa)" },
|
|
127
|
+
});
|
|
128
|
+
} else if (isLiteralBoolean(node.left, false)) {
|
|
129
|
+
context.report({
|
|
130
|
+
node,
|
|
131
|
+
messageId: "constantLogical",
|
|
132
|
+
data: { value: "false", behavior: "executa o lado direito (redundante)" },
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// if ({}) - objetos/arrays/funções são sempre truthy
|
|
139
|
+
IfStatement(node) {
|
|
140
|
+
if (isAlwaysTruthyObject(node.test)) {
|
|
141
|
+
const type = node.test.type === "ObjectExpression" ? "Objetos {}"
|
|
142
|
+
: node.test.type === "ArrayExpression" ? "Arrays []"
|
|
143
|
+
: "Funções";
|
|
144
|
+
context.report({
|
|
145
|
+
node: node.test,
|
|
146
|
+
messageId: "alwaysTruthy",
|
|
147
|
+
data: { type },
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
ConditionalExpression(node) {
|
|
153
|
+
if (isAlwaysTruthyObject(node.test)) {
|
|
154
|
+
const type = node.test.type === "ObjectExpression" ? "Objetos {}"
|
|
155
|
+
: node.test.type === "ArrayExpression" ? "Arrays []"
|
|
156
|
+
: "Funções";
|
|
157
|
+
context.report({
|
|
158
|
+
node: node.test,
|
|
159
|
+
messageId: "alwaysTruthy",
|
|
160
|
+
data: { type },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
},
|
|
166
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Alternativas em regex não devem ser vazias
|
|
3
|
+
* /a|/ ou /|a/ ou /a||b/ são problemáticos
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Alternativas em regex não devem ser vazias",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
emptyAlternative:
|
|
13
|
+
"Regex contém alternativa vazia. Remova o '|' desnecessário ou adicione um padrão.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
function checkRegexPattern(pattern, node) {
|
|
18
|
+
// Verifica se tem || (alternativa vazia no meio)
|
|
19
|
+
if (pattern.includes("||")) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Divide por | e verifica se alguma parte é vazia
|
|
24
|
+
// Mas precisa ignorar | dentro de grupos [] ou escapados
|
|
25
|
+
let depth = 0;
|
|
26
|
+
let inCharClass = false;
|
|
27
|
+
let lastWasPipe = false;
|
|
28
|
+
let isFirstChar = true;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
31
|
+
const char = pattern[i];
|
|
32
|
+
const prevChar = i > 0 ? pattern[i - 1] : "";
|
|
33
|
+
|
|
34
|
+
// Ignora caracteres escapados
|
|
35
|
+
if (prevChar === "\\") {
|
|
36
|
+
isFirstChar = false;
|
|
37
|
+
lastWasPipe = false;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (char === "[") {
|
|
42
|
+
inCharClass = true;
|
|
43
|
+
} else if (char === "]" && inCharClass) {
|
|
44
|
+
inCharClass = false;
|
|
45
|
+
} else if (char === "(" && !inCharClass) {
|
|
46
|
+
depth++;
|
|
47
|
+
} else if (char === ")" && !inCharClass) {
|
|
48
|
+
depth--;
|
|
49
|
+
} else if (char === "|" && !inCharClass) {
|
|
50
|
+
// | no início ou depois de outro | é alternativa vazia
|
|
51
|
+
if (isFirstChar || lastWasPipe) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
lastWasPipe = true;
|
|
55
|
+
isFirstChar = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
isFirstChar = false;
|
|
60
|
+
lastWasPipe = false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// | no final é alternativa vazia
|
|
64
|
+
if (lastWasPipe) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
// Regex literal: /pattern/
|
|
73
|
+
Literal(node) {
|
|
74
|
+
if (node.regex) {
|
|
75
|
+
if (checkRegexPattern(node.regex.pattern, node)) {
|
|
76
|
+
context.report({
|
|
77
|
+
node,
|
|
78
|
+
messageId: "emptyAlternative",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// new RegExp("pattern")
|
|
85
|
+
NewExpression(node) {
|
|
86
|
+
if (
|
|
87
|
+
node.callee.name === "RegExp" &&
|
|
88
|
+
node.arguments.length > 0 &&
|
|
89
|
+
node.arguments[0].type === "Literal" &&
|
|
90
|
+
typeof node.arguments[0].value === "string"
|
|
91
|
+
) {
|
|
92
|
+
if (checkRegexPattern(node.arguments[0].value, node)) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: "emptyAlternative",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exemplo de regra ESLint customizada
|
|
3
|
+
*
|
|
4
|
+
* Esta regra proíbe o uso de console.log com a string "DEBUG"
|
|
5
|
+
* É apenas um exemplo para você entender a estrutura.
|
|
6
|
+
*
|
|
7
|
+
* Documentação: https://eslint.org/docs/latest/extend/custom-rules
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "suggestion", // "problem" | "suggestion" | "layout"
|
|
14
|
+
docs: {
|
|
15
|
+
description: "Proíbe console.log com DEBUG (regra de exemplo)",
|
|
16
|
+
recommended: false,
|
|
17
|
+
},
|
|
18
|
+
fixable: null, // "code" se tiver auto-fix, null se não
|
|
19
|
+
schema: [], // opções da regra (deixe vazio se não tiver)
|
|
20
|
+
messages: {
|
|
21
|
+
noDebugLog: 'Não use console.log com "DEBUG". Remova antes de commitar.',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
// Visitor pattern - visita cada nó da AST
|
|
28
|
+
// Veja os tipos de nós em: https://astexplorer.net/
|
|
29
|
+
|
|
30
|
+
CallExpression(node) {
|
|
31
|
+
// Verifica se é console.log
|
|
32
|
+
const isConsoleLog =
|
|
33
|
+
node.callee.type === "MemberExpression" &&
|
|
34
|
+
node.callee.object.name === "console" &&
|
|
35
|
+
node.callee.property.name === "log";
|
|
36
|
+
|
|
37
|
+
if (!isConsoleLog) return;
|
|
38
|
+
|
|
39
|
+
// Verifica se algum argumento contém "DEBUG"
|
|
40
|
+
const hasDebug = node.arguments.some(
|
|
41
|
+
(arg) =>
|
|
42
|
+
arg.type === "Literal" &&
|
|
43
|
+
typeof arg.value === "string" &&
|
|
44
|
+
arg.value.includes("DEBUG")
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (hasDebug) {
|
|
48
|
+
context.report({
|
|
49
|
+
node,
|
|
50
|
+
messageId: "noDebugLog",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Expressões booleanas não devem ser gratuitas
|
|
3
|
+
* Cobre: a === true, a ? true : false, !a ? false : true
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "suggestion",
|
|
8
|
+
docs: {
|
|
9
|
+
description: "Expressões booleanas não devem ser gratuitas ou redundantes",
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
comparisonWithBoolean:
|
|
13
|
+
"Comparação desnecessária com '{{value}}'. Use {{suggestion}}.",
|
|
14
|
+
ternaryBoolean:
|
|
15
|
+
"Ternário redundante retornando booleanos. Use '{{suggestion}}'.",
|
|
16
|
+
doubleNegation:
|
|
17
|
+
"Dupla negação desnecessária. Use '{{suggestion}}'.",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
22
|
+
|
|
23
|
+
function getNodeText(node) {
|
|
24
|
+
return sourceCode.getText(node);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
// a === true, a === false, a !== true, a !== false
|
|
29
|
+
BinaryExpression(node) {
|
|
30
|
+
const { operator, left, right } = node;
|
|
31
|
+
|
|
32
|
+
if (!["===", "==", "!==", "!="].includes(operator)) return;
|
|
33
|
+
|
|
34
|
+
let booleanSide = null;
|
|
35
|
+
let otherSide = null;
|
|
36
|
+
|
|
37
|
+
if (left.type === "Literal" && typeof left.value === "boolean") {
|
|
38
|
+
booleanSide = left;
|
|
39
|
+
otherSide = right;
|
|
40
|
+
} else if (right.type === "Literal" && typeof right.value === "boolean") {
|
|
41
|
+
booleanSide = right;
|
|
42
|
+
otherSide = left;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!booleanSide) return;
|
|
46
|
+
|
|
47
|
+
const boolValue = booleanSide.value;
|
|
48
|
+
const isEquality = operator === "===" || operator === "==";
|
|
49
|
+
const otherText = getNodeText(otherSide);
|
|
50
|
+
|
|
51
|
+
let suggestion;
|
|
52
|
+
if (boolValue === true) {
|
|
53
|
+
suggestion = isEquality ? otherText : `!${otherText}`;
|
|
54
|
+
} else {
|
|
55
|
+
suggestion = isEquality ? `!${otherText}` : otherText;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: "comparisonWithBoolean",
|
|
61
|
+
data: { value: String(boolValue), suggestion },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// a ? true : false, a ? false : true
|
|
66
|
+
ConditionalExpression(node) {
|
|
67
|
+
const { consequent, alternate, test } = node;
|
|
68
|
+
|
|
69
|
+
// Verifica se consequent e alternate são literais booleanos
|
|
70
|
+
if (
|
|
71
|
+
consequent.type !== "Literal" ||
|
|
72
|
+
alternate.type !== "Literal" ||
|
|
73
|
+
typeof consequent.value !== "boolean" ||
|
|
74
|
+
typeof alternate.value !== "boolean"
|
|
75
|
+
) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const testText = getNodeText(test);
|
|
80
|
+
let suggestion;
|
|
81
|
+
|
|
82
|
+
if (consequent.value === true && alternate.value === false) {
|
|
83
|
+
// a ? true : false -> !!a ou apenas a (se já é boolean)
|
|
84
|
+
suggestion = `!!${testText}`;
|
|
85
|
+
} else if (consequent.value === false && alternate.value === true) {
|
|
86
|
+
// a ? false : true -> !a
|
|
87
|
+
suggestion = `!${testText}`;
|
|
88
|
+
} else {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: "ternaryBoolean",
|
|
95
|
+
data: { suggestion },
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// !!a quando a já é boolean (verificação básica para !! em literais)
|
|
100
|
+
UnaryExpression(node) {
|
|
101
|
+
if (node.operator !== "!") return;
|
|
102
|
+
|
|
103
|
+
const arg = node.argument;
|
|
104
|
+
|
|
105
|
+
// !!true, !!false
|
|
106
|
+
if (
|
|
107
|
+
arg.type === "UnaryExpression" &&
|
|
108
|
+
arg.operator === "!" &&
|
|
109
|
+
arg.argument.type === "Literal" &&
|
|
110
|
+
typeof arg.argument.value === "boolean"
|
|
111
|
+
) {
|
|
112
|
+
context.report({
|
|
113
|
+
node,
|
|
114
|
+
messageId: "doubleNegation",
|
|
115
|
+
data: { suggestion: String(arg.argument.value) },
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Branches de if/else/ternário/switch não devem ter implementações idênticas
|
|
3
|
+
*/
|
|
4
|
+
export default {
|
|
5
|
+
meta: {
|
|
6
|
+
type: "problem",
|
|
7
|
+
docs: {
|
|
8
|
+
description: "Branches não devem ter implementações idênticas",
|
|
9
|
+
},
|
|
10
|
+
messages: {
|
|
11
|
+
identicalIfBranches:
|
|
12
|
+
"Todos os branches do if/else têm a mesma implementação. Remova a condição ou diferencie os branches.",
|
|
13
|
+
identicalTernary:
|
|
14
|
+
"Os dois lados do ternário são idênticos. Use o valor diretamente.",
|
|
15
|
+
identicalSwitchCases:
|
|
16
|
+
"Todos os cases do switch têm a mesma implementação. Remova o switch ou diferencie os cases.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
21
|
+
|
|
22
|
+
function getNodeText(node) {
|
|
23
|
+
if (!node) return "";
|
|
24
|
+
return sourceCode.getText(node);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Verifica if/else
|
|
28
|
+
function checkIfStatement(node) {
|
|
29
|
+
// Precisa ter else para comparar (sem else não é erro conforme exceção)
|
|
30
|
+
if (!node.alternate) return;
|
|
31
|
+
|
|
32
|
+
const consequentText = getNodeText(node.consequent);
|
|
33
|
+
const alternateText = getNodeText(node.alternate);
|
|
34
|
+
|
|
35
|
+
if (consequentText === alternateText) {
|
|
36
|
+
context.report({
|
|
37
|
+
node,
|
|
38
|
+
messageId: "identicalIfBranches",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Verifica ternário: condition ? a : a
|
|
44
|
+
function checkConditionalExpression(node) {
|
|
45
|
+
const consequentText = getNodeText(node.consequent);
|
|
46
|
+
const alternateText = getNodeText(node.alternate);
|
|
47
|
+
|
|
48
|
+
if (consequentText === alternateText) {
|
|
49
|
+
context.report({
|
|
50
|
+
node,
|
|
51
|
+
messageId: "identicalTernary",
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Verifica switch com todos os cases iguais
|
|
57
|
+
function checkSwitchStatement(node) {
|
|
58
|
+
const cases = node.cases;
|
|
59
|
+
|
|
60
|
+
// Precisa ter default para ser erro (conforme exceção)
|
|
61
|
+
const hasDefault = cases.some((c) => c.test === null);
|
|
62
|
+
if (!hasDefault) return;
|
|
63
|
+
|
|
64
|
+
// Precisa ter pelo menos 2 cases
|
|
65
|
+
if (cases.length < 2) return;
|
|
66
|
+
|
|
67
|
+
// Extrai o código de cada case (sem o break no final)
|
|
68
|
+
const caseTexts = cases.map((caseNode) => {
|
|
69
|
+
const statements = caseNode.consequent.filter(
|
|
70
|
+
(stmt) => stmt.type !== "BreakStatement"
|
|
71
|
+
);
|
|
72
|
+
return statements.map((s) => getNodeText(s)).join("\n");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Verifica se todos são idênticos
|
|
76
|
+
const firstCase = caseTexts[0];
|
|
77
|
+
const allIdentical = caseTexts.every((text) => text === firstCase);
|
|
78
|
+
|
|
79
|
+
if (allIdentical && firstCase !== "") {
|
|
80
|
+
context.report({
|
|
81
|
+
node,
|
|
82
|
+
messageId: "identicalSwitchCases",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
IfStatement: checkIfStatement,
|
|
89
|
+
ConditionalExpression: checkConditionalExpression,
|
|
90
|
+
SwitchStatement: checkSwitchStatement,
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Não usar operador "in" em arrays
|
|
3
|
+
* O operador "in" verifica propriedades do objeto, não índices do array.
|
|
4
|
+
* Use includes(), indexOf() ou hasOwnProperty() para arrays.
|
|
5
|
+
*/
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: "problem",
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Não usar operador "in" em arrays',
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
noInArray:
|
|
14
|
+
'Não use o operador "in" em arrays. Use includes(), indexOf() ou um loop for-of.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
BinaryExpression(node) {
|
|
20
|
+
if (node.operator !== "in") return;
|
|
21
|
+
|
|
22
|
+
const right = node.right;
|
|
23
|
+
|
|
24
|
+
// Detecta array literal: x in [1, 2, 3]
|
|
25
|
+
if (right.type === "ArrayExpression") {
|
|
26
|
+
context.report({
|
|
27
|
+
node,
|
|
28
|
+
messageId: "noInArray",
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Detecta identificadores com nomes comuns de array
|
|
34
|
+
if (right.type === "Identifier") {
|
|
35
|
+
const name = right.name.toLowerCase();
|
|
36
|
+
// Heurística: nomes que terminam com 's', 'list', 'array', 'items', etc.
|
|
37
|
+
if (
|
|
38
|
+
name.endsWith("s") ||
|
|
39
|
+
name.endsWith("list") ||
|
|
40
|
+
name.endsWith("array") ||
|
|
41
|
+
name.endsWith("items") ||
|
|
42
|
+
name.endsWith("arr") ||
|
|
43
|
+
name === "arr"
|
|
44
|
+
) {
|
|
45
|
+
context.report({
|
|
46
|
+
node,
|
|
47
|
+
messageId: "noInArray",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Detecta chamadas que retornam arrays: x in arr.filter() ou x in Object.keys()
|
|
53
|
+
if (right.type === "CallExpression") {
|
|
54
|
+
const callee = right.callee;
|
|
55
|
+
|
|
56
|
+
// Métodos de array que retornam arrays
|
|
57
|
+
if (callee.type === "MemberExpression" && callee.property) {
|
|
58
|
+
const methodName = callee.property.name;
|
|
59
|
+
const arrayMethods = [
|
|
60
|
+
"filter",
|
|
61
|
+
"map",
|
|
62
|
+
"slice",
|
|
63
|
+
"concat",
|
|
64
|
+
"flat",
|
|
65
|
+
"flatMap",
|
|
66
|
+
"reverse",
|
|
67
|
+
"sort",
|
|
68
|
+
"splice",
|
|
69
|
+
"from",
|
|
70
|
+
"of",
|
|
71
|
+
];
|
|
72
|
+
if (arrayMethods.includes(methodName)) {
|
|
73
|
+
context.report({
|
|
74
|
+
node,
|
|
75
|
+
messageId: "noInArray",
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Object.keys(), Object.values(), Object.entries(), Array.from()
|
|
82
|
+
if (
|
|
83
|
+
callee.type === "MemberExpression" &&
|
|
84
|
+
callee.object.type === "Identifier"
|
|
85
|
+
) {
|
|
86
|
+
const objName = callee.object.name;
|
|
87
|
+
const methodName = callee.property.name;
|
|
88
|
+
if (
|
|
89
|
+
(objName === "Object" &&
|
|
90
|
+
["keys", "values", "entries"].includes(methodName)) ||
|
|
91
|
+
(objName === "Array" && ["from", "of"].includes(methodName))
|
|
92
|
+
) {
|
|
93
|
+
context.report({
|
|
94
|
+
node,
|
|
95
|
+
messageId: "noInArray",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Não usar operador "in" com tipos primitivos
|
|
3
|
+
* O operador "in" com primitivos (string, number, boolean) causa TypeError.
|
|
4
|
+
*/
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: "problem",
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Não usar operador "in" com tipos primitivos',
|
|
10
|
+
},
|
|
11
|
+
messages: {
|
|
12
|
+
noInWithPrimitive:
|
|
13
|
+
'O operador "in" com tipo primitivo ({{type}}) causa TypeError. Use o objeto equivalente (new String(), etc.) se necessário.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
return {
|
|
18
|
+
BinaryExpression(node) {
|
|
19
|
+
if (node.operator !== "in") return;
|
|
20
|
+
|
|
21
|
+
const right = node.right;
|
|
22
|
+
|
|
23
|
+
// Detecta literais primitivos
|
|
24
|
+
if (right.type === "Literal") {
|
|
25
|
+
const value = right.value;
|
|
26
|
+
let primitiveType = null;
|
|
27
|
+
|
|
28
|
+
if (typeof value === "string") {
|
|
29
|
+
primitiveType = "string";
|
|
30
|
+
} else if (typeof value === "number") {
|
|
31
|
+
primitiveType = "number";
|
|
32
|
+
} else if (typeof value === "boolean") {
|
|
33
|
+
primitiveType = "boolean";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (primitiveType) {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: "noInWithPrimitive",
|
|
40
|
+
data: { type: primitiveType },
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Detecta template literals
|
|
46
|
+
if (right.type === "TemplateLiteral") {
|
|
47
|
+
context.report({
|
|
48
|
+
node,
|
|
49
|
+
messageId: "noInWithPrimitive",
|
|
50
|
+
data: { type: "string" },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|