this-in-template.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. /**
  2. * @fileoverview disallow usage of `this` in template.
  3. * @author Armano
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))
  8. module.exports = {
  9. meta: {
  10. type: 'suggestion',
  11. docs: {
  12. description: 'disallow usage of `this` in template',
  13. categories: ['vue3-recommended', 'recommended'],
  14. url: 'https://eslint.vuejs.org/rules/this-in-template.html'
  15. },
  16. fixable: 'code',
  17. schema: [
  18. {
  19. enum: ['always', 'never']
  20. }
  21. ]
  22. },
  23. /**
  24. * Creates AST event handlers for this-in-template.
  25. *
  26. * @param {RuleContext} context - The rule context.
  27. * @returns {Object} AST event handlers.
  28. */
  29. create(context) {
  30. const options = context.options[0] !== 'always' ? 'never' : 'always'
  31. /**
  32. * @typedef {object} ScopeStack
  33. * @property {ScopeStack | null} parent
  34. * @property {Identifier[]} nodes
  35. */
  36. /** @type {ScopeStack | null} */
  37. let scopeStack = null
  38. return utils.defineTemplateBodyVisitor(context, {
  39. /** @param {VElement} node */
  40. VElement(node) {
  41. scopeStack = {
  42. parent: scopeStack,
  43. nodes: scopeStack
  44. ? [...scopeStack.nodes] // make copy
  45. : []
  46. }
  47. if (node.variables) {
  48. for (const variable of node.variables) {
  49. const varNode = variable.id
  50. const name = varNode.name
  51. if (!scopeStack.nodes.some((node) => node.name === name)) {
  52. // Prevent adding duplicates
  53. scopeStack.nodes.push(varNode)
  54. }
  55. }
  56. }
  57. },
  58. 'VElement:exit'() {
  59. scopeStack = scopeStack && scopeStack.parent
  60. },
  61. ...(options === 'never'
  62. ? {
  63. /** @param { ThisExpression & { parent: MemberExpression } } node */
  64. 'VExpressionContainer MemberExpression > ThisExpression'(node) {
  65. if (!scopeStack) {
  66. return
  67. }
  68. const propertyName = utils.getStaticPropertyName(node.parent)
  69. if (
  70. !propertyName ||
  71. scopeStack.nodes.some((el) => el.name === propertyName) ||
  72. RESERVED_NAMES.has(propertyName) || // this.class | this['class']
  73. /^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
  74. ) {
  75. return
  76. }
  77. context.report({
  78. node,
  79. loc: node.loc,
  80. fix(fixer) {
  81. // node.parent should be some code like `this.test`, `this?.['result']`
  82. return fixer.replaceText(node.parent, propertyName)
  83. },
  84. message: "Unexpected usage of 'this'."
  85. })
  86. }
  87. }
  88. : {
  89. /** @param {VExpressionContainer} node */
  90. VExpressionContainer(node) {
  91. if (!scopeStack) {
  92. return
  93. }
  94. if (node.parent.type === 'VDirectiveKey') {
  95. // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
  96. // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
  97. return
  98. }
  99. if (node.references) {
  100. for (const reference of node.references) {
  101. if (
  102. !scopeStack.nodes.some(
  103. (el) => el.name === reference.id.name
  104. )
  105. ) {
  106. context.report({
  107. node: reference.id,
  108. loc: reference.id.loc,
  109. message: "Expected 'this'.",
  110. fix(fixer) {
  111. return fixer.insertTextBefore(reference.id, 'this.')
  112. }
  113. })
  114. }
  115. }
  116. }
  117. }
  118. })
  119. })
  120. }
  121. }