this-in-template.js 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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', 'vue2-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. messages: {
  23. unexpected: "Unexpected usage of 'this'.",
  24. expected: "Expected 'this'."
  25. }
  26. },
  27. /**
  28. * Creates AST event handlers for this-in-template.
  29. *
  30. * @param {RuleContext} context - The rule context.
  31. * @returns {Object} AST event handlers.
  32. */
  33. create(context) {
  34. const options = context.options[0] === 'always' ? 'always' : 'never'
  35. /**
  36. * @typedef {object} ScopeStack
  37. * @property {ScopeStack | null} parent
  38. * @property {Identifier[]} nodes
  39. */
  40. /** @type {ScopeStack | null} */
  41. let scopeStack = null
  42. return utils.defineTemplateBodyVisitor(context, {
  43. /** @param {VElement} node */
  44. VElement(node) {
  45. scopeStack = {
  46. parent: scopeStack,
  47. nodes: scopeStack
  48. ? [...scopeStack.nodes] // make copy
  49. : []
  50. }
  51. if (node.variables) {
  52. for (const variable of node.variables) {
  53. const varNode = variable.id
  54. const name = varNode.name
  55. if (!scopeStack.nodes.some((node) => node.name === name)) {
  56. // Prevent adding duplicates
  57. scopeStack.nodes.push(varNode)
  58. }
  59. }
  60. }
  61. },
  62. 'VElement:exit'() {
  63. scopeStack = scopeStack && scopeStack.parent
  64. },
  65. ...(options === 'never'
  66. ? {
  67. /** @param { ThisExpression & { parent: MemberExpression } } node */
  68. 'VExpressionContainer MemberExpression > ThisExpression'(node) {
  69. if (!scopeStack) {
  70. return
  71. }
  72. const propertyName = utils.getStaticPropertyName(node.parent)
  73. if (
  74. !propertyName ||
  75. scopeStack.nodes.some((el) => el.name === propertyName) ||
  76. RESERVED_NAMES.has(propertyName) || // this.class | this['class']
  77. /^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
  78. ) {
  79. return
  80. }
  81. context.report({
  82. node,
  83. loc: node.loc,
  84. fix(fixer) {
  85. // node.parent should be some code like `this.test`, `this?.['result']`
  86. return fixer.replaceText(node.parent, propertyName)
  87. },
  88. messageId: 'unexpected'
  89. })
  90. }
  91. }
  92. : {
  93. /** @param {VExpressionContainer} node */
  94. VExpressionContainer(node) {
  95. if (!scopeStack) {
  96. return
  97. }
  98. if (node.parent.type === 'VDirectiveKey') {
  99. // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
  100. // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
  101. return
  102. }
  103. if (node.references) {
  104. for (const reference of node.references) {
  105. if (
  106. !scopeStack.nodes.some(
  107. (el) => el.name === reference.id.name
  108. )
  109. ) {
  110. context.report({
  111. node: reference.id,
  112. loc: reference.id.loc,
  113. messageId: 'expected',
  114. fix(fixer) {
  115. return fixer.insertTextBefore(reference.id, 'this.')
  116. }
  117. })
  118. }
  119. }
  120. }
  121. }
  122. })
  123. })
  124. }
  125. }