explicit-length-check.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict';
  2. const {isParenthesized, getStaticValue} = require('@eslint-community/eslint-utils');
  3. const {checkVueTemplate} = require('./utils/rule.js');
  4. const isLogicalExpression = require('./utils/is-logical-expression.js');
  5. const {isBooleanNode, getBooleanAncestor} = require('./utils/boolean.js');
  6. const {memberExpressionSelector} = require('./selectors/index.js');
  7. const {fixSpaceAroundKeyword} = require('./fix/index.js');
  8. const {isLiteral} = require('./ast/index.js');
  9. const TYPE_NON_ZERO = 'non-zero';
  10. const TYPE_ZERO = 'zero';
  11. const MESSAGE_ID_SUGGESTION = 'suggestion';
  12. const messages = {
  13. [TYPE_NON_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is not zero.',
  14. [TYPE_ZERO]: 'Use `.{{property}} {{code}}` when checking {{property}} is zero.',
  15. [MESSAGE_ID_SUGGESTION]: 'Replace `.{{property}}` with `.{{property}} {{code}}`.',
  16. };
  17. const isCompareRight = (node, operator, value) =>
  18. node.type === 'BinaryExpression'
  19. && node.operator === operator
  20. && isLiteral(node.right, value);
  21. const isCompareLeft = (node, operator, value) =>
  22. node.type === 'BinaryExpression'
  23. && node.operator === operator
  24. && isLiteral(node.left, value);
  25. const nonZeroStyles = new Map([
  26. [
  27. 'greater-than',
  28. {
  29. code: '> 0',
  30. test: node => isCompareRight(node, '>', 0),
  31. },
  32. ],
  33. [
  34. 'not-equal',
  35. {
  36. code: '!== 0',
  37. test: node => isCompareRight(node, '!==', 0),
  38. },
  39. ],
  40. ]);
  41. const zeroStyle = {
  42. code: '=== 0',
  43. test: node => isCompareRight(node, '===', 0),
  44. };
  45. const lengthSelector = memberExpressionSelector(['length', 'size']);
  46. function getLengthCheckNode(node) {
  47. node = node.parent;
  48. // Zero length check
  49. if (
  50. // `foo.length === 0`
  51. isCompareRight(node, '===', 0)
  52. // `foo.length == 0`
  53. || isCompareRight(node, '==', 0)
  54. // `foo.length < 1`
  55. || isCompareRight(node, '<', 1)
  56. // `0 === foo.length`
  57. || isCompareLeft(node, '===', 0)
  58. // `0 == foo.length`
  59. || isCompareLeft(node, '==', 0)
  60. // `1 > foo.length`
  61. || isCompareLeft(node, '>', 1)
  62. ) {
  63. return {isZeroLengthCheck: true, node};
  64. }
  65. // Non-Zero length check
  66. if (
  67. // `foo.length !== 0`
  68. isCompareRight(node, '!==', 0)
  69. // `foo.length != 0`
  70. || isCompareRight(node, '!=', 0)
  71. // `foo.length > 0`
  72. || isCompareRight(node, '>', 0)
  73. // `foo.length >= 1`
  74. || isCompareRight(node, '>=', 1)
  75. // `0 !== foo.length`
  76. || isCompareLeft(node, '!==', 0)
  77. // `0 !== foo.length`
  78. || isCompareLeft(node, '!=', 0)
  79. // `0 < foo.length`
  80. || isCompareLeft(node, '<', 0)
  81. // `1 <= foo.length`
  82. || isCompareLeft(node, '<=', 1)
  83. ) {
  84. return {isZeroLengthCheck: false, node};
  85. }
  86. return {};
  87. }
  88. function create(context) {
  89. const options = {
  90. 'non-zero': 'greater-than',
  91. ...context.options[0],
  92. };
  93. const nonZeroStyle = nonZeroStyles.get(options['non-zero']);
  94. const sourceCode = context.getSourceCode();
  95. function getProblem({node, isZeroLengthCheck, lengthNode, autoFix}) {
  96. const {code, test} = isZeroLengthCheck ? zeroStyle : nonZeroStyle;
  97. if (test(node)) {
  98. return;
  99. }
  100. let fixed = `${sourceCode.getText(lengthNode)} ${code}`;
  101. if (
  102. !isParenthesized(node, sourceCode)
  103. && node.type === 'UnaryExpression'
  104. && (node.parent.type === 'UnaryExpression' || node.parent.type === 'AwaitExpression')
  105. ) {
  106. fixed = `(${fixed})`;
  107. }
  108. const fix = function * (fixer) {
  109. yield fixer.replaceText(node, fixed);
  110. yield * fixSpaceAroundKeyword(fixer, node, sourceCode);
  111. };
  112. const problem = {
  113. node,
  114. messageId: isZeroLengthCheck ? TYPE_ZERO : TYPE_NON_ZERO,
  115. data: {code, property: lengthNode.property.name},
  116. };
  117. if (autoFix) {
  118. problem.fix = fix;
  119. } else {
  120. problem.suggest = [
  121. {
  122. messageId: MESSAGE_ID_SUGGESTION,
  123. fix,
  124. },
  125. ];
  126. }
  127. return problem;
  128. }
  129. return {
  130. [lengthSelector](lengthNode) {
  131. if (lengthNode.object.type === 'ThisExpression') {
  132. return;
  133. }
  134. const staticValue = getStaticValue(lengthNode, context.getScope());
  135. if (staticValue && (!Number.isInteger(staticValue.value) || staticValue.value < 0)) {
  136. // Ignore known, non-positive-integer length properties.
  137. return;
  138. }
  139. let node;
  140. let autoFix = true;
  141. let {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(lengthNode);
  142. if (lengthCheckNode) {
  143. const {isNegative, node: ancestor} = getBooleanAncestor(lengthCheckNode);
  144. node = ancestor;
  145. if (isNegative) {
  146. isZeroLengthCheck = !isZeroLengthCheck;
  147. }
  148. } else {
  149. const {isNegative, node: ancestor} = getBooleanAncestor(lengthNode);
  150. if (isBooleanNode(ancestor)) {
  151. isZeroLengthCheck = isNegative;
  152. node = ancestor;
  153. } else if (isLogicalExpression(lengthNode.parent)) {
  154. isZeroLengthCheck = isNegative;
  155. node = lengthNode;
  156. autoFix = false;
  157. }
  158. }
  159. if (node) {
  160. return getProblem({node, isZeroLengthCheck, lengthNode, autoFix});
  161. }
  162. },
  163. };
  164. }
  165. const schema = [
  166. {
  167. type: 'object',
  168. additionalProperties: false,
  169. properties: {
  170. 'non-zero': {
  171. enum: [...nonZeroStyles.keys()],
  172. default: 'greater-than',
  173. },
  174. },
  175. },
  176. ];
  177. /** @type {import('eslint').Rule.RuleModule} */
  178. module.exports = {
  179. create: checkVueTemplate(create),
  180. meta: {
  181. type: 'problem',
  182. docs: {
  183. description: 'Enforce explicitly comparing the `length` or `size` property of a value.',
  184. },
  185. fixable: 'code',
  186. schema,
  187. messages,
  188. hasSuggestions: true,
  189. },
  190. };