From 2a1a2a6d6ac7bbb6424f51d1c5d26b23a8ccd569 Mon Sep 17 00:00:00 2001 From: ota Date: Fri, 12 Jun 2020 20:27:53 +0900 Subject: [PATCH] Supports Optional Chaining --- .eslintrc.js | 4 +- .../components/eslint-code-block.vue | 27 +-- docs/rules/no-arrow-functions-in-watch.md | 2 +- docs/rules/no-deprecated-events-api.md | 8 + docs/rules/no-template-target-blank.md | 22 +- docs/rules/valid-v-bind-sync.md | 6 +- docs/rules/valid-v-model.md | 5 +- .../component-name-in-template-casing.js | 13 +- lib/rules/custom-event-name-casing.js | 2 +- lib/rules/html-self-closing.js | 4 +- lib/rules/no-async-in-computed-properties.js | 33 ++- lib/rules/no-deprecated-events-api.js | 25 +- lib/rules/no-deprecated-v-bind-sync.js | 2 +- .../no-deprecated-v-on-number-modifiers.js | 2 +- .../no-deprecated-vue-config-keycodes.js | 4 +- lib/rules/no-multiple-slot-args.js | 9 +- lib/rules/no-setup-props-destructure.js | 8 +- lib/rules/no-unused-properties.js | 10 +- lib/rules/no-useless-mustaches.js | 2 +- lib/rules/no-useless-v-bind.js | 11 +- lib/rules/no-watch-after-await.js | 6 +- lib/rules/order-in-components.js | 22 +- lib/rules/padding-line-between-blocks.js | 8 +- lib/rules/require-default-prop.js | 20 +- lib/rules/require-explicit-emits.js | 22 +- lib/rules/require-slots-as-functions.js | 15 +- lib/rules/require-valid-default-prop.js | 7 +- lib/rules/syntaxes/slot-attribute.js | 21 +- lib/rules/syntaxes/slot-scope-attribute.js | 21 +- lib/rules/syntaxes/v-slot.js | 2 +- lib/rules/v-on-function-call.js | 4 + lib/rules/valid-v-bind-sync.js | 69 +++++- lib/rules/valid-v-model.js | 101 +++++++-- lib/utils/index.js | 135 ++++------- tests/lib/rules/custom-event-name-casing.js | 62 ++++- .../rules/no-async-in-computed-properties.js | 90 +++++++- .../no-deprecated-dollar-listeners-api.js | 26 ++- .../no-deprecated-dollar-scopedslots-api.js | 37 ++- tests/lib/rules/no-deprecated-events-api.js | 52 ++++- .../no-deprecated-vue-config-keycodes.js | 12 +- tests/lib/rules/no-lifecycle-after-await.js | 22 +- tests/lib/rules/no-multiple-slot-args.js | 86 ++++++- tests/lib/rules/no-mutating-props.js | 68 ++++++ tests/lib/rules/no-ref-as-operand.js | 16 +- tests/lib/rules/no-setup-props-destructure.js | 34 ++- .../no-side-effects-in-computed-properties.js | 23 +- tests/lib/rules/no-unused-properties.js | 35 +++ tests/lib/rules/no-watch-after-await.js | 23 +- tests/lib/rules/order-in-components.js | 4 +- tests/lib/rules/require-default-prop.js | 23 +- tests/lib/rules/require-explicit-emits.js | 39 +++- tests/lib/rules/require-slots-as-functions.js | 24 +- tests/lib/rules/require-valid-default-prop.js | 25 ++ tests/lib/rules/this-in-template.js | 17 +- tests/lib/rules/v-on-function-call.js | 7 +- tests/lib/rules/valid-v-bind-sync.js | 38 +++- tests/lib/rules/valid-v-model.js | 32 ++- tests/lib/utils/index.js | 213 ++++++++++++------ 58 files changed, 1296 insertions(+), 364 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index f8f46d5fa..ea550d095 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -49,7 +49,9 @@ module.exports = { { pattern: `https://eslint.vuejs.org/rules/{{name}}.html` } - ] + ], + + 'eslint-plugin/fixer-return': 'off' } } ] diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue index fed685d91..391ae5490 100644 --- a/docs/.vuepress/components/eslint-code-block.vue +++ b/docs/.vuepress/components/eslint-code-block.vue @@ -32,7 +32,7 @@ export default { }, rules: { type: Object, - default () { + default() { return {} } }, @@ -46,7 +46,7 @@ export default { } }, - data () { + data() { return { linter: null, preprocess: processors['.vue'].preprocess, @@ -59,7 +59,7 @@ export default { }, computed: { - config () { + config() { return { globals: { // ES2015 globals @@ -89,7 +89,7 @@ export default { rules: this.rules, parser: 'vue-eslint-parser', parserOptions: { - ecmaVersion: 2019, + ecmaVersion: 2020, sourceType: 'module', ecmaFeatures: { jsx: true @@ -98,33 +98,30 @@ export default { } }, - code () { + code() { return `${this.computeCodeFromSlot(this.$slots.default).trim()}\n` }, - height () { + height() { const lines = this.code.split('\n').length return `${Math.max(120, 19 * lines)}px` } }, methods: { - computeCodeFromSlot (nodes) { + computeCodeFromSlot(nodes) { if (!Array.isArray(nodes)) { return '' } - return nodes.map(node => - node.text || this.computeCodeFromSlot(node.children) - ).join('') + return nodes + .map((node) => node.text || this.computeCodeFromSlot(node.children)) + .join('') } }, - async mounted () { + async mounted() { // Load linter. - const [ - { default: Linter }, - { parseForESLint } - ] = await Promise.all([ + const [{ default: Linter }, { parseForESLint }] = await Promise.all([ import('eslint4b/dist/linter'), import('espree').then(() => import('vue-eslint-parser')) ]) diff --git a/docs/rules/no-arrow-functions-in-watch.md b/docs/rules/no-arrow-functions-in-watch.md index 4378fe567..998c05f0f 100644 --- a/docs/rules/no-arrow-functions-in-watch.md +++ b/docs/rules/no-arrow-functions-in-watch.md @@ -40,7 +40,7 @@ export default { /* ... */ } ], - 'e.f': function (val, oldVal) { /* ... */ } + 'e.f': function (val, oldVal) { /* ... */ }, /* ✗ BAD */ foo: (val, oldVal) => { diff --git a/docs/rules/no-deprecated-events-api.md b/docs/rules/no-deprecated-events-api.md index 11fb96a32..b4a04fa86 100644 --- a/docs/rules/no-deprecated-events-api.md +++ b/docs/rules/no-deprecated-events-api.md @@ -26,7 +26,15 @@ export default { this.$emit('start') } } + +``` + + + + +```vue + + `, + errors: [ + "Custom event name 'fooBar' must be kebab-case.", + "Custom event name 'barBaz' must be kebab-case.", + "Custom event name 'bazQux' must be kebab-case." + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + "Custom event name 'fooBar' must be kebab-case.", + "Custom event name 'barBaz' must be kebab-case.", + "Custom event name 'bazQux' must be kebab-case." + ] } ] }) diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js index b7f0283eb..afdfc82d3 100644 --- a/tests/lib/rules/no-async-in-computed-properties.js +++ b/tests/lib/rules/no-async-in-computed-properties.js @@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-async-in-computed-properties') const RuleTester = require('eslint').RuleTester const parserOptions = { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module' } @@ -294,6 +294,34 @@ ruleTester.run('no-async-in-computed-properties', rule, { } ] }, + { + filename: 'test.vue', + code: ` + export default { + computed: { + foo: function () { + return bar?.then?.(response => {}) + } + } + } + `, + parserOptions, + errors: ['Unexpected asynchronous action in "foo" computed property.'] + }, + { + filename: 'test.vue', + code: ` + export default { + computed: { + foo: function () { + return (bar?.then)?.(response => {}) + } + } + } + `, + parserOptions, + errors: ['Unexpected asynchronous action in "foo" computed property.'] + }, { filename: 'test.vue', code: ` @@ -543,6 +571,66 @@ ruleTester.run('no-async-in-computed-properties', rule, { line: 12 } ] + }, + { + filename: 'test.vue', + code: ` + export default { + computed: { + foo: function () { + setTimeout?.(() => { }, 0) + window?.setTimeout?.(() => { }, 0) + setInterval(() => { }, 0) + window?.setInterval?.(() => { }, 0) + setImmediate?.(() => { }) + window?.setImmediate?.(() => { }) + requestAnimationFrame?.(() => {}) + window?.requestAnimationFrame?.(() => {}) + } + } + } + `, + parserOptions, + errors: [ + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.' + ] + }, + { + filename: 'test.vue', + code: ` + export default { + computed: { + foo: function () { + setTimeout?.(() => { }, 0) + ;(window?.setTimeout)?.(() => { }, 0) + setInterval(() => { }, 0) + ;(window?.setInterval)?.(() => { }, 0) + setImmediate?.(() => { }) + ;(window?.setImmediate)?.(() => { }) + requestAnimationFrame?.(() => {}) + ;(window?.requestAnimationFrame)?.(() => {}) + } + } + } + `, + parserOptions, + errors: [ + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.', + 'Unexpected timed function in "foo" computed property.' + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-dollar-listeners-api.js b/tests/lib/rules/no-deprecated-dollar-listeners-api.js index deaff0758..8644d13aa 100644 --- a/tests/lib/rules/no-deprecated-dollar-listeners-api.js +++ b/tests/lib/rules/no-deprecated-dollar-listeners-api.js @@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) ruleTester.run('no-deprecated-dollar-listeners-api', rule, { valid: [ @@ -240,6 +240,30 @@ ruleTester.run('no-deprecated-dollar-listeners-api', rule, { messageId: 'deprecated' } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'deprecated' + }, + { + messageId: 'deprecated' + } + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js index 303e5de99..a29211a22 100644 --- a/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js +++ b/tests/lib/rules/no-deprecated-dollar-scopedslots-api.js @@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, { valid: [ @@ -283,6 +283,41 @@ ruleTester.run('no-deprecated-dollar-scopedslots-api', rule, { messageId: 'deprecated' } ] + }, + { + filename: 'test.vue', + code: ` + + `, + output: ` + + `, + errors: [ + { + messageId: 'deprecated' + }, + { + messageId: 'deprecated' + } + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-events-api.js b/tests/lib/rules/no-deprecated-events-api.js index b52651aff..3022690b1 100644 --- a/tests/lib/rules/no-deprecated-events-api.js +++ b/tests/lib/rules/no-deprecated-events-api.js @@ -13,7 +13,7 @@ const rule = require('../../../lib/rules/no-deprecated-events-api') const RuleTester = require('eslint').RuleTester const parserOptions = { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module' } @@ -113,6 +113,20 @@ ruleTester.run('no-deprecated-events-api', rule, { } `, parserOptions + }, + { + filename: 'test.js', + code: ` + app.component('some-comp', { + mounted () { + // It is OK because checking whether it is deprecated. + this.$on?.('start', foo) + this.$off?.('start', foo) + this.$once?.('start', foo) + } + }) + `, + parserOptions } ], @@ -195,6 +209,42 @@ ruleTester.run('no-deprecated-events-api', rule, { line: 5 } ] + }, + { + filename: 'test.js', + code: ` + app.component('some-comp', { + mounted () { + this?.$on('start') + this?.$off('start') + this?.$once('start') + } + }) + `, + parserOptions, + errors: [ + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.', + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.', + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.' + ] + }, + { + filename: 'test.js', + code: ` + app.component('some-comp', { + mounted () { + ;(this?.$on)('start') + ;(this?.$off)('start') + ;(this?.$once)('start') + } + }) + `, + parserOptions, + errors: [ + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.', + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.', + 'The Events api `$on`, `$off` `$once` is deprecated. Using external library instead, for example mitt.' + ] } ] }) diff --git a/tests/lib/rules/no-deprecated-vue-config-keycodes.js b/tests/lib/rules/no-deprecated-vue-config-keycodes.js index eebd7943c..dae0ec800 100644 --- a/tests/lib/rules/no-deprecated-vue-config-keycodes.js +++ b/tests/lib/rules/no-deprecated-vue-config-keycodes.js @@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015 } + parserOptions: { ecmaVersion: 2020 } }) ruleTester.run('no-deprecated-vue-config-keycodes', rule, { @@ -51,6 +51,16 @@ ruleTester.run('no-deprecated-vue-config-keycodes', rule, { endColumn: 20 } ] + }, + { + filename: 'test.js', + code: 'Vue?.config?.keyCodes', + errors: ['`Vue.config.keyCodes` are deprecated.'] + }, + { + filename: 'test.js', + code: '(Vue?.config)?.keyCodes', + errors: ['`Vue.config.keyCodes` are deprecated.'] } ] }) diff --git a/tests/lib/rules/no-lifecycle-after-await.js b/tests/lib/rules/no-lifecycle-after-await.js index dd1660790..17dfada47 100644 --- a/tests/lib/rules/no-lifecycle-after-await.js +++ b/tests/lib/rules/no-lifecycle-after-await.js @@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-lifecycle-after-await') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2019, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) tester.run('no-lifecycle-after-await', rule, { @@ -204,6 +204,26 @@ tester.run('no-lifecycle-after-await', rule, { line: 18 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'forbidden' + } + ] } ] }) diff --git a/tests/lib/rules/no-multiple-slot-args.js b/tests/lib/rules/no-multiple-slot-args.js index fb42767b9..0720a951f 100644 --- a/tests/lib/rules/no-multiple-slot-args.js +++ b/tests/lib/rules/no-multiple-slot-args.js @@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) ruleTester.run('no-multiple-slot-args', rule, { valid: [ @@ -109,6 +109,90 @@ ruleTester.run('no-multiple-slot-args', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected multiple arguments.', + 'Unexpected multiple arguments.' + ] + }, { filename: 'test.vue', code: ` diff --git a/tests/lib/rules/no-mutating-props.js b/tests/lib/rules/no-mutating-props.js index 1e9e09737..e46ae0fd2 100644 --- a/tests/lib/rules/no-mutating-props.js +++ b/tests/lib/rules/no-mutating-props.js @@ -343,6 +343,52 @@ ruleTester.run('no-mutating-props', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + 'Unexpected mutation of "prop1" prop.', + 'Unexpected mutation of "prop5" prop.' + ] + }, + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + 'Unexpected mutation of "prop1" prop.', + 'Unexpected mutation of "prop2" prop.', + 'Unexpected mutation of "prop3" prop.' + ] + }, { filename: 'test.vue', code: ` @@ -419,6 +465,28 @@ ruleTester.run('no-mutating-props', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Unexpected mutation of "foo" prop.', + 'Unexpected mutation of "bar" prop.', + 'Unexpected mutation of "baz" prop.' + ] + }, { filename: 'test.vue', code: ` diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js index 5d53a5571..035b9029b 100644 --- a/tests/lib/rules/no-ref-as-operand.js +++ b/tests/lib/rules/no-ref-as-operand.js @@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-ref-as-operand') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2019, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) tester.run('no-ref-as-operand', rule, { @@ -433,6 +433,20 @@ tester.run('no-ref-as-operand', rule, { messageId: 'requireDotValue' } ] + }, + { + code: ` + + `, + errors: [ + { + messageId: 'requireDotValue' + } + ] } ] }) diff --git a/tests/lib/rules/no-setup-props-destructure.js b/tests/lib/rules/no-setup-props-destructure.js index e81d680fc..171b1c10f 100644 --- a/tests/lib/rules/no-setup-props-destructure.js +++ b/tests/lib/rules/no-setup-props-destructure.js @@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-setup-props-destructure') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2019, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) tester.run('no-setup-props-destructure', rule, { @@ -336,6 +336,38 @@ tester.run('no-setup-props-destructure', rule, { } ] }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + messageId: 'getProperty', + line: 5 + }, + { + messageId: 'getProperty', + line: 6 + }, + { + messageId: 'getProperty', + line: 7 + } + ] + }, { filename: 'test.vue', code: ` diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js index b2f341a18..400c32575 100644 --- a/tests/lib/rules/no-side-effects-in-computed-properties.js +++ b/tests/lib/rules/no-side-effects-in-computed-properties.js @@ -12,7 +12,7 @@ const rule = require('../../../lib/rules/no-side-effects-in-computed-properties' const RuleTester = require('eslint').RuleTester const parserOptions = { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module' } @@ -331,6 +331,27 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, { message: 'Unexpected side effect in "test1" computed property.' } ] + }, + { + code: `Vue.component('test', { + computed: { + test1() { + return this?.something?.reverse?.() + }, + test2() { + return (this?.something)?.reverse?.() + }, + test3() { + return (this?.something?.reverse)?.() + }, + } + })`, + parserOptions, + errors: [ + 'Unexpected side effect in "test1" computed property.', + 'Unexpected side effect in "test2" computed property.', + 'Unexpected side effect in "test3" computed property.' + ] } ] }) diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index c5796e752..838c47df8 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -981,6 +981,41 @@ tester.run('no-unused-properties', rule, { } `, options: [{ groups: ['props', 'setup'] }] + }, + // optional chaining + { + filename: 'test.vue', + code: ` + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo', 'bar'], + render: function (createElement, ctx) { + const a = ctx + const b = a?.props?.foo + const c = (a?.props)?.bar + } + }) + ` } ], diff --git a/tests/lib/rules/no-watch-after-await.js b/tests/lib/rules/no-watch-after-await.js index 9d91fbb2b..5b7a0b34a 100644 --- a/tests/lib/rules/no-watch-after-await.js +++ b/tests/lib/rules/no-watch-after-await.js @@ -8,7 +8,7 @@ const rule = require('../../../lib/rules/no-watch-after-await') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2019, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) tester.run('no-watch-after-await', rule, { @@ -91,6 +91,27 @@ tester.run('no-watch-after-await', rule, { } ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], invalid: [ diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js index 5c99608fc..76d510964 100644 --- a/tests/lib/rules/order-in-components.js +++ b/tests/lib/rules/order-in-components.js @@ -879,6 +879,7 @@ ruleTester.run('order-in-components', rule, { testYield: function* () {}, testTemplate: \`a:\${a},b:\${b},c:\${c}.\`, testNullish: a ?? b, + testOptionalChaining: a?.b?.c, name: 'burger', }; `, @@ -897,13 +898,14 @@ ruleTester.run('order-in-components', rule, { testYield: function* () {}, testTemplate: \`a:\${a},b:\${b},c:\${c}.\`, testNullish: a ?? b, + testOptionalChaining: a?.b?.c, }; `, errors: [ { message: 'The "name" property should be above the "data" property on line 3.', - line: 14 + line: 15 } ] } diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js index ce292774e..1b8e6e083 100644 --- a/tests/lib/rules/require-default-prop.js +++ b/tests/lib/rules/require-default-prop.js @@ -11,7 +11,7 @@ const rule = require('../../../lib/rules/require-default-prop') const RuleTester = require('eslint').RuleTester const parserOptions = { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module' } @@ -148,7 +148,8 @@ ruleTester.run('require-default-prop', rule, { props: { bar, baz: prop, - bar1: foo() + baz1: prop.foo, + bar2: foo() } } ` @@ -270,7 +271,7 @@ ruleTester.run('require-default-prop', rule, { ] }, - // computed propertys + // computed properties { filename: 'test.vue', code: ` @@ -355,6 +356,22 @@ ruleTester.run('require-default-prop', rule, { } `, errors: ["Prop 'foo' requires default value to be set."] + }, + { + filename: 'test.vue', + code: ` + export default { + props: { + bar, + baz: prop?.foo, + bar1: foo?.(), + } + } + `, + errors: [ + "Prop 'baz' requires default value to be set.", + "Prop 'bar1' requires default value to be set." + ] } ] }) diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index cef95dd75..d59d78600 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -10,7 +10,7 @@ const rule = require('../../../lib/rules/require-explicit-emits') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), parserOptions: { - ecmaVersion: 2019, + ecmaVersion: 2020, sourceType: 'module' } }) @@ -1514,6 +1514,43 @@ emits: {'foo': null} ] } ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'The "foo" event has been triggered but not declared on `emits` option.', + 'The "bar" event has been triggered but not declared on `emits` option.' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'The "foo" event has been triggered but not declared on `emits` option.', + 'The "bar" event has been triggered but not declared on `emits` option.' + ] } ] }) diff --git a/tests/lib/rules/require-slots-as-functions.js b/tests/lib/rules/require-slots-as-functions.js index 9d79a1ec7..ea83b5696 100644 --- a/tests/lib/rules/require-slots-as-functions.js +++ b/tests/lib/rules/require-slots-as-functions.js @@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2018, sourceType: 'module' } + parserOptions: { ecmaVersion: 2020, sourceType: 'module' } }) ruleTester.run('require-slots-as-functions', rule, { valid: [ @@ -86,29 +86,27 @@ ruleTester.run('require-slots-as-functions', rule, { } ] }, - { filename: 'test.vue', code: ` `, errors: [ - 'Property in `$slots` should be used as function.', - 'Property in `$slots` should be used as function.', - 'Property in `$slots` should be used as function.' + { messageId: 'unexpected', line: 5 }, + { messageId: 'unexpected', line: 7 }, + { messageId: 'unexpected', line: 9 } ] } ] diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js index 9f8ff7275..150029246 100644 --- a/tests/lib/rules/require-valid-default-prop.js +++ b/tests/lib/rules/require-valid-default-prop.js @@ -181,6 +181,18 @@ ruleTester.run('require-valid-default-prop', rule, { } }`, parserOptions + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: Number, + default: Number?.() + } + } + }`, + parserOptions } ], @@ -742,6 +754,19 @@ ruleTester.run('require-valid-default-prop', rule, { line: 11 } ] + }, + { + filename: 'test.vue', + code: `export default { + props: { + foo: { + type: String, + default: Number?.() + } + } + }`, + parserOptions, + errors: errorMessage('string') } ] }) diff --git a/tests/lib/rules/this-in-template.js b/tests/lib/rules/this-in-template.js index 2d7bd46c1..5f36b570e 100644 --- a/tests/lib/rules/this-in-template.js +++ b/tests/lib/rules/this-in-template.js @@ -18,7 +18,7 @@ const RuleTester = require('eslint').RuleTester const ruleTester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015 } + parserOptions: { ecmaVersion: 2020 } }) function createValidTests(prefix, options) { @@ -188,7 +188,8 @@ ruleTester.run('this-in-template', rule, { valid: ['', '', ''] .concat(createValidTests('', [])) .concat(createValidTests('', ['never'])) - .concat(createValidTests('this.', ['always'])), + .concat(createValidTests('this.', ['always'])) + .concat(createValidTests('this?.', ['always'])), invalid: [] .concat( createInvalidTests( @@ -196,6 +197,12 @@ ruleTester.run('this-in-template', rule, { [], "Unexpected usage of 'this'.", 'ThisExpression' + ), + createInvalidTests( + 'this?.', + [], + "Unexpected usage of 'this'.", + 'ThisExpression' ) ) .concat( @@ -204,6 +211,12 @@ ruleTester.run('this-in-template', rule, { ['never'], "Unexpected usage of 'this'.", 'ThisExpression' + ), + createInvalidTests( + 'this?.', + ['never'], + "Unexpected usage of 'this'.", + 'ThisExpression' ) ) .concat( diff --git a/tests/lib/rules/v-on-function-call.js b/tests/lib/rules/v-on-function-call.js index b78a92370..4ba6f5b00 100644 --- a/tests/lib/rules/v-on-function-call.js +++ b/tests/lib/rules/v-on-function-call.js @@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/v-on-function-call') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015 } + parserOptions: { ecmaVersion: 2020 } }) tester.run('v-on-function-call', rule, { @@ -106,6 +106,11 @@ tester.run('v-on-function-call', rule, {
`, options: ['never', { ignoreIncludesComment: true }] + }, + { + filename: 'test.vue', + code: '', + options: ['never'] } ], invalid: [ diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js index ea8bdf333..dfaf161ea 100644 --- a/tests/lib/rules/valid-v-bind-sync.js +++ b/tests/lib/rules/valid-v-bind-sync.js @@ -16,7 +16,7 @@ const rule = require('../../../lib/rules/valid-v-bind-sync') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015 } + parserOptions: { ecmaVersion: 2020 } }) tester.run('valid-v-bind-sync', rule, { @@ -350,6 +350,42 @@ tester.run('valid-v-bind-sync', rule, { errors: [ "'.sync' modifiers require the attribute value which is valid as LHS." ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers." + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers." + ] + }, + { + filename: 'test.vue', + code: '', + errors: [ + "Optional chaining cannot appear in 'v-bind' with '.sync' modifiers." + ] + }, + { + filename: 'test.vue', + code: '', + errors: ["'.sync' modifier has potential null object property access."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'.sync' modifier has potential null object property access."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'.sync' modifier has potential null object property access."] } ] }) diff --git a/tests/lib/rules/valid-v-model.js b/tests/lib/rules/valid-v-model.js index 9e78ef47a..2369fe4df 100644 --- a/tests/lib/rules/valid-v-model.js +++ b/tests/lib/rules/valid-v-model.js @@ -18,7 +18,7 @@ const rule = require('../../../lib/rules/valid-v-model') const tester = new RuleTester({ parser: require.resolve('vue-eslint-parser'), - parserOptions: { ecmaVersion: 2015 } + parserOptions: { ecmaVersion: 2020 } }) tester.run('valid-v-model', rule, { @@ -232,6 +232,36 @@ tester.run('valid-v-model', rule, { filename: 'empty-value.vue', code: '', errors: ["'v-model' directives require that attribute value."] + }, + { + filename: 'test.vue', + code: '', + errors: ["Optional chaining cannot appear in 'v-model' directives."] + }, + { + filename: 'test.vue', + code: '', + errors: ["Optional chaining cannot appear in 'v-model' directives."] + }, + { + filename: 'test.vue', + code: '', + errors: ["Optional chaining cannot appear in 'v-model' directives."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'v-model' directive has potential null object property access."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'v-model' directive has potential null object property access."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'v-model' directive has potential null object property access."] } ] }) diff --git a/tests/lib/utils/index.js b/tests/lib/utils/index.js index d1b8c076d..fe5bf77fa 100644 --- a/tests/lib/utils/index.js +++ b/tests/lib/utils/index.js @@ -1,44 +1,19 @@ 'use strict' const babelEslint = require('babel-eslint') +const espree = require('espree') const utils = require('../../../lib/utils/index') const chai = require('chai') const assert = chai.assert -describe('parseMemberExpression', () => { - let node - - const parse = function (code) { - return babelEslint.parse(code).body[0].expression - } - - it('should parse member expression', () => { - node = parse('this.some.nested.property') - assert.deepEqual(utils.parseMemberExpression(node), [ - 'this', - 'some', - 'nested', - 'property' - ]) - - node = parse('another.property') - assert.deepEqual(utils.parseMemberExpression(node), ['another', 'property']) - - node = parse('this.something') - assert.deepEqual(utils.parseMemberExpression(node), ['this', 'something']) - }) -}) - describe('getComputedProperties', () => { - let node - const parse = function (code) { return babelEslint.parse(code).body[0].declarations[0].init } it('should return empty array when there is no computed property', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', data() { return {} @@ -49,7 +24,7 @@ describe('getComputedProperties', () => { }) it('should return computed properties', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', data() { return {} @@ -93,7 +68,7 @@ describe('getComputedProperties', () => { }) it('should not collide with object spread operator', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', computed: { ...mapGetters(['test']), @@ -115,7 +90,7 @@ describe('getComputedProperties', () => { }) it('should not collide with object spread operator inside CP', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', computed: { foo: { @@ -138,83 +113,175 @@ describe('getComputedProperties', () => { }) describe('getStaticPropertyName', () => { - let node - const parse = function (code) { return babelEslint.parse(code).body[0].declarations[0].init } it('should parse property expression with identifier', () => { - node = parse(`const test = { computed: { } }`) + const node = parse(`const test = { computed: { } }`) const parsed = utils.getStaticPropertyName(node.properties[0]) assert.ok(parsed === 'computed') }) it('should parse property expression with literal', () => { - node = parse(`const test = { ['computed'] () {} }`) + const node = parse(`const test = { ['computed'] () {} }`) const parsed = utils.getStaticPropertyName(node.properties[0]) assert.ok(parsed === 'computed') }) it('should parse property expression with template literal', () => { - node = parse(`const test = { [\`computed\`] () {} }`) + const node = parse(`const test = { [\`computed\`] () {} }`) const parsed = utils.getStaticPropertyName(node.properties[0]) assert.ok(parsed === 'computed') }) - // it('should parse identifier', () => { - // node = parse(`const test = { computed: { } }`) +}) + +describe('getStringLiteralValue', () => { + const parse = function (code) { + return babelEslint.parse(code).body[0].declarations[0].init + } - // const parsed = utils.getStaticPropertyName(node.properties[0].key) - // assert.ok(parsed === 'computed') - // }) it('should parse literal', () => { - node = parse(`const test = { ['computed'] () {} }`) + const node = parse(`const test = { ['computed'] () {} }`) const parsed = utils.getStringLiteralValue(node.properties[0].key) assert.ok(parsed === 'computed') }) it('should parse template literal', () => { - node = parse(`const test = { [\`computed\`] () {} }`) + const node = parse(`const test = { [\`computed\`] () {} }`) const parsed = utils.getStringLiteralValue(node.properties[0].key) assert.ok(parsed === 'computed') }) }) -describe('parseMemberOrCallExpression', () => { - let node - +describe('getMemberChaining', () => { const parse = function (code) { - return babelEslint.parse(code).body[0].declarations[0].init + return espree.parse(code, { ecmaVersion: 2020 }).body[0].declarations[0] + .init } - it('should parse CallExpression', () => { - node = parse( - `const test = this.lorem['ipsum'].map(d => d.id).filter((a, b) => a > b).reduce((acc, d) => acc + d, 0)` + const jsonIgnoreKeys = ['expression', 'object'] + + it('should parse MemberExpression', () => { + const node = parse(`const test = this.lorem['ipsum'].foo.bar`) + const parsed = utils.getMemberChaining(node) + assert.equal( + nodeToJson(parsed, jsonIgnoreKeys), + nodeToJson([ + { + type: 'ThisExpression' + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'lorem' + }, + computed: false, + optional: false + }, + { + type: 'MemberExpression', + property: { + type: 'Literal', + value: 'ipsum', + raw: "'ipsum'" + }, + computed: true, + optional: false + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'foo' + }, + computed: false, + optional: false + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'bar' + }, + computed: false, + optional: false + } + ]) ) - const parsed = utils.parseMemberOrCallExpression(node) - assert.equal(parsed, 'this.lorem[].map().filter().reduce()') }) - it('should parse MemberExpression', () => { - node = parse( - `const test = this.lorem['ipsum'][0].map(d => d.id).dolor.reduce((acc, d) => acc + d, 0).sit` + it('should parse optional Chaining ', () => { + const node = parse(`const test = (this?.lorem)['ipsum']?.[0]?.foo?.bar`) + const parsed = utils.getMemberChaining(node) + assert.equal( + nodeToJson(parsed, jsonIgnoreKeys), + nodeToJson([ + { + type: 'ThisExpression' + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'lorem' + }, + computed: false, + optional: true + }, + { + type: 'MemberExpression', + property: { + type: 'Literal', + value: 'ipsum', + raw: "'ipsum'" + }, + computed: true, + optional: false + }, + { + type: 'MemberExpression', + property: { + type: 'Literal', + value: 0, + raw: '0' + }, + computed: true, + optional: true + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'foo' + }, + computed: false, + optional: true + }, + { + type: 'MemberExpression', + property: { + type: 'Identifier', + name: 'bar' + }, + computed: false, + optional: true + } + ]) ) - const parsed = utils.parseMemberOrCallExpression(node) - assert.equal(parsed, 'this.lorem[][].map().dolor.reduce().sit') }) }) describe('getRegisteredComponents', () => { - let node - const parse = function (code) { return babelEslint.parse(code).body[0].declarations[0].init } it('should return empty array when there are no components registered', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', }`) @@ -222,7 +289,7 @@ describe('getRegisteredComponents', () => { }) it('should return an array with all registered components', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', components: { ...test, @@ -249,7 +316,7 @@ describe('getRegisteredComponents', () => { }) it('should return an array of only components whose names can be identified', () => { - node = parse(`const test = { + const node = parse(`const test = { name: 'test', components: { ...test, @@ -269,15 +336,13 @@ describe('getRegisteredComponents', () => { }) describe('getComponentProps', () => { - let props - const parse = function (code) { const data = babelEslint.parse(code).body[0].declarations[0].init return utils.getComponentProps(data) } it('should return empty array when there is no component props', () => { - props = parse(`const test = { + const props = parse(`const test = { name: 'test', data() { return {} @@ -288,7 +353,7 @@ describe('getComponentProps', () => { }) it('should return empty array when component props is empty array', () => { - props = parse(`const test = { + const props = parse(`const test = { name: 'test', props: [] }`) @@ -297,7 +362,7 @@ describe('getComponentProps', () => { }) it('should return empty array when component props is empty object', () => { - props = parse(`const test = { + const props = parse(`const test = { name: 'test', props: {} }`) @@ -306,7 +371,7 @@ describe('getComponentProps', () => { }) it('should return computed props', () => { - props = parse(`const test = { + const props = parse(`const test = { name: 'test', ...test, data() { @@ -341,7 +406,7 @@ describe('getComponentProps', () => { }) it('should return computed from array props', () => { - props = parse(`const test = { + const props = parse(`const test = { name: 'test', data() { return {} @@ -382,3 +447,15 @@ describe('editdistance', () => { assert.equal(editDistance('computed', 'computd'), 1) }) }) +function nodeToJson(nodes, ignores = []) { + return JSON.stringify(nodes, replacer, 2) + function replacer(key, value) { + if (key === 'parent' || key === 'start' || key === 'end') { + return undefined + } + if (ignores.includes(key)) { + return undefined + } + return value + } +}