attributes-order.js 11 KB

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