attributes-order.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. /**
  2. * @fileoverview enforce ordering of attributes
  3. * @author Erin Depew
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. // ------------------------------------------------------------------------------
  8. // Rule Definition
  9. // ------------------------------------------------------------------------------
  10. const ATTRS = {
  11. DEFINITION: 'DEFINITION',
  12. LIST_RENDERING: 'LIST_RENDERING',
  13. CONDITIONALS: 'CONDITIONALS',
  14. RENDER_MODIFIERS: 'RENDER_MODIFIERS',
  15. GLOBAL: 'GLOBAL',
  16. UNIQUE: 'UNIQUE',
  17. TWO_WAY_BINDING: 'TWO_WAY_BINDING',
  18. OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
  19. OTHER_ATTR: 'OTHER_ATTR',
  20. EVENTS: 'EVENTS',
  21. CONTENT: 'CONTENT'
  22. }
  23. /**
  24. * @param {VAttribute | VDirective} attribute
  25. * @param {SourceCode} sourceCode
  26. */
  27. function getAttributeName(attribute, sourceCode) {
  28. if (attribute.directive) {
  29. if (attribute.key.name.name === 'bind') {
  30. return attribute.key.argument
  31. ? sourceCode.getText(attribute.key.argument)
  32. : ''
  33. } else {
  34. return getDirectiveKeyName(attribute.key, sourceCode)
  35. }
  36. } else {
  37. return attribute.key.name
  38. }
  39. }
  40. /**
  41. * @param {VDirectiveKey} directiveKey
  42. * @param {SourceCode} sourceCode
  43. */
  44. function getDirectiveKeyName(directiveKey, sourceCode) {
  45. let text = `v-${directiveKey.name.name}`
  46. if (directiveKey.argument) {
  47. text += `:${sourceCode.getText(directiveKey.argument)}`
  48. }
  49. for (const modifier of directiveKey.modifiers) {
  50. text += `.${modifier.name}`
  51. }
  52. return text
  53. }
  54. /**
  55. * @param {VAttribute | VDirective} attribute
  56. * @param {SourceCode} sourceCode
  57. */
  58. function getAttributeType(attribute, sourceCode) {
  59. let propName
  60. if (attribute.directive) {
  61. if (attribute.key.name.name !== 'bind') {
  62. const name = attribute.key.name.name
  63. if (name === 'for') {
  64. return ATTRS.LIST_RENDERING
  65. } else if (
  66. name === 'if' ||
  67. name === 'else-if' ||
  68. name === 'else' ||
  69. name === 'show' ||
  70. name === 'cloak'
  71. ) {
  72. return ATTRS.CONDITIONALS
  73. } else if (name === 'pre' || name === 'once') {
  74. return ATTRS.RENDER_MODIFIERS
  75. } else if (name === 'model') {
  76. return ATTRS.TWO_WAY_BINDING
  77. } else if (name === 'on') {
  78. return ATTRS.EVENTS
  79. } else if (name === 'html' || name === 'text') {
  80. return ATTRS.CONTENT
  81. } else if (name === 'slot') {
  82. return ATTRS.UNIQUE
  83. } else if (name === 'is') {
  84. return ATTRS.DEFINITION
  85. } else {
  86. return ATTRS.OTHER_DIRECTIVES
  87. }
  88. }
  89. propName = attribute.key.argument
  90. ? sourceCode.getText(attribute.key.argument)
  91. : ''
  92. } else {
  93. propName = attribute.key.name
  94. }
  95. if (propName === 'is') {
  96. return ATTRS.DEFINITION
  97. } else if (propName === 'id') {
  98. return ATTRS.GLOBAL
  99. } else if (
  100. propName === 'ref' ||
  101. propName === 'key' ||
  102. propName === 'slot' ||
  103. propName === 'slot-scope'
  104. ) {
  105. return ATTRS.UNIQUE
  106. } else {
  107. return ATTRS.OTHER_ATTR
  108. }
  109. }
  110. /**
  111. * @param {VAttribute | VDirective} attribute
  112. * @param { { [key: string]: number } } attributePosition
  113. * @param {SourceCode} sourceCode
  114. */
  115. function getPosition(attribute, attributePosition, sourceCode) {
  116. const attributeType = getAttributeType(attribute, sourceCode)
  117. return attributePosition.hasOwnProperty(attributeType)
  118. ? attributePosition[attributeType]
  119. : -1
  120. }
  121. /**
  122. * @param {VAttribute | VDirective} prevNode
  123. * @param {VAttribute | VDirective} currNode
  124. * @param {SourceCode} sourceCode
  125. */
  126. function isAlphabetical(prevNode, currNode, sourceCode) {
  127. const isSameType =
  128. getAttributeType(prevNode, sourceCode) ===
  129. getAttributeType(currNode, sourceCode)
  130. if (isSameType) {
  131. const prevName = getAttributeName(prevNode, sourceCode)
  132. const currName = getAttributeName(currNode, sourceCode)
  133. if (prevName === currName) {
  134. const prevIsBind = Boolean(
  135. prevNode.directive && prevNode.key.name.name === 'bind'
  136. )
  137. const currIsBind = Boolean(
  138. currNode.directive && currNode.key.name.name === 'bind'
  139. )
  140. return prevIsBind <= currIsBind
  141. }
  142. return prevName < currName
  143. }
  144. return true
  145. }
  146. /**
  147. * @param {RuleContext} context - The rule context.
  148. * @returns {RuleListener} AST event handlers.
  149. */
  150. function create(context) {
  151. const sourceCode = context.getSourceCode()
  152. let attributeOrder = [
  153. ATTRS.DEFINITION,
  154. ATTRS.LIST_RENDERING,
  155. ATTRS.CONDITIONALS,
  156. ATTRS.RENDER_MODIFIERS,
  157. ATTRS.GLOBAL,
  158. ATTRS.UNIQUE,
  159. ATTRS.TWO_WAY_BINDING,
  160. ATTRS.OTHER_DIRECTIVES,
  161. ATTRS.OTHER_ATTR,
  162. ATTRS.EVENTS,
  163. ATTRS.CONTENT
  164. ]
  165. if (context.options[0] && context.options[0].order) {
  166. attributeOrder = context.options[0].order
  167. }
  168. const alphabetical = Boolean(
  169. context.options[0] && context.options[0].alphabetical
  170. )
  171. /** @type { { [key: string]: number } } */
  172. const attributePosition = {}
  173. attributeOrder.forEach((item, i) => {
  174. if (Array.isArray(item)) {
  175. item.forEach((attr) => {
  176. attributePosition[attr] = i
  177. })
  178. } else attributePosition[item] = i
  179. })
  180. /**
  181. * @typedef {object} State
  182. * @property {number} currentPosition
  183. * @property {VAttribute | VDirective} previousNode
  184. */
  185. /**
  186. * @type {State | null}
  187. */
  188. let state
  189. /**
  190. * @param {VAttribute | VDirective} node
  191. * @param {VAttribute | VDirective} previousNode
  192. */
  193. function reportIssue(node, previousNode) {
  194. const currentNode = sourceCode.getText(node.key)
  195. const prevNode = sourceCode.getText(previousNode.key)
  196. context.report({
  197. node: node.key,
  198. loc: node.loc,
  199. message: `Attribute "${currentNode}" should go before "${prevNode}".`,
  200. data: {
  201. currentNode
  202. },
  203. fix(fixer) {
  204. const attributes = node.parent.attributes
  205. const shiftAttrs = attributes.slice(
  206. attributes.indexOf(previousNode),
  207. attributes.indexOf(node) + 1
  208. )
  209. return shiftAttrs.map((attr, i) => {
  210. const text =
  211. attr === previousNode
  212. ? sourceCode.getText(node)
  213. : sourceCode.getText(shiftAttrs[i - 1])
  214. return fixer.replaceText(attr, text)
  215. })
  216. }
  217. })
  218. }
  219. return utils.defineTemplateBodyVisitor(context, {
  220. VStartTag() {
  221. state = null
  222. },
  223. VAttribute(node) {
  224. let inAlphaOrder = true
  225. if (state && alphabetical) {
  226. inAlphaOrder = isAlphabetical(state.previousNode, node, sourceCode)
  227. }
  228. if (
  229. !state ||
  230. (state.currentPosition <=
  231. getPosition(node, attributePosition, sourceCode) &&
  232. inAlphaOrder)
  233. ) {
  234. state = {
  235. currentPosition: getPosition(node, attributePosition, sourceCode),
  236. previousNode: node
  237. }
  238. } else {
  239. reportIssue(node, state.previousNode)
  240. }
  241. }
  242. })
  243. }
  244. module.exports = {
  245. meta: {
  246. type: 'suggestion',
  247. docs: {
  248. description: 'enforce order of attributes',
  249. categories: ['vue3-recommended', 'recommended'],
  250. url: 'https://eslint.vuejs.org/rules/attributes-order.html'
  251. },
  252. fixable: 'code',
  253. schema: {
  254. type: 'array',
  255. properties: {
  256. order: {
  257. items: {
  258. type: 'string'
  259. },
  260. maxItems: 10,
  261. minItems: 10
  262. }
  263. }
  264. }
  265. },
  266. create
  267. }