diff --git a/docs/index.md b/docs/index.md index f9b1e49f1..3fd2b6d66 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,11 +14,6 @@ This plugin allows us to check the ` - `, - options: allOptions + ` }, - - // async data passed in a component + // a property passed in a component as $props member expression { filename: 'test.vue', code: ` - `, - options: allOptions + ` }, - // ignores unused data when marked with eslint-disable + // a property used in v-on { filename: 'test.vue', code: ` + + ` + }, + // a property used in v-on as $props member expression + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in a script expression + { + filename: 'test.vue', + code: ` @@ -719,35 +646,38 @@ tester.run('no-unused-properties', rule, { options: allOptions }, - // trace this + // data being watched { filename: 'test.vue', code: ` - ` + `, + options: allOptions }, { filename: 'test.vue', code: ` ` @@ -757,24 +687,31 @@ tester.run('no-unused-properties', rule, { code: ` - ` + `, + options: allOptions }, - // use rest + + // data used as a template identifier { filename: 'test.vue', code: ` - ` + `, + options: allOptions }, - // function trace + // data used in a template expression { filename: 'test.vue', code: ` + - ` + `, + options: allOptions }, + + // data used in v-if { filename: 'test.vue', code: ` + - ` + `, + options: allOptions }, - // render & functional + // data used in v-for { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('smart-list', { - functional: true, - props: { - items: { - type: Array, - required: true - }, - isOrdered: Boolean - }, - render: function (createElement, context) { - function appropriateListComponent () { - var items = context.props.items - - if (items.length === 0) return EmptyList - if (typeof items[0] === 'object') return TableList - if (context.props.isOrdered) return OrderedList - - return UnorderedList - } - - return createElement( - appropriateListComponent(), - context.data, - context.children - ) - } - }) - ` + + + `, + options: allOptions }, + + // data used in v-html { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, {props}) { - return createElement('button', props.foo) - } - }) - ` + + + `, + options: allOptions }, - { - filename: 'test.js', - code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, ctx) { - return createElement('button', fn(ctx.props)) - } - }) - function fn(props) { - return props.foo - } - ` - }, + // data used in v-model { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, ctx) { - return createElement('button', fn(ctx)) - } - }) - + + + `, + options: allOptions + }, + + // data passed in a component + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // data used in v-on + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in a script expression + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // computed property being watched + { + filename: 'test.vue', + code: ` + + `, + options: allOptions + }, + + // computed property used as a template identifier + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed properties used in a template expression + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-if + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-for + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-html + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property used in v-model + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // computed property passed in a component + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // async data passed in a component + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // ignores unused data when marked with eslint-disable + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions + }, + + // trace this + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // use rest + { + filename: 'test.vue', + code: ` + + + ` + }, + + // function trace + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + + // render & functional + { + filename: 'test.js', + code: ` + Vue.component('smart-list', { + functional: true, + props: { + items: { + type: Array, + required: true + }, + isOrdered: Boolean + }, + render: function (createElement, context) { + function appropriateListComponent () { + var items = context.props.items + + if (items.length === 0) return EmptyList + if (typeof items[0] === 'object') return TableList + if (context.props.isOrdered) return OrderedList + + return UnorderedList + } + + return createElement( + appropriateListComponent(), + context.data, + context.children + ) + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, {props}) { + return createElement('button', props.foo) + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, ctx) { + return createElement('button', fn(ctx.props)) + } + }) + + function fn(props) { + return props.foo + } + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, ctx) { + return createElement('button', fn(ctx)) + } + }) + function fn({props}) { return props.foo } ` }, { - filename: 'test.js', + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, {props:{foo}}) { + return createElement('button', foo) + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, {props:[bar]}) { + return createElement('button') + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, {props:bar={}}) { + return createElement('button', bar.foo) + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo'], + render: function (createElement, {...foo}) { + return createElement('button') + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo', 'bar'], + render: function (createElement, ctx) { + const a = ctx.props + const b = ctx.props + return createElement('button', a.foo + b.bar) + } + }) + ` + }, + { + filename: 'test.js', + code: ` + Vue.component('MyButton', { + functional: true, + props: ['foo', 'bar'], + render: function (createElement, {props: a, props: b}) { + return createElement('button', a.foo + b.bar) + } + }) + ` + }, + // render for Vue 3.x + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + render (props) { + return h('button', props.foo) + } + }) + ` + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + render ({foo}) { + return h('button', foo) + } + }) + ` + }, + { + filename: 'test.vue', + code: ` + export default { + props: ['foo'], + render (bar) { + const {...baz} = bar + return h('button') + } + }) + ` + }, + // Vue.js 3.x Template Refs + { + filename: 'test.vue', + code: ` + + + `, + options: [{ groups: ['props', 'setup'] }] + }, + + // sparse array + { + filename: 'test.vue', + code: ` + + + ` + }, + // 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 + } + }) + ` + }, + // handlers + { + filename: 'test.vue', + code: ` + + `, + options: [{ groups: ['props', 'methods'] }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ groups: ['props', 'data'] }] + }, + // contexts + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, {props:{foo}}) { - return createElement('button', foo) - } - }) + ` }, { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, {props:[bar]}) { - return createElement('button') - } - }) + ` }, + + // deep data { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, {props:bar={}}) { - return createElement('button', bar.foo) - } - }) - ` + + + `, + options: deepDataOptions }, { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo'], - render: function (createElement, {...foo}) { - return createElement('button') - } - }) - ` + + `, + options: deepDataOptions + }, + { + filename: 'test.vue', + code: ` + + `, + options: deepDataOptions + }, + { + filename: 'test.vue', + code: ` + + + `, + options: deepDataOptions + }, + { + filename: 'test.vue', + code: ` + + `, + options: deepDataOptions + }, + { + filename: 'test.vue', + code: ` + + + + `, + options: deepDataOptions + }, + { + filename: 'test.vue', + code: ` + + + + `, + options: deepDataOptions }, { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo', 'bar'], - render: function (createElement, ctx) { - const a = ctx.props - const b = ctx.props - return createElement('button', a.foo + b.bar) - } - }) - ` + + + + `, + options: deepDataOptions }, { - filename: 'test.js', + filename: 'test.vue', code: ` - Vue.component('MyButton', { - functional: true, - props: ['foo', 'bar'], - render: function (createElement, {props: a, props: b}) { - return createElement('button', a.foo + b.bar) - } - }) - ` + + `, + options: deepDataOptions }, - // render for Vue 3.x { filename: 'test.vue', code: ` + + `, + options: deepDataOptions }, + + // ignore public members { filename: 'test.vue', code: ` - export default { - props: ['foo'], - render ({foo}) { - return h('button', foo) + + `, + options: [{ groups: ['data'], ignorePublicMembers: true }] }, { filename: 'test.vue', code: ` - export default { - props: ['foo'], - render (bar) { - const {...baz} = bar - return h('button') + + `, + options: [{ groups: ['computed'], ignorePublicMembers: true }] }, - // Vue.js 3.x Template Refs { filename: 'test.vue', code: ` - - - `, - options: [{ groups: ['props', 'setup'] }] + + `, + options: [{ groups: ['methods'], ignorePublicMembers: true }] }, - - // sparse array { filename: 'test.vue', code: ` - - ` + `, + options: [{ groups: ['setup'], ignorePublicMembers: true }] }, - // optional chaining { filename: 'test.vue', code: ` + `, + options: [ + { + groups: ['props', 'data', 'computed', 'methods', 'setup'], + ignorePublicMembers: true } - - function fn(a) { - return a?.foo + a?.bar - } - ` + ] }, { - filename: 'test.js', + filename: 'test.vue', 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 + + `, + options: [ + { + groups: ['props', 'computed'], + ignorePublicMembers: true } - }) - ` + ] }, - // handlers + + // expose { filename: 'test.vue', code: ` + - `, - options: [{ groups: ['props', 'methods'] }] + }, + computed:{ + c() {}, + }, + methods:{ + d() {}, + } + } + `, + options: allOptions }, + + //style vars { filename: 'test.vue', code: ` - - `, - options: [{ groups: ['props', 'data'] }] + + + + ` }, - // contexts + + // toRefs { + // https://github.com/vuejs/eslint-plugin-vue/issues/1643 filename: 'test.vue', code: ` - - ` + + `, + parserOptions: { + parser: '@typescript-eslint/parser' + } }, + + // Vue2 functional component { filename: 'test.vue', code: ` + + - ` + export default { + props: { + a: String, + b: String, + }, + }; + ` }, { + // defineModel filename: 'test.vue', code: ` - + ` - }, - - // deep data + } + ], + invalid: [ { filename: 'test.vue', code: ` + + `, + errors: [ + { + message: "'add2' of method found, but never used.", + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 6 + } + ] }, { filename: 'test.vue', code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 4 + } + ] }, { filename: 'test.vue', code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 6 + } + ] }, { filename: 'test.vue', code: ` - + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 6 + } + ] }, { filename: 'test.vue', code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 5 + } + ] }, { filename: 'test.vue', code: ` + - + `, + errors: [ + { + message: "'add2' of method found, but never used.", + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'add2' of method found, but never used.", + line: 5 + } + ] }, { filename: 'test.vue', code: ` + - + `, + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 5 + } + ] + }, + { + filename: 'test.vue', + code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 6 + } + ] }, { filename: 'test.vue', code: ` - - + `, - options: deepDataOptions + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 5 + } + ] }, { filename: 'test.vue', code: ` + `, - options: deepDataOptions + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 5 + } + ] }, { filename: 'test.vue', code: ` - - + + `, + errors: [ + { + message: "'count' of computed property found, but never used.", + line: 4 } - } - `, - options: deepDataOptions + ] }, - - // ignore public members { filename: 'test.vue', code: ` + `, - options: [{ groups: ['data'], ignorePublicMembers: true }] + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 5 + } + ] }, { filename: 'test.vue', code: ` + `, - options: [{ groups: ['computed'], ignorePublicMembers: true }] + errors: [ + { + message: "'count3' of computed property found, but never used.", + line: 7 + } + ] }, { filename: 'test.vue', code: ` + `, - options: [{ groups: ['methods'], ignorePublicMembers: true }] + errors: [ + { + message: "'a' of computed property found, but never used.", + line: 6 + } + ] }, { filename: 'test.vue', code: ` + `, - options: [{ groups: ['setup'], ignorePublicMembers: true }] - }, - { - filename: 'test.vue', - code: ` - - `, - options: [ + errors: [ { - groups: ['props', 'data', 'computed', 'methods', 'setup'], - ignorePublicMembers: true + message: "'a' of computed property found, but never used.", + line: 6 } ] }, - // expose + // vuex unused state { filename: 'test.vue', code: ` - - + + `, + errors: [ + { + message: "'count' of computed property found, but never used.", + line: 5 } - } - `, - options: allOptions + ] }, - //style vars + // vuex unused state { filename: 'test.vue', code: ` - - - - - ` + + `, + errors: [ + { + message: "'count2' of computed property found, but never used.", + line: 6 + } + ] }, - // toRefs + // vuex unused state { - // https://github.com/vuejs/eslint-plugin-vue/issues/1643 filename: 'test.vue', code: ` - - - + + `, + errors: [ { - currentPage: 1, - totalRows: 100, - rowsPerPage: 10, - } - ); - const { currentPage, totalRows, rowsPerPage } = toRefs(props); - - const totalPages = computed(() => - Math.ceil(totalRows.value / rowsPerPage.value) - ); - - const pages: ComputedRef<(number | '...')[]> = computed(() => - getPagesOnPagination(currentPage.value, totalPages.value) - ); - - const emit = defineEmits<{ - (e: 'changePage', page: number): void; - }>(); - - function changePage(page: number | '...') { - if (page === '...') { - return; + message: "'additional' of computed property found, but never used.", + line: 5 } - emit('changePage', page); - } - - - `, - parserOptions: { - parser: '@typescript-eslint/parser' - } + ] }, - // Vue2 functional component - { - filename: 'test.vue', - code: ` - - - ` - } - ], - invalid: [ // unused property { filename: 'test.vue', @@ -2982,6 +3871,28 @@ tester.run('no-unused-properties', rule, { } ], ...getTypeScriptFixtureTestOptions() + }, + + { + // defineModel + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: "'unused' of property found, but never used.", + line: 6 + } + ] } ] }) diff --git a/tests/lib/rules/no-v-text-v-html-on-component.js b/tests/lib/rules/no-v-text-v-html-on-component.js index c50071346..1dafc17d8 100644 --- a/tests/lib/rules/no-v-text-v-html-on-component.js +++ b/tests/lib/rules/no-v-text-v-html-on-component.js @@ -40,6 +40,25 @@ tester.run('no-v-text-v-html-on-component', rule, { ` + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ allow: ['router-link'] }] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ allow: ['RouterLink', 'nuxt-link'] }] } ], invalid: [ @@ -132,6 +151,22 @@ tester.run('no-v-text-v-html-on-component', rule, { column: 30 } ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ allow: ['nuxt-link'] }], + errors: [ + { + message: "Using v-html on component may break component's content.", + line: 3, + column: 22 + } + ] } ] }) diff --git a/tests/lib/rules/require-explicit-emits.js b/tests/lib/rules/require-explicit-emits.js index af90569dd..89ae47627 100644 --- a/tests/lib/rules/require-explicit-emits.js +++ b/tests/lib/rules/require-explicit-emits.js @@ -461,6 +461,19 @@ tester.run('require-explicit-emits', rule, { `, parserOptions: { parser: require.resolve('@typescript-eslint/parser') } }, + { + filename: 'test.vue', + code: ` + + + `, + parserOptions: { parser: require.resolve('@typescript-eslint/parser') } + }, // unknown emits definition { @@ -1983,6 +1996,25 @@ emits: {'foo': null} } ], ...getTypeScriptFixtureTestOptions() + }, + { + filename: 'test.vue', + code: ` + + + `, + parserOptions: { parser: require.resolve('@typescript-eslint/parser') }, + errors: [ + { + message: + 'The "bar" event has been triggered but not declared on `defineEmits`.', + line: 3 + } + ] } ] }) diff --git a/tests/lib/rules/require-prop-types.js b/tests/lib/rules/require-prop-types.js index fdb91dd30..f50b4b116 100644 --- a/tests/lib/rules/require-prop-types.js +++ b/tests/lib/rules/require-prop-types.js @@ -187,6 +187,40 @@ ruleTester.run('require-prop-types', rule, { defineProps() `, ...getTypeScriptFixtureTestOptions() + }, + { + // defineModel + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + // defineModel + code: ` + + `, + parser: require.resolve('vue-eslint-parser') + }, + { + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 6, + sourceType: 'module', + parser: require.resolve('@typescript-eslint/parser') + } } ], @@ -368,6 +402,46 @@ ruleTester.run('require-prop-types', rule, { line: 3 } ] + }, + { + // defineModel + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: 'Prop "modelValue" should define at least its type.', + line: 3 + }, + { + message: 'Prop "foo" should define at least its type.', + line: 4 + } + ] + }, + { + // defineModel + code: ` + + `, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: 'Prop "modelValue" should define at least its type.', + line: 3 + }, + { + message: 'Prop "foo" should define at least its type.', + line: 4 + } + ] } ] }) diff --git a/tests/lib/rules/script-setup-uses-vars.js b/tests/lib/rules/script-setup-uses-vars.js index 9d20f5748..93f20d551 100644 --- a/tests/lib/rules/script-setup-uses-vars.js +++ b/tests/lib/rules/script-setup-uses-vars.js @@ -23,6 +23,21 @@ const ruleTester = new RuleTester({ const linter = ruleTester.linter || eslint.linter linter.defineRule('script-setup-uses-vars', rule) +ruleTester.run('script-setup-uses-vars', rule, { + // Visually check that there are no warnings in the console. + valid: [ + ` + + + + ` + ], + invalid: [] +}) describe('script-setup-uses-vars', () => { ruleTester.run('no-unused-vars', ruleNoUnusedVars, { valid: [ diff --git a/tests/lib/rules/v-bind-style.js b/tests/lib/rules/v-bind-style.js index 0cb67f62f..5b4e88ab2 100644 --- a/tests/lib/rules/v-bind-style.js +++ b/tests/lib/rules/v-bind-style.js @@ -106,6 +106,21 @@ tester.run('v-bind-style', rule, { output: '', options: ['longform'], errors: ["Expected 'v-bind:' instead of '.'."] + }, + // v-bind same-name shorthand (Vue 3.4+) + { + filename: 'test.vue', + code: '', + output: '', + options: ['shorthand'], + errors: ["Unexpected 'v-bind' before ':'."] + }, + { + filename: 'test.vue', + code: '', + output: '', + options: ['longform'], + errors: ["Expected 'v-bind' before ':'."] } ] }) diff --git a/tests/lib/rules/valid-v-bind.js b/tests/lib/rules/valid-v-bind.js index 05c3d2b03..2259f9129 100644 --- a/tests/lib/rules/valid-v-bind.js +++ b/tests/lib/rules/valid-v-bind.js @@ -71,6 +71,15 @@ tester.run('valid-v-bind', rule, { filename: 'test.vue', code: "" }, + // v-bind same-name shorthand (Vue 3.4+) + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, // parsing error { filename: 'parsing-error.vue', @@ -88,11 +97,6 @@ tester.run('valid-v-bind', rule, { code: '', errors: ["'v-bind' directives require an attribute value."] }, - { - filename: 'test.vue', - code: '', - errors: ["'v-bind' directives require an attribute value."] - }, { filename: 'test.vue', code: "", diff --git a/tests/lib/utils/ref-object-references.js b/tests/lib/utils/ref-object-references.js index 66ee89d0c..5cd473f8f 100644 --- a/tests/lib/utils/ref-object-references.js +++ b/tests/lib/utils/ref-object-references.js @@ -3,6 +3,7 @@ const fs = require('fs') const path = require('path') const assert = require('assert') +const vueESLintParser = require('vue-eslint-parser') const Linter = require('eslint').Linter @@ -18,23 +19,45 @@ const FIXTURE_ROOT = path.resolve( const REF_OBJECTS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'ref-objects') const REACTIVE_VARS_FIXTURE_ROOT = path.resolve(FIXTURE_ROOT, 'reactive-vars') +/** + * @typedef {object} LoadedPattern + * @property {string} code The code to test. + * @property {string} name The name of the pattern. + * @property {string} sourceFilePath + * @property {string} resultFilePath + * @property {object} [options] + * @property {string} [options.parser] + */ /** * Load test patterns from fixtures. * - * @returns {object} The loaded patterns. + * @returns {LoadedPattern[]} The loaded patterns. */ function loadPatterns(rootDir) { return fs.readdirSync(rootDir).map((name) => { - const code0 = fs.readFileSync(path.join(rootDir, name, 'source.js'), 'utf8') - const code = code0.replace(/^/, ``) - return { code, name } + for (const [sourceFile, resultFile, options] of [ + ['source.js', 'result.js'], + ['source.vue', 'result.vue', { parser: 'vue-eslint-parser' }] + ]) { + const sourceFilePath = path.join(rootDir, name, sourceFile) + if (fs.existsSync(sourceFilePath)) { + return { + code: fs.readFileSync(sourceFilePath, 'utf8'), + name, + sourceFilePath, + resultFilePath: path.join(rootDir, name, resultFile), + options + } + } + } }) } -function extractRefs(code, extract) { +function extractRefs(code, extract, options) { const linter = new Linter() const references = [] + linter.defineParser('vue-eslint-parser', vueESLintParser) linter.defineRule('vue/extract-test', (context) => { const refs = extract(context) @@ -56,6 +79,7 @@ function extractRefs(code, extract) { const messages = linter.verify( code, { + ...options, parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, rules: { 'vue/extract-test': 'error' }, globals: { @@ -81,11 +105,15 @@ function extractRefs(code, extract) { } describe('extractRefObjectReferences()', () => { - for (const { name, code } of loadPatterns(REF_OBJECTS_FIXTURE_ROOT)) { - describe(`'test/fixtures/utils/ref-object-references/ref-objects/${name}/source.vue'`, () => { + for (const { code, sourceFilePath, resultFilePath, options } of loadPatterns( + REF_OBJECTS_FIXTURE_ROOT + )) { + describe(sourceFilePath, () => { it('should to extract the references to match the expected references.', () => { /** @type {import('../../../lib/utils/ref-object-references').RefObjectReference[]} */ - const references = [...extractRefs(code, extractRefObjectReferences)] + const references = [ + ...extractRefs(code, extractRefObjectReferences, options) + ] let result = '' let start = 0 @@ -104,29 +132,26 @@ describe('extractRefObjectReferences()', () => { const actual = result - // update fixture - // fs.writeFileSync( - // path.join(REF_OBJECTS_FIXTURE_ROOT, name, 'result.js'), - // actual, - // 'utf8' - // ) - - const expected = fs.readFileSync( - path.join(REF_OBJECTS_FIXTURE_ROOT, name, 'result.js'), - 'utf8' - ) + if (!fs.existsSync(resultFilePath)) { + // update fixture + fs.writeFileSync(resultFilePath, actual, 'utf8') + } + + const expected = fs.readFileSync(resultFilePath, 'utf8') assert.strictEqual(actual, expected) }) }) } }) describe('extractReactiveVariableReferences()', () => { - for (const { name, code } of loadPatterns(REACTIVE_VARS_FIXTURE_ROOT)) { - describe(`'test/fixtures/utils/ref-object-references/reactive-vars/${name}/source.vue'`, () => { + for (const { code, sourceFilePath, resultFilePath, options } of loadPatterns( + REACTIVE_VARS_FIXTURE_ROOT + )) { + describe(sourceFilePath, () => { it('should to extract the references to match the expected references.', () => { /** @type {import('../../../lib/utils/ref-object-references').ReactiveVariableReference[]} */ const references = [ - ...extractRefs(code, extractReactiveVariableReferences) + ...extractRefs(code, extractReactiveVariableReferences, options) ] let result = '' @@ -146,17 +171,12 @@ describe('extractReactiveVariableReferences()', () => { const actual = result - // update fixture - // fs.writeFileSync( - // path.join(REACTIVE_VARS_FIXTURE_ROOT, name, 'result.js'), - // actual, - // 'utf8' - // ) - - const expected = fs.readFileSync( - path.join(REACTIVE_VARS_FIXTURE_ROOT, name, 'result.js'), - 'utf8' - ) + if (!fs.existsSync(resultFilePath)) { + // update fixture + fs.writeFileSync(resultFilePath, actual, 'utf8') + } + + const expected = fs.readFileSync(resultFilePath, 'utf8') assert.strictEqual(actual, expected) }) }) diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts index fc3676a56..3205dc74e 100644 --- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts @@ -641,7 +641,7 @@ export type AssignmentProperty = | AssignmentPropertyComputedName export interface ArrayPattern extends HasParentNode { type: 'ArrayPattern' - elements: Pattern[] + elements: (Pattern | null)[] } export interface RestElement extends HasParentNode { type: 'RestElement' diff --git a/typings/eslint-plugin-vue/util-types/utils.ts b/typings/eslint-plugin-vue/util-types/utils.ts index b2704769f..3e9184262 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -44,10 +44,15 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { onDefineOptionsExit?(node: CallExpression): void onDefineSlotsEnter?(node: CallExpression): void onDefineSlotsExit?(node: CallExpression): void + onDefineExposeEnter?(node: CallExpression): void + onDefineExposeExit?(node: CallExpression): void + onDefineModelEnter?(node: CallExpression, model: ComponentModel): void + onDefineModelExit?(node: CallExpression, model: ComponentModel): void [query: string]: | ((node: VAST.ParamNode) => void) | ((node: CallExpression, props: ComponentProp[]) => void) | ((node: CallExpression, emits: ComponentEmit[]) => void) + | ((node: CallExpression, model: ComponentModel) => void) | undefined } @@ -185,3 +190,13 @@ export type ComponentEmit = | ComponentTypeEmit | ComponentInferTypeEmit | ComponentUnknownEmit + +export type ComponentModelName = { + modelName: string + node: Literal | null +} +export type ComponentModel = { + name: ComponentModelName + options: Expression | null + typeNode: TypeNode | null +} diff --git a/typings/eslint/index.d.ts b/typings/eslint/index.d.ts index 7d85f665f..a11b29f11 100644 --- a/typings/eslint/index.d.ts +++ b/typings/eslint/index.d.ts @@ -18,7 +18,8 @@ export namespace Scope { scopes: Scope[] globalScope: Scope | null acquire(node: VAST.ESNode | VAST.Program, inner?: boolean): Scope | null - getDeclaredVariables(node: VAST.ESNode): Variable[] + /** @since ESLint v8.38.0 */ + getDeclaredVariables?(node: VAST.ESNode): Variable[] } interface Scope { type: @@ -230,6 +231,11 @@ export class SourceCode /*extends ESLintSourceCode*/ { getCommentsBefore(nodeOrToken: VNODE.HasLocation): VNODE.Comment[] getCommentsAfter(nodeOrToken: VNODE.HasLocation): VNODE.Comment[] getCommentsInside(node: VNODE.HasLocation): VNODE.Comment[] + + /** @since ESLint v8.39.0 */ + markVariableAsUsed?(name: string, node?: VNODE.HasLocation): void + /** @since ESLint v8.37.0 */ + getScope?(node: VNODE.HasLocation): Scope.Scope } export namespace SourceCode { interface Config { @@ -317,21 +323,35 @@ export namespace Rule { id: string options: ESLintRule.RuleContext['options'] settings: { [name: string]: any } - parserPath: string - parserOptions: any - parserServices: parserServices.ParserServices - - getAncestors(): VAST.ESNode[] - - getDeclaredVariables(node: VAST.ESNode): Scope.Variable[] + /** @deprecated removed in ESLint v10? */ + parserPath?: string + /** @deprecated removed in ESLint v10? */ + parserOptions?: ESLintLinter.ParserOptions + /** For flat config */ + languageOptions?: ESLintLinter.FlatConfig['languageOptions'] + /** @deprecated removed in ESLint v9 */ + parserServices?: parserServices.ParserServices + + /** @deprecated removed in ESLint v9 */ + getAncestors?(): VAST.ESNode[] + /** @deprecated removed in ESLint v9 */ + getDeclaredVariables?(node: VAST.ESNode): Scope.Variable[] getFilename(): string - getScope(): Scope.Scope + /** @since ESLint v8.40.0 */ + filename?: string + /** @deprecated removed in ESLint v9 */ + getScope?(): Scope.Scope getSourceCode(): SourceCode - markVariableAsUsed(name: string): boolean + /** @since ESLint v8.40.0 */ + sourceCode?: SourceCode + /** @deprecated removed in ESLint v9 */ + markVariableAsUsed?(name: string): boolean report(descriptor: ReportDescriptor): void // eslint@6 does not have this method. getCwd?: () => string + /** @since ESLint v8.40.0 */ + cwd?: string } type ReportDescriptor =