attribute-hyphenation.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  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' &&
  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', '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: '^\\s*$' } }
  50. ]
  51. },
  52. uniqueItems: true,
  53. additionalItems: false
  54. }
  55. },
  56. additionalProperties: false
  57. }
  58. ]
  59. },
  60. /** @param {RuleContext} context */
  61. create(context) {
  62. const sourceCode = context.getSourceCode()
  63. const option = context.options[0]
  64. const optionsPayload = context.options[1]
  65. const useHyphenated = option !== 'never'
  66. const ignoredAttributes = ['data-', 'aria-', 'slot-scope', ...svgAttributes]
  67. if (optionsPayload && optionsPayload.ignore) {
  68. ignoredAttributes.push(...optionsPayload.ignore)
  69. }
  70. const caseConverter = casing.getExactConverter(
  71. useHyphenated ? 'kebab-case' : 'camelCase'
  72. )
  73. /**
  74. * @param {VDirective | VAttribute} node
  75. * @param {string} name
  76. */
  77. function reportIssue(node, name) {
  78. const text = sourceCode.getText(node.key)
  79. context.report({
  80. node: node.key,
  81. loc: node.loc,
  82. message: useHyphenated
  83. ? "Attribute '{{text}}' must be hyphenated."
  84. : "Attribute '{{text}}' can't be hyphenated.",
  85. data: {
  86. text
  87. },
  88. fix: (fixer) => {
  89. if (text.includes('_')) {
  90. return null
  91. }
  92. return fixer.replaceText(
  93. node.key,
  94. text.replace(name, caseConverter(name))
  95. )
  96. }
  97. })
  98. }
  99. /**
  100. * @param {string} value
  101. */
  102. function isIgnoredAttribute(value) {
  103. const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))
  104. if (isIgnored) {
  105. return true
  106. }
  107. return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
  108. }
  109. return utils.defineTemplateBodyVisitor(context, {
  110. VAttribute(node) {
  111. if (
  112. !utils.isCustomComponent(node.parent.parent) &&
  113. node.parent.parent.name !== 'slot'
  114. )
  115. return
  116. const name = getAttributeName(node)
  117. if (name === null || isIgnoredAttribute(name)) return
  118. reportIssue(node, name)
  119. }
  120. })
  121. }
  122. }