Skip to content

Commit

Permalink
Fixed false positives for Vue 3 functional component in `vue/require-…
Browse files Browse the repository at this point in the history
…direct-export` rule. (#1199)

And, add option `disallowFunctionalComponentFunction` to revert to the old behavior.
  • Loading branch information
ota-meshi committed Jun 7, 2020
1 parent 476d647 commit 376048e
Show file tree
Hide file tree
Showing 3 changed files with 285 additions and 30 deletions.
36 changes: 35 additions & 1 deletion docs/rules/require-direct-export.md
Expand Up @@ -51,7 +51,41 @@ export default ComponentA

## :wrench: Options

Nothing.
```json
{
"vue/require-direct-export": ["error", {
"disallowFunctionalComponentFunction": false
}]
}
```

- `"disallowFunctionalComponentFunction"` ... If `true`, disallow functional component functions, available in Vue 3.x. default `false`

### `"disallowFunctionalComponentFunction": false`

<eslint-code-block :rules="{'vue/require-direct-export': ['error', {disallowFunctionalComponentFunction: false}]}">

```vue
<script>
/* ✓ GOOD */
export default props => h('div', props.msg)
</script>
```

</eslint-code-block>

### `"disallowFunctionalComponentFunction": true`

<eslint-code-block :rules="{'vue/require-direct-export': ['error', {disallowFunctionalComponentFunction: true}]}">

```vue
<script>
/* ✗ BAD */
export default props => h('div', props.msg)
</script>
```

</eslint-code-block>

## :mag: Implementation

Expand Down
93 changes: 78 additions & 15 deletions lib/rules/require-direct-export.js
Expand Up @@ -6,6 +6,13 @@

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

/**
* @typedef {import('vue-eslint-parser').AST.ESLintExportDefaultDeclaration} ExportDefaultDeclaration
* @typedef {import('vue-eslint-parser').AST.ESLintDeclaration} Declaration
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
* @typedef {import('vue-eslint-parser').AST.ESLintReturnStatement} ReturnStatement
*
*/
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
Expand All @@ -19,28 +26,84 @@ module.exports = {
url: 'https://eslint.vuejs.org/rules/require-direct-export.html'
},
fixable: null, // or "code" or "whitespace"
schema: []
schema: [{
type: 'object',
properties: {
disallowFunctionalComponentFunction: { type: 'boolean' }
},
additionalProperties: false
}]
},

create (context) {
const filePath = context.getFilename()
if (!utils.isVueFile(filePath)) return {}

const disallowFunctional = (context.options[0] || {}).disallowFunctionalComponentFunction

let maybeVue3Functional
let scopeStack = null

return {
'ExportDefaultDeclaration:exit' (node) {
if (!utils.isVueFile(filePath)) return

const isObjectExpression = (
node.type === 'ExportDefaultDeclaration' &&
node.declaration.type === 'ObjectExpression'
)

if (!isObjectExpression) {
context.report({
node,
message: `Expected the component literal to be directly exported.`
})
/** @param {Declaration | Expression} node */
'ExportDefaultDeclaration > *' (node) {
if (node.type === 'ObjectExpression') {
// OK
return
}
if (!disallowFunctional) {
if (node.type === 'ArrowFunctionExpression') {
if (node.body.type !== 'BlockStatement') {
// OK
return
}
maybeVue3Functional = {
body: node.body
}
return
}
if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
maybeVue3Functional = {
body: node.body
}
return
}
}

context.report({
node: node.parent,
message: `Expected the component literal to be directly exported.`
})
},
...(disallowFunctional ? {} : {
':function > BlockStatement' (node) {
if (!maybeVue3Functional) {
return
}
scopeStack = { upper: scopeStack, withinVue3FunctionalBody: maybeVue3Functional.body === node }
},
/** @param {ReturnStatement} node */
ReturnStatement (node) {
if (scopeStack && scopeStack.withinVue3FunctionalBody && node.argument) {
maybeVue3Functional.hasReturnArgument = true
}
},
':function > BlockStatement:exit' (node) {
scopeStack = scopeStack && scopeStack.upper
},
/** @param {ExportDefaultDeclaration} node */
'ExportDefaultDeclaration:exit' (node) {
if (!maybeVue3Functional) {
return
}
if (!maybeVue3Functional.hasReturnArgument) {
context.report({
node,
message: `Expected the component literal to be directly exported.`
})
}
}
}
})
}
}
}
186 changes: 172 additions & 14 deletions tests/lib/rules/require-direct-export.js
Expand Up @@ -11,46 +11,204 @@
const rule = require('../../../lib/rules/require-direct-export')
const RuleTester = require('eslint').RuleTester

const parserOptions = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: { jsx: true }
}

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester()
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: { jsx: true }
}
})
ruleTester.run('require-direct-export', rule, {

valid: [
{
filename: 'test.vue',
code: ''
},
{
filename: 'test.vue',
code: `export default {}`
},
{
filename: 'test.vue',
code: `export default {}`,
options: [{ disallowFunctionalComponentFunction: true }]
},
{
filename: 'test.js',
code: `export default Foo`
},
{
filename: 'test.vue',
code: `
export default {}
`,
parserOptions
import { h } from 'vue'
export default function (props) {
return h('div', \`Hello! \${props.name}\`)
}
`
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default function Component () {
return h('div')
}
`
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default (props) => {
return h('div', \`Hello! \${props.name}\`)
}
`
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default props => h('div', props.msg)
`
}
],

invalid: [

{
filename: 'test.vue',
code: `
const A = {};
export default A`,
parserOptions,
const A = {};
export default A`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 3
}]
},
{
filename: 'test.vue',
code: `
function A(props) {
return h('div', props.msg)
};
export default A`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 5
}]
},
{
filename: 'test.vue',
code: `export default function NoReturn() {}`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 1
}]
},
{
filename: 'test.vue',
code: `export default function () {}`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 1
}]
},
{
filename: 'test.vue',
code: `export default () => {}`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 1
}]
},
{
filename: 'test.vue',
code: `export default () => {
const foo = () => {
return b
}
}`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 1
}]
},
{
filename: 'test.vue',
code: `export default () => {
return
}`,
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 1
}]
},
{
filename: 'test.vue',
code: `
function A(props) {
return h('div', props.msg)
};
export default A`,
options: [{ disallowFunctionalComponentFunction: true }],
errors: [{
message: 'Expected the component literal to be directly exported.',
type: 'ExportDefaultDeclaration',
line: 5
}]
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default function (props) {
return h('div', \`Hello! \${props.name}\`)
}
`,
options: [{ disallowFunctionalComponentFunction: true }],
errors: ['Expected the component literal to be directly exported.']
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default function Component () {
return h('div')
}
`,
options: [{ disallowFunctionalComponentFunction: true }],
errors: ['Expected the component literal to be directly exported.']
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default (props) => {
return h('div', \`Hello! \${props.name}\`)
}
`,
options: [{ disallowFunctionalComponentFunction: true }],
errors: ['Expected the component literal to be directly exported.']
},
{
filename: 'test.vue',
code: `
import { h } from 'vue'
export default props => h('div', props.msg)
`,
options: [{ disallowFunctionalComponentFunction: true }],
errors: ['Expected the component literal to be directly exported.']
}
]
})

0 comments on commit 376048e

Please sign in to comment.