sort-keys.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. /**
  2. * @fileoverview enforce sort-keys in a manner that is compatible with order-in-components
  3. * @author Loren Klingman
  4. * Original ESLint sort-keys by Toru Nagashima
  5. */
  6. 'use strict'
  7. const naturalCompare = require('natural-compare')
  8. const utils = require('../utils')
  9. /**
  10. * Gets the property name of the given `Property` node.
  11. *
  12. * - If the property's key is an `Identifier` node, this returns the key's name
  13. * whether it's a computed property or not.
  14. * - If the property has a static name, this returns the static name.
  15. * - Otherwise, this returns null.
  16. * @param {Property} node The `Property` node to get.
  17. * @returns {string|null} The property name or null.
  18. * @private
  19. */
  20. function getPropertyName(node) {
  21. const staticName = utils.getStaticPropertyName(node)
  22. if (staticName !== null) {
  23. return staticName
  24. }
  25. return node.key.type === 'Identifier' ? node.key.name : null
  26. }
  27. /**
  28. * Functions which check that the given 2 names are in specific order.
  29. *
  30. * Postfix `I` is meant insensitive.
  31. * Postfix `N` is meant natural.
  32. * @private
  33. * @type { { [key: string]: (a:string, b:string) => boolean } }
  34. */
  35. const isValidOrders = {
  36. asc(a, b) {
  37. return a <= b
  38. },
  39. ascI(a, b) {
  40. return a.toLowerCase() <= b.toLowerCase()
  41. },
  42. ascN(a, b) {
  43. return naturalCompare(a, b) <= 0
  44. },
  45. ascIN(a, b) {
  46. return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0
  47. },
  48. desc(a, b) {
  49. return isValidOrders.asc(b, a)
  50. },
  51. descI(a, b) {
  52. return isValidOrders.ascI(b, a)
  53. },
  54. descN(a, b) {
  55. return isValidOrders.ascN(b, a)
  56. },
  57. descIN(a, b) {
  58. return isValidOrders.ascIN(b, a)
  59. }
  60. }
  61. module.exports = {
  62. meta: {
  63. type: 'suggestion',
  64. docs: {
  65. description:
  66. 'enforce sort-keys in a manner that is compatible with order-in-components',
  67. categories: null,
  68. url: 'https://eslint.vuejs.org/rules/sort-keys.html'
  69. },
  70. fixable: null,
  71. schema: [
  72. {
  73. enum: ['asc', 'desc']
  74. },
  75. {
  76. type: 'object',
  77. properties: {
  78. caseSensitive: {
  79. type: 'boolean'
  80. },
  81. ignoreChildrenOf: {
  82. type: 'array'
  83. },
  84. ignoreGrandchildrenOf: {
  85. type: 'array'
  86. },
  87. minKeys: {
  88. type: 'integer',
  89. minimum: 2
  90. },
  91. natural: {
  92. type: 'boolean'
  93. },
  94. runOutsideVue: {
  95. type: 'boolean'
  96. }
  97. },
  98. additionalProperties: false
  99. }
  100. ],
  101. messages: {
  102. sortKeys:
  103. "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'."
  104. }
  105. },
  106. /**
  107. * @param {RuleContext} context - The rule context.
  108. * @returns {RuleListener} AST event handlers.
  109. */
  110. create(context) {
  111. // Parse options.
  112. const options = context.options[1]
  113. const order = context.options[0] || 'asc'
  114. /** @type {Set<string>} */
  115. const ignoreGrandchildrenOf = new Set(
  116. (options && options.ignoreGrandchildrenOf) || [
  117. 'computed',
  118. 'directives',
  119. 'inject',
  120. 'props',
  121. 'watch'
  122. ]
  123. )
  124. /** @type {Set<string>} */
  125. const ignoreChildrenOf = new Set(
  126. (options && options.ignoreChildrenOf) || ['model']
  127. )
  128. const insensitive = options && options.caseSensitive === false
  129. const minKeys = options?.minKeys ?? 2
  130. const natural = options && options.natural
  131. const isValidOrder =
  132. isValidOrders[order + (insensitive ? 'I' : '') + (natural ? 'N' : '')]
  133. /**
  134. * @typedef {object} ObjectStack
  135. * @property {ObjectStack | null} ObjectStack.upper
  136. * @property {string | null} ObjectStack.prevName
  137. * @property {number} ObjectStack.numKeys
  138. * @property {VueState} ObjectStack.vueState
  139. *
  140. * @typedef {object} VueState
  141. * @property {Property} [VueState.currentProperty]
  142. * @property {boolean} [VueState.isVueObject]
  143. * @property {boolean} [VueState.within]
  144. * @property {string} [VueState.propName]
  145. * @property {number} [VueState.chainLevel]
  146. * @property {boolean} [VueState.ignore]
  147. */
  148. /**
  149. * The stack to save the previous property's name for each object literals.
  150. * @type {ObjectStack | null}
  151. */
  152. let objectStack
  153. return {
  154. ObjectExpression(node) {
  155. /** @type {VueState} */
  156. const vueState = {}
  157. const upperVueState = (objectStack && objectStack.vueState) || {}
  158. objectStack = {
  159. upper: objectStack,
  160. prevName: null,
  161. numKeys: node.properties.length,
  162. vueState
  163. }
  164. vueState.isVueObject = utils.getVueObjectType(context, node) != null
  165. if (vueState.isVueObject) {
  166. vueState.within = vueState.isVueObject
  167. // Ignore Vue object properties
  168. vueState.ignore = true
  169. } else {
  170. if (upperVueState.within && upperVueState.currentProperty) {
  171. const isChain = utils.isPropertyChain(
  172. upperVueState.currentProperty,
  173. node
  174. )
  175. if (isChain) {
  176. let propName
  177. let chainLevel
  178. if (upperVueState.isVueObject) {
  179. propName =
  180. utils.getStaticPropertyName(upperVueState.currentProperty) ||
  181. ''
  182. chainLevel = 1
  183. } else {
  184. propName = upperVueState.propName || ''
  185. chainLevel = (upperVueState.chainLevel || 0) + 1
  186. }
  187. vueState.propName = propName
  188. vueState.chainLevel = chainLevel
  189. // chaining
  190. vueState.within = true
  191. // Judge whether to ignore the property.
  192. if (
  193. (chainLevel === 1 && ignoreChildrenOf.has(propName)) ||
  194. (chainLevel === 2 && ignoreGrandchildrenOf.has(propName))
  195. ) {
  196. vueState.ignore = true
  197. }
  198. } else {
  199. // chaining has broken.
  200. vueState.within = false
  201. }
  202. }
  203. }
  204. },
  205. 'ObjectExpression:exit'() {
  206. objectStack = objectStack && objectStack.upper
  207. },
  208. SpreadElement(node) {
  209. if (!objectStack) {
  210. return
  211. }
  212. if (node.parent.type === 'ObjectExpression') {
  213. objectStack.prevName = null
  214. }
  215. },
  216. 'ObjectExpression > Property'(node) {
  217. if (!objectStack) {
  218. return
  219. }
  220. objectStack.vueState.currentProperty = node
  221. if (objectStack.vueState.ignore) {
  222. return
  223. }
  224. const prevName = objectStack.prevName
  225. const numKeys = objectStack.numKeys
  226. const thisName = getPropertyName(node)
  227. if (thisName !== null) {
  228. objectStack.prevName = thisName
  229. }
  230. if (prevName === null || thisName === null || numKeys < minKeys) {
  231. return
  232. }
  233. if (!isValidOrder(prevName, thisName)) {
  234. context.report({
  235. node,
  236. loc: node.key.loc,
  237. messageId: 'sortKeys',
  238. data: {
  239. thisName,
  240. prevName,
  241. order,
  242. insensitive: insensitive ? 'insensitive ' : '',
  243. natural: natural ? 'natural ' : ''
  244. }
  245. })
  246. }
  247. }
  248. }
  249. }
  250. }