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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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', '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 tokenStore =
  82. context.parserServices.getTemplateBodyTokenStore &&
  83. context.parserServices.getTemplateBodyTokenStore()
  84. /**
  85. * Determines whether the two given nodes are considered to be equal. In particular, given that the nodes
  86. * represent expressions in a boolean context, `||` and `&&` can be considered as commutative operators.
  87. * @param {VExpression} a First node.
  88. * @param {VExpression} b Second node.
  89. * @returns {boolean} `true` if the nodes are considered to be equal.
  90. */
  91. function equal(a, b) {
  92. if (a.type !== b.type) {
  93. return false
  94. }
  95. if (
  96. a.type === 'LogicalExpression' &&
  97. b.type === 'LogicalExpression' &&
  98. (a.operator === '||' || a.operator === '&&') &&
  99. a.operator === b.operator
  100. ) {
  101. return (
  102. (equal(a.left, b.left) && equal(a.right, b.right)) ||
  103. (equal(a.left, b.right) && equal(a.right, b.left))
  104. )
  105. }
  106. return utils.equalTokens(a, b, tokenStore)
  107. }
  108. /**
  109. * Determines whether the first given AndOperands is a subset of the second given AndOperands.
  110. *
  111. * e.g. A: (a && b), B: (a && b && c): B is a subset of A.
  112. *
  113. * @param {AndOperands} operandsA The AndOperands to compare from.
  114. * @param {AndOperands} operandsB The AndOperands to compare against.
  115. * @returns {boolean} `true` if the `andOperandsA` is a subset of the `andOperandsB`.
  116. */
  117. function isSubset(operandsA, operandsB) {
  118. return operandsA.operands.every((operandA) =>
  119. operandsB.operands.some((operandB) => equal(operandA, operandB))
  120. )
  121. }
  122. return utils.defineTemplateBodyVisitor(context, {
  123. "VAttribute[directive=true][key.name.name='else-if']"(node) {
  124. if (!node.value || !node.value.expression) {
  125. return
  126. }
  127. const test = node.value.expression
  128. const conditionsToCheck =
  129. test.type === 'LogicalExpression' && test.operator === '&&'
  130. ? [...splitByAnd(test), test]
  131. : [test]
  132. const listToCheck = conditionsToCheck.map(buildOrOperands)
  133. /** @type {VElement | null} */
  134. let current = node.parent.parent
  135. while (current && (current = utils.prevSibling(current))) {
  136. const vIf = utils.getDirective(current, 'if')
  137. const currentTestDir = vIf || utils.getDirective(current, 'else-if')
  138. if (!currentTestDir) {
  139. return
  140. }
  141. if (currentTestDir.value && currentTestDir.value.expression) {
  142. const currentOrOperands = buildOrOperands(
  143. currentTestDir.value.expression
  144. )
  145. for (const condition of listToCheck) {
  146. const operands = (condition.operands = condition.operands.filter(
  147. (orOperand) =>
  148. !currentOrOperands.operands.some((currentOrOperand) =>
  149. isSubset(currentOrOperand, orOperand)
  150. )
  151. ))
  152. if (operands.length === 0) {
  153. context.report({
  154. node: condition.node,
  155. messageId: 'unexpected'
  156. })
  157. return
  158. }
  159. }
  160. }
  161. if (vIf) {
  162. return
  163. }
  164. }
  165. }
  166. })
  167. }
  168. }