valid-v-model.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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. const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
  15. /**
  16. * Check whether the given node is valid or not.
  17. * @param {ASTNode} node The element node to check.
  18. * @returns {boolean} `true` if the node is valid.
  19. */
  20. function isValidElement (node) {
  21. const name = node.name
  22. return (
  23. name === 'input' ||
  24. name === 'select' ||
  25. name === 'textarea' ||
  26. (
  27. name !== 'keep-alive' &&
  28. name !== 'slot' &&
  29. name !== 'transition' &&
  30. name !== 'transition-group' &&
  31. utils.isCustomComponent(node)
  32. )
  33. )
  34. }
  35. /**
  36. * Check whether the given node can be LHS.
  37. * @param {ASTNode} node The node to check.
  38. * @returns {boolean} `true` if the node can be LHS.
  39. */
  40. function isLhs (node) {
  41. return node != null && (
  42. node.type === 'Identifier' ||
  43. node.type === 'MemberExpression'
  44. )
  45. }
  46. /**
  47. * Get the variable by names.
  48. * @param {string} name The variable name to find.
  49. * @param {ASTNode} leafNode The node to look up.
  50. * @returns {Variable|null} The found variable or null.
  51. */
  52. function getVariable (name, leafNode) {
  53. let node = leafNode
  54. while (node != null) {
  55. const variables = node.variables
  56. const variable = variables && variables.find(v => v.id.name === name)
  57. if (variable != null) {
  58. return variable
  59. }
  60. node = node.parent
  61. }
  62. return null
  63. }
  64. // ------------------------------------------------------------------------------
  65. // Rule Definition
  66. // ------------------------------------------------------------------------------
  67. module.exports = {
  68. meta: {
  69. type: 'problem',
  70. docs: {
  71. description: 'enforce valid `v-model` directives',
  72. category: 'essential',
  73. url: 'https://eslint.vuejs.org/rules/valid-v-model.html'
  74. },
  75. fixable: null,
  76. schema: []
  77. },
  78. create (context) {
  79. return utils.defineTemplateBodyVisitor(context, {
  80. "VAttribute[directive=true][key.name='model']" (node) {
  81. const element = node.parent.parent
  82. const name = element.name
  83. if (!isValidElement(element)) {
  84. context.report({
  85. node,
  86. loc: node.loc,
  87. message: "'v-model' directives aren't supported on <{{name}}> elements.",
  88. data: { name }
  89. })
  90. }
  91. if (name === 'input' && utils.hasAttribute(element, 'type', 'file')) {
  92. context.report({
  93. node,
  94. loc: node.loc,
  95. message: "'v-model' directives don't support 'file' input type."
  96. })
  97. }
  98. if (node.key.argument) {
  99. context.report({
  100. node,
  101. loc: node.loc,
  102. message: "'v-model' directives require no argument."
  103. })
  104. }
  105. for (const modifier of node.key.modifiers) {
  106. if (!VALID_MODIFIERS.has(modifier)) {
  107. context.report({
  108. node,
  109. loc: node.loc,
  110. message: "'v-model' directives don't support the modifier '{{name}}'.",
  111. data: { name: modifier }
  112. })
  113. }
  114. }
  115. if (!utils.hasAttributeValue(node)) {
  116. context.report({
  117. node,
  118. loc: node.loc,
  119. message: "'v-model' directives require that attribute value."
  120. })
  121. }
  122. if (node.value) {
  123. if (!isLhs(node.value.expression)) {
  124. context.report({
  125. node,
  126. loc: node.loc,
  127. message: "'v-model' directives require the attribute value which is valid as LHS."
  128. })
  129. }
  130. for (const reference of node.value.references) {
  131. const id = reference.id
  132. if (id.parent.type !== 'VExpressionContainer') {
  133. continue
  134. }
  135. const variable = getVariable(id.name, element)
  136. if (variable != null) {
  137. context.report({
  138. node,
  139. loc: node.loc,
  140. message: "'v-model' directives cannot update the iteration variable '{{varName}}' itself.",
  141. data: { varName: id.name }
  142. })
  143. }
  144. }
  145. }
  146. }
  147. })
  148. }
  149. }