valid-v-for.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /**
  2. * @author Toru Nagashima
  3. * @copyright 2017 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. const utils = require('../utils')
  8. /**
  9. * Check whether the given attribute is using the variables which are defined by `v-for` directives.
  10. * @param {VDirective} vFor The attribute node of `v-for` to check.
  11. * @param {VDirective} vBindKey The attribute node of `v-bind:key` to check.
  12. * @returns {boolean} `true` if the node is using the variables which are defined by `v-for` directives.
  13. */
  14. function isUsingIterationVar(vFor, vBindKey) {
  15. if (vBindKey.value == null) {
  16. return false
  17. }
  18. const references = vBindKey.value.references
  19. const variables = vFor.parent.parent.variables
  20. return references.some((reference) =>
  21. variables.some(
  22. (variable) =>
  23. variable.id.name === reference.id.name && variable.kind === 'v-for'
  24. )
  25. )
  26. }
  27. /**
  28. * Check the child element in tempalte v-for about `v-bind:key` attributes.
  29. * @param {RuleContext} context The rule context to report.
  30. * @param {VDirective} vFor The attribute node of `v-for` to check.
  31. * @param {VElement} child The child node to check.
  32. */
  33. function checkChildKey(context, vFor, child) {
  34. const childFor = utils.getDirective(child, 'for')
  35. // if child has v-for, check if parent iterator is used in v-for
  36. if (childFor != null) {
  37. const childForRefs = (childFor.value && childFor.value.references) || []
  38. const variables = vFor.parent.parent.variables
  39. const usedInFor = childForRefs.some((cref) =>
  40. variables.some(
  41. (variable) =>
  42. cref.id.name === variable.id.name && variable.kind === 'v-for'
  43. )
  44. )
  45. // if parent iterator is used, skip other checks
  46. // iterator usage will be checked later by child v-for
  47. if (usedInFor) {
  48. return
  49. }
  50. }
  51. // otherwise, check if parent iterator is directly used in child's key
  52. checkKey(context, vFor, child)
  53. }
  54. /**
  55. * Check the given element about `v-bind:key` attributes.
  56. * @param {RuleContext} context The rule context to report.
  57. * @param {VDirective} vFor The attribute node of `v-for` to check.
  58. * @param {VElement} element The element node to check.
  59. */
  60. function checkKey(context, vFor, element) {
  61. const vBindKey = utils.getDirective(element, 'bind', 'key')
  62. if (vBindKey == null && element.name === 'template') {
  63. for (const child of element.children) {
  64. if (child.type === 'VElement') {
  65. checkChildKey(context, vFor, child)
  66. }
  67. }
  68. return
  69. }
  70. if (utils.isCustomComponent(element) && vBindKey == null) {
  71. context.report({
  72. node: element.startTag,
  73. messageId: 'requireKey'
  74. })
  75. }
  76. if (vBindKey != null && !isUsingIterationVar(vFor, vBindKey)) {
  77. context.report({
  78. node: vBindKey,
  79. messageId: 'keyUseFVorVars'
  80. })
  81. }
  82. }
  83. module.exports = {
  84. meta: {
  85. type: 'problem',
  86. docs: {
  87. description: 'enforce valid `v-for` directives',
  88. categories: ['vue3-essential', 'vue2-essential'],
  89. url: 'https://eslint.vuejs.org/rules/valid-v-for.html'
  90. },
  91. fixable: null,
  92. schema: [],
  93. messages: {
  94. requireKey:
  95. "Custom elements in iteration require 'v-bind:key' directives.",
  96. keyUseFVorVars:
  97. "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive.",
  98. unexpectedArgument: "'v-for' directives require no argument.",
  99. unexpectedModifier: "'v-for' directives require no modifier.",
  100. expectedValue: "'v-for' directives require that attribute value.",
  101. unexpectedExpression:
  102. "'v-for' directives require the special syntax '<alias> in <expression>'.",
  103. invalidEmptyAlias: "Invalid alias ''.",
  104. invalidAlias: "Invalid alias '{{text}}'."
  105. }
  106. },
  107. /** @param {RuleContext} context */
  108. create(context) {
  109. const sourceCode = context.getSourceCode()
  110. return utils.defineTemplateBodyVisitor(context, {
  111. /** @param {VDirective} node */
  112. "VAttribute[directive=true][key.name.name='for']"(node) {
  113. const element = node.parent.parent
  114. checkKey(context, node, element)
  115. if (node.key.argument) {
  116. context.report({
  117. node: node.key.argument,
  118. messageId: 'unexpectedArgument'
  119. })
  120. }
  121. if (node.key.modifiers.length > 0) {
  122. context.report({
  123. node,
  124. loc: {
  125. start: node.key.modifiers[0].loc.start,
  126. end: node.key.modifiers[node.key.modifiers.length - 1].loc.end
  127. },
  128. messageId: 'unexpectedModifier'
  129. })
  130. }
  131. if (!node.value || utils.isEmptyValueDirective(node, context)) {
  132. context.report({
  133. node,
  134. messageId: 'expectedValue'
  135. })
  136. return
  137. }
  138. const expr = node.value.expression
  139. if (expr == null) {
  140. return
  141. }
  142. if (expr.type !== 'VForExpression') {
  143. context.report({
  144. node: node.value,
  145. messageId: 'unexpectedExpression'
  146. })
  147. return
  148. }
  149. const lhs = expr.left
  150. const value = lhs[0]
  151. const key = lhs[1]
  152. const index = lhs[2]
  153. if (value === null) {
  154. context.report({
  155. node: expr,
  156. messageId: 'invalidEmptyAlias'
  157. })
  158. }
  159. if (key !== undefined && (!key || key.type !== 'Identifier')) {
  160. context.report({
  161. node: key || expr,
  162. messageId: 'invalidAlias',
  163. data: { text: key ? sourceCode.getText(key) : '' }
  164. })
  165. }
  166. if (index !== undefined && (!index || index.type !== 'Identifier')) {
  167. context.report({
  168. node: index || expr,
  169. messageId: 'invalidAlias',
  170. data: { text: index ? sourceCode.getText(index) : '' }
  171. })
  172. }
  173. }
  174. })
  175. }
  176. }