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

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