sort-keys.js 7.9 KB

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