explicit-length-check.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. 'use strict';
  2. const {isParenthesized, getStaticValue} = require('eslint-utils');
  3. const {checkVueTemplate} = require('./utils/rule.js');
  4. const isLiteralValue = require('./utils/is-literal-value.js');
  5. const isLogicalExpression = require('./utils/is-logical-expression.js');
  6. const {isBooleanNode, getBooleanAncestor} = require('./utils/boolean.js');
  7. const {memberExpressionSelector} = require('./selectors/index.js');
  8. const {fixSpaceAroundKeyword} = require('./fix/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. && isLiteralValue(node.right, value);
  21. const isCompareLeft = (node, operator, value) =>
  22. node.type === 'BinaryExpression'
  23. && node.operator === operator
  24. && isLiteralValue(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. data: problem.data,
  124. fix,
  125. },
  126. ];
  127. }
  128. return problem;
  129. }
  130. return {
  131. [lengthSelector](lengthNode) {
  132. if (lengthNode.object.type === 'ThisExpression') {
  133. return;
  134. }
  135. const staticValue = getStaticValue(lengthNode, context.getScope());
  136. if (staticValue && (!Number.isInteger(staticValue.value) || staticValue.value < 0)) {
  137. // Ignore known, non-positive-integer length properties.
  138. return;
  139. }
  140. let node;
  141. let autoFix = true;
  142. let {isZeroLengthCheck, node: lengthCheckNode} = getLengthCheckNode(lengthNode);
  143. if (lengthCheckNode) {
  144. const {isNegative, node: ancestor} = getBooleanAncestor(lengthCheckNode);
  145. node = ancestor;
  146. if (isNegative) {
  147. isZeroLengthCheck = !isZeroLengthCheck;
  148. }
  149. } else {
  150. const {isNegative, node: ancestor} = getBooleanAncestor(lengthNode);
  151. if (isBooleanNode(ancestor)) {
  152. isZeroLengthCheck = isNegative;
  153. node = ancestor;
  154. } else if (isLogicalExpression(lengthNode.parent)) {
  155. isZeroLengthCheck = isNegative;
  156. node = lengthNode;
  157. autoFix = false;
  158. }
  159. }
  160. if (node) {
  161. return getProblem({node, isZeroLengthCheck, lengthNode, autoFix});
  162. }
  163. },
  164. };
  165. }
  166. const schema = [
  167. {
  168. type: 'object',
  169. additionalProperties: false,
  170. properties: {
  171. 'non-zero': {
  172. enum: [...nonZeroStyles.keys()],
  173. default: 'greater-than',
  174. },
  175. },
  176. },
  177. ];
  178. /** @type {import('eslint').Rule.RuleModule} */
  179. module.exports = {
  180. create: checkVueTemplate(create),
  181. meta: {
  182. type: 'problem',
  183. docs: {
  184. description: 'Enforce explicitly comparing the `length` or `size` property of a value.',
  185. },
  186. fixable: 'code',
  187. schema,
  188. messages,
  189. hasSuggestions: true,
  190. },
  191. };