eslint-plugin-th-rules 3.3.2 → 3.4.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
@@ -166,17 +166,18 @@ Do not edit below this line.
166
166
  🎲 Set in the `recommendedTypescriptReact` configuration.\
167
167
  🔧 Automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/user-guide/command-line-interface#--fix).
168
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. | ✅ ⚛️ 🟦 🎲 | |
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-explicit-nil-check](docs/rules/prefer-explicit-nil-check.md) | Disallow implicit truthy/falsy checks in boolean-test positions. Prefer explicit _.isNil(value) or _.isEmpty(value) (depending on the value type). | ✅ ⚛️ 🟦 🎲 | 🔧 |
177
+ | [prefer-is-empty](docs/rules/prefer-is-empty.md) | Require _.isEmpty instead of length comparisons or boolean checks on .length. | ✅ ⚛️ 🟦 🎲 | 🔧 |
178
+ | [schemas-in-schemas-file](docs/rules/schemas-in-schemas-file.md) | Require Zod schema declarations to be placed in a .schemas.ts file. | ✅ ⚛️ 🟦 🎲 | |
179
+ | [top-level-functions](docs/rules/top-level-functions.md) | Require all top-level functions to be named regular functions. | ✅ ⚛️ 🟦 🎲 | 🔧 |
180
+ | [types-in-dts](docs/rules/types-in-dts.md) | Require TypeScript type declarations (type/interface/enum) to be placed in .d.ts files. | ✅ ⚛️ 🟦 🎲 | |
180
181
 
181
182
  <!-- end auto-generated rules list -->
182
183
 
package/dist/plugin.d.ts CHANGED
@@ -20,7 +20,7 @@ export declare const rules: {
20
20
  'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
21
21
  name: string;
22
22
  };
23
- 'no-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
23
+ 'prefer-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
24
24
  name: string;
25
25
  };
26
26
  'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
@@ -42,6 +42,9 @@ export declare const rules: {
42
42
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
43
43
  name: string;
44
44
  };
45
+ 'no-isnil-isempty-on-boolean': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
46
+ name: string;
47
+ };
45
48
  };
46
49
  declare const plugin: {
47
50
  rules: {
@@ -66,7 +69,7 @@ declare const plugin: {
66
69
  'no-explicit-nil-compare': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNull" | "useIsUndefined", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
67
70
  name: string;
68
71
  };
69
- 'no-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
72
+ 'prefer-explicit-nil-check': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsNil", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
70
73
  name: string;
71
74
  };
72
75
  'prefer-is-empty': import("@typescript-eslint/utils/ts-eslint").RuleModule<"useIsEmpty" | "useIsEmptyUnary" | "useIsEmptyBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
@@ -88,6 +91,9 @@ declare const plugin: {
88
91
  }], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
89
92
  name: string;
90
93
  };
94
+ 'no-isnil-isempty-on-boolean': import("@typescript-eslint/utils/ts-eslint").RuleModule<"noBoolean", [], unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
95
+ name: string;
96
+ };
91
97
  };
92
98
  };
93
99
  export default plugin;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYjB,CAAC;AAEF,QAAA,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAAY,CAAC;AACzB,eAAe,MAAM,CAAC"}
package/dist/plugin.js CHANGED
@@ -8,17 +8,19 @@ import preferIsEmpty from './rules/prefer-is-empty.js';
8
8
  import schemasInSchemasFile from './rules/schemas-in-schemas-file.js';
9
9
  import topLevelFunctions from './rules/top-level-functions.js';
10
10
  import typesInDts from './rules/types-in-dts.js';
11
+ import noIsNilOrIsEmptyOnBoolean from './rules/no-isnil-isempty-on-boolean.js';
11
12
  export const rules = {
12
13
  'no-boolean-coercion': noBooleanCoercion,
13
14
  'no-comments': noComments,
14
15
  'no-default-export': noDefaultExport,
15
16
  'no-destructuring': noDestructuring,
16
17
  'no-explicit-nil-compare': noExplicitNilCompare,
17
- 'no-explicit-nil-check': preferExplicitNilCheck,
18
+ 'prefer-explicit-nil-check': preferExplicitNilCheck,
18
19
  'prefer-is-empty': preferIsEmpty,
19
20
  'schemas-in-schemas-file': schemasInSchemasFile,
20
21
  'top-level-functions': topLevelFunctions,
21
22
  'types-in-dts': typesInDts,
23
+ 'no-isnil-isempty-on-boolean': noIsNilOrIsEmptyOnBoolean,
22
24
  };
23
25
  const plugin = { rules };
24
26
  export default plugin;
@@ -0,0 +1,6 @@
1
+ import { ESLintUtils } from '@typescript-eslint/utils';
2
+ declare const noIsNilOrIsEmptyOnBoolean: ESLintUtils.RuleModule<"noBoolean", [], unknown, ESLintUtils.RuleListener> & {
3
+ name: string;
4
+ };
5
+ export default noIsNilOrIsEmptyOnBoolean;
6
+ //# sourceMappingURL=no-isnil-isempty-on-boolean.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-isnil-isempty-on-boolean.d.ts","sourceRoot":"","sources":["../../src/rules/no-isnil-isempty-on-boolean.ts"],"names":[],"mappings":"AAIA,OAAO,EAAkB,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAQtF,QAAA,MAAM,yBAAyB;;CA+H7B,CAAC;AAEH,eAAe,yBAAyB,CAAC"}
@@ -0,0 +1,122 @@
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/no-isnil-isempty-on-boolean.md');
7
+ const LODASH_IDENT = '_';
8
+ const DISALLOWED_METHODS = new Set(['isNil', 'isEmpty']);
9
+ const noIsNilOrIsEmptyOnBoolean = createRule({
10
+ name: 'no-isnil-isempty-on-boolean',
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Disallow _.isNil(...) / _.isEmpty(...) when the argument type is boolean or a union containing boolean (e.g., boolean | undefined, boolean | X).',
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ noBoolean: 'Do not use _.{{method}}(...) on boolean or unions containing boolean. Use explicit boolean checks/comparisons instead.',
19
+ },
20
+ },
21
+ defaultOptions: [],
22
+ create(context) {
23
+ const services = ESLintUtils.getParserServices(context);
24
+ const checker = services.program.getTypeChecker();
25
+ function unwrapChainExpression(node) {
26
+ return node.type === AST_NODE_TYPES.ChainExpression ? node.expression : node;
27
+ }
28
+ function getTypeFromSymbolAnnotation(symbol) {
29
+ const decl = symbol.valueDeclaration ?? symbol.declarations?.[0];
30
+ if (_.isNil(decl))
31
+ return null;
32
+ if ('type' in decl) {
33
+ const typeNode = decl.type;
34
+ if (!_.isNil(typeNode)) {
35
+ return checker.getTypeFromTypeNode(typeNode);
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ function getTsTypeForCheck(node) {
41
+ const unwrapped = unwrapChainExpression(node);
42
+ const tsNode = services.esTreeNodeToTSNodeMap.get(unwrapped);
43
+ if (_.isNil(tsNode))
44
+ return null;
45
+ let type;
46
+ if (unwrapped.type === AST_NODE_TYPES.Identifier) {
47
+ const symbol = checker.getSymbolAtLocation(tsNode);
48
+ const annotated = _.isNil(symbol) ? null : getTypeFromSymbolAnnotation(symbol);
49
+ type = annotated ?? checker.getTypeAtLocation(tsNode);
50
+ }
51
+ else {
52
+ type = checker.getTypeAtLocation(tsNode);
53
+ }
54
+ return checker.getWidenedType(type);
55
+ }
56
+ function isNullableFlag(flags) {
57
+ return (flags & ts.TypeFlags.Null) !== 0 || (flags & ts.TypeFlags.Undefined) !== 0;
58
+ }
59
+ function isBooleanLikeFlag(flags) {
60
+ return (flags & ts.TypeFlags.BooleanLike) !== 0;
61
+ }
62
+ /**
63
+ * Returns true if the type is boolean-like, or a union containing any boolean-like
64
+ * non-nullish constituent.
65
+ */
66
+ function hasBooleanInType(node) {
67
+ const type = getTsTypeForCheck(node);
68
+ if (_.isNil(type))
69
+ return false;
70
+ if (!type.isUnion()) {
71
+ const flags = type.getFlags();
72
+ if (isNullableFlag(flags))
73
+ return false;
74
+ return isBooleanLikeFlag(flags);
75
+ }
76
+ for (const t of type.types) {
77
+ const flags = t.getFlags();
78
+ if (isNullableFlag(flags))
79
+ continue;
80
+ if (isBooleanLikeFlag(flags))
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+ function isTargetLodashCall(node) {
86
+ if (node.callee.type !== AST_NODE_TYPES.MemberExpression)
87
+ return null;
88
+ const { callee } = node;
89
+ if (callee.object.type !== AST_NODE_TYPES.Identifier)
90
+ return null;
91
+ if (callee.object.name !== LODASH_IDENT)
92
+ return null;
93
+ if (callee.property.type !== AST_NODE_TYPES.Identifier)
94
+ return null;
95
+ const method = callee.property.name;
96
+ if (!DISALLOWED_METHODS.has(method))
97
+ return null;
98
+ return { method: method };
99
+ }
100
+ return {
101
+ CallExpression(node) {
102
+ const target = isTargetLodashCall(node);
103
+ if (_.isNil(target))
104
+ return;
105
+ const [arg] = node.arguments;
106
+ if (_.isNil(arg))
107
+ return;
108
+ if (arg.type === AST_NODE_TYPES.SpreadElement)
109
+ return;
110
+ const expr = arg;
111
+ if (!hasBooleanInType(expr))
112
+ return;
113
+ context.report({
114
+ node,
115
+ messageId: 'noBoolean',
116
+ data: { method: target.method },
117
+ });
118
+ },
119
+ };
120
+ },
121
+ });
122
+ export default noIsNilOrIsEmptyOnBoolean;
@@ -1 +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;;CA4X1B,CAAC;AAEH,eAAe,sBAAsB,CAAC"}
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;;CA0Z1B,CAAC;AAEH,eAAe,sBAAsB,CAAC"}
@@ -65,6 +65,27 @@ const preferExplicitNilCheck = createRule({
65
65
  function isBooleanLikeFlag(flags) {
66
66
  return (flags & ts.TypeFlags.BooleanLike) !== 0;
67
67
  }
68
+ function isAnyOrUnknownFlag(flags) {
69
+ return (flags & ts.TypeFlags.Any) !== 0 || (flags & ts.TypeFlags.Unknown) !== 0;
70
+ }
71
+ /**
72
+ * Skip when the type is `any` or `unknown` (including union constituents),
73
+ * because we can't safely decide whether to prefer isNil/isEmpty without
74
+ * risking semantic changes.
75
+ */
76
+ function isAnyOrUnknownByTS(node) {
77
+ const type = getTsType(node);
78
+ if (_.isNil(type))
79
+ return false;
80
+ if (!type.isUnion()) {
81
+ return isAnyOrUnknownFlag(type.getFlags());
82
+ }
83
+ for (const t of type.types) {
84
+ if (isAnyOrUnknownFlag(t.getFlags()))
85
+ return true;
86
+ }
87
+ return false;
88
+ }
68
89
  /**
69
90
  * Returns true iff the expression type is effectively:
70
91
  * string | null | undefined
@@ -176,6 +197,8 @@ const preferExplicitNilCheck = createRule({
176
197
  });
177
198
  }
178
199
  function transformTruthy(node) {
200
+ if (isAnyOrUnknownByTS(node))
201
+ return;
179
202
  if (isNumberByTS(node))
180
203
  return;
181
204
  const text = context.sourceCode.getText(node);
@@ -187,6 +210,8 @@ const preferExplicitNilCheck = createRule({
187
210
  }
188
211
  function transformFalsyUnary(node) {
189
212
  const arg = node.argument;
213
+ if (isAnyOrUnknownByTS(arg))
214
+ return;
190
215
  if (isNumberByTS(arg))
191
216
  return;
192
217
  const text = context.sourceCode.getText(arg);
@@ -277,6 +302,8 @@ const preferExplicitNilCheck = createRule({
277
302
  return;
278
303
  }
279
304
  if (isImplicitOperand(arg)) {
305
+ if (isAnyOrUnknownByTS(arg))
306
+ return;
280
307
  if (isBooleanByTS(arg))
281
308
  return;
282
309
  if (isNumberByTS(arg))
@@ -293,6 +320,8 @@ const preferExplicitNilCheck = createRule({
293
320
  }
294
321
  default: {
295
322
  if (mode === 'test' && isImplicitOperand(node)) {
323
+ if (isAnyOrUnknownByTS(node))
324
+ return;
296
325
  if (isBooleanByTS(node))
297
326
  return;
298
327
  if (isNumberByTS(node))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-th-rules",
3
- "version": "3.3.2",
3
+ "version": "3.4.0",
4
4
  "description": "A List of custom ESLint rules created by Tomer Horowitz",
5
5
  "keywords": [
6
6
  "eslint",