From 376048efd60b98baa7a7ae2a183d37386dc493b0 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Sun, 7 Jun 2020 17:26:27 +0900 Subject: [PATCH] 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. --- docs/rules/require-direct-export.md | 36 ++++- lib/rules/require-direct-export.js | 93 ++++++++++-- tests/lib/rules/require-direct-export.js | 186 +++++++++++++++++++++-- 3 files changed, 285 insertions(+), 30 deletions(-) diff --git a/docs/rules/require-direct-export.md b/docs/rules/require-direct-export.md index 63d9fb19e..e51751df0 100644 --- a/docs/rules/require-direct-export.md +++ b/docs/rules/require-direct-export.md @@ -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` + + + +```vue + +``` + + + +### `"disallowFunctionalComponentFunction": true` + + + +```vue + +``` + + ## :mag: Implementation diff --git a/lib/rules/require-direct-export.js b/lib/rules/require-direct-export.js index 41e1a9824..2bc0abd12 100644 --- a/lib/rules/require-direct-export.js +++ b/lib/rules/require-direct-export.js @@ -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 // ------------------------------------------------------------------------------ @@ -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.` + }) + } } - } + }) } } } diff --git a/tests/lib/rules/require-direct-export.js b/tests/lib/rules/require-direct-export.js index 72f71bf93..16b6aa96a 100644 --- a/tests/lib/rules/require-direct-export.js +++ b/tests/lib/rules/require-direct-export.js @@ -11,17 +11,17 @@ 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: [ @@ -29,28 +29,186 @@ ruleTester.run('require-direct-export', rule, { 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.'] } ] })