valid-v-model.js 6.5 KB

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