attribute-hyphenation.js 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  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. // Rule Definition
  11. // ------------------------------------------------------------------------------
  12. module.exports = {
  13. meta: {
  14. type: 'suggestion',
  15. docs: {
  16. description:
  17. 'enforce attribute naming style on custom components in template',
  18. categories: ['vue3-strongly-recommended', 'strongly-recommended'],
  19. url: 'https://eslint.vuejs.org/rules/attribute-hyphenation.html'
  20. },
  21. fixable: 'code',
  22. schema: [
  23. {
  24. enum: ['always', 'never']
  25. },
  26. {
  27. type: 'object',
  28. properties: {
  29. ignore: {
  30. type: 'array',
  31. items: {
  32. allOf: [
  33. { type: 'string' },
  34. { not: { type: 'string', pattern: ':exit$' } },
  35. { not: { type: 'string', pattern: '^\\s*$' } }
  36. ]
  37. },
  38. uniqueItems: true,
  39. additionalItems: false
  40. }
  41. },
  42. additionalProperties: false
  43. }
  44. ]
  45. },
  46. /** @param {RuleContext} context */
  47. create(context) {
  48. const sourceCode = context.getSourceCode()
  49. const option = context.options[0]
  50. const optionsPayload = context.options[1]
  51. const useHyphenated = option !== 'never'
  52. let ignoredAttributes = ['data-', 'aria-', 'slot-scope'].concat(
  53. svgAttributes
  54. )
  55. if (optionsPayload && optionsPayload.ignore) {
  56. ignoredAttributes = ignoredAttributes.concat(optionsPayload.ignore)
  57. }
  58. const caseConverter = casing.getExactConverter(
  59. useHyphenated ? 'kebab-case' : 'camelCase'
  60. )
  61. /**
  62. * @param {VDirective | VAttribute} node
  63. * @param {string} name
  64. */
  65. function reportIssue(node, name) {
  66. const text = sourceCode.getText(node.key)
  67. context.report({
  68. node: node.key,
  69. loc: node.loc,
  70. message: useHyphenated
  71. ? "Attribute '{{text}}' must be hyphenated."
  72. : "Attribute '{{text}}' can't be hyphenated.",
  73. data: {
  74. text
  75. },
  76. fix: (fixer) =>
  77. fixer.replaceText(node.key, text.replace(name, caseConverter(name)))
  78. })
  79. }
  80. /**
  81. * @param {string} value
  82. */
  83. function isIgnoredAttribute(value) {
  84. const isIgnored = ignoredAttributes.some((attr) => {
  85. return value.includes(attr)
  86. })
  87. if (isIgnored) {
  88. return true
  89. }
  90. return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
  91. }
  92. // ----------------------------------------------------------------------
  93. // Public
  94. // ----------------------------------------------------------------------
  95. return utils.defineTemplateBodyVisitor(context, {
  96. VAttribute(node) {
  97. if (
  98. !utils.isCustomComponent(node.parent.parent) &&
  99. node.parent.parent.name !== 'slot'
  100. )
  101. return
  102. const name = !node.directive
  103. ? node.key.rawName
  104. : node.key.name.name === 'bind'
  105. ? node.key.argument &&
  106. node.key.argument.type === 'VIdentifier' &&
  107. node.key.argument.rawName
  108. : /* otherwise */ false
  109. if (!name || isIgnoredAttribute(name)) return
  110. reportIssue(node, name)
  111. }
  112. })
  113. }
  114. }