Skip to content

Commit 376048e

Browse files
authored
Fixed false positives for Vue 3 functional component in vue/require-direct-export rule. (#1199)
And, add option `disallowFunctionalComponentFunction` to revert to the old behavior.
1 parent 476d647 commit 376048e

File tree

3 files changed

+285
-30
lines changed

3 files changed

+285
-30
lines changed

docs/rules/require-direct-export.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,41 @@ export default ComponentA
5151

5252
## :wrench: Options
5353

54-
Nothing.
54+
```json
55+
{
56+
"vue/require-direct-export": ["error", {
57+
"disallowFunctionalComponentFunction": false
58+
}]
59+
}
60+
```
61+
62+
- `"disallowFunctionalComponentFunction"` ... If `true`, disallow functional component functions, available in Vue 3.x. default `false`
63+
64+
### `"disallowFunctionalComponentFunction": false`
65+
66+
<eslint-code-block :rules="{'vue/require-direct-export': ['error', {disallowFunctionalComponentFunction: false}]}">
67+
68+
```vue
69+
<script>
70+
/* ✓ GOOD */
71+
export default props => h('div', props.msg)
72+
</script>
73+
```
74+
75+
</eslint-code-block>
76+
77+
### `"disallowFunctionalComponentFunction": true`
78+
79+
<eslint-code-block :rules="{'vue/require-direct-export': ['error', {disallowFunctionalComponentFunction: true}]}">
80+
81+
```vue
82+
<script>
83+
/* ✗ BAD */
84+
export default props => h('div', props.msg)
85+
</script>
86+
```
87+
88+
</eslint-code-block>
5589

5690
## :mag: Implementation
5791

lib/rules/require-direct-export.js

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66

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

9+
/**
10+
* @typedef {import('vue-eslint-parser').AST.ESLintExportDefaultDeclaration} ExportDefaultDeclaration
11+
* @typedef {import('vue-eslint-parser').AST.ESLintDeclaration} Declaration
12+
* @typedef {import('vue-eslint-parser').AST.ESLintExpression} Expression
13+
* @typedef {import('vue-eslint-parser').AST.ESLintReturnStatement} ReturnStatement
14+
*
15+
*/
916
// ------------------------------------------------------------------------------
1017
// Rule Definition
1118
// ------------------------------------------------------------------------------
@@ -19,28 +26,84 @@ module.exports = {
1926
url: 'https://eslint.vuejs.org/rules/require-direct-export.html'
2027
},
2128
fixable: null, // or "code" or "whitespace"
22-
schema: []
29+
schema: [{
30+
type: 'object',
31+
properties: {
32+
disallowFunctionalComponentFunction: { type: 'boolean' }
33+
},
34+
additionalProperties: false
35+
}]
2336
},
2437

2538
create (context) {
2639
const filePath = context.getFilename()
40+
if (!utils.isVueFile(filePath)) return {}
41+
42+
const disallowFunctional = (context.options[0] || {}).disallowFunctionalComponentFunction
43+
44+
let maybeVue3Functional
45+
let scopeStack = null
2746

2847
return {
29-
'ExportDefaultDeclaration:exit' (node) {
30-
if (!utils.isVueFile(filePath)) return
31-
32-
const isObjectExpression = (
33-
node.type === 'ExportDefaultDeclaration' &&
34-
node.declaration.type === 'ObjectExpression'
35-
)
36-
37-
if (!isObjectExpression) {
38-
context.report({
39-
node,
40-
message: `Expected the component literal to be directly exported.`
41-
})
48+
/** @param {Declaration | Expression} node */
49+
'ExportDefaultDeclaration > *' (node) {
50+
if (node.type === 'ObjectExpression') {
51+
// OK
52+
return
53+
}
54+
if (!disallowFunctional) {
55+
if (node.type === 'ArrowFunctionExpression') {
56+
if (node.body.type !== 'BlockStatement') {
57+
// OK
58+
return
59+
}
60+
maybeVue3Functional = {
61+
body: node.body
62+
}
63+
return
64+
}
65+
if (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration') {
66+
maybeVue3Functional = {
67+
body: node.body
68+
}
69+
return
70+
}
71+
}
72+
73+
context.report({
74+
node: node.parent,
75+
message: `Expected the component literal to be directly exported.`
76+
})
77+
},
78+
...(disallowFunctional ? {} : {
79+
':function > BlockStatement' (node) {
80+
if (!maybeVue3Functional) {
81+
return
82+
}
83+
scopeStack = { upper: scopeStack, withinVue3FunctionalBody: maybeVue3Functional.body === node }
84+
},
85+
/** @param {ReturnStatement} node */
86+
ReturnStatement (node) {
87+
if (scopeStack && scopeStack.withinVue3FunctionalBody && node.argument) {
88+
maybeVue3Functional.hasReturnArgument = true
89+
}
90+
},
91+
':function > BlockStatement:exit' (node) {
92+
scopeStack = scopeStack && scopeStack.upper
93+
},
94+
/** @param {ExportDefaultDeclaration} node */
95+
'ExportDefaultDeclaration:exit' (node) {
96+
if (!maybeVue3Functional) {
97+
return
98+
}
99+
if (!maybeVue3Functional.hasReturnArgument) {
100+
context.report({
101+
node,
102+
message: `Expected the component literal to be directly exported.`
103+
})
104+
}
42105
}
43-
}
106+
})
44107
}
45108
}
46109
}

tests/lib/rules/require-direct-export.js

Lines changed: 172 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,46 +11,204 @@
1111
const rule = require('../../../lib/rules/require-direct-export')
1212
const RuleTester = require('eslint').RuleTester
1313

14-
const parserOptions = {
15-
ecmaVersion: 2018,
16-
sourceType: 'module',
17-
ecmaFeatures: { jsx: true }
18-
}
19-
2014
// ------------------------------------------------------------------------------
2115
// Tests
2216
// ------------------------------------------------------------------------------
2317

24-
const ruleTester = new RuleTester()
18+
const ruleTester = new RuleTester({
19+
parserOptions: {
20+
ecmaVersion: 2018,
21+
sourceType: 'module',
22+
ecmaFeatures: { jsx: true }
23+
}
24+
})
2525
ruleTester.run('require-direct-export', rule, {
2626

2727
valid: [
2828
{
2929
filename: 'test.vue',
3030
code: ''
3131
},
32+
{
33+
filename: 'test.vue',
34+
code: `export default {}`
35+
},
36+
{
37+
filename: 'test.vue',
38+
code: `export default {}`,
39+
options: [{ disallowFunctionalComponentFunction: true }]
40+
},
41+
{
42+
filename: 'test.js',
43+
code: `export default Foo`
44+
},
3245
{
3346
filename: 'test.vue',
3447
code: `
35-
export default {}
36-
`,
37-
parserOptions
48+
import { h } from 'vue'
49+
export default function (props) {
50+
return h('div', \`Hello! \${props.name}\`)
51+
}
52+
`
53+
},
54+
{
55+
filename: 'test.vue',
56+
code: `
57+
import { h } from 'vue'
58+
export default function Component () {
59+
return h('div')
60+
}
61+
`
62+
},
63+
{
64+
filename: 'test.vue',
65+
code: `
66+
import { h } from 'vue'
67+
export default (props) => {
68+
return h('div', \`Hello! \${props.name}\`)
69+
}
70+
`
71+
},
72+
{
73+
filename: 'test.vue',
74+
code: `
75+
import { h } from 'vue'
76+
export default props => h('div', props.msg)
77+
`
3878
}
3979
],
4080

4181
invalid: [
42-
4382
{
4483
filename: 'test.vue',
4584
code: `
46-
const A = {};
47-
export default A`,
48-
parserOptions,
85+
const A = {};
86+
export default A`,
4987
errors: [{
5088
message: 'Expected the component literal to be directly exported.',
5189
type: 'ExportDefaultDeclaration',
5290
line: 3
5391
}]
92+
},
93+
{
94+
filename: 'test.vue',
95+
code: `
96+
function A(props) {
97+
return h('div', props.msg)
98+
};
99+
export default A`,
100+
errors: [{
101+
message: 'Expected the component literal to be directly exported.',
102+
type: 'ExportDefaultDeclaration',
103+
line: 5
104+
}]
105+
},
106+
{
107+
filename: 'test.vue',
108+
code: `export default function NoReturn() {}`,
109+
errors: [{
110+
message: 'Expected the component literal to be directly exported.',
111+
type: 'ExportDefaultDeclaration',
112+
line: 1
113+
}]
114+
},
115+
{
116+
filename: 'test.vue',
117+
code: `export default function () {}`,
118+
errors: [{
119+
message: 'Expected the component literal to be directly exported.',
120+
type: 'ExportDefaultDeclaration',
121+
line: 1
122+
}]
123+
},
124+
{
125+
filename: 'test.vue',
126+
code: `export default () => {}`,
127+
errors: [{
128+
message: 'Expected the component literal to be directly exported.',
129+
type: 'ExportDefaultDeclaration',
130+
line: 1
131+
}]
132+
},
133+
{
134+
filename: 'test.vue',
135+
code: `export default () => {
136+
const foo = () => {
137+
return b
138+
}
139+
}`,
140+
errors: [{
141+
message: 'Expected the component literal to be directly exported.',
142+
type: 'ExportDefaultDeclaration',
143+
line: 1
144+
}]
145+
},
146+
{
147+
filename: 'test.vue',
148+
code: `export default () => {
149+
return
150+
}`,
151+
errors: [{
152+
message: 'Expected the component literal to be directly exported.',
153+
type: 'ExportDefaultDeclaration',
154+
line: 1
155+
}]
156+
},
157+
{
158+
filename: 'test.vue',
159+
code: `
160+
function A(props) {
161+
return h('div', props.msg)
162+
};
163+
export default A`,
164+
options: [{ disallowFunctionalComponentFunction: true }],
165+
errors: [{
166+
message: 'Expected the component literal to be directly exported.',
167+
type: 'ExportDefaultDeclaration',
168+
line: 5
169+
}]
170+
},
171+
{
172+
filename: 'test.vue',
173+
code: `
174+
import { h } from 'vue'
175+
export default function (props) {
176+
return h('div', \`Hello! \${props.name}\`)
177+
}
178+
`,
179+
options: [{ disallowFunctionalComponentFunction: true }],
180+
errors: ['Expected the component literal to be directly exported.']
181+
},
182+
{
183+
filename: 'test.vue',
184+
code: `
185+
import { h } from 'vue'
186+
export default function Component () {
187+
return h('div')
188+
}
189+
`,
190+
options: [{ disallowFunctionalComponentFunction: true }],
191+
errors: ['Expected the component literal to be directly exported.']
192+
},
193+
{
194+
filename: 'test.vue',
195+
code: `
196+
import { h } from 'vue'
197+
export default (props) => {
198+
return h('div', \`Hello! \${props.name}\`)
199+
}
200+
`,
201+
options: [{ disallowFunctionalComponentFunction: true }],
202+
errors: ['Expected the component literal to be directly exported.']
203+
},
204+
{
205+
filename: 'test.vue',
206+
code: `
207+
import { h } from 'vue'
208+
export default props => h('div', props.msg)
209+
`,
210+
options: [{ disallowFunctionalComponentFunction: true }],
211+
errors: ['Expected the component literal to be directly exported.']
54212
}
55213
]
56214
})

0 commit comments

Comments
 (0)