attributes-order.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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. }
  118. case 'if':
  119. case 'else-if':
  120. case 'else':
  121. case 'show':
  122. case 'cloak': {
  123. return ATTRS.CONDITIONALS
  124. }
  125. case 'pre':
  126. case 'once': {
  127. return ATTRS.RENDER_MODIFIERS
  128. }
  129. case 'model': {
  130. return ATTRS.TWO_WAY_BINDING
  131. }
  132. case 'on': {
  133. return ATTRS.EVENTS
  134. }
  135. case 'html':
  136. case 'text': {
  137. return ATTRS.CONTENT
  138. }
  139. case 'slot': {
  140. return ATTRS.SLOT
  141. }
  142. case 'is': {
  143. return ATTRS.DEFINITION
  144. }
  145. default: {
  146. return ATTRS.OTHER_DIRECTIVES
  147. }
  148. }
  149. }
  150. propName =
  151. attribute.key.argument && attribute.key.argument.type === 'VIdentifier'
  152. ? attribute.key.argument.rawName
  153. : ''
  154. } else {
  155. propName = attribute.key.name
  156. }
  157. switch (propName) {
  158. case 'is': {
  159. return ATTRS.DEFINITION
  160. }
  161. case 'id': {
  162. return ATTRS.GLOBAL
  163. }
  164. case 'ref':
  165. case 'key': {
  166. return ATTRS.UNIQUE
  167. }
  168. case 'slot':
  169. case 'slot-scope': {
  170. return ATTRS.SLOT
  171. }
  172. default: {
  173. if (isVBind(attribute)) {
  174. return ATTRS.ATTR_DYNAMIC
  175. }
  176. if (isVShorthandBoolean(attribute)) {
  177. return ATTRS.ATTR_SHORTHAND_BOOL
  178. }
  179. return ATTRS.ATTR_STATIC
  180. }
  181. }
  182. }
  183. /**
  184. * @param {VAttribute | VDirective} attribute
  185. * @param { { [key: string]: number } } attributePosition
  186. * @returns {number | null} If the value is null, the order is omitted. Do not force the order.
  187. */
  188. function getPosition(attribute, attributePosition) {
  189. const attributeType = getAttributeType(attribute)
  190. return attributePosition[attributeType] == null
  191. ? null
  192. : attributePosition[attributeType]
  193. }
  194. /**
  195. * @param {VAttribute | VDirective} prevNode
  196. * @param {VAttribute | VDirective} currNode
  197. * @param {SourceCode} sourceCode
  198. */
  199. function isAlphabetical(prevNode, currNode, sourceCode) {
  200. const prevName = getAttributeName(prevNode, sourceCode)
  201. const currName = getAttributeName(currNode, sourceCode)
  202. if (prevName === currName) {
  203. const prevIsBind = isVBind(prevNode)
  204. const currIsBind = isVBind(currNode)
  205. return prevIsBind <= currIsBind
  206. }
  207. return prevName < currName
  208. }
  209. /**
  210. * @param {RuleContext} context - The rule context.
  211. * @returns {RuleListener} AST event handlers.
  212. */
  213. function create(context) {
  214. const sourceCode = context.getSourceCode()
  215. const otherAttrs = [
  216. ATTRS.ATTR_DYNAMIC,
  217. ATTRS.ATTR_STATIC,
  218. ATTRS.ATTR_SHORTHAND_BOOL
  219. ]
  220. let attributeOrder = [
  221. ATTRS.DEFINITION,
  222. ATTRS.LIST_RENDERING,
  223. ATTRS.CONDITIONALS,
  224. ATTRS.RENDER_MODIFIERS,
  225. ATTRS.GLOBAL,
  226. [ATTRS.UNIQUE, ATTRS.SLOT],
  227. ATTRS.TWO_WAY_BINDING,
  228. ATTRS.OTHER_DIRECTIVES,
  229. otherAttrs,
  230. ATTRS.EVENTS,
  231. ATTRS.CONTENT
  232. ]
  233. if (context.options[0] && context.options[0].order) {
  234. attributeOrder = [...context.options[0].order]
  235. // check if `OTHER_ATTR` is valid
  236. for (const item of attributeOrder.flat()) {
  237. if (item === ATTRS.OTHER_ATTR) {
  238. for (const attribute of attributeOrder.flat()) {
  239. if (otherAttrs.includes(attribute)) {
  240. throw new Error(
  241. `Value "${ATTRS.OTHER_ATTR}" is not allowed with "${attribute}".`
  242. )
  243. }
  244. }
  245. }
  246. }
  247. // expand `OTHER_ATTR` alias
  248. for (const [index, item] of attributeOrder.entries()) {
  249. if (item === ATTRS.OTHER_ATTR) {
  250. attributeOrder[index] = otherAttrs
  251. } else if (Array.isArray(item) && item.includes(ATTRS.OTHER_ATTR)) {
  252. const attributes = item.filter((i) => i !== ATTRS.OTHER_ATTR)
  253. attributes.push(...otherAttrs)
  254. attributeOrder[index] = attributes
  255. }
  256. }
  257. }
  258. const alphabetical = Boolean(
  259. context.options[0] && context.options[0].alphabetical
  260. )
  261. /** @type { { [key: string]: number } } */
  262. const attributePosition = {}
  263. for (const [i, item] of attributeOrder.entries()) {
  264. if (Array.isArray(item)) {
  265. for (const attr of item) {
  266. attributePosition[attr] = i
  267. }
  268. } else attributePosition[item] = i
  269. }
  270. /**
  271. * @param {VAttribute | VDirective} node
  272. * @param {VAttribute | VDirective} previousNode
  273. */
  274. function reportIssue(node, previousNode) {
  275. const currentNode = sourceCode.getText(node.key)
  276. const prevNode = sourceCode.getText(previousNode.key)
  277. context.report({
  278. node,
  279. messageId: 'expectedOrder',
  280. data: {
  281. currentNode,
  282. prevNode
  283. },
  284. fix(fixer) {
  285. const attributes = node.parent.attributes
  286. /** @type { (node: VAttribute | VDirective | undefined) => boolean } */
  287. let isMoveUp
  288. if (isVBindObject(node)) {
  289. // prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
  290. isMoveUp = isVAttributeOrVBindOrVModel
  291. } else if (isVAttributeOrVBindOrVModel(node)) {
  292. // prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
  293. isMoveUp = isVBindObject
  294. } else {
  295. isMoveUp = () => false
  296. }
  297. const previousNodes = attributes.slice(
  298. attributes.indexOf(previousNode),
  299. attributes.indexOf(node)
  300. )
  301. const moveNodes = [node]
  302. for (const node of previousNodes) {
  303. if (isMoveUp(node)) {
  304. moveNodes.unshift(node)
  305. } else {
  306. moveNodes.push(node)
  307. }
  308. }
  309. return moveNodes.map((moveNode, index) => {
  310. const text = sourceCode.getText(moveNode)
  311. return fixer.replaceText(previousNodes[index] || node, text)
  312. })
  313. }
  314. })
  315. }
  316. return utils.defineTemplateBodyVisitor(context, {
  317. VStartTag(node) {
  318. const attributeAndPositions = getAttributeAndPositionList(node)
  319. if (attributeAndPositions.length <= 1) {
  320. return
  321. }
  322. let { attr: previousNode, position: previousPosition } =
  323. attributeAndPositions[0]
  324. for (let index = 1; index < attributeAndPositions.length; index++) {
  325. const { attr, position } = attributeAndPositions[index]
  326. let valid = previousPosition <= position
  327. if (valid && alphabetical && previousPosition === position) {
  328. valid = isAlphabetical(previousNode, attr, sourceCode)
  329. }
  330. if (valid) {
  331. previousNode = attr
  332. previousPosition = position
  333. } else {
  334. reportIssue(attr, previousNode)
  335. }
  336. }
  337. }
  338. })
  339. /**
  340. * @param {VStartTag} node
  341. * @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
  342. */
  343. function getAttributeAndPositionList(node) {
  344. const attributes = node.attributes.filter((node, index, attributes) => {
  345. if (
  346. isVBindObject(node) &&
  347. (isVAttributeOrVBindOrVModel(attributes[index - 1]) ||
  348. isVAttributeOrVBindOrVModel(attributes[index + 1]))
  349. ) {
  350. // In Vue 3, ignore `v-bind="object"`, which is
  351. // a pair of `v-bind:foo="..."` and `v-bind="object"` and
  352. // a pair of `v-model="..."` and `v-bind="object"`,
  353. // because changing the order behaves differently.
  354. return false
  355. }
  356. return true
  357. })
  358. const results = []
  359. for (const [index, attr] of attributes.entries()) {
  360. const position = getPositionFromAttrIndex(index)
  361. if (position == null) {
  362. // The omitted order is skipped.
  363. continue
  364. }
  365. results.push({ attr, position })
  366. }
  367. return results
  368. /**
  369. * @param {number} index
  370. * @returns {number | null}
  371. */
  372. function getPositionFromAttrIndex(index) {
  373. const node = attributes[index]
  374. if (isVBindObject(node)) {
  375. // node is `v-bind ="object"` syntax
  376. // In Vue 3, if change the order of `v-bind:foo="..."`, `v-model="..."` and `v-bind="object"`,
  377. // the behavior will be different, so adjust so that there is no change in behavior.
  378. const len = attributes.length
  379. for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
  380. const next = attributes[nextIndex]
  381. if (isVAttributeOrVBindOrVModel(next) && !isVBindObject(next)) {
  382. // It is considered to be in the same order as the next bind prop node.
  383. return getPositionFromAttrIndex(nextIndex)
  384. }
  385. }
  386. }
  387. return getPosition(node, attributePosition)
  388. }
  389. }
  390. }
  391. module.exports = {
  392. meta: {
  393. type: 'suggestion',
  394. docs: {
  395. description: 'enforce order of attributes',
  396. categories: ['vue3-recommended', 'vue2-recommended'],
  397. url: 'https://eslint.vuejs.org/rules/attributes-order.html'
  398. },
  399. fixable: 'code',
  400. schema: [
  401. {
  402. type: 'object',
  403. properties: {
  404. order: {
  405. type: 'array',
  406. items: {
  407. anyOf: [
  408. { enum: Object.values(ATTRS) },
  409. {
  410. type: 'array',
  411. items: {
  412. enum: Object.values(ATTRS),
  413. uniqueItems: true,
  414. additionalItems: false
  415. }
  416. }
  417. ]
  418. },
  419. uniqueItems: true,
  420. additionalItems: false
  421. },
  422. alphabetical: { type: 'boolean' }
  423. },
  424. additionalProperties: false
  425. }
  426. ],
  427. messages: {
  428. expectedOrder: `Attribute "{{currentNode}}" should go before "{{prevNode}}".`
  429. }
  430. },
  431. create
  432. }