v-slot-style.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * @author Toru Nagashima
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const { pascalCase } = require('../utils/casing')
  7. const utils = require('../utils')
  8. /**
  9. * @typedef {Object} Options
  10. * @property {"shorthand" | "longform" | "v-slot"} atComponent The style for the default slot at a custom component directly.
  11. * @property {"shorthand" | "longform" | "v-slot"} default The style for the default slot at a template wrapper.
  12. * @property {"shorthand" | "longform"} named The style for named slots at a template wrapper.
  13. */
  14. /**
  15. * Normalize options.
  16. * @param {any} options The raw options to normalize.
  17. * @returns {Options} The normalized options.
  18. */
  19. function normalizeOptions (options) {
  20. const normalized = {
  21. atComponent: 'v-slot',
  22. default: 'shorthand',
  23. named: 'shorthand'
  24. }
  25. if (typeof options === 'string') {
  26. normalized.atComponent = normalized.default = normalized.named = options
  27. } else if (options != null) {
  28. for (const key of ['atComponent', 'default', 'named']) {
  29. if (options[key] != null) {
  30. normalized[key] = options[key]
  31. }
  32. }
  33. }
  34. return normalized
  35. }
  36. /**
  37. * Get the expected style.
  38. * @param {Options} options The options that defined expected types.
  39. * @param {VAttribute} node The `v-slot` node to check.
  40. * @returns {"shorthand" | "longform" | "v-slot"} The expected style.
  41. */
  42. function getExpectedStyle (options, node) {
  43. const { argument } = node.key
  44. if (argument == null || (argument.type === 'VIdentifier' && argument.name === 'default')) {
  45. const element = node.parent.parent
  46. return element.name === 'template' ? options.default : options.atComponent
  47. }
  48. return options.named
  49. }
  50. /**
  51. * Get the expected style.
  52. * @param {VAttribute} node The `v-slot` node to check.
  53. * @returns {"shorthand" | "longform" | "v-slot"} The expected style.
  54. */
  55. function getActualStyle (node) {
  56. const { name, argument } = node.key
  57. if (name.rawName === '#') {
  58. return 'shorthand'
  59. }
  60. if (argument != null) {
  61. return 'longform'
  62. }
  63. return 'v-slot'
  64. }
  65. module.exports = {
  66. meta: {
  67. type: 'suggestion',
  68. docs: {
  69. description: 'enforce `v-slot` directive style',
  70. category: undefined, // strongly-recommended
  71. // TODO Change with major version.
  72. // category: 'strongly-recommended',
  73. url: 'https://eslint.vuejs.org/rules/v-slot-style.html'
  74. },
  75. fixable: 'code',
  76. schema: [
  77. {
  78. anyOf: [
  79. { enum: ['shorthand', 'longform'] },
  80. {
  81. type: 'object',
  82. properties: {
  83. atComponent: { enum: ['shorthand', 'longform', 'v-slot'] },
  84. default: { enum: ['shorthand', 'longform', 'v-slot'] },
  85. named: { enum: ['shorthand', 'longform'] }
  86. },
  87. additionalProperties: false
  88. }
  89. ]
  90. }
  91. ],
  92. messages: {
  93. expectedShorthand: "Expected '#{{argument}}' instead of '{{actual}}'.",
  94. expectedLongform: "Expected 'v-slot:{{argument}}' instead of '{{actual}}'.",
  95. expectedVSlot: "Expected 'v-slot' instead of '{{actual}}'."
  96. }
  97. },
  98. create (context) {
  99. const sourceCode = context.getSourceCode()
  100. const options = normalizeOptions(context.options[0])
  101. return utils.defineTemplateBodyVisitor(context, {
  102. "VAttribute[directive=true][key.name.name='slot']" (node) {
  103. const expected = getExpectedStyle(options, node)
  104. const actual = getActualStyle(node)
  105. if (actual === expected) {
  106. return
  107. }
  108. const { name, argument } = node.key
  109. const range = [name.range[0], (argument || name).range[1]]
  110. const argumentText = argument ? sourceCode.getText(argument) : 'default'
  111. context.report({
  112. node,
  113. messageId: `expected${pascalCase(expected)}`,
  114. data: {
  115. actual: sourceCode.text.slice(range[0], range[1]),
  116. argument: argumentText
  117. },
  118. fix (fixer) {
  119. switch (expected) {
  120. case 'shorthand':
  121. return fixer.replaceTextRange(range, `#${argumentText}`)
  122. case 'longform':
  123. return fixer.replaceTextRange(range, `v-slot:${argumentText}`)
  124. case 'v-slot':
  125. return fixer.replaceTextRange(range, 'v-slot')
  126. default:
  127. return null
  128. }
  129. }
  130. })
  131. }
  132. })
  133. }
  134. }