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,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
+ };