attributes-order.js 12 KB

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