Skip to content

Commit

Permalink
Add new rule: vue/define-macros-order (#1855)
Browse files Browse the repository at this point in the history
  • Loading branch information
edikdeisling committed Apr 19, 2022
1 parent a473a0d commit 2d6114b
Show file tree
Hide file tree
Showing 5 changed files with 598 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/rules/README.md
Expand Up @@ -12,6 +12,7 @@ sidebarDepth: 0
:bulb: Indicates that some problems reported by the rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
:::


## Base Rules (Enabling Correct ESLint Parsing)

Enforce all the rules in this category, as well as all higher priority rules, with:
Expand Down Expand Up @@ -312,6 +313,7 @@ For example:
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench: |
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
Expand Down
72 changes: 72 additions & 0 deletions docs/rules/define-macros-order.md
@@ -0,0 +1,72 @@
---
pageClass: rule-details
sidebarDepth: 0
title: vue/define-macros-order
description: enforce order of `defineEmits` and `defineProps` compiler macros
---
# vue/define-macros-order

> enforce order of `defineEmits` and `defineProps` compiler macros
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.

## :book: Rule Details

This rule reports the situation when `defineProps` or `defineEmits` not on the top or have wrong order

## :wrench: Options

```json
{
"vue/define-macros-order": ["error", {
"order": [ "defineEmits", "defineProps" ]
}]
}
```

- `order` (`string[]`) ... The order of defineEmits and defineProps macros

### `{ "order": [ "defineEmits", "defineProps" ] }` (default)

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">

```vue
<!-- ✓ GOOD -->
<script setup>
defineEmits(/* ... */)
defineProps(/* ... */)
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">

```vue
<!-- ✗ BAD -->
<script setup>
defineProps(/* ... */)
defineEmits(/* ... */)
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/define-macros-order': ['error']}">

```vue
<!-- ✗ BAD -->
<script setup>
const bar = ref()
defineEmits(/* ... */)
defineProps(/* ... */)
</script>
```

</eslint-code-block>

## :mag: Implementation

- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/define-macros-order.js)
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/define-macros-order.js)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'component-options-name-casing': require('./rules/component-options-name-casing'),
'component-tags-order': require('./rules/component-tags-order'),
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
'define-macros-order': require('./rules/define-macros-order'),
'dot-location': require('./rules/dot-location'),
'dot-notation': require('./rules/dot-notation'),
eqeqeq: require('./rules/eqeqeq'),
Expand Down
248 changes: 248 additions & 0 deletions lib/rules/define-macros-order.js
@@ -0,0 +1,248 @@
/**
* @author Eduard Deisling
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const utils = require('../utils')

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------

const MACROS_EMITS = 'defineEmits'
const MACROS_PROPS = 'defineProps'
const ORDER = [MACROS_EMITS, MACROS_PROPS]
const DEFAULT_ORDER = [MACROS_EMITS, MACROS_PROPS]

/**
* Get an index of the first statement after imports in order to place
* defineEmits and defineProps before this statement
* @param {Program} program
*/
function getStatementAfterImportsIndex(program) {
let index = -1

program.body.some((item, i) => {
index = i
return item.type !== 'ImportDeclaration'
})

return index
}

/**
* We need to handle cases like "const props = defineProps(...)"
* Define macros must be used only on top, so we can look for "Program" type
* inside node.parent.type
* @param {CallExpression|ASTNode} node
* @return {ASTNode}
*/
function getDefineMacrosStatement(node) {
if (!node.parent) {
throw new Error('Macros has parent')
}

if (node.parent.type === 'Program') {
return node
}

return getDefineMacrosStatement(node.parent)
}

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

/** @param {RuleContext} context */
function create(context) {
const scriptSetup = utils.getScriptSetupElement(context)

if (!scriptSetup) {
return {}
}

const sourceCode = context.getSourceCode()
const options = context.options
/** @type {[string, string]} */
const order = (options[0] && options[0].order) || DEFAULT_ORDER
/** @type {Map<string, ASTNode>} */
const macrosNodes = new Map()

return utils.compositingVisitors(
utils.defineScriptSetupVisitor(context, {
onDefinePropsExit(node) {
macrosNodes.set(MACROS_PROPS, getDefineMacrosStatement(node))
},
onDefineEmitsExit(node) {
macrosNodes.set(MACROS_EMITS, getDefineMacrosStatement(node))
}
}),
{
'Program:exit'(program) {
const shouldFirstNode = macrosNodes.get(order[0])
const shouldSecondNode = macrosNodes.get(order[1])
const firstStatementIndex = getStatementAfterImportsIndex(program)
const firstStatement = program.body[firstStatementIndex]

// have both defineEmits and defineProps
if (shouldFirstNode && shouldSecondNode) {
const secondStatement = program.body[firstStatementIndex + 1]

// need move only first
if (firstStatement === shouldSecondNode) {
reportNotOnTop(order[1], shouldFirstNode, firstStatement)
return
}

// need move both defineEmits and defineProps
if (firstStatement !== shouldFirstNode) {
reportBothNotOnTop(
shouldFirstNode,
shouldSecondNode,
firstStatement
)
return
}

// need move only second
if (secondStatement !== shouldSecondNode) {
reportNotOnTop(order[1], shouldSecondNode, shouldFirstNode)
}

return
}

// have only first and need to move it
if (shouldFirstNode && firstStatement !== shouldFirstNode) {
reportNotOnTop(order[0], shouldFirstNode, firstStatement)
return
}

// have only second and need to move it
if (shouldSecondNode && firstStatement !== shouldSecondNode) {
reportNotOnTop(order[1], shouldSecondNode, firstStatement)
}
}
}
)

/**
* @param {ASTNode} shouldFirstNode
* @param {ASTNode} shouldSecondNode
* @param {ASTNode} before
*/
function reportBothNotOnTop(shouldFirstNode, shouldSecondNode, before) {
context.report({
node: shouldFirstNode,
loc: shouldFirstNode.loc,
messageId: 'macrosNotOnTop',
data: {
macro: order[0]
},
fix(fixer) {
return [
...moveNodeBefore(fixer, shouldFirstNode, before),
...moveNodeBefore(fixer, shouldSecondNode, before)
]
}
})
}

/**
* @param {string} macro
* @param {ASTNode} node
* @param {ASTNode} before
*/
function reportNotOnTop(macro, node, before) {
context.report({
node,
loc: node.loc,
messageId: 'macrosNotOnTop',
data: {
macro
},
fix(fixer) {
return moveNodeBefore(fixer, node, before)
}
})
}

/**
* Move one newline with "node" to before the "beforeNode"
* @param {RuleFixer} fixer
* @param {ASTNode} node
* @param {ASTNode} beforeNode
*/
function moveNodeBefore(fixer, node, beforeNode) {
const beforeNodeToken = sourceCode.getTokenBefore(node, {
includeComments: true
})
const beforeNodeIndex = getNewLineIndex(node)
const textNode = sourceCode.getText(node, node.range[0] - beforeNodeIndex)
/** @type {[number, number]} */
const removeRange = [beforeNodeToken.range[1], node.range[1]]
const index = getNewLineIndex(beforeNode)

return [
fixer.insertTextAfterRange([index, index], textNode),
fixer.removeRange(removeRange)
]
}

/**
* Get index of first new line before the "node"
* @param {ASTNode} node
* @return {number}
*/
function getNewLineIndex(node) {
const after = sourceCode.getTokenBefore(node, { includeComments: true })
const hasWhitespace = node.loc.start.line - after.loc.end.line > 1

if (!hasWhitespace) {
return after.range[1]
}

return sourceCode.getIndexFromLoc({
line: node.loc.start.line - 1,
column: 0
})
}
}

module.exports = {
meta: {
type: 'layout',
docs: {
description:
'enforce order of `defineEmits` and `defineProps` compiler macros',
categories: undefined,
url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
order: {
type: 'array',
items: {
enum: Object.values(ORDER)
},
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
macrosNotOnTop: '{{macro}} must be on top.'
}
},
create
}

0 comments on commit 2d6114b

Please sign in to comment.