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.
Files changed (33) hide show
  1. package/README.MD +66 -0
  2. package/index.js +116 -0
  3. package/package.json +24 -0
  4. package/rules/array-method-missing-args.js +44 -0
  5. package/rules/await-only-promises.js +84 -0
  6. package/rules/for-loop-update-counter.js +103 -0
  7. package/rules/max-switch-cases.js +39 -0
  8. package/rules/no-array-delete.js +69 -0
  9. package/rules/no-bitwise-in-boolean.js +57 -0
  10. package/rules/no-constant-binary-expression.js +166 -0
  11. package/rules/no-empty-regex-alternatives.js +102 -0
  12. package/rules/no-example-rule.js +56 -0
  13. package/rules/no-gratuitous-boolean.js +121 -0
  14. package/rules/no-identical-branches.js +93 -0
  15. package/rules/no-in-array.js +103 -0
  16. package/rules/no-in-with-primitives.js +56 -0
  17. package/rules/no-index-of-compare-positive.js +82 -0
  18. package/rules/no-inverted-boolean-check.js +52 -0
  19. package/rules/no-label-in-switch.js +55 -0
  20. package/rules/no-misleading-array-mutation.js +54 -0
  21. package/rules/no-nested-ternary.js +43 -0
  22. package/rules/no-non-numeric-array-index.js +56 -0
  23. package/rules/no-redundant-boolean-condition.js +121 -0
  24. package/rules/no-redundant-call-apply.js +73 -0
  25. package/rules/no-this-in-functional-component.js +102 -0
  26. package/rules/no-unsafe-window-open.js +53 -0
  27. package/rules/prefer-for-of.js +119 -0
  28. package/rules/prefer-regex-exec.js +72 -0
  29. package/rules/prefer-while.js +28 -0
  30. package/rules/regex-anchor-with-alternation.js +125 -0
  31. package/rules/require-reduce-initial-value.js +43 -0
  32. package/rules/require-sort-compare.js +43 -0
  33. 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
+ };