define-macros-order.js 8.3 KB

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