define-macros-order.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /**
  2. * @author Eduard Deisling
  3. * See LICENSE file in root directory for full license.
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. const MACROS_EMITS = 'defineEmits'
  8. const MACROS_PROPS = 'defineProps'
  9. const MACROS_OPTIONS = 'defineOptions'
  10. const MACROS_SLOTS = 'defineSlots'
  11. const ORDER_SCHEMA = [MACROS_EMITS, MACROS_PROPS, MACROS_OPTIONS, MACROS_SLOTS]
  12. const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]
  13. /**
  14. * @param {VElement} scriptSetup
  15. * @param {ASTNode} node
  16. */
  17. function inScriptSetup(scriptSetup, node) {
  18. return (
  19. scriptSetup.range[0] <= node.range[0] &&
  20. node.range[1] <= scriptSetup.range[1]
  21. )
  22. }
  23. /**
  24. * @param {ASTNode} node
  25. */
  26. function isUseStrictStatement(node) {
  27. return (
  28. node.type === 'ExpressionStatement' &&
  29. node.expression.type === 'Literal' &&
  30. node.expression.value === 'use strict'
  31. )
  32. }
  33. /**
  34. * Get an index of the first statement after imports and interfaces in order
  35. * to place defineEmits and defineProps before this statement
  36. * @param {VElement} scriptSetup
  37. * @param {Program} program
  38. */
  39. function getTargetStatementPosition(scriptSetup, program) {
  40. const skipStatements = new Set([
  41. 'ImportDeclaration',
  42. 'TSInterfaceDeclaration',
  43. 'TSTypeAliasDeclaration',
  44. 'DebuggerStatement',
  45. 'EmptyStatement',
  46. 'ExportNamedDeclaration'
  47. ])
  48. for (const [index, item] of program.body.entries()) {
  49. if (
  50. inScriptSetup(scriptSetup, item) &&
  51. !skipStatements.has(item.type) &&
  52. !isUseStrictStatement(item)
  53. ) {
  54. return index
  55. }
  56. }
  57. return -1
  58. }
  59. /**
  60. * We need to handle cases like "const props = defineProps(...)"
  61. * Define macros must be used only on top, so we can look for "Program" type
  62. * inside node.parent.type
  63. * @param {CallExpression|ASTNode} node
  64. * @return {ASTNode}
  65. */
  66. function getDefineMacrosStatement(node) {
  67. if (!node.parent) {
  68. throw new Error('Node has no parent')
  69. }
  70. if (node.parent.type === 'Program') {
  71. return node
  72. }
  73. return getDefineMacrosStatement(node.parent)
  74. }
  75. /** @param {RuleContext} context */
  76. function create(context) {
  77. const scriptSetup = utils.getScriptSetupElement(context)
  78. if (!scriptSetup) {
  79. return {}
  80. }
  81. const sourceCode = context.getSourceCode()
  82. const options = context.options
  83. /** @type {[string, string]} */
  84. const order = (options[0] && options[0].order) || DEFAULT_ORDER
  85. /** @type {Map<string, ASTNode>} */
  86. const macrosNodes = new Map()
  87. return utils.compositingVisitors(
  88. utils.defineScriptSetupVisitor(context, {
  89. onDefinePropsExit(node) {
  90. macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
  91. },
  92. onDefineEmitsExit(node) {
  93. macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
  94. },
  95. onDefineOptionsExit(node) {
  96. macrosNodes.set(MACROS_OPTIONS, getDefineMacrosStatement(node))
  97. },
  98. onDefineSlotsExit(node) {
  99. macrosNodes.set(MACROS_SLOTS, getDefineMacrosStatement(node))
  100. }
  101. }),
  102. {
  103. 'Program:exit'(program) {
  104. /**
  105. * @typedef {object} OrderedData
  106. * @property {string} name
  107. * @property {ASTNode} node
  108. */
  109. const firstStatementIndex = getTargetStatementPosition(
  110. scriptSetup,
  111. program
  112. )
  113. const orderedList = order
  114. .map((name) => ({ name, node: macrosNodes.get(name) }))
  115. .filter(
  116. /** @returns {data is OrderedData} */
  117. (data) => utils.isDef(data.node)
  118. )
  119. for (const [index, should] of orderedList.entries()) {
  120. const targetStatement = program.body[firstStatementIndex + index]
  121. if (should.node !== targetStatement) {
  122. let moveTargetNodes = orderedList
  123. .slice(index)
  124. .map(({ node }) => node)
  125. const targetStatementIndex =
  126. moveTargetNodes.indexOf(targetStatement)
  127. if (targetStatementIndex >= 0) {
  128. moveTargetNodes = moveTargetNodes.slice(0, targetStatementIndex)
  129. }
  130. reportNotOnTop(should.name, moveTargetNodes, targetStatement)
  131. return
  132. }
  133. }
  134. }
  135. }
  136. )
  137. /**
  138. * @param {string} macro
  139. * @param {ASTNode[]} nodes
  140. * @param {ASTNode} before
  141. */
  142. function reportNotOnTop(macro, nodes, before) {
  143. context.report({
  144. node: nodes[0],
  145. loc: nodes[0].loc,
  146. messageId: 'macrosNotOnTop',
  147. data: {
  148. macro
  149. },
  150. *fix(fixer) {
  151. for (const node of nodes) {
  152. yield* moveNodeBefore(fixer, node, before)
  153. }
  154. }
  155. })
  156. }
  157. /**
  158. * Move all lines of "node" with its comments to before the "target"
  159. * @param {RuleFixer} fixer
  160. * @param {ASTNode} node
  161. * @param {ASTNode} target
  162. */
  163. function moveNodeBefore(fixer, node, target) {
  164. // get comments under tokens(if any)
  165. const beforeNodeToken = sourceCode.getTokenBefore(node)
  166. const nodeComment = sourceCode.getTokenAfter(beforeNodeToken, {
  167. includeComments: true
  168. })
  169. const nextNodeComment = sourceCode.getTokenAfter(node, {
  170. includeComments: true
  171. })
  172. // get positions of what we need to remove
  173. const cutStart = getLineStartIndex(nodeComment, beforeNodeToken)
  174. const cutEnd = getLineStartIndex(nextNodeComment, node)
  175. // get space before target
  176. const beforeTargetToken = sourceCode.getTokenBefore(target)
  177. const targetComment = sourceCode.getTokenAfter(beforeTargetToken, {
  178. includeComments: true
  179. })
  180. // make insert text: comments + node + space before target
  181. const textNode = sourceCode.getText(
  182. node,
  183. node.range[0] - nodeComment.range[0]
  184. )
  185. const insertText = getInsertText(textNode, target)
  186. return [
  187. fixer.insertTextBefore(targetComment, insertText),
  188. fixer.removeRange([cutStart, cutEnd])
  189. ]
  190. }
  191. /**
  192. * Get result text to insert
  193. * @param {string} textNode
  194. * @param {ASTNode} target
  195. */
  196. function getInsertText(textNode, target) {
  197. const afterTargetComment = sourceCode.getTokenAfter(target, {
  198. includeComments: true
  199. })
  200. const afterText = sourceCode.text.slice(
  201. target.range[1],
  202. afterTargetComment.range[0]
  203. )
  204. // handle case when a();b() -> b()a();
  205. const invalidResult = !textNode.endsWith(';') && !afterText.includes('\n')
  206. return textNode + afterText + (invalidResult ? ';' : '')
  207. }
  208. /**
  209. * Get position of the beginning of the token's line(or prevToken end if no line)
  210. * @param {ASTNode|Token} token
  211. * @param {ASTNode|Token} prevToken
  212. */
  213. function getLineStartIndex(token, prevToken) {
  214. // if we have next token on the same line - get index right before that token
  215. if (token.loc.start.line === prevToken.loc.end.line) {
  216. return prevToken.range[1]
  217. }
  218. return sourceCode.getIndexFromLoc({
  219. line: token.loc.start.line,
  220. column: 0
  221. })
  222. }
  223. }
  224. module.exports = {
  225. meta: {
  226. type: 'layout',
  227. docs: {
  228. description:
  229. 'enforce order of `defineEmits` and `defineProps` compiler macros',
  230. categories: undefined,
  231. url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
  232. },
  233. fixable: 'code',
  234. schema: [
  235. {
  236. type: 'object',
  237. properties: {
  238. order: {
  239. type: 'array',
  240. items: {
  241. enum: ORDER_SCHEMA
  242. },
  243. uniqueItems: true,
  244. additionalItems: false
  245. }
  246. },
  247. additionalProperties: false
  248. }
  249. ],
  250. messages: {
  251. macrosNotOnTop:
  252. '{{macro}} should be the first statement in `<script setup>` (after any potential import statements or type definitions).'
  253. }
  254. },
  255. create
  256. }