eslint-plugin-th-rules 3.2.2 → 3.3.0

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
 
package/dist/plugin.d.ts CHANGED
@@ -17,7 +17,13 @@ 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" | "useIsEmptyUnary", [], 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
+ 'no-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
+ name: string;
25
+ };
26
+ 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
27
  name: string;
22
28
  };
23
29
  'schemas-in-schemas-file': import("@typescript-eslint/utils/ts-eslint").RuleModule<"moveSchema", [{
@@ -36,9 +42,6 @@ export declare const rules: {
36
42
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
37
43
  name: string;
38
44
  };
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
45
  };
43
46
  declare const plugin: {
44
47
  rules: {
@@ -60,7 +63,13 @@ declare const plugin: {
60
63
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
61
64
  name: string;
62
65
  };
63
- 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
66
+ 'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
67
+ name: string;
68
+ };
69
+ 'no-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
70
+ name: string;
71
+ };
72
+ 'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
64
73
  name: string;
65
74
  };
66
75
  'schemas-in-schemas-file': import("@typescript-eslint/utils/ts-eslint").RuleModule<"moveSchema", [{
@@ -79,9 +88,6 @@ declare const plugin: {
79
88
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
80
89
  name: string;
81
90
  };
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
91
  };
86
92
  };
87
93
  export default plugin;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAUjB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAY,CAAC;AACzB,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWjB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAY,CAAC;AACzB,eAAe,MAAM,CAAC"}
package/dist/plugin.js CHANGED
@@ -3,6 +3,7 @@ import noComments from './rules/no-comments.js';
3
3
  import noDefaultExport from './rules/no-default-export.js';
4
4
  import noDestructuring from './rules/no-destructuring.js';
5
5
  import noExplicitNilCompare from './rules/no-explicit-nil-compare.js';
6
+ import preferExplicitNilCheck from './rules/prefer-explicit-nil-check.js';
6
7
  import preferIsEmpty from './rules/prefer-is-empty.js';
7
8
  import schemasInSchemasFile from './rules/schemas-in-schemas-file.js';
8
9
  import topLevelFunctions from './rules/top-level-functions.js';
@@ -12,11 +13,12 @@ export const rules = {
12
13
  'no-comments': noComments,
13
14
  'no-default-export': noDefaultExport,
14
15
  'no-destructuring': noDestructuring,
16
+ 'no-explicit-nil-compare': noExplicitNilCompare,
17
+ 'no-explicit-nil-check': preferExplicitNilCheck,
15
18
  'prefer-is-empty': preferIsEmpty,
16
19
  'schemas-in-schemas-file': schemasInSchemasFile,
17
20
  'top-level-functions': topLevelFunctions,
18
21
  'types-in-dts': typesInDts,
19
- 'no-explicit-nil-compare': noExplicitNilCompare,
20
22
  };
21
23
  const plugin = { rules };
22
24
  export default plugin;
@@ -1 +1 @@
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
+ {"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;AAStF,QAAA,MAAM,iBAAiB;;CAyHrB,CAAC;AAEH,eAAe,iBAAiB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable new-cap */
2
- /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
+ import _ from 'lodash';
3
3
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
4
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');
@@ -32,7 +32,7 @@ const noBooleanCoercion = createRule({
32
32
  }
33
33
  const firstNode = context.sourceCode.ast.body[0];
34
34
  const importText = `import ${LODASH_IDENT} from '${LODASH_MODULE}';\n`;
35
- if (!firstNode) {
35
+ if (_.isNil(firstNode)) {
36
36
  return fixer.insertTextAfterRange([0, 0], importText);
37
37
  }
38
38
  return fixer.insertTextBefore(firstNode, importText);
@@ -45,7 +45,7 @@ const noBooleanCoercion = createRule({
45
45
  }
46
46
  function isBooleanByTS(node) {
47
47
  const tsNode = services.esTreeNodeToTSNodeMap.get(node);
48
- if (!tsNode) {
48
+ if (_.isNil(tsNode)) {
49
49
  return false;
50
50
  }
51
51
  const type = checker.getTypeAtLocation(tsNode);
@@ -53,7 +53,7 @@ const noBooleanCoercion = createRule({
53
53
  }
54
54
  function isCollectionLikeByTS(node) {
55
55
  const tsNode = services.esTreeNodeToTSNodeMap.get(node);
56
- if (!tsNode) {
56
+ if (_.isNil(tsNode)) {
57
57
  return false;
58
58
  }
59
59
  const type = checker.getTypeAtLocation(tsNode);
@@ -83,7 +83,7 @@ const noBooleanCoercion = createRule({
83
83
  fix(fixer) {
84
84
  const fixes = [fixer.replaceText(node, replacement)];
85
85
  const importFix = getLodashImportFixer(fixer);
86
- if (importFix) {
86
+ if (!_.isNil(importFix)) {
87
87
  fixes.push(importFix);
88
88
  }
89
89
  return fixes;
@@ -20,7 +20,7 @@ const noDefaultExport = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
20
20
  function generateExportNameFromFileName(fileName) {
21
21
  const cleaned = fileName.replaceAll(/[^a-zA-Z\d]+/g, ' ');
22
22
  const parts = cleaned.trim().split(/\s+/g).filter(Boolean);
23
- if (parts.length === 0) {
23
+ if (_.isEmpty(parts)) {
24
24
  return 'defaultExport';
25
25
  }
26
26
  const [first, ...rest] = parts;
@@ -1 +1 @@
1
- {"version":3,"file":"no-destructuring.d.ts","sourceRoot":"","sources":["../../src/rules/no-destructuring.ts"],"names":[],"mappings":"AAEA,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,QAAA,MAAM,eAAe;;;;;CAyInB,CAAC;AACH,eAAe,eAAe,CAAC"}
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable new-cap */
2
2
  /* eslint-disable @typescript-eslint/no-unsafe-assignment */
3
+ import _ from 'lodash';
3
4
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
4
5
  const MAX_TAB_COUNT = 3;
5
6
  const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/no-destructuring.md')({
@@ -35,7 +36,7 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
35
36
  const MAX_VARIABLES = options.maximumDestructuredVariables ?? 2;
36
37
  const MAX_LINE_LENGTH = options.maximumLineLength ?? 100;
37
38
  function reportIfNeeded(patternNode, reportNode = patternNode) {
38
- if (patternNode?.type !== AST_NODE_TYPES.ObjectPattern || !patternNode.loc) {
39
+ if (patternNode?.type !== AST_NODE_TYPES.ObjectPattern || _.isNil(patternNode.loc)) {
39
40
  return;
40
41
  }
41
42
  const startLine = patternNode.loc.start.line;
@@ -81,7 +82,7 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
81
82
  }
82
83
  function checkParameters(parameters) {
83
84
  for (const p of parameters || []) {
84
- if (!p) {
85
+ if (_.isNil(p)) {
85
86
  continue;
86
87
  }
87
88
  if (p.type === AST_NODE_TYPES.AssignmentPattern) {
@@ -105,12 +106,12 @@ const noDestructuring = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh
105
106
  checkParameters(node.params);
106
107
  },
107
108
  MethodDefinition(node) {
108
- if (node.value?.params) {
109
+ if (!_.isNil(node.value?.params)) {
109
110
  checkParameters(node.value.params);
110
111
  }
111
112
  },
112
113
  TSDeclareFunction(node) {
113
- if (node.params) {
114
+ if (!_.isNil(node.params)) {
114
115
  checkParameters(node.params);
115
116
  }
116
117
  },
@@ -23,7 +23,7 @@ const noExplicitNilCompare = createRule({
23
23
  /** Ensure lodash default import exists */
24
24
  function ensureLodashImport(fixer) {
25
25
  const existingImport = context.sourceCode.ast.body.find((node) => node.type === AST_NODE_TYPES.ImportDeclaration && node.source.value === 'lodash');
26
- if (existingImport)
26
+ if (!_.isNil(existingImport))
27
27
  return null;
28
28
  return fixer.insertTextBeforeRange([0, 0], `import _ from 'lodash';\n`);
29
29
  }
@@ -51,7 +51,7 @@ const noExplicitNilCompare = createRule({
51
51
  else if (isUndefinedIdentifier(left)) {
52
52
  targetNode = right;
53
53
  }
54
- if (!targetNode)
54
+ if (_.isNil(targetNode))
55
55
  return;
56
56
  const text = context.sourceCode.getText(targetNode);
57
57
  const positive = isNull ? `_.isNull(${text})` : `_.isUndefined(${text})`;
@@ -63,7 +63,7 @@ const noExplicitNilCompare = createRule({
63
63
  fix(fixer) {
64
64
  const fixes = [];
65
65
  const importFix = ensureLodashImport(fixer);
66
- if (importFix)
66
+ if (!_.isNil(importFix))
67
67
  fixes.push(importFix);
68
68
  fixes.push(fixer.replaceText(node, replacement));
69
69
  return fixes;
@@ -0,0 +1,6 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const preferExplicitNilCheck: ESLintUtils.RuleModule<"useIsNil", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default preferExplicitNilCheck;
6
+ //# sourceMappingURL=prefer-explicit-nil-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prefer-explicit-nil-check.d.ts","sourceRoot":"","sources":["../../src/rules/prefer-explicit-nil-check.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAWtF,QAAA,MAAM,sBAAsB;;CA2P1B,CAAC;AAEH,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,223 @@
1
+ /* eslint-disable no-bitwise */
2
+ /* eslint-disable new-cap */
3
+ import _ from 'lodash';
4
+ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
5
+ import * as ts from 'typescript';
6
+ const createRule = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/prefer-explicit-nil-check.md');
7
+ const LODASH_MODULE = 'lodash';
8
+ const LODASH_IDENT = '_';
9
+ const preferExplicitNilCheck = createRule({
10
+ name: 'prefer-explicit-nil-check',
11
+ meta: {
12
+ type: 'problem',
13
+ fixable: 'code',
14
+ docs: {
15
+ description: 'Disallow implicit truthy/falsy checks anywhere. Require explicit _.isNil(value).',
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ useIsNil: 'Implicit truthy/falsy checks are not allowed. Use _.isNil(value) or !_.isNil(value).',
20
+ },
21
+ },
22
+ defaultOptions: [],
23
+ create(context) {
24
+ const services = ESLintUtils.getParserServices(context);
25
+ const checker = services.program.getTypeChecker();
26
+ const reported = new WeakSet();
27
+ let lodashImportFixAdded = false;
28
+ function hasLodashImport() {
29
+ return context.sourceCode.ast.body.some((node) => node.type === AST_NODE_TYPES.ImportDeclaration && node.source.value === LODASH_MODULE);
30
+ }
31
+ function getLodashImportFixer(fixer) {
32
+ if (hasLodashImport())
33
+ return null;
34
+ if (lodashImportFixAdded)
35
+ return null;
36
+ lodashImportFixAdded = true;
37
+ const firstNode = context.sourceCode.ast.body[0];
38
+ const importText = `import ${LODASH_IDENT} from '${LODASH_MODULE}';\n`;
39
+ if (_.isNil(firstNode)) {
40
+ return fixer.insertTextAfterRange([0, 0], importText);
41
+ }
42
+ return fixer.insertTextBefore(firstNode, importText);
43
+ }
44
+ function isBooleanByTS(node) {
45
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
46
+ if (_.isNil(tsNode))
47
+ return false;
48
+ const type = checker.getTypeAtLocation(tsNode);
49
+ return (type.flags & ts.TypeFlags.Boolean) !== 0 || (type.flags & ts.TypeFlags.BooleanLiteral) !== 0;
50
+ }
51
+ function isAlreadyExplicitCheck(node) {
52
+ return (node.type === AST_NODE_TYPES.CallExpression &&
53
+ node.callee.type === AST_NODE_TYPES.MemberExpression &&
54
+ node.callee.object.type === AST_NODE_TYPES.Identifier &&
55
+ node.callee.object.name === LODASH_IDENT &&
56
+ node.callee.property.type === AST_NODE_TYPES.Identifier &&
57
+ (node.callee.property.name === 'isNil' || node.callee.property.name === 'isEmpty'));
58
+ }
59
+ function isImplicitOperand(node) {
60
+ if (node.type === AST_NODE_TYPES.Identifier)
61
+ return true;
62
+ if (node.type === AST_NODE_TYPES.MemberExpression)
63
+ return true;
64
+ if (node.type === AST_NODE_TYPES.ChainExpression) {
65
+ const inner = node.expression;
66
+ return inner.type === AST_NODE_TYPES.MemberExpression;
67
+ }
68
+ return false;
69
+ }
70
+ function reportFull(node, replacement) {
71
+ if (reported.has(node))
72
+ return;
73
+ reported.add(node);
74
+ context.report({
75
+ node,
76
+ messageId: 'useIsNil',
77
+ fix(fixer) {
78
+ const fixes = [fixer.replaceText(node, replacement)];
79
+ const importFix = getLodashImportFixer(fixer);
80
+ if (!_.isNil(importFix))
81
+ fixes.push(importFix);
82
+ return fixes;
83
+ },
84
+ });
85
+ }
86
+ function transformTruthy(node) {
87
+ const text = context.sourceCode.getText(node);
88
+ reportFull(node, `!${LODASH_IDENT}.isNil(${text})`);
89
+ }
90
+ function transformFalsyUnary(node) {
91
+ const arg = node.argument;
92
+ const text = context.sourceCode.getText(arg);
93
+ reportFull(node, `${LODASH_IDENT}.isNil(${text})`);
94
+ }
95
+ function expressionHasSideEffects(node) {
96
+ switch (node.type) {
97
+ case AST_NODE_TYPES.CallExpression:
98
+ case AST_NODE_TYPES.NewExpression:
99
+ case AST_NODE_TYPES.AssignmentExpression:
100
+ case AST_NODE_TYPES.UpdateExpression:
101
+ case AST_NODE_TYPES.AwaitExpression:
102
+ case AST_NODE_TYPES.YieldExpression:
103
+ case AST_NODE_TYPES.ImportExpression:
104
+ case AST_NODE_TYPES.TaggedTemplateExpression: {
105
+ return true;
106
+ }
107
+ case AST_NODE_TYPES.SequenceExpression: {
108
+ return node.expressions.some(expressionHasSideEffects);
109
+ }
110
+ case AST_NODE_TYPES.UnaryExpression: {
111
+ return node.operator === 'delete';
112
+ }
113
+ case AST_NODE_TYPES.ConditionalExpression: {
114
+ return expressionHasSideEffects(node.test) || expressionHasSideEffects(node.consequent) || expressionHasSideEffects(node.alternate);
115
+ }
116
+ case AST_NODE_TYPES.LogicalExpression: {
117
+ return expressionHasSideEffects(node.left) || expressionHasSideEffects(node.right);
118
+ }
119
+ case AST_NODE_TYPES.ChainExpression: {
120
+ return expressionHasSideEffects(node.expression);
121
+ }
122
+ default: {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ /**
128
+ * Inspect an expression for truthy/falsy coercions.
129
+ *
130
+ * mode:
131
+ * - 'test': expression is in a boolean-test position (must enforce)
132
+ * - 'shortCircuit': expression-statement short-circuit control flow (must enforce gating)
133
+ * - 'value': value context (must NOT rewrite operands)
134
+ */
135
+ function inspectExpression(node, mode) {
136
+ if (isAlreadyExplicitCheck(node))
137
+ return;
138
+ switch (node.type) {
139
+ case AST_NODE_TYPES.ChainExpression: {
140
+ inspectExpression(node.expression, mode);
141
+ return;
142
+ }
143
+ case AST_NODE_TYPES.LogicalExpression: {
144
+ if (node.operator === '??') {
145
+ inspectExpression(node.left, 'value');
146
+ inspectExpression(node.right, 'value');
147
+ return;
148
+ }
149
+ if (node.operator === '&&' || node.operator === '||') {
150
+ if (mode === 'test') {
151
+ inspectExpression(node.left, 'test');
152
+ inspectExpression(node.right, 'test');
153
+ return;
154
+ }
155
+ if (mode === 'shortCircuit') {
156
+ inspectExpression(node.left, 'test');
157
+ inspectExpression(node.right, 'shortCircuit');
158
+ return;
159
+ }
160
+ inspectExpression(node.left, 'value');
161
+ inspectExpression(node.right, 'value');
162
+ return;
163
+ }
164
+ inspectExpression(node.left, 'value');
165
+ inspectExpression(node.right, 'value');
166
+ return;
167
+ }
168
+ case AST_NODE_TYPES.UnaryExpression: {
169
+ if (node.operator !== '!')
170
+ return;
171
+ const arg = node.argument;
172
+ if (arg.type === AST_NODE_TYPES.UnaryExpression && arg.operator === '!') {
173
+ return;
174
+ }
175
+ if (isImplicitOperand(arg)) {
176
+ if (isBooleanByTS(arg))
177
+ return;
178
+ transformFalsyUnary(node);
179
+ return;
180
+ }
181
+ inspectExpression(arg, 'test');
182
+ return;
183
+ }
184
+ case AST_NODE_TYPES.ConditionalExpression: {
185
+ inspectExpression(node.test, 'test');
186
+ return;
187
+ }
188
+ default: {
189
+ if (mode === 'test' && isImplicitOperand(node)) {
190
+ if (isBooleanByTS(node))
191
+ return;
192
+ transformTruthy(node);
193
+ }
194
+ }
195
+ }
196
+ }
197
+ return {
198
+ IfStatement(node) {
199
+ inspectExpression(node.test, 'test');
200
+ },
201
+ WhileStatement(node) {
202
+ inspectExpression(node.test, 'test');
203
+ },
204
+ DoWhileStatement(node) {
205
+ inspectExpression(node.test, 'test');
206
+ },
207
+ ForStatement(node) {
208
+ if (!_.isNil(node.test))
209
+ inspectExpression(node.test, 'test');
210
+ },
211
+ ConditionalExpression(node) {
212
+ inspectExpression(node.test, 'test');
213
+ },
214
+ ExpressionStatement(node) {
215
+ const expr = node.expression;
216
+ if (expr.type === AST_NODE_TYPES.LogicalExpression && (expr.operator === '&&' || expr.operator === '||') && expressionHasSideEffects(expr.right)) {
217
+ inspectExpression(expr, 'shortCircuit');
218
+ }
219
+ },
220
+ };
221
+ },
222
+ });
223
+ export default preferExplicitNilCheck;
@@ -1,5 +1,5 @@
1
1
  import { ESLintUtils } from '@typescript-eslint/utils';
2
- declare const preferIsEmpty: ESLintUtils.RuleModule<"useIsEmpty" | "useIsEmptyUnary", [], 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":"AAKA,OAAO,EAAkB,WAAW,EAAgC,MAAM,0BAA0B,CAAC;AAIrG,QAAA,MAAM,aAAa;;CAiHjB,CAAC;AAEH,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"}
@@ -8,13 +8,14 @@ const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh20
8
8
  meta: {
9
9
  type: 'problem',
10
10
  docs: {
11
- description: 'Require _.isEmpty instead of length comparisons or !x.length checks.',
11
+ description: 'Require _.isEmpty instead of length comparisons or boolean checks on .length.',
12
12
  },
13
13
  fixable: 'code',
14
14
  schema: [],
15
15
  messages: {
16
16
  useIsEmpty: 'Use _.isEmpty({{collection}}) instead of checking {{collection}}.length {{operator}} {{value}}.',
17
17
  useIsEmptyUnary: 'Use _.isEmpty({{collection}}) instead of negating {{collection}}.length.',
18
+ useIsEmptyBoolean: 'Use _.isEmpty({{collection}}) instead of boolean checking {{collection}}.length.',
18
19
  },
19
20
  },
20
21
  defaultOptions: [],
@@ -26,14 +27,56 @@ const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh20
26
27
  if (hasLodash)
27
28
  return null;
28
29
  const firstImport = imports[0];
29
- return firstImport ? fixer.insertTextBefore(firstImport, `import _ from 'lodash';\n`) : fixer.insertTextBeforeRange([0, 0], `import _ from 'lodash';\n`);
30
+ return _.isNil(firstImport) ? fixer.insertTextBeforeRange([0, 0], `import _ from 'lodash';\n`) : fixer.insertTextBefore(firstImport, `import _ from 'lodash';\n`);
31
+ }
32
+ function unwrapChain(node) {
33
+ return node?.type === AST_NODE_TYPES.ChainExpression ? node.expression : node;
30
34
  }
31
35
  function isLengthAccess(node) {
32
- return !_.isNil(node) && 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);
33
45
  }
34
46
  function isNumericLiteral(node) {
35
47
  return !_.isNil(node) && node.type === AST_NODE_TYPES.Literal && typeof node.value === 'number';
36
48
  }
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 (_.isNil(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
+ }
37
80
  function reportBinary(node, lengthNode, operator, value, isEmptyCheck) {
38
81
  const collection = sourceCode.getText(lengthNode.object);
39
82
  const replacement = isEmptyCheck ? `_.isEmpty(${collection})` : `!_.isEmpty(${collection})`;
@@ -44,7 +87,7 @@ const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh20
44
87
  fix(fixer) {
45
88
  const fixes = [fixer.replaceText(node, replacement)];
46
89
  const importFix = ensureLodashImport(fixer);
47
- if (importFix)
90
+ if (!_.isNil(importFix))
48
91
  fixes.push(importFix);
49
92
  return fixes;
50
93
  },
@@ -59,7 +102,22 @@ const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh20
59
102
  fix(fixer) {
60
103
  const fixes = [fixer.replaceText(node, `_.isEmpty(${collection})`)];
61
104
  const importFix = ensureLodashImport(fixer);
62
- if (importFix)
105
+ if (!_.isNil(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 (!_.isNil(importFix))
63
121
  fixes.push(importFix);
64
122
  return fixes;
65
123
  },
@@ -68,31 +126,61 @@ const preferIsEmpty = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh20
68
126
  return {
69
127
  BinaryExpression(node) {
70
128
  if (isLengthAccess(node.left) && isNumericLiteral(node.right)) {
71
- if ((node.operator === '===' && node.right.value === 0) || (node.operator === '<=' && node.right.value === 0) || (node.operator === '<' && node.right.value === 1)) {
72
- reportBinary(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);
73
132
  return;
74
133
  }
75
- if ((node.operator === '>' && node.right.value === 0) ||
76
- (node.operator === '>=' && node.right.value === 1) ||
77
- ((node.operator === '!=' || node.operator === '!==') && node.right.value === 0)) {
78
- reportBinary(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;
79
137
  }
80
138
  }
81
139
  if (isNumericLiteral(node.left) && isLengthAccess(node.right)) {
82
- if ((node.operator === '===' && node.left.value === 0) || (node.operator === '>=' && node.left.value === 0) || (node.operator === '>' && node.left.value === 0)) {
83
- reportBinary(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);
84
143
  return;
85
144
  }
86
- if ((node.operator === '<' && node.left.value === 1) || (node.operator === '<=' && node.left.value === 0)) {
87
- reportBinary(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);
88
147
  }
89
148
  }
90
149
  },
91
150
  UnaryExpression(node) {
92
- if (node.operator !== '!')
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))
93
162
  return;
94
- if (isLengthAccess(node.argument)) {
95
- reportUnary(node, node.argument);
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));
96
184
  }
97
185
  },
98
186
  };
@@ -1 +1 @@
1
- {"version":3,"file":"schemas-in-schemas-file.d.ts","sourceRoot":"","sources":["../../src/rules/schemas-in-schemas-file.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtF,QAAA,MAAM,oBAAoB;;;;;;CAmIxB,CAAC;AAEH,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"schemas-in-schemas-file.d.ts","sourceRoot":"","sources":["../../src/rules/schemas-in-schemas-file.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtF,QAAA,MAAM,oBAAoB;;;;;;CAmIxB,CAAC;AAEH,eAAe,oBAAoB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable @typescript-eslint/no-unnecessary-condition */
2
2
  /* eslint-disable new-cap */
3
+ import _ from 'lodash';
3
4
  import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
4
5
  const schemasInSchemasFile = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/schemas-in-schemas-file.md')({
5
6
  name: 'schemas-in-schemas-file',
@@ -41,7 +42,7 @@ const schemasInSchemasFile = ESLintUtils.RuleCreator(() => 'https://github.com/t
41
42
  };
42
43
  const zodIdentifiers = new Set();
43
44
  function filenameAllowed(filename) {
44
- if (!filename || filename === '<input>') {
45
+ if (_.isNil(filename) || filename === '<input>') {
45
46
  return false;
46
47
  }
47
48
  if (options.allowInTests && /\.(test|spec)\.[jt]sx?$/.test(filename)) {
@@ -1 +1 @@
1
- {"version":3,"file":"top-level-functions.d.ts","sourceRoot":"","sources":["../../src/rules/top-level-functions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE,QAAA,MAAM,iBAAiB;;CA4JrB,CAAC;AACH,eAAe,iBAAiB,CAAC"}
1
+ {"version":3,"file":"top-level-functions.d.ts","sourceRoot":"","sources":["../../src/rules/top-level-functions.ts"],"names":[],"mappings":"AACA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtF,QAAA,MAAM,iBAAiB;;CA4JrB,CAAC;AACH,eAAe,iBAAiB,CAAC"}
@@ -1,4 +1,5 @@
1
- import { ESLintUtils } from '@typescript-eslint/utils';
1
+ import _ from 'lodash';
2
+ import { AST_NODE_TYPES, ESLintUtils } from '@typescript-eslint/utils';
2
3
  const topLevelFunctions = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/top-level-functions.md')({
3
4
  name: 'top-level-functions',
4
5
  meta: {
@@ -62,7 +63,7 @@ const topLevelFunctions = ESLintUtils.RuleCreator(() => 'https://github.com/tome
62
63
  VariableDeclarator(node) {
63
64
  const declParent = node.parent;
64
65
  const grandParent = declParent?.parent;
65
- if (!grandParent) {
66
+ if (_.isNil(grandParent)) {
66
67
  return;
67
68
  }
68
69
  const topLevel = grandParent.type === 'Program' || grandParent.type === 'ExportNamedDeclaration' || grandParent.type === 'ExportDefaultDeclaration';
@@ -70,11 +71,11 @@ const topLevelFunctions = ESLintUtils.RuleCreator(() => 'https://github.com/tome
70
71
  return;
71
72
  }
72
73
  const isExport = grandParent.type === 'ExportNamedDeclaration' || grandParent.type === 'ExportDefaultDeclaration';
73
- if (!node.init) {
74
+ if (_.isNil(node.init)) {
74
75
  return;
75
76
  }
76
77
  const functionName = node.id.type === 'Identifier' ? node.id.name : null;
77
- if (!functionName) {
78
+ if (_.isNil(functionName)) {
78
79
  return;
79
80
  }
80
81
  if (node.init.type === 'ArrowFunctionExpression') {
@@ -88,7 +89,7 @@ const topLevelFunctions = ESLintUtils.RuleCreator(() => 'https://github.com/tome
88
89
  },
89
90
  });
90
91
  }
91
- if (node.init.type === 'FunctionExpression') {
92
+ if (node.init.type === AST_NODE_TYPES.FunctionExpression) {
92
93
  const funcExpr = node.init;
93
94
  context.report({
94
95
  node: funcExpr,
@@ -101,7 +102,7 @@ const topLevelFunctions = ESLintUtils.RuleCreator(() => 'https://github.com/tome
101
102
  }
102
103
  },
103
104
  FunctionDeclaration(node) {
104
- if (node.id) {
105
+ if (!_.isNil(node.id)) {
105
106
  return;
106
107
  }
107
108
  if (!isTopLevel(node)) {
@@ -1 +1 @@
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
+ {"version":3,"file":"types-in-dts.d.ts","sourceRoot":"","sources":["../../src/rules/types-in-dts.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAEtE,QAAA,MAAM,UAAU;;;;;CAkFd,CAAC;AACH,eAAe,UAAU,CAAC"}
@@ -1,4 +1,5 @@
1
1
  /* eslint-disable new-cap */
2
+ import _ from 'lodash';
2
3
  import { ESLintUtils } from '@typescript-eslint/utils';
3
4
  const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/eslint-plugin-th-rules/blob/main/docs/rules/types-in-dts.md')({
4
5
  name: 'types-in-dts',
@@ -29,7 +30,7 @@ const typesInDts = ESLintUtils.RuleCreator(() => 'https://github.com/tomerh2001/
29
30
  ],
30
31
  create(context, [options]) {
31
32
  function isDtsFile(filename) {
32
- if (!filename || filename === '<input>') {
33
+ if (_.isNil(filename) || filename === '<input>') {
33
34
  return false;
34
35
  }
35
36
  return filename.endsWith('.d.ts');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "3.2.2",
3
+ "version": "3.3.0",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",