valid-v-for.js 6.3 KB

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