eslint-plugin-unicorn 63.0.0 → 65.0.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.
Files changed (146) hide show
  1. package/index.js +26 -18
  2. package/package.json +32 -30
  3. package/readme.md +183 -147
  4. package/rules/ast/call-or-new-expression.js +3 -5
  5. package/rules/ast/literal.js +1 -6
  6. package/rules/better-dom-traversing.js +291 -0
  7. package/rules/catch-error-name.js +3 -3
  8. package/rules/consistent-compound-words.js +452 -0
  9. package/rules/consistent-destructuring.js +234 -22
  10. package/rules/consistent-function-scoping.js +35 -11
  11. package/rules/consistent-json-file-read.js +245 -0
  12. package/rules/consistent-template-literal-escape.js +52 -0
  13. package/rules/custom-error-definition.js +176 -71
  14. package/rules/dom-node-dataset.js +431 -0
  15. package/rules/escape-case.js +2 -1
  16. package/rules/expiring-todo-comments.js +56 -102
  17. package/rules/explicit-length-check.js +145 -33
  18. package/rules/filename-case.js +97 -13
  19. package/rules/fix/append-argument.js +1 -1
  20. package/rules/fix/remove-expression-statement.js +1 -0
  21. package/rules/import-style.js +30 -10
  22. package/rules/index.js +37 -3
  23. package/rules/isolated-functions.js +11 -5
  24. package/rules/no-accessor-recursion.js +1 -2
  25. package/rules/no-anonymous-default-export.js +9 -11
  26. package/rules/no-array-callback-reference.js +148 -22
  27. package/rules/no-array-fill-with-reference-type.js +127 -0
  28. package/rules/no-array-for-each.js +18 -3
  29. package/rules/no-array-from-fill.js +76 -0
  30. package/rules/no-array-method-this-argument.js +1 -1
  31. package/rules/no-array-reduce.js +509 -2
  32. package/rules/no-blob-to-file.js +299 -0
  33. package/rules/no-canvas-to-image.js +152 -0
  34. package/rules/no-confusing-array-splice.js +164 -0
  35. package/rules/no-duplicate-set-values.js +57 -0
  36. package/rules/no-empty-file.js +31 -2
  37. package/rules/no-exports-in-scripts.js +34 -0
  38. package/rules/no-for-loop.js +1 -5
  39. package/rules/no-hex-escape.js +1 -1
  40. package/rules/no-incorrect-query-selector.js +347 -0
  41. package/rules/no-instanceof-builtins.js +4 -6
  42. package/rules/no-invalid-fetch-options.js +1 -1
  43. package/rules/no-invalid-file-input-accept.js +475 -0
  44. package/rules/no-keyword-prefix.js +23 -5
  45. package/rules/no-late-current-target-access.js +166 -0
  46. package/rules/no-lonely-if.js +152 -52
  47. package/rules/no-manually-wrapped-comments.js +151 -0
  48. package/rules/no-null.js +16 -5
  49. package/rules/no-process-exit.js +1 -1
  50. package/rules/no-this-outside-of-class.js +75 -0
  51. package/rules/no-typeof-undefined.js +7 -4
  52. package/rules/no-unnecessary-nested-ternary.js +132 -0
  53. package/rules/no-unnecessary-polyfills.js +253 -24
  54. package/rules/no-unreadable-iife.js +33 -3
  55. package/rules/no-unused-array-method-return.js +301 -0
  56. package/rules/no-unused-properties.js +13 -19
  57. package/rules/no-useless-iterator-to-array.js +249 -0
  58. package/rules/no-useless-length-check.js +5 -11
  59. package/rules/no-useless-promise-resolve-reject.js +5 -5
  60. package/rules/no-useless-spread.js +53 -4
  61. package/rules/no-useless-undefined.js +62 -46
  62. package/rules/numeric-separators-style.js +17 -6
  63. package/rules/prefer-add-event-listener.js +1 -0
  64. package/rules/prefer-array-find.js +1 -0
  65. package/rules/prefer-array-flat.js +76 -8
  66. package/rules/prefer-array-last-methods.js +93 -0
  67. package/rules/prefer-array-some.js +74 -8
  68. package/rules/prefer-at.js +118 -57
  69. package/rules/prefer-class-fields.js +2 -4
  70. package/rules/prefer-default-parameters.js +1 -2
  71. package/rules/prefer-dom-node-append.js +2 -2
  72. package/rules/prefer-dom-node-remove.js +24 -18
  73. package/rules/prefer-export-from.js +8 -7
  74. package/rules/prefer-get-or-insert-computed.js +159 -0
  75. package/rules/prefer-global-this.js +33 -29
  76. package/rules/prefer-https.js +83 -0
  77. package/rules/prefer-includes-over-repeated-comparisons.js +199 -0
  78. package/rules/prefer-iterator-concat.js +174 -0
  79. package/rules/prefer-iterator-to-array-at-end.js +123 -0
  80. package/rules/prefer-keyboard-event-key.js +202 -76
  81. package/rules/prefer-logical-operator-over-ternary.js +1 -1
  82. package/rules/prefer-math-abs.js +397 -0
  83. package/rules/prefer-math-min-max.js +54 -26
  84. package/rules/prefer-module.js +5 -7
  85. package/rules/prefer-native-coercion-functions.js +8 -1
  86. package/rules/prefer-node-protocol.js +7 -4
  87. package/rules/prefer-number-properties.js +2 -0
  88. package/rules/prefer-object-from-entries.js +12 -7
  89. package/rules/prefer-prototype-methods.js +1 -3
  90. package/rules/prefer-query-selector.js +119 -7
  91. package/rules/prefer-queue-microtask.js +204 -0
  92. package/rules/prefer-regexp-test.js +166 -20
  93. package/rules/prefer-set-has.js +474 -47
  94. package/rules/prefer-simple-condition-first.js +282 -0
  95. package/rules/prefer-single-call.js +129 -38
  96. package/rules/prefer-split-limit.js +107 -0
  97. package/rules/prefer-spread.js +150 -19
  98. package/rules/prefer-string-match-all.js +352 -0
  99. package/rules/prefer-string-pad-start-end.js +380 -0
  100. package/rules/prefer-string-raw.js +90 -19
  101. package/rules/prefer-string-repeat.js +173 -0
  102. package/rules/prefer-string-replace-all.js +180 -12
  103. package/rules/prefer-string-slice.js +55 -4
  104. package/rules/prefer-string-starts-ends-with.js +2 -2
  105. package/rules/prefer-structured-clone.js +1 -0
  106. package/rules/prefer-switch.js +2 -0
  107. package/rules/prefer-ternary.js +3 -2
  108. package/rules/prefer-top-level-await.js +168 -6
  109. package/rules/prevent-abbreviations.js +144 -158
  110. package/rules/relative-url-style.js +1 -0
  111. package/rules/require-css-escape.js +141 -0
  112. package/rules/require-module-attributes.js +16 -18
  113. package/rules/require-module-specifiers.js +62 -0
  114. package/rules/require-passive-events.js +292 -0
  115. package/rules/rule/to-eslint-rule.js +1 -1
  116. package/rules/rule/to-eslint-rules.js +4 -6
  117. package/rules/shared/abbreviations.js +1 -0
  118. package/rules/shared/dom-events.js +7 -7
  119. package/rules/shared/identifier-checks.js +205 -0
  120. package/rules/shared/package-json.js +1 -1
  121. package/rules/string-content.js +35 -5
  122. package/rules/switch-case-braces.js +2 -3
  123. package/rules/switch-case-break-position.js +138 -0
  124. package/rules/template-indent.js +59 -3
  125. package/rules/text-encoding-identifier-case.js +23 -5
  126. package/rules/throw-new-error.js +4 -0
  127. package/rules/try-complexity.js +184 -0
  128. package/rules/utils/assert-token.js +3 -5
  129. package/rules/utils/boolean.js +37 -28
  130. package/rules/utils/create-deprecated-rules.js +25 -27
  131. package/rules/utils/get-call-expression-arguments-text.js +3 -5
  132. package/rules/utils/get-duplicate-array-elements.js +77 -0
  133. package/rules/utils/index.js +4 -1
  134. package/rules/utils/is-node-contains-lexical-this.js +65 -0
  135. package/rules/utils/is-number.js +2 -2
  136. package/rules/utils/is-same-reference.js +25 -17
  137. package/rules/utils/should-add-parentheses-to-await-expression-argument.js +9 -9
  138. package/rules/utils/should-add-parentheses-to-call-expression-callee.js +12 -10
  139. package/rules/utils/should-add-parentheses-to-conditional-expression-child.js +9 -5
  140. package/rules/utils/should-add-parentheses-to-expression-statement-expression.js +1 -1
  141. package/rules/utils/should-add-parentheses-to-logical-expression-child.js +12 -8
  142. package/rules/utils/should-add-parentheses-to-member-expression-object.js +1 -1
  143. package/rules/utils/should-add-parentheses-to-unary-expression.js +10 -10
  144. package/rules/better-regex.js +0 -145
  145. package/rules/prefer-dom-node-dataset.js +0 -135
  146. package/rules/prefer-json-parse-buffer.js +0 -161
@@ -256,20 +256,22 @@ function tryToCoerceVersion(rawVersion) {
256
256
  }
257
257
  }
258
258
 
259
- function semverComparisonForOperator(operator) {
260
- return {
261
- '>': semver.gt,
262
- '>=': semver.gte,
263
- }[operator];
259
+ function satisfiesRange(version, condition, range) {
260
+ return semver.satisfies(version, `${condition}${range}`, {includePrerelease: true});
264
261
  }
265
262
 
266
263
  const DEFAULT_OPTIONS = {
267
264
  terms: ['todo', 'fixme', 'xxx'],
268
265
  ignore: [],
269
- ignoreDatesOnPullRequests: true,
266
+ checkDates: false,
267
+ checkDatesOnPullRequests: false,
270
268
  allowWarningComments: true,
271
269
  };
272
270
 
271
+ function getAllComments(sourceCode) {
272
+ return sourceCode.getAllComments?.() ?? sourceCode.comments ?? [];
273
+ }
274
+
273
275
  /** @param {import('eslint').Rule.RuleContext} context */
274
276
  const create = context => {
275
277
  const options = {
@@ -277,15 +279,13 @@ const create = context => {
277
279
  ...context.options[0],
278
280
  };
279
281
 
280
- const ignoreRegexes = options.ignore.map(
281
- pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'),
282
- );
282
+ const ignoreRegexes = options.ignore.map(pattern => isRegExp(pattern) ? pattern : new RegExp(pattern, 'u'));
283
283
 
284
284
  const dirname = path.dirname(context.filename);
285
285
  const {packageJson, packageDependencies, parseArgument, parseTodoMessage, parseTodoWithArguments} = getPackageHelpers(dirname);
286
286
 
287
287
  const {sourceCode} = context;
288
- const comments = sourceCode.getAllComments();
288
+ const comments = getAllComments(sourceCode);
289
289
  const unusedComments = comments
290
290
  .filter(comment => comment.type !== 'Shebang' && !isEslintDisableOrEnableDirective(context, comment))
291
291
  // Block comments come as one.
@@ -299,8 +299,7 @@ const create = context => {
299
299
  comment.value.split('\n').map(line => ({
300
300
  ...comment,
301
301
  value: line,
302
- })),
303
- ).filter(comment => processComment(comment));
302
+ }))).filter(comment => processComment(comment));
304
303
 
305
304
  // This is highly dependable on ESLint's `no-warning-comments` implementation.
306
305
  // What we do is patch the parts we know the rule will use, `getAllComments`.
@@ -342,63 +341,48 @@ const create = context => {
342
341
  engines = [],
343
342
  unknowns = [],
344
343
  } = parsed;
344
+ const todoMessage = parseTodoMessage(comment.value);
345
345
 
346
- if (dates.length > 1) {
347
- uses++;
346
+ function report(messageId, data = {}) {
348
347
  context.report({
349
- loc: sourceCode.getLoc(comment),
350
- messageId: MESSAGE_ID_AVOID_MULTIPLE_DATES,
348
+ node: comment,
349
+ messageId,
351
350
  data: {
352
- expirationDates: dates.join(', '),
353
- message: parseTodoMessage(comment.value),
351
+ ...data,
352
+ message: todoMessage,
354
353
  },
355
354
  });
355
+ }
356
+
357
+ if (dates.length > 1) {
358
+ uses++;
359
+ report(MESSAGE_ID_AVOID_MULTIPLE_DATES, {
360
+ expirationDates: dates.join(', '),
361
+ });
356
362
  } else if (dates.length === 1) {
357
363
  uses++;
358
364
  const [expirationDate] = dates;
359
365
 
360
- const shouldIgnore = options.ignoreDatesOnPullRequests && ci.isPR;
361
- if (!shouldIgnore && reachedDate(expirationDate, options.date)) {
362
- context.report({
363
- loc: sourceCode.getLoc(comment),
364
- messageId: MESSAGE_ID_EXPIRED_TODO,
365
- data: {
366
- expirationDate,
367
- message: parseTodoMessage(comment.value),
368
- },
369
- });
366
+ const shouldCheckDate = options.checkDates && (options.checkDatesOnPullRequests || !ci.isPR);
367
+ if (shouldCheckDate && reachedDate(expirationDate, options.date)) {
368
+ report(MESSAGE_ID_EXPIRED_TODO, {expirationDate});
370
369
  }
371
370
  }
372
371
 
373
372
  if (packageVersions.length > 1) {
374
373
  uses++;
375
- context.report({
376
- loc: sourceCode.getLoc(comment),
377
- messageId: MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS,
378
- data: {
379
- versions: packageVersions
380
- .map(({condition, version}) => `${condition}${version}`)
381
- .join(', '),
382
- message: parseTodoMessage(comment.value),
383
- },
374
+ report(MESSAGE_ID_AVOID_MULTIPLE_PACKAGE_VERSIONS, {
375
+ versions: packageVersions
376
+ .map(({condition, version}) => `${condition}${version}`)
377
+ .join(', '),
384
378
  });
385
379
  } else if (packageVersions.length === 1) {
386
380
  uses++;
387
381
  const [{condition, version}] = packageVersions;
388
382
 
389
383
  const packageVersion = tryToCoerceVersion(packageJson.version);
390
- const decidedPackageVersion = tryToCoerceVersion(version);
391
-
392
- const compare = semverComparisonForOperator(condition);
393
- if (packageVersion && compare(packageVersion, decidedPackageVersion)) {
394
- context.report({
395
- loc: sourceCode.getLoc(comment),
396
- messageId: MESSAGE_ID_REACHED_PACKAGE_VERSION,
397
- data: {
398
- comparison: `${condition}${version}`,
399
- message: parseTodoMessage(comment.value),
400
- },
401
- });
384
+ if (packageVersion && satisfiesRange(packageVersion, condition, version)) {
385
+ report(MESSAGE_ID_REACHED_PACKAGE_VERSION, {comparison: `${condition}${version}`});
402
386
  }
403
387
  }
404
388
 
@@ -417,20 +401,12 @@ const create = context => {
417
401
  : [!hasTargetPackage, MESSAGE_ID_DONT_HAVE_PACKAGE];
418
402
 
419
403
  if (trigger) {
420
- context.report({
421
- loc: sourceCode.getLoc(comment),
422
- messageId,
423
- data: {
424
- package: dependency.name,
425
- message: parseTodoMessage(comment.value),
426
- },
427
- });
404
+ report(messageId, {package: dependency.name});
428
405
  }
429
406
 
430
407
  continue;
431
408
  }
432
409
 
433
- const todoVersion = tryToCoerceVersion(dependency.version);
434
410
  const targetPackageVersion = tryToCoerceVersion(targetPackageRawVersion);
435
411
 
436
412
  /* c8 ignore start */
@@ -440,17 +416,8 @@ const create = context => {
440
416
  }
441
417
  /* c8 ignore end */
442
418
 
443
- const compare = semverComparisonForOperator(dependency.condition);
444
-
445
- if (compare(targetPackageVersion, todoVersion)) {
446
- context.report({
447
- loc: sourceCode.getLoc(comment),
448
- messageId: MESSAGE_ID_VERSION_MATCHES,
449
- data: {
450
- comparison: `${dependency.name} ${dependency.condition} ${dependency.version}`,
451
- message: parseTodoMessage(comment.value),
452
- },
453
- });
419
+ if (satisfiesRange(targetPackageVersion, dependency.condition, dependency.version)) {
420
+ report(MESSAGE_ID_VERSION_MATCHES, {comparison: `${dependency.name} ${dependency.condition} ${dependency.version}`});
454
421
  }
455
422
  }
456
423
 
@@ -467,22 +434,10 @@ const create = context => {
467
434
  continue;
468
435
  }
469
436
 
470
- const todoEngine = tryToCoerceVersion(engine.version);
471
- const targetPackageEngineVersion = tryToCoerceVersion(
472
- targetPackageRawEngineVersion,
473
- );
474
-
475
- const compare = semverComparisonForOperator(engine.condition);
437
+ const targetPackageEngineVersion = tryToCoerceVersion(targetPackageRawEngineVersion);
476
438
 
477
- if (compare(targetPackageEngineVersion, todoEngine)) {
478
- context.report({
479
- loc: sourceCode.getLoc(comment),
480
- messageId: MESSAGE_ID_ENGINE_MATCHES,
481
- data: {
482
- comparison: `node${engine.condition}${engine.version}`,
483
- message: parseTodoMessage(comment.value),
484
- },
485
- });
439
+ if (targetPackageEngineVersion && satisfiesRange(targetPackageEngineVersion, engine.condition, engine.version)) {
440
+ report(MESSAGE_ID_ENGINE_MATCHES, {comparison: `node${engine.condition}${engine.version}`});
486
441
  }
487
442
  }
488
443
 
@@ -499,14 +454,9 @@ const create = context => {
499
454
 
500
455
  if (parseArgument(testString).type !== 'unknowns') {
501
456
  uses++;
502
- context.report({
503
- loc: sourceCode.getLoc(comment),
504
- messageId: MESSAGE_ID_MISSING_AT_SYMBOL,
505
- data: {
506
- original: unknown,
507
- fix: testString,
508
- message: parseTodoMessage(comment.value),
509
- },
457
+ report(MESSAGE_ID_MISSING_AT_SYMBOL, {
458
+ original: unknown,
459
+ fix: testString,
510
460
  });
511
461
  continue;
512
462
  }
@@ -516,14 +466,9 @@ const create = context => {
516
466
 
517
467
  if (parseArgument(withoutWhitespace).type !== 'unknowns') {
518
468
  uses++;
519
- context.report({
520
- loc: sourceCode.getLoc(comment),
521
- messageId: MESSAGE_ID_REMOVE_WHITESPACE,
522
- data: {
523
- original: unknown,
524
- fix: withoutWhitespace,
525
- message: parseTodoMessage(comment.value),
526
- },
469
+ report(MESSAGE_ID_REMOVE_WHITESPACE, {
470
+ original: unknown,
471
+ fix: withoutWhitespace,
527
472
  });
528
473
  continue;
529
474
  }
@@ -532,7 +477,7 @@ const create = context => {
532
477
  return uses === 0;
533
478
  }
534
479
 
535
- context.on('Program', () => {
480
+ context.on(['Program', 'StyleSheet'], () => {
536
481
  rules.Program(); // eslint-disable-line new-cap
537
482
  });
538
483
  };
@@ -547,20 +492,29 @@ const schema = [
547
492
  items: {
548
493
  type: 'string',
549
494
  },
495
+ description: 'Comment terms to check.',
550
496
  },
551
497
  ignore: {
552
498
  type: 'array',
553
499
  uniqueItems: true,
500
+ description: 'Patterns to ignore.',
501
+ },
502
+ checkDates: {
503
+ type: 'boolean',
504
+ description: 'Whether to check expiration dates.',
554
505
  },
555
- ignoreDatesOnPullRequests: {
506
+ checkDatesOnPullRequests: {
556
507
  type: 'boolean',
508
+ description: 'Whether to check expiration dates on pull requests.',
557
509
  },
558
510
  allowWarningComments: {
559
511
  type: 'boolean',
512
+ description: 'Whether to allow warning comments.',
560
513
  },
561
514
  date: {
562
515
  type: 'string',
563
516
  format: 'date',
517
+ description: 'The reference date.',
564
518
  },
565
519
  },
566
520
  },
@@ -3,11 +3,13 @@ import {
3
3
  isParenthesized,
4
4
  checkVueTemplate,
5
5
  isLogicalExpression,
6
- isBooleanNode,
6
+ isBooleanExpression,
7
+ isControlFlowTest,
7
8
  getBooleanAncestor,
9
+ isSameReference,
8
10
  } from './utils/index.js';
9
11
  import {fixSpaceAroundKeyword} from './fix/index.js';
10
- import {isLiteral, isMemberExpression, isNumericLiteral} from './ast/index.js';
12
+ import {isLiteral, isMemberExpression} from './ast/index.js';
11
13
 
12
14
  const TYPE_NON_ZERO = 'non-zero';
13
15
  const TYPE_ZERO = 'zero';
@@ -47,8 +49,107 @@ const zeroStyle = {
47
49
  test: node => isCompareRight(node, '===', 0),
48
50
  };
49
51
 
50
- function getLengthCheckNode(node) {
52
+ const shapeProperties = new Set(['depth', 'height', 'width']);
53
+
54
+ function isLengthOrSizeMemberExpression(node) {
55
+ return isMemberExpression(node, {
56
+ properties: ['length', 'size'],
57
+ optional: false,
58
+ });
59
+ }
60
+
61
+ function isTypeScriptExpression(node) {
62
+ return node?.type === 'TSAsExpression'
63
+ || node?.type === 'TSSatisfiesExpression'
64
+ || node?.type === 'TSTypeAssertion'
65
+ || node?.type === 'TSNonNullExpression';
66
+ }
67
+
68
+ function unwrapTypeScriptExpression(node) {
69
+ if (isTypeScriptExpression(node)) {
70
+ return unwrapTypeScriptExpression(node.expression);
71
+ }
72
+
73
+ return node;
74
+ }
75
+
76
+ function getLengthCheckParent(node, allowTypeScriptExpression) {
51
77
  node = node.parent;
78
+ if (allowTypeScriptExpression) {
79
+ while (isTypeScriptExpression(node)) {
80
+ node = node.parent;
81
+ }
82
+ }
83
+
84
+ return node;
85
+ }
86
+
87
+ function getLogicalExpressionRoot(node) {
88
+ while (
89
+ isLogicalExpression(node.parent)
90
+ && node.parent.operator === '&&'
91
+ ) {
92
+ node = node.parent;
93
+ }
94
+
95
+ return node;
96
+ }
97
+
98
+ function getLogicalExpressionOperands(node) {
99
+ return [node.left, node.right].flatMap(child =>
100
+ child.type === 'LogicalExpression' && child.operator === node.operator
101
+ ? getLogicalExpressionOperands(child)
102
+ : [child]);
103
+ }
104
+
105
+ function hasSameObjectShapePropertyCheck({node, lengthNode}) {
106
+ const root = getLogicalExpressionRoot(node);
107
+ if (
108
+ root.type !== 'LogicalExpression'
109
+ || root.operator !== '&&'
110
+ ) {
111
+ return false;
112
+ }
113
+
114
+ return getLogicalExpressionOperands(root).some(operand =>
115
+ operand !== node
116
+ && isMemberExpression(operand, {computed: false, optional: false})
117
+ && operand.property.type === 'Identifier'
118
+ && shapeProperties.has(operand.property.name)
119
+ && isSameReference(operand.object, lengthNode.object));
120
+ }
121
+
122
+ function getLengthCheckMemberExpression(node) {
123
+ if (node.type === 'UnaryExpression' && node.operator === '!') {
124
+ return getLengthCheckMemberExpression(node.argument);
125
+ }
126
+
127
+ if (
128
+ node.type === 'CallExpression'
129
+ && node.callee.type === 'Identifier'
130
+ && node.callee.name === 'Boolean'
131
+ && node.arguments.length === 1
132
+ ) {
133
+ return getLengthCheckMemberExpression(node.arguments[0]);
134
+ }
135
+
136
+ if (node.type !== 'BinaryExpression') {
137
+ return;
138
+ }
139
+
140
+ const left = unwrapTypeScriptExpression(node.left);
141
+ if (isLengthOrSizeMemberExpression(left)) {
142
+ return left;
143
+ }
144
+
145
+ const right = unwrapTypeScriptExpression(node.right);
146
+ if (isLengthOrSizeMemberExpression(right)) {
147
+ return right;
148
+ }
149
+ }
150
+
151
+ function getLengthCheckNode(node, {allowTypeScriptExpression = false} = {}) {
152
+ node = getLengthCheckParent(node, allowTypeScriptExpression);
52
153
 
53
154
  // Zero length check
54
155
  if (
@@ -80,7 +181,7 @@ function getLengthCheckNode(node) {
80
181
  || isCompareRight(node, '>=', 1)
81
182
  // `0 !== foo.length`
82
183
  || isCompareLeft(node, '!==', 0)
83
- // `0 !== foo.length`
184
+ // `0 != foo.length`
84
185
  || isCompareLeft(node, '!=', 0)
85
186
  // `0 < foo.length`
86
187
  || isCompareLeft(node, '<', 0)
@@ -93,26 +194,33 @@ function getLengthCheckNode(node) {
93
194
  return {};
94
195
  }
95
196
 
96
- function isNodeValueNumber(node, context) {
97
- if (isNumericLiteral(node)) {
98
- return true;
197
+ function isSameLengthNonZeroCheck(node, lengthNode) {
198
+ const comparisonLengthNode = getLengthCheckMemberExpression(node);
199
+ if (!comparisonLengthNode || !isSameReference(comparisonLengthNode, lengthNode)) {
200
+ return false;
99
201
  }
100
202
 
101
- const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
102
- return staticValue && typeof staticValue.value === 'number';
103
- }
104
-
105
- function isNodeValueString(node, context) {
106
- if (node.type === 'Literal' && typeof node.value === 'string') {
107
- return true;
203
+ const {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(comparisonLengthNode, {allowTypeScriptExpression: true});
204
+ if (!lengthCheckNode) {
205
+ return false;
108
206
  }
109
207
 
110
- if (node.type === 'TemplateLiteral') {
111
- return true;
208
+ const {isNegative, node: ancestor} = getBooleanAncestor(lengthCheckNode);
209
+ return ancestor === node && isNegative === isZeroLengthCheck;
210
+ }
211
+
212
+ function isLengthGuardedByNonZeroCheck(lengthNode) {
213
+ const root = getLogicalExpressionRoot(lengthNode);
214
+ if (
215
+ root.type !== 'LogicalExpression'
216
+ || root.operator !== '&&'
217
+ ) {
218
+ return false;
112
219
  }
113
220
 
114
- const staticValue = getStaticValue(node, context.sourceCode.getScope(node));
115
- return staticValue && typeof staticValue.value === 'string';
221
+ return getLogicalExpressionOperands(root).some(operand =>
222
+ operand !== lengthNode
223
+ && isSameLengthNonZeroCheck(operand, lengthNode));
116
224
  }
117
225
 
118
226
  function create(context) {
@@ -120,7 +228,7 @@ function create(context) {
120
228
  const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
121
229
  const {sourceCode} = context;
122
230
 
123
- function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) {
231
+ function getProblem({node, isZeroLengthCheck, lengthNode, autoFix, shouldSuggest = true}) {
124
232
  const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
125
233
  if (test(node)) {
126
234
  return;
@@ -148,7 +256,7 @@ function create(context) {
148
256
 
149
257
  if (autoFix) {
150
258
  problem.fix = fix;
151
- } else {
259
+ } else if (shouldSuggest) {
152
260
  problem.suggest = [
153
261
  {
154
262
  messageId: MESSAGE_ID_SUGGESTION,
@@ -162,10 +270,7 @@ function create(context) {
162
270
 
163
271
  context.on('MemberExpression', memberExpression => {
164
272
  if (
165
- !isMemberExpression(memberExpression, {
166
- properties: ['length', 'size'],
167
- optional: false,
168
- })
273
+ !isLengthOrSizeMemberExpression(memberExpression)
169
274
  || memberExpression.object.type === 'ThisExpression'
170
275
  ) {
171
276
  return;
@@ -189,16 +294,10 @@ function create(context) {
189
294
  }
190
295
  } else {
191
296
  const {isNegative, node: ancestor} = getBooleanAncestor(lengthNode);
192
- if (isBooleanNode(ancestor)) {
297
+ if (isBooleanExpression(ancestor) || isControlFlowTest(ancestor)) {
193
298
  isZeroLengthCheck = isNegative;
194
299
  node = ancestor;
195
- } else if (
196
- isLogicalExpression(lengthNode.parent)
197
- && !(
198
- lengthNode.parent.operator === '||'
199
- && (isNodeValueNumber(lengthNode.parent.right, context) || isNodeValueString(lengthNode.parent.right, context))
200
- )
201
- ) {
300
+ } else if (isLogicalExpression(lengthNode.parent) && lengthNode.parent.operator === '&&') {
202
301
  isZeroLengthCheck = isNegative;
203
302
  node = lengthNode;
204
303
  autoFix = false;
@@ -206,11 +305,24 @@ function create(context) {
206
305
  }
207
306
 
208
307
  if (node) {
308
+ if (
309
+ (node === lengthNode && isLengthGuardedByNonZeroCheck(lengthNode))
310
+ || hasSameObjectShapePropertyCheck({node, lengthNode})
311
+ ) {
312
+ return;
313
+ }
314
+
315
+ const isUnsafeNegationInBinaryExpression = node.type === 'UnaryExpression'
316
+ && node.operator === '!'
317
+ && node.parent.type === 'BinaryExpression'
318
+ && node.parent.left === node;
319
+
209
320
  return getProblem({
210
321
  node,
211
322
  isZeroLengthCheck,
212
323
  lengthNode,
213
- autoFix,
324
+ autoFix: autoFix && !isUnsafeNegationInBinaryExpression,
325
+ shouldSuggest: !isUnsafeNegationInBinaryExpression,
214
326
  });
215
327
  }
216
328
  });