eslint-plugin-th-rules 3.5.0 → 3.6.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/dist/plugin.d.ts CHANGED
@@ -11,9 +11,9 @@ export declare const rules: {
11
11
  'no-default-export': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnamed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
12
12
  name: string;
13
13
  };
14
- 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
15
- maximumDestructuredVariables: number;
16
- maximumLineLength: number;
14
+ 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong" | "tooManyCumulative", [{
15
+ maximumDestructuredVariables?: number;
16
+ maximumLineLength?: number;
17
17
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
18
18
  name: string;
19
19
  };
@@ -63,9 +63,9 @@ declare const plugin: {
63
63
  'no-default-export': import("@typescript-eslint/utils/ts-eslint").RuleModule<"unnamed", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
64
64
  name: string;
65
65
  };
66
- 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
67
- maximumDestructuredVariables: number;
68
- maximumLineLength: number;
66
+ 'no-destructuring': import("@typescript-eslint/utils/ts-eslint").RuleModule<"tooDeep" | "tooMany" | "tooLong" | "tooManyCumulative", [{
67
+ maximumDestructuredVariables?: number;
68
+ maximumLineLength?: number;
69
69
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
70
70
  name: string;
71
71
  };
@@ -1,8 +1,12 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const noDestructuring: ESLintUtils.RuleModule<"tooDeep" | "tooMany" | "tooLong", [{
3
- maximumDestructuredVariables: number;
4
- maximumLineLength: number;
5
- }], unknown, ESLintUtils.RuleListener> & {
2
+ type Options = [
3
+ {
4
+ maximumDestructuredVariables?: number;
5
+ maximumLineLength?: number;
6
+ }
7
+ ];
8
+ type MessageIds = 'tooDeep' | 'tooMany' | 'tooLong' | 'tooManyCumulative';
9
+ declare const noDestructuring: ESLintUtils.RuleModule<MessageIds, Options, unknown, ESLintUtils.RuleListener> & {
6
10
  name: string;
7
11
  };
8
12
  export default noDestructuring;
@@ -1 +1 @@
1
- {"version":3,"file":"no-destructuring.d.ts","sourceRoot":"","sources":["../../src/rules/no-destructuring.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAItF,QAAA,MAAM,eAAe;;;;;CAyInB,CAAC;AACH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"no-destructuring.d.ts","sourceRoot":"","sources":["../../src/rules/no-destructuring.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAItF,KAAK,OAAO,GAAG;IACd;QACC,4BAA4B,CAAC,EAAE,MAAM,CAAC;QACtC,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;CACD,CAAC;AAEF,KAAK,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAE1E,QAAA,MAAM,eAAe;;CA+NnB,CAAC;AAEH,eAAe,eAAe,CAAC"}
@@ -24,6 +24,7 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
24
24
  tooDeep: 'Destructuring at a nesting level above {{max}} is not allowed; found {{actual}} levels of nesting.',
25
25
  tooMany: 'Destructuring of more than {{max}} variables is not allowed.',
26
26
  tooLong: 'Destructuring spanning a line exceeding {{max}} characters is not allowed.',
27
+ tooManyCumulative: 'Destructuring of more than {{max}} variables from "{{source}}" in the same scope is not allowed; found {{total}}.',
27
28
  },
28
29
  },
29
30
  defaultOptions: [
@@ -33,24 +34,62 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
33
34
  },
34
35
  ],
35
36
  create(context, [options]) {
36
- const MAX_VARIABLES = options.maximumDestructuredVariables ?? 2;
37
- const MAX_LINE_LENGTH = options.maximumLineLength ?? 100;
37
+ const maxVariables = options.maximumDestructuredVariables ?? 2;
38
+ const maxLineLength = options.maximumLineLength ?? 100;
39
+ /**
40
+ * Tracks total destructured properties per initializer expression text per scope node.
41
+ * WeakMap is used so scopes can be GC'ed and we do not leak memory across files.
42
+ */
43
+ const totalsByScope = new WeakMap();
44
+ function getLineText(lineNumber) {
45
+ return context.sourceCode.lines[lineNumber - 1] ?? '';
46
+ }
47
+ function getMaxSpannedLineLength(startLine, endLine) {
48
+ let max = 0;
49
+ for (let i = startLine; i <= endLine; i++) {
50
+ const line = getLineText(i);
51
+ if (line.length > max) {
52
+ max = line.length;
53
+ }
54
+ }
55
+ return max;
56
+ }
57
+ function getIndentCountForLine(lineNumber) {
58
+ const lineText = getLineText(lineNumber);
59
+ return lineText.search(/\S|$/);
60
+ }
61
+ function getScopeNode(node) {
62
+ const ancestors = context.sourceCode.getAncestors(node);
63
+ for (let i = ancestors.length - 1; i >= 0; i--) {
64
+ const a = ancestors[i];
65
+ if (a.type === AST_NODE_TYPES.Program ||
66
+ a.type === AST_NODE_TYPES.FunctionDeclaration ||
67
+ a.type === AST_NODE_TYPES.FunctionExpression ||
68
+ a.type === AST_NODE_TYPES.ArrowFunctionExpression ||
69
+ a.type === AST_NODE_TYPES.TSDeclareFunction) {
70
+ return a;
71
+ }
72
+ }
73
+ return context.sourceCode.ast;
74
+ }
75
+ function getScopeMap(scopeNode) {
76
+ const existing = totalsByScope.get(scopeNode);
77
+ if (!_.isNil(existing)) {
78
+ return existing;
79
+ }
80
+ const created = new Map();
81
+ totalsByScope.set(scopeNode, created);
82
+ return created;
83
+ }
38
84
  function reportIfNeeded(patternNode, reportNode = patternNode) {
39
85
  if (patternNode?.type !== AST_NODE_TYPES.ObjectPattern || _.isNil(patternNode.loc)) {
40
86
  return;
41
87
  }
42
88
  const startLine = patternNode.loc.start.line;
43
89
  const endLine = patternNode.loc.end.line;
44
- const lineText = context.sourceCode.lines[startLine - 1] ?? '';
45
- const indentCount = lineText.search(/\S|$/);
90
+ const indentCount = getIndentCountForLine(startLine);
46
91
  const propertyCount = patternNode.properties?.length ?? 0;
47
- let maxSpannedLineLength = 0;
48
- for (let i = startLine; i <= endLine; i++) {
49
- const t = context.sourceCode.lines[i - 1] ?? '';
50
- if (t.length > maxSpannedLineLength) {
51
- maxSpannedLineLength = t.length;
52
- }
53
- }
92
+ const maxSpannedLineLength = getMaxSpannedLineLength(startLine, endLine);
54
93
  if (indentCount > MAX_TAB_COUNT) {
55
94
  context.report({
56
95
  node: reportNode,
@@ -61,21 +100,21 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
61
100
  },
62
101
  });
63
102
  }
64
- if (propertyCount > MAX_VARIABLES) {
103
+ if (propertyCount > maxVariables) {
65
104
  context.report({
66
105
  node: reportNode,
67
106
  messageId: 'tooMany',
68
107
  data: {
69
- max: MAX_VARIABLES,
108
+ max: maxVariables,
70
109
  },
71
110
  });
72
111
  }
73
- if (maxSpannedLineLength > MAX_LINE_LENGTH) {
112
+ if (maxSpannedLineLength > maxLineLength) {
74
113
  context.report({
75
114
  node: reportNode,
76
115
  messageId: 'tooLong',
77
116
  data: {
78
- max: MAX_LINE_LENGTH,
117
+ max: maxLineLength,
79
118
  },
80
119
  });
81
120
  }
@@ -92,9 +131,39 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
92
131
  reportIfNeeded(p, p);
93
132
  }
94
133
  }
134
+ function checkCumulativeVariableDeclarator(node) {
135
+ if (node.id.type !== AST_NODE_TYPES.ObjectPattern) {
136
+ return;
137
+ }
138
+ if (_.isNil(node.init)) {
139
+ return;
140
+ }
141
+ const propertyCount = node.id.properties?.length ?? 0;
142
+ if (propertyCount > maxVariables) {
143
+ return;
144
+ }
145
+ const scopeNode = getScopeNode(node);
146
+ const scopeMap = getScopeMap(scopeNode);
147
+ const sourceText = context.sourceCode.getText(node.init);
148
+ const previousTotal = scopeMap.get(sourceText) ?? 0;
149
+ const newTotal = previousTotal + propertyCount;
150
+ scopeMap.set(sourceText, newTotal);
151
+ if (previousTotal > 0 && newTotal > maxVariables) {
152
+ context.report({
153
+ node,
154
+ messageId: 'tooManyCumulative',
155
+ data: {
156
+ source: sourceText,
157
+ max: maxVariables,
158
+ total: newTotal,
159
+ },
160
+ });
161
+ }
162
+ }
95
163
  return {
96
164
  VariableDeclarator(node) {
97
165
  reportIfNeeded(node.id, node);
166
+ checkCumulativeVariableDeclarator(node);
98
167
  },
99
168
  FunctionDeclaration(node) {
100
169
  checkParameters(node.params);
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-lodash-iteratee-shorthand.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-lodash-iteratee-shorthand.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,WAAW,EAAgC,MAAM,0BAA0B,CAAC;AAYrG,eAAO,MAAM,sBAAsB,6KAezB,CAAC;AAEX,eAAO,MAAM,qBAAqB,wPAsBxB,CAAC;AAEX,eAAO,MAAM,6BAA6B;;;;;;;;;;CAUhC,CAAC;AAUX,QAAA,MAAM,6BAA6B;;CAwpBjC,CAAC;AAEH,eAAe,6BAA6B,CAAC"}
1
+ {"version":3,"file":"prefer-lodash-iteratee-shorthand.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-lodash-iteratee-shorthand.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,WAAW,EAAgC,MAAM,0BAA0B,CAAC;AAYrG,eAAO,MAAM,sBAAsB,6KAezB,CAAC;AAEX,eAAO,MAAM,qBAAqB,wPAsBxB,CAAC;AAEX,eAAO,MAAM,6BAA6B;;;;;;;;;;CAUhC,CAAC;AAUX,QAAA,MAAM,6BAA6B;;CAmsBjC,CAAC;AAEH,eAAe,6BAA6B,CAAC"}
@@ -100,6 +100,9 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
100
100
  function isStringLiteral(node) {
101
101
  return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string';
102
102
  }
103
+ function isNumberLiteral(node) {
104
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number';
105
+ }
103
106
  function isTemplateLiteralWithoutExpressions(node) {
104
107
  return !_.isNil(node) && node.type === AST_NODE_TYPES.TemplateLiteral && _.isEmpty(node.expressions) && node.quasis.length === 1;
105
108
  }
@@ -154,57 +157,6 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
154
157
  function isFunctionLike(node) {
155
158
  return !_.isNil(node) && (node.type === AST_NODE_TYPES.ArrowFunctionExpression || node.type === AST_NODE_TYPES.FunctionExpression);
156
159
  }
157
- function isGetCallOnParameter(expr, parameterName) {
158
- const unwrapped = unwrapChain(expr);
159
- if (_.isNil(unwrapped) || unwrapped.type !== AST_NODE_TYPES.CallExpression)
160
- return null;
161
- const callee = unwrapChain(unwrapped.callee);
162
- if (!isLodashMember(callee, 'get'))
163
- return null;
164
- if (unwrapped.arguments.length < 2)
165
- return null;
166
- const arg0 = unwrapped.arguments[0];
167
- const arg1 = unwrapped.arguments[1];
168
- if (_.isNil(arg0) || arg0.type === AST_NODE_TYPES.SpreadElement)
169
- return null;
170
- if (_.isNil(arg1) || arg1.type === AST_NODE_TYPES.SpreadElement)
171
- return null;
172
- const object = unwrapChain(arg0);
173
- if (_.isNil(object) || object.type !== AST_NODE_TYPES.Identifier || object.name !== parameterName)
174
- return null;
175
- if (isStringLiteral(arg1)) {
176
- return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
177
- }
178
- if (isTemplateLiteralWithoutExpressions(arg1)) {
179
- return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
180
- }
181
- if (arg1.type === AST_NODE_TYPES.Identifier) {
182
- return { kind: 'get', pathText: arg1.name, pathIsStaticString: false };
183
- }
184
- return null;
185
- }
186
- function isMemberAccessOnParameter(expr, parameterName) {
187
- const unwrapped = unwrapChain(expr);
188
- if (_.isNil(unwrapped) || unwrapped.type !== AST_NODE_TYPES.MemberExpression)
189
- return null;
190
- const object = unwrapChain(unwrapped.object);
191
- if (_.isNil(object) || object.type !== AST_NODE_TYPES.Identifier || object.name !== parameterName)
192
- return null;
193
- if (!unwrapped.computed && unwrapped.property.type === AST_NODE_TYPES.Identifier) {
194
- const key = unwrapped.property.name;
195
- return { kind: 'member', keyText: key, keyIsIdentifier: true };
196
- }
197
- if (unwrapped.computed && unwrapped.property.type === AST_NODE_TYPES.Literal && typeof unwrapped.property.value === 'string') {
198
- return { kind: 'member', keyText: sourceCode.getText(unwrapped.property), keyIsIdentifier: false };
199
- }
200
- if (unwrapped.computed && unwrapped.property.type === AST_NODE_TYPES.Identifier) {
201
- return { kind: 'member', keyText: unwrapped.property.name, keyIsIdentifier: false };
202
- }
203
- return null;
204
- }
205
- function extractPathFromExpression(expr, parameterName) {
206
- return isMemberAccessOnParameter(expr, parameterName) ?? isGetCallOnParameter(expr, parameterName);
207
- }
208
160
  function normalizeStaticPathText(pathText) {
209
161
  const trimmed = pathText.trim();
210
162
  if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
@@ -222,6 +174,27 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
222
174
  const escaped = value.replaceAll('\\', '\\\\').replaceAll("'", String.raw `\'`);
223
175
  return `'${escaped}'`;
224
176
  }
177
+ function toDoubleQuotedRaw(value) {
178
+ return value.replaceAll('\\', '\\\\').replaceAll('"', String.raw `\"`);
179
+ }
180
+ function buildLodashPathStringFromSegments(segments) {
181
+ if (_.isEmpty(segments))
182
+ return null;
183
+ let out = '';
184
+ for (const seg of segments) {
185
+ if (typeof seg === 'number') {
186
+ out += `[${seg}]`;
187
+ continue;
188
+ }
189
+ if (isValidIdentifierName(seg)) {
190
+ out += _.isEmpty(out) ? seg : `.${seg}`;
191
+ continue;
192
+ }
193
+ const inner = toDoubleQuotedRaw(seg);
194
+ out += `["${inner}"]`;
195
+ }
196
+ return out;
197
+ }
225
198
  function containsIdentifier(node, name) {
226
199
  const { visitorKeys } = sourceCode;
227
200
  const seen = new Set();
@@ -353,6 +326,80 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
353
326
  return '{}';
354
327
  return `{${props.join(', ')}}`;
355
328
  }
329
+ function isGetCallOnParameter(expr, parameterName) {
330
+ const unwrapped = unwrapChain(expr);
331
+ if (_.isNil(unwrapped) || unwrapped.type !== AST_NODE_TYPES.CallExpression)
332
+ return null;
333
+ const callee = unwrapChain(unwrapped.callee);
334
+ if (!isLodashMember(callee, 'get'))
335
+ return null;
336
+ if (unwrapped.arguments.length < 2)
337
+ return null;
338
+ const arg0 = unwrapped.arguments[0];
339
+ const arg1 = unwrapped.arguments[1];
340
+ if (_.isNil(arg0) || arg0.type === AST_NODE_TYPES.SpreadElement)
341
+ return null;
342
+ if (_.isNil(arg1) || arg1.type === AST_NODE_TYPES.SpreadElement)
343
+ return null;
344
+ const object = unwrapChain(arg0);
345
+ if (_.isNil(object) || object.type !== AST_NODE_TYPES.Identifier || object.name !== parameterName)
346
+ return null;
347
+ if (isStringLiteral(arg1)) {
348
+ return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
349
+ }
350
+ if (isTemplateLiteralWithoutExpressions(arg1)) {
351
+ return { kind: 'get', pathText: sourceCode.getText(arg1), pathIsStaticString: true };
352
+ }
353
+ if (arg1.type === AST_NODE_TYPES.Identifier) {
354
+ return { kind: 'get', pathText: arg1.name, pathIsStaticString: false };
355
+ }
356
+ return null;
357
+ }
358
+ function extractMemberPathSegments(expr, parameterName) {
359
+ const unwrapped = unwrapChainExpr(expr) ?? expr;
360
+ const segments = [];
361
+ let current = unwrapped;
362
+ while (current.type === AST_NODE_TYPES.MemberExpression) {
363
+ if (current.computed) {
364
+ const prop = unwrapChain(current.property);
365
+ if (!_.isNil(prop) && prop.type === AST_NODE_TYPES.Identifier)
366
+ return null;
367
+ if (isStringLiteral(prop)) {
368
+ segments.unshift(prop.value);
369
+ }
370
+ else if (isNumberLiteral(prop)) {
371
+ segments.unshift(prop.value);
372
+ }
373
+ else if (isTemplateLiteralWithoutExpressions(prop)) {
374
+ const cooked = prop.quasis[0]?.value.cooked ?? prop.quasis[0]?.value.raw ?? '';
375
+ segments.unshift(cooked);
376
+ }
377
+ else {
378
+ return null;
379
+ }
380
+ }
381
+ else {
382
+ if (current.property.type !== AST_NODE_TYPES.Identifier)
383
+ return null;
384
+ segments.unshift(current.property.name);
385
+ }
386
+ const object = unwrapChain(current.object);
387
+ if (_.isNil(object))
388
+ return null;
389
+ current = object;
390
+ }
391
+ if (current.type !== AST_NODE_TYPES.Identifier || current.name !== parameterName)
392
+ return null;
393
+ if (_.isEmpty(segments))
394
+ return null;
395
+ return segments;
396
+ }
397
+ function extractPathFromExpression(expr, parameterName) {
398
+ const memberSegments = extractMemberPathSegments(expr, parameterName);
399
+ if (!_.isNil(memberSegments))
400
+ return { kind: 'memberPath', segments: memberSegments };
401
+ return isGetCallOnParameter(expr, parameterName);
402
+ }
356
403
  function extractPredicateClausesFromExpression(expr, parameterName) {
357
404
  const unwrapped = unwrapChainExpr(expr);
358
405
  if (_.isNil(unwrapped))
@@ -392,21 +439,15 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
392
439
  }
393
440
  return clauses;
394
441
  }
395
- function buildObjectKeyTextForMemberPath(path) {
396
- if (path.keyIsIdentifier) {
397
- return path.keyText;
398
- }
399
- return null;
400
- }
401
442
  function buildMatchesObjectFromClauses(clauses) {
402
443
  const literals = [];
403
444
  for (const clause of clauses) {
404
445
  const valueText = sourceCode.getText(clause.valueExpr);
405
- if (clause.path.kind === 'member') {
406
- const keyText = buildObjectKeyTextForMemberPath(clause.path);
407
- if (_.isNil(keyText))
446
+ if (clause.path.kind === 'memberPath') {
447
+ const nested = buildNestedLiteralFromSegments(clause.path.segments, valueText);
448
+ if (_.isNil(nested))
408
449
  return null;
409
- literals.push(`{${keyText}: ${valueText}}`);
450
+ literals.push(nested);
410
451
  continue;
411
452
  }
412
453
  if (!clause.path.pathIsStaticString) {
@@ -430,19 +471,24 @@ const preferLodashIterateeShorthand = ESLintUtils.RuleCreator(() => `https://git
430
471
  const expr = getReturnedExpression(fn);
431
472
  if (_.isNil(expr))
432
473
  return null;
474
+ const memberSegments = extractMemberPathSegments(expr, parameterName);
475
+ if (!_.isNil(memberSegments)) {
476
+ const raw = buildLodashPathStringFromSegments(memberSegments);
477
+ if (_.isNil(raw))
478
+ return null;
479
+ return toSingleQuotedStringLiteral(raw);
480
+ }
433
481
  const unwrapped = unwrapChainExpr(expr) ?? expr;
434
- const member = isMemberAccessOnParameter(unwrapped, parameterName);
435
- if (!_.isNil(member) && member.kind === 'member') {
436
- if (!member.keyIsIdentifier && isValidIdentifierName(member.keyText)) {
437
- return member.keyText;
438
- }
439
- if (member.keyIsIdentifier) {
440
- return toSingleQuotedStringLiteral(member.keyText);
482
+ if (unwrapped.type === AST_NODE_TYPES.MemberExpression) {
483
+ const object = unwrapChain(unwrapped.object);
484
+ if (!_.isNil(object) && object.type === AST_NODE_TYPES.Identifier && object.name === parameterName && unwrapped.computed) {
485
+ const prop = unwrapChain(unwrapped.property);
486
+ if (!_.isNil(prop) && prop.type === AST_NODE_TYPES.Identifier) {
487
+ return prop.name;
488
+ }
441
489
  }
442
- const normalized = normalizeStaticPathText(member.keyText);
443
- return toSingleQuotedStringLiteral(normalized);
444
490
  }
445
- const get = isGetCallOnParameter(unwrapped, parameterName);
491
+ const get = isGetCallOnParameter(expr, parameterName);
446
492
  if (!_.isNil(get) && get.kind === 'get') {
447
493
  return get.pathText;
448
494
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",