define-macros-order.js 8.0 KB

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