no-lone-template.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * @author Yosuke Ota
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. // ------------------------------------------------------------------------------
  11. // Helpers
  12. // ------------------------------------------------------------------------------
  13. // https://github.com/vuejs/vue-next/blob/64e2f4643602c5980361e66674141e61ba60ef70/packages/compiler-core/src/parse.ts#L405
  14. const SPECIAL_TEMPLATE_DIRECTIVES = new Set([
  15. 'if',
  16. 'else',
  17. 'else-if',
  18. 'for',
  19. 'slot'
  20. ])
  21. // ------------------------------------------------------------------------------
  22. // Rule Definition
  23. // ------------------------------------------------------------------------------
  24. module.exports = {
  25. meta: {
  26. type: 'problem',
  27. docs: {
  28. description: 'disallow unnecessary `<template>`',
  29. categories: ['vue3-recommended', 'recommended'],
  30. url: 'https://eslint.vuejs.org/rules/no-lone-template.html'
  31. },
  32. fixable: null,
  33. schema: [
  34. {
  35. type: 'object',
  36. properties: {
  37. ignoreAccessible: {
  38. type: 'boolean'
  39. }
  40. },
  41. additionalProperties: false
  42. }
  43. ],
  44. messages: {
  45. requireDirective: '`<template>` require directive.'
  46. }
  47. },
  48. /** @param {RuleContext} context */
  49. create(context) {
  50. const options = context.options[0] || {}
  51. const ignoreAccessible = options.ignoreAccessible === true
  52. /**
  53. * @param {VAttribute | VDirective} attr
  54. */
  55. function getKeyName(attr) {
  56. if (attr.directive) {
  57. if (attr.key.name.name !== 'bind') {
  58. // no v-bind
  59. return null
  60. }
  61. if (
  62. !attr.key.argument ||
  63. attr.key.argument.type === 'VExpressionContainer'
  64. ) {
  65. // unknown
  66. return null
  67. }
  68. return attr.key.argument.name
  69. }
  70. return attr.key.name
  71. }
  72. return utils.defineTemplateBodyVisitor(context, {
  73. /** @param {VStartTag} node */
  74. "VElement[name='template'][parent.type='VElement'] > VStartTag"(node) {
  75. if (
  76. node.attributes.some((attr) => {
  77. if (attr.directive) {
  78. const directiveName = attr.key.name.name
  79. if (SPECIAL_TEMPLATE_DIRECTIVES.has(directiveName)) {
  80. return true
  81. }
  82. if (directiveName === 'slot-scope') {
  83. // `slot-scope` is deprecated in Vue.js 2.6
  84. return true
  85. }
  86. if (directiveName === 'scope') {
  87. // `scope` is deprecated in Vue.js 2.5
  88. return true
  89. }
  90. }
  91. const keyName = getKeyName(attr)
  92. if (keyName === 'slot') {
  93. // `slot` is deprecated in Vue.js 2.6
  94. return true
  95. }
  96. return false
  97. })
  98. ) {
  99. return
  100. }
  101. if (
  102. ignoreAccessible &&
  103. node.attributes.some((attr) => {
  104. const keyName = getKeyName(attr)
  105. return keyName === 'id' || keyName === 'ref'
  106. })
  107. ) {
  108. return
  109. }
  110. context.report({
  111. node,
  112. messageId: 'requireDirective'
  113. })
  114. }
  115. })
  116. }
  117. }