attribute-hyphenation.js 3.3 KB

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