valid-v-bind-sync.js 4.9 KB

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