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
package/README.MD ADDED
@@ -0,0 +1,66 @@
1
+ # eslint-plugin-sonar-config
2
+
3
+ Plugin ESLint com regras customizadas para qualidade de código em projeto com SonarQube implementado.
4
+
5
+ ## Instalação
6
+
7
+ ```bash
8
+ npm install --save-dev eslint-plugin-sonar-config
9
+ ```
10
+
11
+ ## Uso
12
+
13
+ ### Configuração Básica
14
+
15
+ No seu `.eslintrc.js` ou `.eslintrc.json`:
16
+
17
+ ```json
18
+ {
19
+ "plugins": ["sonar-config"],
20
+ "rules": {
21
+ "sonar-config/no-nested-ternary": "error"
22
+ }
23
+ }
24
+ ```
25
+
26
+ ### Usando Configuração Recomendada
27
+
28
+ ```json
29
+ {
30
+ "extends": ["plugin:sonar-config/recommended"]
31
+ }
32
+ ```
33
+
34
+ ## Regras Disponíveis
35
+
36
+ ### `no-nested-ternary`
37
+
38
+ Proíbe o uso de ternários aninhados em código de produção.
39
+
40
+ ---
41
+
42
+ ## Integração com a CLI
43
+
44
+ Este plugin foi desenvolvido para ser usado automaticamente pela CLI de qualidade de código Liferay. A CLI adiciona e configura este plugin automaticamente em novos projetos.
45
+
46
+ ## Desenvolvimento
47
+
48
+ ### Estrutura do Projeto
49
+
50
+ ```
51
+ eslint-plugin-sonar-config/
52
+ ├── index.js # Entry point
53
+ ├── package.json
54
+ ├── README.md
55
+ └── rules/
56
+ ├── no-console-in-production.js
57
+ ├── require-jsdoc-comments.js
58
+ └── no-unused-liferay-imports.js
59
+ ```
60
+
61
+ ### Testando Localmente
62
+
63
+ 1. Clone o repositório
64
+ 2. Execute `npm link` na pasta do plugin
65
+ 3. No seu projeto de teste, execute `npm link eslint-plugin-sonar-config`
66
+ 4. Configure o `.eslintrc` e teste as regras
package/index.js ADDED
@@ -0,0 +1,116 @@
1
+ import noNestedTernary from "./rules/no-nested-ternary.js";
2
+ import awaitOnlyPromises from "./rules/await-only-promises.js";
3
+ import forLoopUpdateCounter from "./rules/for-loop-update-counter.js";
4
+ import maxSwitchCases from "./rules/max-switch-cases.js";
5
+ import noArrayDelete from "./rules/no-array-delete.js";
6
+ import noExampleRule from "./rules/no-example-rule.js";
7
+ import noInArray from "./rules/no-in-array.js";
8
+ import noInWithPrimitives from "./rules/no-in-with-primitives.js";
9
+ import noIndexOfComparePositive from "./rules/no-index-of-compare-positive.js";
10
+ import noLabelInSwitch from "./rules/no-label-in-switch.js";
11
+ import noThisInFunctionalComponent from "./rules/no-this-in-functional-component.js";
12
+ import preferForOf from "./rules/prefer-for-of.js";
13
+ import preferRegexExec from "./rules/prefer-regex-exec.js";
14
+ import requireReduceInitialValue from "./rules/require-reduce-initial-value.js";
15
+ import requireSortCompare from "./rules/require-sort-compare.js";
16
+ import useStateCallback from "./rules/use-state-callback.js";
17
+ import preferWhile from "./rules/prefer-while.js";
18
+ import noIdenticalBranches from "./rules/no-identical-branches.js";
19
+ import noEmptyRegexAlternatives from "./rules/no-empty-regex-alternatives.js";
20
+ import regexAnchorWithAlternation from "./rules/regex-anchor-with-alternation.js";
21
+ import noNonNumericArrayIndex from "./rules/no-non-numeric-array-index.js";
22
+ import arrayMethodMissingArgs from "./rules/array-method-missing-args.js";
23
+ import noMisleadingArrayMutation from "./rules/no-misleading-array-mutation.js";
24
+ import noUnsafeWindowOpen from "./rules/no-unsafe-window-open.js";
25
+ import noConstantBinaryExpression from "./rules/no-constant-binary-expression.js";
26
+ import noBitwiseInBoolean from "./rules/no-bitwise-in-boolean.js";
27
+ import noInvertedBooleanCheck from "./rules/no-inverted-boolean-check.js";
28
+ import noGratuitousBoolean from "./rules/no-gratuitous-boolean.js";
29
+ import noRedundantBooleanCondition from "./rules/no-redundant-boolean-condition.js";
30
+ import noRedundantCallApply from "./rules/no-redundant-call-apply.js";
31
+
32
+ const rulesConfig = {
33
+ "no-nested-ternary": { rule: noNestedTernary, level: "error" },
34
+ "no-example-rule": { rule: noExampleRule, level: "error" },
35
+ "require-sort-compare": { rule: requireSortCompare, level: "error" },
36
+ "require-reduce-initial-value": {
37
+ rule: requireReduceInitialValue,
38
+ level: "error",
39
+ },
40
+ "await-only-promises": { rule: awaitOnlyPromises, level: "error" },
41
+ "no-array-delete": { rule: noArrayDelete, level: "error" },
42
+ "no-in-array": { rule: noInArray, level: "error" },
43
+ "prefer-for-of": { rule: preferForOf, level: "warn" },
44
+ "for-loop-update-counter": { rule: forLoopUpdateCounter, level: "error" },
45
+ "no-in-with-primitives": { rule: noInWithPrimitives, level: "error" },
46
+ "no-index-of-compare-positive": {
47
+ rule: noIndexOfComparePositive,
48
+ level: "error",
49
+ },
50
+ "prefer-regex-exec": { rule: preferRegexExec, level: "error" },
51
+ "no-label-in-switch": { rule: noLabelInSwitch, level: "error" },
52
+ "max-switch-cases": { rule: maxSwitchCases, level: "error" },
53
+ "use-state-callback": { rule: useStateCallback, level: "error" },
54
+ "no-this-in-functional-component": {
55
+ rule: noThisInFunctionalComponent,
56
+ level: "error",
57
+ },
58
+ "prefer-while": { rule: preferWhile, level: "error" },
59
+ "no-identical-branches": { rule: noIdenticalBranches, level: "error" },
60
+ "no-empty-regex-alternatives": {
61
+ rule: noEmptyRegexAlternatives,
62
+ level: "error",
63
+ },
64
+ "regex-anchor-with-alternation": {
65
+ rule: regexAnchorWithAlternation,
66
+ level: "error",
67
+ },
68
+ "no-non-numeric-array-index": {
69
+ rule: noNonNumericArrayIndex,
70
+ level: "error",
71
+ },
72
+ "array-method-missing-args": { rule: arrayMethodMissingArgs, level: "error" },
73
+ "no-misleading-array-mutation": {
74
+ rule: noMisleadingArrayMutation,
75
+ level: "error",
76
+ },
77
+ "no-unsafe-window-open": { rule: noUnsafeWindowOpen, level: "error" },
78
+ "no-constant-binary-expression": {
79
+ rule: noConstantBinaryExpression,
80
+ level: "error",
81
+ },
82
+ "no-bitwise-in-boolean": { rule: noBitwiseInBoolean, level: "error" },
83
+ "no-inverted-boolean-check": { rule: noInvertedBooleanCheck, level: "error" },
84
+ "no-gratuitous-boolean": { rule: noGratuitousBoolean, level: "error" },
85
+ "no-redundant-boolean-condition": {
86
+ rule: noRedundantBooleanCondition,
87
+ level: "error",
88
+ },
89
+ "no-redundant-call-apply": { rule: noRedundantCallApply, level: "error" },
90
+ };
91
+
92
+ const rules = Object.fromEntries(
93
+ Object.entries(rulesConfig).map(([name, { rule }]) => [name, rule])
94
+ );
95
+
96
+ const recommendedRules = Object.fromEntries(
97
+ Object.entries(rulesConfig).map(([name, { level }]) => [
98
+ `sonar-config/${name}`,
99
+ level,
100
+ ])
101
+ );
102
+
103
+ const plugin = {
104
+ meta: {
105
+ name: "eslint-plugin-sonar-config",
106
+ version: "0.1.1",
107
+ },
108
+ rules,
109
+ configs: {
110
+ recommended: {
111
+ rules: recommendedRules,
112
+ },
113
+ },
114
+ };
115
+
116
+ export default plugin;
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "eslint-plugin-sonar-config",
3
+ "version": "0.1.1",
4
+ "description": "ESLint plugin com regras customizadas para melhorar a qualidade de código em projeto com Sonar implementado.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "keywords": [
8
+ "eslint",
9
+ "eslintplugin",
10
+ "eslint-plugin",
11
+ "sonar",
12
+ "code-quality"
13
+ ],
14
+ "license": "MIT",
15
+ "peerDependencies": {
16
+ "eslint": "8.57.0"
17
+ },
18
+ "engines": {
19
+ "node": "16.0.0"
20
+ },
21
+ "exports": {
22
+ ".": "./index.js"
23
+ }
24
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @fileoverview Métodos push/unshift devem receber pelo menos 1 argumento
3
+ * [].push() ou arr.push() sem argumentos é inútil e provavelmente um bug
4
+ */
5
+ export default {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description: "Métodos push/unshift devem receber pelo menos 1 argumento",
10
+ },
11
+ messages: {
12
+ missingArgs:
13
+ "O método '{{method}}' foi chamado sem argumentos. Isso não faz nada.",
14
+ },
15
+ },
16
+ create(context) {
17
+ // Apenas push e unshift - claramente bugs quando sem argumentos
18
+ const methodsNeedingArgs = new Set(["push", "unshift"]);
19
+
20
+ return {
21
+ CallExpression(node) {
22
+ // Verifica se é chamada de método: algo.push()
23
+ if (node.callee.type !== "MemberExpression") return;
24
+ if (node.callee.property.type !== "Identifier") return;
25
+
26
+ const methodName = node.callee.property.name;
27
+
28
+ // Só verifica push e unshift
29
+ if (!methodsNeedingArgs.has(methodName)) return;
30
+
31
+ // Se foi chamado sem argumentos, é um problema
32
+ if (node.arguments.length === 0) {
33
+ context.report({
34
+ node,
35
+ messageId: "missingArgs",
36
+ data: {
37
+ method: methodName,
38
+ },
39
+ });
40
+ }
41
+ },
42
+ };
43
+ },
44
+ };
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Regra: "await" deve ser usado apenas com Promises
3
+ *
4
+ * Sonar: javascript:S4123
5
+ *
6
+ * Nota: Esta é uma verificação simplificada que detecta await em valores literais.
7
+ * Para verificação completa de tipos, seria necessário type checking do TypeScript.
8
+ */
9
+
10
+ /** @type {import('eslint').Rule.RuleModule} */
11
+ export default {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: '"await" deve ser usado apenas com Promises',
16
+ },
17
+ messages: {
18
+ awaitOnlyPromises:
19
+ '"await" deve ser usado apenas com Promises. Usar await em valores não-Promise é desnecessário.',
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ return {
25
+ AwaitExpression(node) {
26
+ const argument = node.argument;
27
+
28
+ // Detecta await em literais (string, number, boolean, null, etc.)
29
+ if (argument.type === "Literal") {
30
+ context.report({
31
+ node,
32
+ messageId: "awaitOnlyPromises",
33
+ });
34
+ return;
35
+ }
36
+
37
+ // Detecta await em arrays literais
38
+ if (argument.type === "ArrayExpression") {
39
+ context.report({
40
+ node,
41
+ messageId: "awaitOnlyPromises",
42
+ });
43
+ return;
44
+ }
45
+
46
+ // Detecta await em objetos literais
47
+ if (argument.type === "ObjectExpression") {
48
+ context.report({
49
+ node,
50
+ messageId: "awaitOnlyPromises",
51
+ });
52
+ return;
53
+ }
54
+
55
+ // Detecta await em template literals
56
+ if (argument.type === "TemplateLiteral") {
57
+ context.report({
58
+ node,
59
+ messageId: "awaitOnlyPromises",
60
+ });
61
+ return;
62
+ }
63
+
64
+ // Detecta await em arrow functions (sem chamar)
65
+ if (argument.type === "ArrowFunctionExpression") {
66
+ context.report({
67
+ node,
68
+ messageId: "awaitOnlyPromises",
69
+ });
70
+ return;
71
+ }
72
+
73
+ // Detecta await em function expressions (sem chamar)
74
+ if (argument.type === "FunctionExpression") {
75
+ context.report({
76
+ node,
77
+ messageId: "awaitOnlyPromises",
78
+ });
79
+ return;
80
+ }
81
+ },
82
+ };
83
+ },
84
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @fileoverview O update do for loop deve modificar a variável usada na condição
3
+ * Evita bugs onde o incremento modifica uma variável diferente da condição.
4
+ */
5
+ export default {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description:
10
+ "O update do for loop deve modificar a variável usada na condição",
11
+ },
12
+ messages: {
13
+ updateWrongCounter:
14
+ 'O update do for modifica "{{updateVar}}" mas a condição usa "{{conditionVar}}". Isso pode causar loop infinito.',
15
+ },
16
+ },
17
+ create(context) {
18
+ /**
19
+ * Extrai o nome da variável sendo modificada em um update expression
20
+ */
21
+ function getUpdatedVariable(node) {
22
+ if (!node) return null;
23
+
24
+ // i++ ou i--
25
+ if (node.type === "UpdateExpression" && node.argument.type === "Identifier") {
26
+ return node.argument.name;
27
+ }
28
+
29
+ // i += 1 ou i = i + 1
30
+ if (node.type === "AssignmentExpression" && node.left.type === "Identifier") {
31
+ return node.left.name;
32
+ }
33
+
34
+ // Expressão de sequência: i++, j++
35
+ if (node.type === "SequenceExpression") {
36
+ // Retorna array de todas as variáveis modificadas
37
+ return node.expressions.map(getUpdatedVariable).filter(Boolean);
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ /**
44
+ * Extrai variáveis usadas na condição do for
45
+ */
46
+ function getConditionVariables(node) {
47
+ const variables = [];
48
+
49
+ function extract(n) {
50
+ if (!n) return;
51
+
52
+ if (n.type === "Identifier") {
53
+ variables.push(n.name);
54
+ } else if (n.type === "BinaryExpression" || n.type === "LogicalExpression") {
55
+ extract(n.left);
56
+ extract(n.right);
57
+ } else if (n.type === "UnaryExpression") {
58
+ extract(n.argument);
59
+ } else if (n.type === "MemberExpression") {
60
+ extract(n.object);
61
+ }
62
+ }
63
+
64
+ extract(node);
65
+ return variables;
66
+ }
67
+
68
+ return {
69
+ ForStatement(node) {
70
+ const update = node.update;
71
+ const test = node.test;
72
+
73
+ if (!update || !test) return;
74
+
75
+ const updatedVars = getUpdatedVariable(update);
76
+ const conditionVars = getConditionVariables(test);
77
+
78
+ if (!updatedVars || conditionVars.length === 0) return;
79
+
80
+ // Converte para array se não for
81
+ const updatedVarsArray = Array.isArray(updatedVars)
82
+ ? updatedVars
83
+ : [updatedVars];
84
+
85
+ // Verifica se pelo menos uma variável do update está na condição
86
+ const hasMatchingVar = updatedVarsArray.some((v) =>
87
+ conditionVars.includes(v)
88
+ );
89
+
90
+ if (!hasMatchingVar) {
91
+ context.report({
92
+ node: update,
93
+ messageId: "updateWrongCounter",
94
+ data: {
95
+ updateVar: updatedVarsArray.join(", "),
96
+ conditionVar: conditionVars[0],
97
+ },
98
+ });
99
+ }
100
+ },
101
+ };
102
+ },
103
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @fileoverview Switch não deve ter muitos cases
3
+ * Quando há muitos cases, considere usar um objeto Map para melhor legibilidade.
4
+ */
5
+ const MAX_CASES = 30;
6
+
7
+ export default {
8
+ meta: {
9
+ type: "suggestion",
10
+ docs: {
11
+ description: "Switch não deve ter muitos cases",
12
+ },
13
+ messages: {
14
+ tooManyCases:
15
+ "Switch tem {{count}} cases. Considere usar um objeto/Map ao invés (máximo recomendado: {{max}}).",
16
+ },
17
+ },
18
+ create(context) {
19
+ return {
20
+ SwitchStatement(node) {
21
+ // Conta apenas cases não vazios (ignora fall-through)
22
+ const nonEmptyCases = node.cases.filter(
23
+ (c) => c.consequent.length > 0
24
+ ).length;
25
+
26
+ if (nonEmptyCases > MAX_CASES) {
27
+ context.report({
28
+ node,
29
+ messageId: "tooManyCases",
30
+ data: {
31
+ count: nonEmptyCases,
32
+ max: MAX_CASES,
33
+ },
34
+ });
35
+ }
36
+ },
37
+ };
38
+ },
39
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Regra: Não usar "delete" em elementos de array
3
+ *
4
+ * Sonar: javascript:S2870
5
+ *
6
+ * delete em arrays deixa um "buraco" (undefined) e não atualiza o length.
7
+ * Use splice() ou filter() ao invés.
8
+ */
9
+
10
+ /** @type {import('eslint').Rule.RuleModule} */
11
+ export default {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description: 'Não usar "delete" em elementos de array',
16
+ },
17
+ messages: {
18
+ noArrayDelete:
19
+ 'Não use "delete" em arrays. Use splice() ou filter() para remover elementos.',
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ return {
25
+ UnaryExpression(node) {
26
+ // Verifica se é operador delete
27
+ if (node.operator !== "delete") return;
28
+
29
+ const argument = node.argument;
30
+
31
+ // Verifica se é acesso por índice: arr[0], arr[i], arr[expr]
32
+ if (
33
+ argument.type === "MemberExpression" &&
34
+ argument.computed === true
35
+ ) {
36
+ // Verifica se o índice é um número ou expressão numérica
37
+ const property = argument.property;
38
+
39
+ // delete arr[0] - índice literal numérico
40
+ if (property.type === "Literal" && typeof property.value === "number") {
41
+ context.report({
42
+ node,
43
+ messageId: "noArrayDelete",
44
+ });
45
+ return;
46
+ }
47
+
48
+ // delete arr[i] - variável como índice (provavelmente número)
49
+ if (property.type === "Identifier") {
50
+ context.report({
51
+ node,
52
+ messageId: "noArrayDelete",
53
+ });
54
+ return;
55
+ }
56
+
57
+ // delete arr[i + 1] - expressão como índice
58
+ if (property.type === "BinaryExpression") {
59
+ context.report({
60
+ node,
61
+ messageId: "noArrayDelete",
62
+ });
63
+ return;
64
+ }
65
+ }
66
+ },
67
+ };
68
+ },
69
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Operadores bitwise não devem ser usados em contextos booleanos
3
+ * if (a & b) provavelmente deveria ser if (a && b)
4
+ */
5
+ export default {
6
+ meta: {
7
+ type: "problem",
8
+ docs: {
9
+ description: "Operadores bitwise não devem ser usados em contextos booleanos",
10
+ },
11
+ messages: {
12
+ bitwiseInBoolean:
13
+ "Operador bitwise '{{operator}}' em contexto booleano. Use '{{suggestion}}' para lógica booleana.",
14
+ },
15
+ },
16
+ create(context) {
17
+ function checkBitwiseInBoolean(node, testNode) {
18
+ if (testNode.type !== "BinaryExpression") return;
19
+
20
+ const operator = testNode.operator;
21
+
22
+ if (operator === "&") {
23
+ context.report({
24
+ node: testNode,
25
+ messageId: "bitwiseInBoolean",
26
+ data: { operator: "&", suggestion: "&&" },
27
+ });
28
+ } else if (operator === "|") {
29
+ context.report({
30
+ node: testNode,
31
+ messageId: "bitwiseInBoolean",
32
+ data: { operator: "|", suggestion: "||" },
33
+ });
34
+ }
35
+ }
36
+
37
+ return {
38
+ IfStatement(node) {
39
+ checkBitwiseInBoolean(node, node.test);
40
+ },
41
+ WhileStatement(node) {
42
+ checkBitwiseInBoolean(node, node.test);
43
+ },
44
+ DoWhileStatement(node) {
45
+ checkBitwiseInBoolean(node, node.test);
46
+ },
47
+ ForStatement(node) {
48
+ if (node.test) {
49
+ checkBitwiseInBoolean(node, node.test);
50
+ }
51
+ },
52
+ ConditionalExpression(node) {
53
+ checkBitwiseInBoolean(node, node.test);
54
+ },
55
+ };
56
+ },
57
+ };