valid-v-bind-sync.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. /**
  2. * @fileoverview enforce valid `.sync` modifier on `v-bind` directives
  3. * @author Yosuke Ota
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * Check whether the given node is valid or not.
  9. * @param {VElement} node The element node to check.
  10. * @returns {boolean} `true` if the node is valid.
  11. */
  12. function isValidElement(node) {
  13. if (!utils.isCustomComponent(node)) {
  14. // non Vue-component
  15. return false
  16. }
  17. return true
  18. }
  19. /**
  20. * Check whether the given node is a MemberExpression containing an optional chaining.
  21. * e.g.
  22. * - `a?.b`
  23. * - `a?.b.c`
  24. * @param {ASTNode} node The node to check.
  25. * @returns {boolean} `true` if the node is a MemberExpression containing an optional chaining.
  26. */
  27. function isOptionalChainingMemberExpression(node) {
  28. return (
  29. node.type === 'ChainExpression' &&
  30. node.expression.type === 'MemberExpression'
  31. )
  32. }
  33. /**
  34. * Check whether the given node can be LHS (left-hand side).
  35. * @param {ASTNode} node The node to check.
  36. * @returns {boolean} `true` if the node can be LHS.
  37. */
  38. function isLhs(node) {
  39. return node.type === 'Identifier' || node.type === 'MemberExpression'
  40. }
  41. /**
  42. * Check whether the given node is a MemberExpression of a possibly null object.
  43. * e.g.
  44. * - `(a?.b).c`
  45. * - `(null).foo`
  46. * @param {ASTNode} node The node to check.
  47. * @returns {boolean} `true` if the node is a MemberExpression of a possibly null object.
  48. */
  49. function maybeNullObjectMemberExpression(node) {
  50. if (node.type !== 'MemberExpression') {
  51. return false
  52. }
  53. const { object } = node
  54. if (object.type === 'ChainExpression') {
  55. // `(a?.b).c`
  56. return true
  57. }
  58. if (object.type === 'Literal' && object.value === null && !object.bigint) {
  59. // `(null).foo`
  60. return true
  61. }
  62. if (object.type === 'MemberExpression') {
  63. return maybeNullObjectMemberExpression(object)
  64. }
  65. return false
  66. }
  67. module.exports = {
  68. meta: {
  69. type: 'problem',
  70. docs: {
  71. description: 'enforce valid `.sync` modifier on `v-bind` directives',
  72. categories: ['vue2-essential'],
  73. url: 'https://eslint.vuejs.org/rules/valid-v-bind-sync.html'
  74. },
  75. fixable: null,
  76. schema: [],
  77. messages: {
  78. unexpectedInvalidElement:
  79. "'.sync' modifiers aren't supported on <{{name}}> non Vue-components.",
  80. unexpectedOptionalChaining:
  81. "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers.",
  82. unexpectedNonLhsExpression:
  83. "'.sync' modifiers require the attribute value which is valid as LHS.",
  84. unexpectedNullObject:
  85. "'.sync' modifier has potential null object property access.",
  86. unexpectedUpdateIterationVariable:
  87. "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself."
  88. }
  89. },
  90. /** @param {RuleContext} context */
  91. create(context) {
  92. return utils.defineTemplateBodyVisitor(context, {
  93. /** @param {VDirective} node */
  94. "VAttribute[directive=true][key.name.name='bind']"(node) {
  95. if (!node.key.modifiers.map((mod) => mod.name).includes('sync')) {
  96. return
  97. }
  98. const element = node.parent.parent
  99. const name = element.name
  100. if (!isValidElement(element)) {
  101. context.report({
  102. node,
  103. messageId: 'unexpectedInvalidElement',
  104. data: { name }
  105. })
  106. }
  107. if (!node.value) {
  108. return
  109. }
  110. const expression = node.value.expression
  111. if (!expression) {
  112. // Parsing error
  113. return
  114. }
  115. if (isOptionalChainingMemberExpression(expression)) {
  116. context.report({
  117. node: expression,
  118. messageId: 'unexpectedOptionalChaining'
  119. })
  120. } else if (!isLhs(expression)) {
  121. context.report({
  122. node: expression,
  123. messageId: 'unexpectedNonLhsExpression'
  124. })
  125. } else if (maybeNullObjectMemberExpression(expression)) {
  126. context.report({
  127. node: expression,
  128. messageId: 'unexpectedNullObject'
  129. })
  130. }
  131. for (const reference of node.value.references) {
  132. const id = reference.id
  133. if (id.parent.type !== 'VExpressionContainer') {
  134. continue
  135. }
  136. const variable = reference.variable
  137. if (variable) {
  138. context.report({
  139. node: expression,
  140. messageId: 'unexpectedUpdateIterationVariable',
  141. data: { varName: id.name }
  142. })
  143. }
  144. }
  145. }
  146. })
  147. }
  148. }