eslint-plugin-th-rules 3.2.1 → 3.2.3

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 CHANGED
@@ -164,19 +164,19 @@ Do not edit below this line.
164
164
  ⚛️ Set in the `recommendedReact` configuration.\
165
165
  🟦 Set in the `recommendedTypescript` configuration.\
166
166
  🎲 Set in the `recommendedTypescriptReact` configuration.\
167
- 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).\
168
- 💡 Manually fixable by [editor suggestions](https://eslint.org/docs/latest/use/core-concepts#rule-suggestions).
169
-
170
- | Name                 | Description | 💼 | 🔧 | 💡 |
171
- | :--------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :- | :- |
172
- | [noBooleanCoercion](docs/rules/noBooleanCoercion.md) | Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects. | ✅ ⚛️ 🟦 🎲 | | 💡 |
173
- | [noComments](docs/rules/noComments.md) | Disallow comments except for specified allowed patterns. | ✅ ⚛️ 🟦 🎲 | 🔧 | |
174
- | [noDefaultExport](docs/rules/noDefaultExport.md) | Convert unnamed default exports to named default exports based on the file name. | ✅ ⚛️ 🟦 🎲 | 🔧 | |
175
- | [noDestructuring](docs/rules/noDestructuring.md) | Disallow destructuring that does not meet certain conditions. | ✅ ⚛️ 🟦 🎲 | | |
176
- | [preferIsEmpty](docs/rules/preferIsEmpty.md) | Require _.isEmpty instead of length comparisons. | ✅ ⚛️ 🟦 🎲 | | 💡 |
177
- | [schemasInSchemasFile](docs/rules/schemasInSchemasFile.md) | Require Zod schema declarations to be placed in a .schemas.ts file. | ✅ ⚛️ 🟦 🎲 | | |
178
- | [topLevelFunctions](docs/rules/topLevelFunctions.md) | Require all top-level functions to be named regular functions. | ✅ ⚛️ 🟦 🎲 | 🔧 | |
179
- | [typesInDts](docs/rules/typesInDts.md) | Require TypeScript type declarations (type/interface/enum) to be placed in .d.ts files. | ✅ ⚛️ 🟦 🎲 | | |
167
+ 🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
168
+
169
+ | Name                    | Description | 💼 | 🔧 |
170
+ | :--------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :- |
171
+ | [no-boolean-coercion](docs/rules/no-boolean-coercion.md) | Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects. If the value is already boolean, remove coercion. | ⚛️ 🟦 🎲 | 🔧 |
172
+ | [no-comments](docs/rules/no-comments.md) | Disallow comments except for specified allowed patterns. | ✅ ⚛️ 🟦 🎲 | 🔧 |
173
+ | [no-default-export](docs/rules/no-default-export.md) | Convert unnamed default exports to named default exports based on the file name. | ✅ ⚛️ 🟦 🎲 | 🔧 |
174
+ | [no-destructuring](docs/rules/no-destructuring.md) | Disallow destructuring that does not meet certain conditions. | ✅ ⚛️ 🟦 🎲 | |
175
+ | [no-explicit-nil-compare](docs/rules/no-explicit-nil-compare.md) | Disallow direct comparisons to null or undefined. Use _.isNull(x) / _.isUndefined(x) instead. | ✅ ⚛️ 🟦 🎲 | 🔧 |
176
+ | [prefer-is-empty](docs/rules/prefer-is-empty.md) | Require _.isEmpty instead of length comparisons or !x.length checks. | ✅ ⚛️ 🟦 🎲 | 🔧 |
177
+ | [schemas-in-schemas-file](docs/rules/schemas-in-schemas-file.md) | Require Zod schema declarations to be placed in a .schemas.ts file. | ✅ ⚛️ 🟦 🎲 | |
178
+ | [top-level-functions](docs/rules/top-level-functions.md) | Require all top-level functions to be named regular functions. | ✅ ⚛️ 🟦 🎲 | 🔧 |
179
+ | [types-in-dts](docs/rules/types-in-dts.md) | Require TypeScript type declarations (type/interface/enum) to be placed in .d.ts files. | ✅ ⚛️ 🟦 🎲 | |
180
180
 
181
181
  <!-- end auto-generated rules list -->
182
182
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,CAAC;AAQhD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,eAAO,MAAM,OAAO;;;;;CAKnB,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,QAAA,MAAM,KAAK,EAAoC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC;AACpI,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,IAAI,EAAE,MAAM,QAAQ,CAAC;AAQhD,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,eAAO,MAAM,OAAO;;;;;CAKnB,CAAC;AAEF,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAE1E,QAAA,MAAM,KAAK,EAAoC;IAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC;AACpI,eAAe,KAAK,CAAC"}
package/dist/plugin.d.ts CHANGED
@@ -17,7 +17,10 @@ export declare const rules: {
17
17
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
18
18
  name: string;
19
19
  };
20
- 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
20
+ 'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
+ name: string;
22
+ };
23
+ 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
24
  name: string;
22
25
  };
23
26
  'schemas-in-schemas-file': import("@typescript-eslint/utils/ts-eslint").RuleModule<"moveSchema", [{
@@ -36,9 +39,6 @@ export declare const rules: {
36
39
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
37
40
  name: string;
38
41
  };
39
- 'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
40
- name: string;
41
- };
42
42
  };
43
43
  declare const plugin: {
44
44
  rules: {
@@ -60,7 +60,10 @@ declare const plugin: {
60
60
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
61
61
  name: string;
62
62
  };
63
- 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
63
+ 'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
64
+ name: string;
65
+ };
66
+ 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
64
67
  name: string;
65
68
  };
66
69
  'schemas-in-schemas-file': import("@typescript-eslint/utils/ts-eslint").RuleModule<"moveSchema", [{
@@ -79,9 +82,6 @@ declare const plugin: {
79
82
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
80
83
  name: string;
81
84
  };
82
- 'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
83
- name: string;
84
- };
85
85
  };
86
86
  };
87
87
  export default plugin;
package/dist/plugin.js CHANGED
@@ -12,11 +12,11 @@ export const rules = {
12
12
  'no-comments': noComments,
13
13
  'no-default-export': noDefaultExport,
14
14
  'no-destructuring': noDestructuring,
15
+ 'no-explicit-nil-compare': noExplicitNilCompare,
15
16
  'prefer-is-empty': preferIsEmpty,
16
17
  'schemas-in-schemas-file': schemasInSchemasFile,
17
18
  'top-level-functions': topLevelFunctions,
18
19
  'types-in-dts': typesInDts,
19
- 'no-explicit-nil-compare': noExplicitNilCompare,
20
20
  };
21
21
  const plugin = { rules };
22
22
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"no-boolean-coercion.d.ts","sourceRoot":"","sources":["../../src/rules/no-boolean-coercion.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAQtF,QAAA,MAAM,iBAAiB;;CAmGrB,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"no-boolean-coercion.d.ts","sourceRoot":"","sources":["../../src/rules/no-boolean-coercion.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAStF,QAAA,MAAM,iBAAiB;;CAyHrB,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
@@ -1,7 +1,7 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-assignment */
2
1
  /* eslint-disable new-cap */
3
2
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
4
3
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
4
+ import * as ts from 'typescript';
5
5
  const createRule = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-boolean-coercion.md');
6
6
  const LODASH_MODULE = 'lodash';
7
7
  const LODASH_IDENT = '_';
@@ -11,7 +11,7 @@ const noBooleanCoercion = createRule({
11
11
  type: 'problem',
12
12
  fixable: 'code',
13
13
  docs: {
14
- description: 'Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects.',
14
+ description: 'Disallow Boolean(value) or !!value. Enforce explicit checks: !_.isNil(value) for scalars and !_.isEmpty(value) for strings, arrays, and objects. If the value is already boolean, remove coercion.',
15
15
  },
16
16
  schema: [],
17
17
  messages: {
@@ -43,6 +43,14 @@ const noBooleanCoercion = createRule({
43
43
  function isDoubleNegation(node) {
44
44
  return node.type === AST_NODE_TYPES.UnaryExpression && node.operator === '!' && node.argument.type === AST_NODE_TYPES.UnaryExpression && node.argument.operator === '!';
45
45
  }
46
+ function isBooleanByTS(node) {
47
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
48
+ if (!tsNode) {
49
+ return false;
50
+ }
51
+ const type = checker.getTypeAtLocation(tsNode);
52
+ return (type.flags & ts.TypeFlags.Boolean) !== 0 || (type.flags & ts.TypeFlags.BooleanLiteral) !== 0; // eslint-disable-line no-bitwise
53
+ }
46
54
  function isCollectionLikeByTS(node) {
47
55
  const tsNode = services.esTreeNodeToTSNodeMap.get(node);
48
56
  if (!tsNode) {
@@ -56,6 +64,16 @@ const noBooleanCoercion = createRule({
56
64
  return node.type === AST_NODE_TYPES.ArrayExpression || node.type === AST_NODE_TYPES.ObjectExpression || (node.type === AST_NODE_TYPES.Literal && typeof node.value === 'string');
57
65
  }
58
66
  function report(node, valueNode) {
67
+ if (isBooleanByTS(valueNode)) {
68
+ context.report({
69
+ node,
70
+ messageId: 'useIsNil',
71
+ fix(fixer) {
72
+ return fixer.replaceText(node, context.sourceCode.getText(valueNode));
73
+ },
74
+ });
75
+ return;
76
+ }
59
77
  const isCollection = isCollectionLikeBySyntax(valueNode) || isCollectionLikeByTS(valueNode);
60
78
  const fnName = isCollection ? 'isEmpty' : 'isNil';
61
79
  const replacement = `!${LODASH_IDENT}.${fnName}(${context.sourceCode.getText(valueNode)})`;
@@ -1 +1 @@
1
- {"version":3,"file":"no-default-export.d.ts","sourceRoot":"","sources":["../../src/rules/no-default-export.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE,QAAA,MAAM,eAAe;;CA2DnB,CAAC;AACH,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"no-default-export.d.ts","sourceRoot":"","sources":["../../src/rules/no-default-export.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE,QAAA,MAAM,eAAe;;CA2DnB,CAAC;AACH,eAAe,eAAe,CAAC"}
@@ -1,4 +1,6 @@
1
+ /* eslint-disable new-cap */
1
2
  import * as path from 'node:path';
3
+ import _ from 'lodash';
2
4
  import { ESLintUtils } from '@typescript-eslint/utils';
3
5
  const noDefaultExport = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-default-export.md')({
4
6
  name: 'no-default-export',
@@ -18,7 +20,7 @@ const noDefaultExport = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
18
20
  function generateExportNameFromFileName(fileName) {
19
21
  const cleaned = fileName.replaceAll(/[^a-zA-Z\d]+/g, ' ');
20
22
  const parts = cleaned.trim().split(/\s+/g).filter(Boolean);
21
- if (parts.length === 0) {
23
+ if (_.isEmpty(parts)) {
22
24
  return 'defaultExport';
23
25
  }
24
26
  const [first, ...rest] = parts;
@@ -29,7 +31,7 @@ const noDefaultExport = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
29
31
  if (node.declaration.type === 'Identifier') {
30
32
  return;
31
33
  }
32
- if ('id' in node.declaration && node.declaration.id != null) {
34
+ if ('id' in node.declaration && !_.isNull(node.declaration.id)) {
33
35
  return;
34
36
  }
35
37
  const fileName = context.getFilename();
@@ -1,5 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const preferIsEmpty: ESLintUtils.RuleModule<"useIsEmpty", [], unknown, ESLintUtils.RuleListener> & {
2
+ declare const preferIsEmpty: ESLintUtils.RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, ESLintUtils.RuleListener> & {
3
3
  name: string;
4
4
  };
5
5
  export default preferIsEmpty;
@@ -1 +1 @@
1
- {"version":3,"file":"prefer-is-empty.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-is-empty.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtF,QAAA,MAAM,aAAa;;CAoFjB,CAAC;AACH,eAAe,aAAa,CAAC"}
1
+ {"version":3,"file":"prefer-is-empty.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-is-empty.ts"],"names":[],"mappings":"AAKA,OAAO,EAAkB,WAAW,EAAgC,MAAM,0BAA0B,CAAC;AAIrG,QAAA,MAAM,aAAa;;CA2NjB,CAAC;AAEH,eAAe,aAAa,CAAC"}
@@ -1,76 +1,188 @@
1
+ /* eslint-disable th-rules/types-in-dts */
1
2
  /* eslint-disable new-cap */
2
3
  /* eslint-disable complexity */
4
+ import _ from 'lodash';
3
5
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
4
6
  const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/prefer-is-empty.md')({
5
7
  name: 'prefer-is-empty',
6
8
  meta: {
7
9
  type: 'problem',
8
10
  docs: {
9
- description: 'Require _.isEmpty instead of length comparisons.',
11
+ description: 'Require _.isEmpty instead of length comparisons or boolean checks on .length.',
10
12
  },
11
- hasSuggestions: true,
13
+ fixable: 'code',
12
14
  schema: [],
13
15
  messages: {
14
16
  useIsEmpty: 'Use _.isEmpty({{collection}}) instead of checking {{collection}}.length {{operator}} {{value}}.',
17
+ useIsEmptyUnary: 'Use _.isEmpty({{collection}}) instead of negating {{collection}}.length.',
18
+ useIsEmptyBoolean: 'Use _.isEmpty({{collection}}) instead of boolean checking {{collection}}.length.',
15
19
  },
16
20
  },
17
21
  defaultOptions: [],
18
22
  create(context) {
23
+ const { sourceCode } = context;
24
+ function ensureLodashImport(fixer) {
25
+ const imports = sourceCode.ast.body.filter((node) => node.type === AST_NODE_TYPES.ImportDeclaration);
26
+ const hasLodash = imports.some((imp) => imp.source.value === 'lodash' && imp.specifiers.some((s) => s.type === AST_NODE_TYPES.ImportDefaultSpecifier || s.type === AST_NODE_TYPES.ImportNamespaceSpecifier));
27
+ if (hasLodash)
28
+ return null;
29
+ const firstImport = imports[0];
30
+ return firstImport ? fixer.insertTextBefore(firstImport, `import _ from 'lodash';\n`) : fixer.insertTextBeforeRange([0, 0], `import _ from 'lodash';\n`);
31
+ }
32
+ function unwrapChain(node) {
33
+ return node?.type === AST_NODE_TYPES.ChainExpression ? node.expression : node;
34
+ }
19
35
  function isLengthAccess(node) {
20
- return node?.type === AST_NODE_TYPES.MemberExpression && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === 'length' && !node.computed;
36
+ const unwrapped = unwrapChain(node);
37
+ return (!_.isNil(unwrapped) &&
38
+ unwrapped.type === AST_NODE_TYPES.MemberExpression &&
39
+ unwrapped.property.type === AST_NODE_TYPES.Identifier &&
40
+ unwrapped.property.name === 'length' &&
41
+ !unwrapped.computed);
42
+ }
43
+ function getLengthMember(node) {
44
+ return unwrapChain(node);
21
45
  }
22
46
  function isNumericLiteral(node) {
23
- return node?.type === AST_NODE_TYPES.Literal && typeof node.value === 'number';
47
+ return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number';
24
48
  }
25
- function report(node, collectionNode, operator, value, isEmptyCheck) {
26
- const collectionText = context.sourceCode.getText(collectionNode.object);
27
- const replacement = isEmptyCheck ? `_.isEmpty(${collectionText})` : `!_.isEmpty(${collectionText})`;
49
+ function isDoubleNegationLength(node) {
50
+ return node.operator === '!' && node.argument.type === AST_NODE_TYPES.UnaryExpression && node.argument.operator === '!' && isLengthAccess(node.argument.argument);
51
+ }
52
+ function isBooleanContext(node) {
53
+ const { parent } = node;
54
+ if (!parent)
55
+ return false;
56
+ switch (parent.type) {
57
+ case AST_NODE_TYPES.IfStatement:
58
+ case AST_NODE_TYPES.WhileStatement:
59
+ case AST_NODE_TYPES.DoWhileStatement:
60
+ case AST_NODE_TYPES.ForStatement: {
61
+ return parent.test === node;
62
+ }
63
+ case AST_NODE_TYPES.UnaryExpression: {
64
+ return parent.operator === '!';
65
+ }
66
+ case AST_NODE_TYPES.LogicalExpression: {
67
+ return parent.operator === '&&' || parent.operator === '||';
68
+ }
69
+ case AST_NODE_TYPES.ConditionalExpression: {
70
+ return parent.test === node;
71
+ }
72
+ case AST_NODE_TYPES.CallExpression: {
73
+ return parent.callee.type === AST_NODE_TYPES.Identifier && parent.callee.name === 'Boolean' && parent.arguments.length === 1 && parent.arguments[0] === node;
74
+ }
75
+ default: {
76
+ return false;
77
+ }
78
+ }
79
+ }
80
+ function reportBinary(node, lengthNode, operator, value, isEmptyCheck) {
81
+ const collection = sourceCode.getText(lengthNode.object);
82
+ const replacement = isEmptyCheck ? `_.isEmpty(${collection})` : `!_.isEmpty(${collection})`;
28
83
  context.report({
29
84
  node,
30
85
  messageId: 'useIsEmpty',
31
- data: {
32
- collection: collectionText,
33
- operator,
34
- value,
86
+ data: { collection, operator, value },
87
+ fix(fixer) {
88
+ const fixes = [fixer.replaceText(node, replacement)];
89
+ const importFix = ensureLodashImport(fixer);
90
+ if (importFix)
91
+ fixes.push(importFix);
92
+ return fixes;
93
+ },
94
+ });
95
+ }
96
+ function reportUnary(node, lengthNode) {
97
+ const collection = sourceCode.getText(lengthNode.object);
98
+ context.report({
99
+ node,
100
+ messageId: 'useIsEmptyUnary',
101
+ data: { collection },
102
+ fix(fixer) {
103
+ const fixes = [fixer.replaceText(node, `_.isEmpty(${collection})`)];
104
+ const importFix = ensureLodashImport(fixer);
105
+ if (importFix)
106
+ fixes.push(importFix);
107
+ return fixes;
108
+ },
109
+ });
110
+ }
111
+ function reportBoolean(node, lengthNode) {
112
+ const collection = sourceCode.getText(lengthNode.object);
113
+ context.report({
114
+ node,
115
+ messageId: 'useIsEmptyBoolean',
116
+ data: { collection },
117
+ fix(fixer) {
118
+ const fixes = [fixer.replaceText(node, `!_.isEmpty(${collection})`)];
119
+ const importFix = ensureLodashImport(fixer);
120
+ if (importFix)
121
+ fixes.push(importFix);
122
+ return fixes;
35
123
  },
36
- suggest: [
37
- {
38
- messageId: 'useIsEmpty',
39
- data: {
40
- collection: collectionText,
41
- operator,
42
- value,
43
- },
44
- fix(fixer) {
45
- return fixer.replaceText(node, replacement);
46
- },
47
- },
48
- ],
49
124
  });
50
125
  }
51
126
  return {
52
127
  BinaryExpression(node) {
53
128
  if (isLengthAccess(node.left) && isNumericLiteral(node.right)) {
54
- if ((node.operator === '===' && node.right.value === 0) || (node.operator === '<=' && node.right.value === 0) || (node.operator === '<' && node.right.value === 1)) {
55
- report(node, node.left, node.operator, node.right.value, true);
129
+ const right = node.right.value;
130
+ if ((node.operator === '===' && right === 0) || (node.operator === '<=' && right === 0) || (node.operator === '<' && right === 1)) {
131
+ reportBinary(node, getLengthMember(node.left), node.operator, right, true);
56
132
  return;
57
133
  }
58
- if ((node.operator === '>' && node.right.value === 0) ||
59
- (node.operator === '>=' && node.right.value === 1) ||
60
- ((node.operator === '!=' || node.operator === '!==') && node.right.value === 0)) {
61
- report(node, node.left, node.operator, node.right.value, false);
134
+ if ((node.operator === '>' && right === 0) || (node.operator === '>=' && right === 1) || ((node.operator === '!=' || node.operator === '!==') && right === 0)) {
135
+ reportBinary(node, getLengthMember(node.left), node.operator, right, false);
136
+ return;
62
137
  }
63
138
  }
64
139
  if (isNumericLiteral(node.left) && isLengthAccess(node.right)) {
65
- if ((node.operator === '===' && node.left.value === 0) || (node.operator === '>=' && node.left.value === 0) || (node.operator === '>' && node.left.value === 0)) {
66
- report(node, node.right, node.operator, node.left.value, true);
140
+ const left = node.left.value;
141
+ if ((node.operator === '===' && left === 0) || (node.operator === '>=' && left === 0) || (node.operator === '<=' && left === 0)) {
142
+ reportBinary(node, getLengthMember(node.right), node.operator, left, true);
67
143
  return;
68
144
  }
69
- if ((node.operator === '<' && node.left.value === 1) || (node.operator === '<=' && node.left.value === 0)) {
70
- report(node, node.right, node.operator, node.left.value, false);
145
+ if (node.operator === '<' && left === 0) {
146
+ reportBinary(node, getLengthMember(node.right), node.operator, left, false);
71
147
  }
72
148
  }
73
149
  },
150
+ UnaryExpression(node) {
151
+ if (node.parent?.type === AST_NODE_TYPES.UnaryExpression && node.parent.operator === '!' && isLengthAccess(node.argument)) {
152
+ return;
153
+ }
154
+ if (isDoubleNegationLength(node)) {
155
+ const inner = node.argument;
156
+ const lengthNode = getLengthMember(inner.argument);
157
+ reportBoolean(node, lengthNode);
158
+ return;
159
+ }
160
+ const arg = unwrapChain(node.argument);
161
+ if (!isLengthAccess(arg))
162
+ return;
163
+ if (isBooleanContext(node)) {
164
+ reportBoolean(node, getLengthMember(arg));
165
+ return;
166
+ }
167
+ if (node.operator === '!') {
168
+ reportUnary(node, getLengthMember(arg));
169
+ }
170
+ },
171
+ ConditionalExpression(node) {
172
+ if (isLengthAccess(node.test) && isBooleanContext(node.test)) {
173
+ reportBoolean(node.test, getLengthMember(node.test));
174
+ }
175
+ },
176
+ LogicalExpression(node) {
177
+ if ((node.operator === '&&' || node.operator === '||') && isLengthAccess(node.left)) {
178
+ reportBoolean(node.left, getLengthMember(node.left));
179
+ }
180
+ },
181
+ IfStatement(node) {
182
+ if (isLengthAccess(node.test)) {
183
+ reportBoolean(node.test, getLengthMember(node.test));
184
+ }
185
+ },
74
186
  };
75
187
  },
76
188
  });
@@ -1 +1 @@
1
- {"version":3,"file":"types-in-dts.d.ts","sourceRoot":"","sources":["../../src/rules/types-in-dts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAStE,QAAA,MAAM,UAAU;;;;;CAuFd,CAAC;AACH,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"types-in-dts.d.ts","sourceRoot":"","sources":["../../src/rules/types-in-dts.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE,QAAA,MAAM,UAAU;;;;;CAkFd,CAAC;AACH,eAAe,UAAU,CAAC"}
@@ -1,3 +1,4 @@
1
+ /* eslint-disable new-cap */
1
2
  import { ESLintUtils } from '@typescript-eslint/utils';
2
3
  const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/types-in-dts.md')({
3
4
  name: 'types-in-dts',
@@ -27,8 +28,6 @@ const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/
27
28
  },
28
29
  ],
29
30
  create(context, [options]) {
30
- const allowEnums = Boolean(options.allowEnums);
31
- const allowDeclare = Boolean(options.allowDeclare);
32
31
  function isDtsFile(filename) {
33
32
  if (!filename || filename === '<input>') {
34
33
  return false;
@@ -43,11 +42,10 @@ const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/
43
42
  return modifiers.some((m) => m?.type === 'TSDeclareKeyword');
44
43
  }
45
44
  function reportIfNotDts(node) {
46
- const filename = context.getFilename();
47
- if (isDtsFile(filename)) {
45
+ if (isDtsFile(context.filename)) {
48
46
  return;
49
47
  }
50
- if (allowDeclare && hasDeclareModifier(node)) {
48
+ if (options.allowDeclare && hasDeclareModifier(node)) {
51
49
  return;
52
50
  }
53
51
  context.report({
@@ -63,7 +61,7 @@ const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/
63
61
  reportIfNotDts(node);
64
62
  },
65
63
  TSEnumDeclaration(node) {
66
- if (allowEnums) {
64
+ if (options.allowEnums) {
67
65
  return;
68
66
  }
69
67
  reportIfNotDts(node);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "3.2.1",
3
+ "version": "3.2.3",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",