attribute-hyphenation.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. /**
  2. * @fileoverview Define a style for the props casing in templates.
  3. * @author Armano
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const casing = require('../utils/casing')
  8. const svgAttributes = require('../utils/svg-attributes-weird-case.json')
  9. /**
  10. * @param {VDirective | VAttribute} node
  11. * @returns {string | null}
  12. */
  13. function getAttributeName(node) {
  14. if (!node.directive) {
  15. return node.key.rawName
  16. }
  17. if (
  18. (node.key.name.name === 'bind' || node.key.name.name === 'model') &&
  19. node.key.argument &&
  20. node.key.argument.type === 'VIdentifier'
  21. ) {
  22. return node.key.argument.rawName
  23. }
  24. return null
  25. }
  26. module.exports = {
  27. meta: {
  28. type: 'suggestion',
  29. docs: {
  30. description:
  31. 'enforce attribute naming style on custom components in template',
  32. categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
  33. url: 'https://eslint.vuejs.org/rules/attribute-hyphenation.html'
  34. },
  35. fixable: 'code',
  36. schema: [
  37. {
  38. enum: ['always', 'never']
  39. },
  40. {
  41. type: 'object',
  42. properties: {
  43. ignore: {
  44. type: 'array',
  45. items: {
  46. allOf: [
  47. { type: 'string' },
  48. { not: { type: 'string', pattern: ':exit$' } },
  49. { not: { type: 'string', pattern: String.raw`^\s*$` } }
  50. ]
  51. },
  52. uniqueItems: true,
  53. additionalItems: false
  54. }
  55. },
  56. additionalProperties: false
  57. }
  58. ],
  59. messages: {
  60. mustBeHyphenated: "Attribute '{{text}}' must be hyphenated.",
  61. cannotBeHyphenated: "Attribute '{{text}}' can't be hyphenated."
  62. }
  63. },
  64. /** @param {RuleContext} context */
  65. create(context) {
  66. const sourceCode = context.getSourceCode()
  67. const option = context.options[0]
  68. const optionsPayload = context.options[1]
  69. const useHyphenated = option !== 'never'
  70. const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes]
  71. if (optionsPayload && optionsPayload.ignore) {
  72. ignoredAttributes.push(...optionsPayload.ignore)
  73. }
  74. const caseConverter = casing.getExactConverter(
  75. useHyphenated ? 'kebab-case' : 'camelCase'
  76. )
  77. /**
  78. * @param {VDirective | VAttribute} node
  79. * @param {string} name
  80. */
  81. function reportIssue(node, name) {
  82. const text = sourceCode.getText(node.key)
  83. context.report({
  84. node: node.key,
  85. loc: node.loc,
  86. messageId: useHyphenated ? 'mustBeHyphenated' : 'cannotBeHyphenated',
  87. data: {
  88. text
  89. },
  90. fix: (fixer) => {
  91. if (text.includes('_')) {
  92. return null
  93. }
  94. if (text.endsWith('.sync')) {
  95. return null
  96. }
  97. if (/^[A-Z]/.test(name)) {
  98. return null
  99. }
  100. return fixer.replaceText(
  101. node.key,
  102. text.replace(name, caseConverter(name))
  103. )
  104. }
  105. })
  106. }
  107. /**
  108. * @param {string} value
  109. */
  110. function isIgnoredAttribute(value) {
  111. const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))
  112. if (isIgnored) {
  113. return true
  114. }
  115. return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
  116. }
  117. return utils.defineTemplateBodyVisitor(context, {
  118. VAttribute(node) {
  119. if (
  120. !utils.isCustomComponent(node.parent.parent) &&
  121. node.parent.parent.name !== 'slot'
  122. )
  123. return
  124. const name = getAttributeName(node)
  125. if (name === null || isIgnoredAttribute(name)) return
  126. reportIssue(node, name)
  127. }
  128. })
  129. }
  130. }