diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md index d46fc5108..7df8f42c7 100644 --- a/docs/rules/define-macros-order.md +++ b/docs/rules/define-macros-order.md @@ -27,7 +27,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they } ``` -- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"` and `"defineSlots"`. +- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"`, `"defineSlots"`, and `"defineModel"`. - `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end. ### `{ "order": ["defineProps", "defineEmits"] }` (default) @@ -69,14 +69,15 @@ defineEmits(/* ... */) -### `{ "order": ["defineOptions", "defineProps", "defineEmits", "defineSlots"] }` +### `{ "order": ["defineOptions", "defineModel", "defineProps", "defineEmits", "defineSlots"] }` - + ```vue ``` - + ```vue diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue new file mode 100644 index 000000000..7cca9156c --- /dev/null +++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel01/source.vue @@ -0,0 +1,24 @@ + diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue new file mode 100644 index 000000000..c2903d01f --- /dev/null +++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/result.vue @@ -0,0 +1,16 @@ + diff --git a/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue new file mode 100644 index 000000000..012cb9349 --- /dev/null +++ b/tests/fixtures/utils/ref-object-references/ref-objects/defineModel02/source.vue @@ -0,0 +1,16 @@ + diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js index c58fd516f..878c0ea93 100644 --- a/tests/lib/rules/define-macros-order.js +++ b/tests/lib/rules/define-macros-order.js @@ -168,6 +168,8 @@ tester.run('define-macros-order', rule, { import Foo from 'foo' /** options */ defineOptions({}) + /** model */ + const model = defineModel() /** emits */ defineEmits(['update:foo']) /** props */ @@ -179,7 +181,13 @@ tester.run('define-macros-order', rule, { `, options: [ { - order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots'] + order: [ + 'defineOptions', + 'defineModel', + 'defineEmits', + 'defineProps', + 'defineSlots' + ] } ] }, @@ -611,6 +619,8 @@ tester.run('define-macros-order', rule, { const slots = defineSlots() /** options */ defineOptions({}) + /** model */ + const model = defineModel() `, output: ` @@ -618,6 +628,8 @@ tester.run('define-macros-order', rule, { import Foo from 'foo' /** options */ defineOptions({}) + /** model */ + const model = defineModel() /** emits */ defineEmits(['update:foo']) /** props */ @@ -629,7 +641,13 @@ tester.run('define-macros-order', rule, { `, options: [ { - order: ['defineOptions', 'defineEmits', 'defineProps', 'defineSlots'] + order: [ + 'defineOptions', + 'defineModel', + 'defineEmits', + 'defineProps', + 'defineSlots' + ] } ], errors: [ @@ -651,12 +669,16 @@ tester.run('define-macros-order', rule, { defineEmits(['update:foo']) /** props */ const props = defineProps(['foo']) + /** model */ + const model = defineModel() `, output: ` + `, + ` + + `, + ` + ` ], invalid: [ @@ -719,6 +743,82 @@ tester.run('no-ref-as-operand', rule, { column: 7 } ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `defineModel()`.', + line: 6 + }, + { + message: + 'Must use `.value` to read or write the value wrapped by `defineModel()`.', + line: 9 + } + ] + }, + { + code: ` + + `, + output: ` + + `, + errors: [ + { + message: + 'Must use `.value` to read or write the value wrapped by `defineModel()`.', + line: 6 + }, + { + message: + 'Must use `.value` to read or write the value wrapped by `defineModel()`.', + line: 9 + } + ] } ] }) diff --git a/tests/lib/rules/no-ref-object-reactivity-loss.js b/tests/lib/rules/no-ref-object-reactivity-loss.js index a40f8619f..ef946188b 100644 --- a/tests/lib/rules/no-ref-object-reactivity-loss.js +++ b/tests/lib/rules/no-ref-object-reactivity-loss.js @@ -308,6 +308,31 @@ tester.run('no-ref-object-reactivity-loss', rule, { } ] }, + { + code: ` + `, + parser: require.resolve('vue-eslint-parser'), + errors: [ + { + message: + 'Getting a value from the ref object in the same scope will cause the value to lose reactivity.', + line: 6 + }, + { + message: + 'Getting a value from the ref object in the same scope will cause the value to lose reactivity.', + line: 7 + } + ] + }, // Reactivity Transform { code: ` diff --git a/tests/lib/rules/no-undef-properties.js b/tests/lib/rules/no-undef-properties.js index 7ba344197..66b928b76 100644 --- a/tests/lib/rules/no-undef-properties.js +++ b/tests/lib/rules/no-undef-properties.js @@ -1176,6 +1176,33 @@ tester.run('no-undef-properties', rule, { } ], ...getTypeScriptFixtureTestOptions() + }, + + { + // defineModel + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: "'undef' is not defined.", + line: 14 + } + ] } ] }) diff --git a/tests/lib/rules/no-unsupported-features/define-model.js b/tests/lib/rules/no-unsupported-features/define-model.js new file mode 100644 index 000000000..0307d1f9f --- /dev/null +++ b/tests/lib/rules/no-unsupported-features/define-model.js @@ -0,0 +1,59 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../../lib/rules/no-unsupported-features') +const utils = require('./utils') + +const buildOptions = utils.optionsBuilder('define-model', '^3.3.0') +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2019 + } +}) + +tester.run('no-unsupported-features/define-model', rule, { + valid: [ + { + code: ` + `, + options: buildOptions({ version: '^3.4.0' }) + }, + { + code: ` + `, + options: buildOptions() + }, + { + code: ` + `, + options: buildOptions({ version: '^3.3.0', ignores: ['define-model'] }) + } + ], + invalid: [ + { + code: ` + `, + options: buildOptions(), + errors: [ + { + message: + '`defineModel()` macros are not supported until Vue.js "3.4.0".', + line: 3 + } + ] + } + ] +}) diff --git a/tests/lib/rules/no-unused-emit-declarations.js b/tests/lib/rules/no-unused-emit-declarations.js index 10bf3b33a..f3189ea82 100644 --- a/tests/lib/rules/no-unused-emit-declarations.js +++ b/tests/lib/rules/no-unused-emit-declarations.js @@ -357,6 +357,16 @@ tester.run('no-unused-emit-declarations', rule, { const change = () => emit('foo'); `, ...getTypeScriptFixtureTestOptions() + }, + { + // defineModel + filename: 'test.vue', + code: ` + + ` } ], invalid: [ @@ -718,6 +728,22 @@ tester.run('no-unused-emit-declarations', rule, { } ], ...getTypeScriptFixtureTestOptions() + }, + { + // defineModel + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: '`update:foo` is defined as emit but never used.', + line: 3 + } + ] } ] }) diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index f71a26519..e47f346aa 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -2152,6 +2152,19 @@ tester.run('no-unused-properties', rule, { }, }; ` + }, + { + // defineModel + filename: 'test.vue', + code: ` + + + ` } ], invalid: [ @@ -3811,6 +3824,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/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/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 4d8384d66..3e9184262 100644 --- a/typings/eslint-plugin-vue/util-types/utils.ts +++ b/typings/eslint-plugin-vue/util-types/utils.ts @@ -46,10 +46,13 @@ export interface ScriptSetupVisitor extends ScriptSetupVisitorBase { 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 } @@ -187,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 +}