Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new rule: vue/define-macros-order (#1855) #1856

Merged
merged 8 commits into from Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved

## :wrench: Options

```json
{
"vue/define-macros-order": ["error", {
"order": [ "defineEmits", "defineProps" ]
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved
}]
}
```

- `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]
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved
/** @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))
}
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
}),
{
'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,
FloEdelmann marked this conversation as resolved.
Show resolved Hide resolved
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
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved

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.'
edikdeisling marked this conversation as resolved.
Show resolved Hide resolved
}
},
create
}