valid-v-model.js 6.9 KB

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