attribute-hyphenation.js 4.1 KB

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