attributes-order.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  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. /**
  11. * @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective
  12. */
  13. const ATTRS = {
  14. DEFINITION: 'DEFINITION',
  15. LIST_RENDERING: 'LIST_RENDERING',
  16. CONDITIONALS: 'CONDITIONALS',
  17. RENDER_MODIFIERS: 'RENDER_MODIFIERS',
  18. GLOBAL: 'GLOBAL',
  19. UNIQUE: 'UNIQUE',
  20. SLOT: 'SLOT',
  21. TWO_WAY_BINDING: 'TWO_WAY_BINDING',
  22. OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
  23. OTHER_ATTR: 'OTHER_ATTR',
  24. EVENTS: 'EVENTS',
  25. CONTENT: 'CONTENT'
  26. }
  27. /**
  28. * Check whether the given attribute is `v-bind` directive.
  29. * @param {VAttribute | VDirective | undefined | null} node
  30. * @returns { node is VBindDirective }
  31. */
  32. function isVBind(node) {
  33. return Boolean(node && node.directive && node.key.name.name === 'bind')
  34. }
  35. /**
  36. * Check whether the given attribute is plain attribute.
  37. * @param {VAttribute | VDirective | undefined | null} node
  38. * @returns { node is VAttribute }
  39. */
  40. function isVAttribute(node) {
  41. return Boolean(node && !node.directive)
  42. }
  43. /**
  44. * Check whether the given attribute is plain attribute or `v-bind` directive.
  45. * @param {VAttribute | VDirective | undefined | null} node
  46. * @returns { node is VAttribute }
  47. */
  48. function isVAttributeOrVBind(node) {
  49. return isVAttribute(node) || isVBind(node)
  50. }
  51. /**
  52. * Check whether the given attribute is `v-bind="..."` directive.
  53. * @param {VAttribute | VDirective | undefined | null} node
  54. * @returns { node is VBindDirective }
  55. */
  56. function isVBindObject(node) {
  57. return isVBind(node) && node.key.argument == null
  58. }
  59. /**
  60. * @param {VAttribute | VDirective} attribute
  61. * @param {SourceCode} sourceCode
  62. */
  63. function getAttributeName(attribute, sourceCode) {
  64. if (attribute.directive) {
  65. if (isVBind(attribute)) {
  66. return attribute.key.argument
  67. ? sourceCode.getText(attribute.key.argument)
  68. : ''
  69. } else {
  70. return getDirectiveKeyName(attribute.key, sourceCode)
  71. }
  72. } else {
  73. return attribute.key.name
  74. }
  75. }
  76. /**
  77. * @param {VDirectiveKey} directiveKey
  78. * @param {SourceCode} sourceCode
  79. */
  80. function getDirectiveKeyName(directiveKey, sourceCode) {
  81. let text = `v-${directiveKey.name.name}`
  82. if (directiveKey.argument) {
  83. text += `:${sourceCode.getText(directiveKey.argument)}`
  84. }
  85. for (const modifier of directiveKey.modifiers) {
  86. text += `.${modifier.name}`
  87. }
  88. return text
  89. }
  90. /**
  91. * @param {VAttribute | VDirective} attribute
  92. */
  93. function getAttributeType(attribute) {
  94. let propName
  95. if (attribute.directive) {
  96. if (!isVBind(attribute)) {
  97. const name = attribute.key.name.name
  98. if (name === 'for') {
  99. return ATTRS.LIST_RENDERING
  100. } else if (
  101. name === 'if' ||
  102. name === 'else-if' ||
  103. name === 'else' ||
  104. name === 'show' ||
  105. name === 'cloak'
  106. ) {
  107. return ATTRS.CONDITIONALS
  108. } else if (name === 'pre' || name === 'once') {
  109. return ATTRS.RENDER_MODIFIERS
  110. } else if (name === 'model') {
  111. return ATTRS.TWO_WAY_BINDING
  112. } else if (name === 'on') {
  113. return ATTRS.EVENTS
  114. } else if (name === 'html' || name === 'text') {
  115. return ATTRS.CONTENT
  116. } else if (name === 'slot') {
  117. return ATTRS.SLOT
  118. } else if (name === 'is') {
  119. return ATTRS.DEFINITION
  120. } else {
  121. return ATTRS.OTHER_DIRECTIVES
  122. }
  123. }
  124. propName =
  125. attribute.key.argument && attribute.key.argument.type === 'VIdentifier'
  126. ? attribute.key.argument.rawName
  127. : ''
  128. } else {
  129. propName = attribute.key.name
  130. }
  131. if (propName === 'is') {
  132. return ATTRS.DEFINITION
  133. } else if (propName === 'id') {
  134. return ATTRS.GLOBAL
  135. } else if (propName === 'ref' || propName === 'key') {
  136. return ATTRS.UNIQUE
  137. } else if (propName === 'slot' || propName === 'slot-scope') {
  138. return ATTRS.SLOT
  139. } else {
  140. return ATTRS.OTHER_ATTR
  141. }
  142. }
  143. /**
  144. * @param {VAttribute | VDirective} attribute
  145. * @param { { [key: string]: number } } attributePosition
  146. * @returns {number | null} If the value is null, the order is omitted. Do not force the order.
  147. */
  148. function getPosition(attribute, attributePosition) {
  149. const attributeType = getAttributeType(attribute)
  150. return attributePosition[attributeType] != null
  151. ? attributePosition[attributeType]
  152. : null
  153. }
  154. /**
  155. * @param {VAttribute | VDirective} prevNode
  156. * @param {VAttribute | VDirective} currNode
  157. * @param {SourceCode} sourceCode
  158. */
  159. function isAlphabetical(prevNode, currNode, sourceCode) {
  160. const prevName = getAttributeName(prevNode, sourceCode)
  161. const currName = getAttributeName(currNode, sourceCode)
  162. if (prevName === currName) {
  163. const prevIsBind = isVBind(prevNode)
  164. const currIsBind = isVBind(currNode)
  165. return prevIsBind <= currIsBind
  166. }
  167. return prevName < currName
  168. }
  169. /**
  170. * @param {RuleContext} context - The rule context.
  171. * @returns {RuleListener} AST event handlers.
  172. */
  173. function create(context) {
  174. const sourceCode = context.getSourceCode()
  175. let attributeOrder = [
  176. ATTRS.DEFINITION,
  177. ATTRS.LIST_RENDERING,
  178. ATTRS.CONDITIONALS,
  179. ATTRS.RENDER_MODIFIERS,
  180. ATTRS.GLOBAL,
  181. [ATTRS.UNIQUE, ATTRS.SLOT],
  182. ATTRS.TWO_WAY_BINDING,
  183. ATTRS.OTHER_DIRECTIVES,
  184. ATTRS.OTHER_ATTR,
  185. ATTRS.EVENTS,
  186. ATTRS.CONTENT
  187. ]
  188. if (context.options[0] && context.options[0].order) {
  189. attributeOrder = context.options[0].order
  190. }
  191. const alphabetical = Boolean(
  192. context.options[0] && context.options[0].alphabetical
  193. )
  194. /** @type { { [key: string]: number } } */
  195. const attributePosition = {}
  196. attributeOrder.forEach((item, i) => {
  197. if (Array.isArray(item)) {
  198. for (const attr of item) {
  199. attributePosition[attr] = i
  200. }
  201. } else attributePosition[item] = i
  202. })
  203. /**
  204. * @param {VAttribute | VDirective} node
  205. * @param {VAttribute | VDirective} previousNode
  206. */
  207. function reportIssue(node, previousNode) {
  208. const currentNode = sourceCode.getText(node.key)
  209. const prevNode = sourceCode.getText(previousNode.key)
  210. context.report({
  211. node,
  212. message: `Attribute "${currentNode}" should go before "${prevNode}".`,
  213. data: {
  214. currentNode
  215. },
  216. fix(fixer) {
  217. const attributes = node.parent.attributes
  218. /** @type { (node: VAttribute | VDirective | undefined) => boolean } */
  219. let isMoveUp
  220. if (isVBindObject(node)) {
  221. // prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
  222. isMoveUp = isVAttributeOrVBind
  223. } else if (isVAttributeOrVBind(node)) {
  224. // prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
  225. isMoveUp = isVBindObject
  226. } else {
  227. isMoveUp = () => false
  228. }
  229. const previousNodes = attributes.slice(
  230. attributes.indexOf(previousNode),
  231. attributes.indexOf(node)
  232. )
  233. const moveNodes = [node]
  234. for (const node of previousNodes) {
  235. if (isMoveUp(node)) {
  236. moveNodes.unshift(node)
  237. } else {
  238. moveNodes.push(node)
  239. }
  240. }
  241. return moveNodes.map((moveNode, index) => {
  242. const text = sourceCode.getText(moveNode)
  243. return fixer.replaceText(previousNodes[index] || node, text)
  244. })
  245. }
  246. })
  247. }
  248. return utils.defineTemplateBodyVisitor(context, {
  249. VStartTag(node) {
  250. const attributeAndPositions = getAttributeAndPositionList(node)
  251. if (attributeAndPositions.length <= 1) {
  252. return
  253. }
  254. let { attr: previousNode, position: previousPosition } =
  255. attributeAndPositions[0]
  256. for (let index = 1; index < attributeAndPositions.length; index++) {
  257. const { attr, position } = attributeAndPositions[index]
  258. let valid = previousPosition <= position
  259. if (valid && alphabetical && previousPosition === position) {
  260. valid = isAlphabetical(previousNode, attr, sourceCode)
  261. }
  262. if (valid) {
  263. previousNode = attr
  264. previousPosition = position
  265. } else {
  266. reportIssue(attr, previousNode)
  267. }
  268. }
  269. }
  270. })
  271. /**
  272. * @param {VStartTag} node
  273. * @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
  274. */
  275. function getAttributeAndPositionList(node) {
  276. const attributes = node.attributes.filter((node, index, attributes) => {
  277. if (
  278. isVBindObject(node) &&
  279. (isVAttributeOrVBind(attributes[index - 1]) ||
  280. isVAttributeOrVBind(attributes[index + 1]))
  281. ) {
  282. // In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax
  283. // as they behave differently if you change the order.
  284. return false
  285. }
  286. return true
  287. })
  288. const results = []
  289. for (let index = 0; index < attributes.length; index++) {
  290. const attr = attributes[index]
  291. const position = getPositionFromAttrIndex(index)
  292. if (position == null) {
  293. // The omitted order is skipped.
  294. continue
  295. }
  296. results.push({ attr, position })
  297. }
  298. return results
  299. /**
  300. * @param {number} index
  301. * @returns {number | null}
  302. */
  303. function getPositionFromAttrIndex(index) {
  304. const node = attributes[index]
  305. if (isVBindObject(node)) {
  306. // node is `v-bind ="object"` syntax
  307. // In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`,
  308. // the behavior will be different, so adjust so that there is no change in behavior.
  309. const len = attributes.length
  310. for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
  311. const next = attributes[nextIndex]
  312. if (isVAttributeOrVBind(next) && !isVBindObject(next)) {
  313. // It is considered to be in the same order as the next bind prop node.
  314. return getPositionFromAttrIndex(nextIndex)
  315. }
  316. }
  317. }
  318. return getPosition(node, attributePosition)
  319. }
  320. }
  321. }
  322. module.exports = {
  323. meta: {
  324. type: 'suggestion',
  325. docs: {
  326. description: 'enforce order of attributes',
  327. categories: ['vue3-recommended', 'recommended'],
  328. url: 'https://eslint.vuejs.org/rules/attributes-order.html'
  329. },
  330. fixable: 'code',
  331. schema: [
  332. {
  333. type: 'object',
  334. properties: {
  335. order: {
  336. type: 'array',
  337. items: {
  338. anyOf: [
  339. { enum: Object.values(ATTRS) },
  340. {
  341. type: 'array',
  342. items: {
  343. enum: Object.values(ATTRS),
  344. uniqueItems: true,
  345. additionalItems: false
  346. }
  347. }
  348. ]
  349. },
  350. uniqueItems: true,
  351. additionalItems: false
  352. },
  353. alphabetical: { type: 'boolean' }
  354. },
  355. additionalProperties: false
  356. }
  357. ]
  358. },
  359. create
  360. }