no-dupe-v-else-if.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * @typedef {NonNullable<VExpressionContainer['expression']>} VExpression
  9. */
  10. /**
  11. * @typedef {object} OrOperands
  12. * @property {VExpression} OrOperands.node
  13. * @property {AndOperands[]} OrOperands.operands
  14. *
  15. * @typedef {object} AndOperands
  16. * @property {VExpression} AndOperands.node
  17. * @property {VExpression[]} AndOperands.operands
  18. */
  19. /**
  20. * Splits the given node by the given logical operator.
  21. * @param {string} operator Logical operator `||` or `&&`.
  22. * @param {VExpression} node The node to split.
  23. * @returns {VExpression[]} Array of conditions that makes the node when joined by the operator.
  24. */
  25. function splitByLogicalOperator(operator, node) {
  26. if (node.type === 'LogicalExpression' && node.operator === operator) {
  27. return [
  28. ...splitByLogicalOperator(operator, node.left),
  29. ...splitByLogicalOperator(operator, node.right)
  30. ]
  31. }
  32. return [node]
  33. }
  34. /**
  35. * @param {VExpression} node
  36. */
  37. function splitByOr(node) {
  38. return splitByLogicalOperator('||', node)
  39. }
  40. /**
  41. * @param {VExpression} node
  42. */
  43. function splitByAnd(node) {
  44. return splitByLogicalOperator('&&', node)
  45. }
  46. /**
  47. * @param {VExpression} node
  48. * @returns {OrOperands}
  49. */
  50. function buildOrOperands(node) {
  51. const orOperands = splitByOr(node)
  52. return {
  53. node,
  54. operands: orOperands.map((orOperand) => {
  55. const andOperands = splitByAnd(orOperand)
  56. return {
  57. node: orOperand,
  58. operands: andOperands
  59. }
  60. })
  61. }
  62. }
  63. module.exports = {
  64. meta: {
  65. type: 'problem',
  66. docs: {
  67. description:
  68. 'disallow duplicate conditions in `v-if` / `v-else-if` chains',
  69. categories: ['vue3-essential', 'vue2-essential'],
  70. url: 'https://eslint.vuejs.org/rules/no-dupe-v-else-if.html'
  71. },
  72. fixable: null,
  73. schema: [],
  74. messages: {
  75. unexpected:
  76. 'This branch can never execute. Its condition is a duplicate or covered by previous conditions in the `v-if` / `v-else-if` chain.'
  77. }
  78. },
  79. /** @param {RuleContext} context */
  80. create(context) {
  81. const sourceCode = context.getSourceCode()
  82. const tokenStore =
  83. sourceCode.parserServices.getTemplateBodyTokenStore &&
  84. sourceCode.parserServices.getTemplateBodyTokenStore()
  85. /**
  86. * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
  87. * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
  88. * @param {VExpression} a First node.
  89. * @param {VExpression} b Second node.
  90. * @returns {boolean} `true` if the nodes are considered to be equal.
  91. */
  92. function equal(a, b) {
  93. if (a.type !== b.type) {
  94. return false
  95. }
  96. if (
  97. a.type === 'LogicalExpression' &&
  98. b.type === 'LogicalExpression' &&
  99. (a.operator === '||' || a.operator === '&&') &&
  100. a.operator === b.operator
  101. ) {
  102. return (
  103. (equal(a.left, b.left) && equal(a.right, b.right)) ||
  104. (equal(a.left, b.right) && equal(a.right, b.left))
  105. )
  106. }
  107. return utils.equalTokens(a, b, tokenStore)
  108. }
  109. /**
  110. * Determines whether the first given AndOperands is a subset of the second given AndOperands.
  111. *
  112. * e.g. A: (a && b), B: (a && b && c): B is a subset of A.
  113. *
  114. * @param {AndOperands} operandsA The AndOperands to compare from.
  115. * @param {AndOperands} operandsB The AndOperands to compare against.
  116. * @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
  117. */
  118. function isSubset(operandsA, operandsB) {
  119. return operandsA.operands.every((operandA) =>
  120. operandsB.operands.some((operandB) => equal(operandA, operandB))
  121. )
  122. }
  123. return utils.defineTemplateBodyVisitor(context, {
  124. "VAttribute[directive=true][key.name.name='else-if']"(node) {
  125. if (!node.value || !node.value.expression) {
  126. return
  127. }
  128. const test = node.value.expression
  129. const conditionsToCheck =
  130. test.type === 'LogicalExpression' && test.operator === '&&'
  131. ? [...splitByAnd(test), test]
  132. : [test]
  133. const listToCheck = conditionsToCheck.map(buildOrOperands)
  134. /** @type {VElement | null} */
  135. let current = node.parent.parent
  136. while (current && (current = utils.prevSibling(current))) {
  137. const vIf = utils.getDirective(current, 'if')
  138. const currentTestDir = vIf || utils.getDirective(current, 'else-if')
  139. if (!currentTestDir) {
  140. return
  141. }
  142. if (currentTestDir.value && currentTestDir.value.expression) {
  143. const currentOrOperands = buildOrOperands(
  144. currentTestDir.value.expression
  145. )
  146. for (const condition of listToCheck) {
  147. const operands = (condition.operands = condition.operands.filter(
  148. (orOperand) =>
  149. !currentOrOperands.operands.some((currentOrOperand) =>
  150. isSubset(currentOrOperand, orOperand)
  151. )
  152. ))
  153. if (operands.length === 0) {
  154. context.report({
  155. node: condition.node,
  156. messageId: 'unexpected'
  157. })
  158. return
  159. }
  160. }
  161. }
  162. if (vIf) {
  163. return
  164. }
  165. }
  166. }
  167. })
  168. }
  169. }