valid-v-model.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. const VALID_MODIFIERS = new Set(['lazy', 'number', 'trim'])
  9. /**
  10. * Check whether the given node is valid or not.
  11. * @param {VElement} node The element node to check.
  12. * @returns {boolean} `true` if the node is valid.
  13. */
  14. function isValidElement(node) {
  15. const name = node.name
  16. return (
  17. name === 'input' ||
  18. name === 'select' ||
  19. name === 'textarea' ||
  20. (name !== 'keep-alive' &&
  21. name !== 'slot' &&
  22. name !== 'transition' &&
  23. name !== 'transition-group' &&
  24. utils.isCustomComponent(node))
  25. )
  26. }
  27. /**
  28. * Check whether the given node is a MemberExpression containing an optional chaining.
  29. * e.g.
  30. * - `a?.b`
  31. * - `a?.b.c`
  32. * @param {ASTNode} node The node to check.
  33. * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
  34. */
  35. function isOptionalChainingMemberExpression(node) {
  36. return (
  37. node.type === 'ChainExpression' &&
  38. node.expression.type === 'MemberExpression'
  39. )
  40. }
  41. /**
  42. * Check whether the given node can be LHS (left-hand side).
  43. * @param {ASTNode} node The node to check.
  44. * @returns {boolean} `true` if the node can be LHS.
  45. */
  46. function isLhs(node) {
  47. return node.type === 'Identifier' || node.type === 'MemberExpression'
  48. }
  49. /**
  50. * Check whether the given node is a MemberExpression of a possibly null object.
  51. * e.g.
  52. * - `(a?.b).c`
  53. * - `(null).foo`
  54. * @param {ASTNode} node The node to check.
  55. * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
  56. */
  57. function maybeNullObjectMemberExpression(node) {
  58. if (node.type !== 'MemberExpression') {
  59. return false
  60. }
  61. const { object } = node
  62. if (object.type === 'ChainExpression') {
  63. // `(a?.b).c`
  64. return true
  65. }
  66. if (object.type === 'Literal' && object.value === null && !object.bigint) {
  67. // `(null).foo`
  68. return true
  69. }
  70. if (object.type === 'MemberExpression') {
  71. return maybeNullObjectMemberExpression(object)
  72. }
  73. return false
  74. }
  75. /**
  76. * Get the variable by names.
  77. * @param {string} name The variable name to find.
  78. * @param {VElement} leafNode The node to look up.
  79. * @returns {VVariable|null} The found variable or null.
  80. */
  81. function getVariable(name, leafNode) {
  82. let node = leafNode
  83. while (node != null) {
  84. const variables = node.variables
  85. const variable = variables && variables.find((v) => v.id.name === name)
  86. if (variable != null) {
  87. return variable
  88. }
  89. if (node.parent.type === 'VDocumentFragment') {
  90. break
  91. }
  92. node = node.parent
  93. }
  94. return null
  95. }
  96. /** @type {RuleModule} */
  97. module.exports = {
  98. meta: {
  99. type: 'problem',
  100. docs: {
  101. description: 'enforce valid `v-model` directives',
  102. categories: ['vue3-essential', 'essential'],
  103. url: 'https://eslint.vuejs.org/rules/valid-v-model.html'
  104. },
  105. fixable: null,
  106. schema: [],
  107. messages: {
  108. unexpectedInvalidElement:
  109. "'v-model' directives aren't supported on <{{name}}> elements.",
  110. unexpectedInputFile:
  111. "'v-model' directives don't support 'file' input type.",
  112. unexpectedArgument: "'v-model' directives require no argument.",
  113. unexpectedModifier:
  114. "'v-model' directives don't support the modifier '{{name}}'.",
  115. missingValue: "'v-model' directives require that attribute value.",
  116. unexpectedOptionalChaining:
  117. "Optional chaining cannot appear in 'v-model' directives.",
  118. unexpectedNonLhsExpression:
  119. "'v-model' directives require the attribute value which is valid as LHS.",
  120. unexpectedNullObject:
  121. "'v-model' directive has potential null object property access.",
  122. unexpectedUpdateIterationVariable:
  123. "'v-model' directives cannot update the iteration variable '{{varName}}' itself."
  124. }
  125. },
  126. /** @param {RuleContext} context */
  127. create(context) {
  128. return utils.defineTemplateBodyVisitor(context, {
  129. /** @param {VDirective} node */
  130. "VAttribute[directive=true][key.name.name='model']"(node) {
  131. const element = node.parent.parent
  132. const name = element.name
  133. if (!isValidElement(element)) {
  134. context.report({
  135. node,
  136. messageId: 'unexpectedInvalidElement',
  137. data: { name }
  138. })
  139. }
  140. if (name === 'input' && utils.hasAttribute(element, 'type', 'file')) {
  141. context.report({
  142. node,
  143. messageId: 'unexpectedInputFile'
  144. })
  145. }
  146. if (!utils.isCustomComponent(element)) {
  147. if (node.key.argument) {
  148. context.report({
  149. node: node.key.argument,
  150. messageId: 'unexpectedArgument'
  151. })
  152. }
  153. for (const modifier of node.key.modifiers) {
  154. if (!VALID_MODIFIERS.has(modifier.name)) {
  155. context.report({
  156. node: modifier,
  157. messageId: 'unexpectedModifier',
  158. data: { name: modifier.name }
  159. })
  160. }
  161. }
  162. }
  163. if (!node.value || utils.isEmptyValueDirective(node, context)) {
  164. context.report({
  165. node,
  166. messageId: 'missingValue'
  167. })
  168. return
  169. }
  170. const expression = node.value.expression
  171. if (!expression) {
  172. // Parsing error
  173. return
  174. }
  175. if (isOptionalChainingMemberExpression(expression)) {
  176. context.report({
  177. node: expression,
  178. messageId: 'unexpectedOptionalChaining'
  179. })
  180. } else if (!isLhs(expression)) {
  181. context.report({
  182. node: expression,
  183. messageId: 'unexpectedNonLhsExpression'
  184. })
  185. } else if (maybeNullObjectMemberExpression(expression)) {
  186. context.report({
  187. node: expression,
  188. messageId: 'unexpectedNullObject'
  189. })
  190. }
  191. for (const reference of node.value.references) {
  192. const id = reference.id
  193. if (id.parent.type !== 'VExpressionContainer') {
  194. continue
  195. }
  196. const variable = getVariable(id.name, element)
  197. if (variable != null) {
  198. context.report({
  199. node: expression,
  200. messageId: 'unexpectedUpdateIterationVariable',
  201. data: { varName: id.name }
  202. })
  203. }
  204. }
  205. }
  206. })
  207. }
  208. }