diff --git a/docs/rules/README.md b/docs/rules/README.md index f7a2e01d5..4d87f7cc9 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -338,6 +338,7 @@ For example: | [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro | | | [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro | | | [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench: | +| [vue/valid-v-memo](./valid-v-memo.md) | enforce valid `v-memo` directives | | ### Extension Rules diff --git a/docs/rules/valid-v-memo.md b/docs/rules/valid-v-memo.md new file mode 100644 index 000000000..95d34df70 --- /dev/null +++ b/docs/rules/valid-v-memo.md @@ -0,0 +1,66 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/valid-v-memo +description: enforce valid `v-memo` directives +--- +# vue/valid-v-memo + +> enforce valid `v-memo` directives + +- :exclamation: ***This rule has not been released yet.*** + +This rule checks whether every `v-memo` directive is valid. + +## :book: Rule Details + +This rule reports `v-memo` directives in the following cases: + +- The directive has that argument. E.g. `
` +- The directive has that modifier. E.g. `
` +- The directive does not have that attribute value. E.g. `
` +- The attribute value of the directive is definitely not array. E.g. `
` +- The directive was used inside v-for. E.g. `
` + + + +```vue + +``` + + + +::: warning Note +This rule does not check syntax errors in directives because it's checked by [vue/no-parsing-error] rule. +::: + +## :wrench: Options + +Nothing. + +## :couple: Related Rules + +- [vue/no-parsing-error] + +[vue/no-parsing-error]: ./no-parsing-error.md + +## :books: Further Reading + +- [API - v-memo](https://v3.vuejs.org/api/directives.html#v-memo) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-memo.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-memo.js) diff --git a/lib/index.js b/lib/index.js index 1fe6e4eb3..d9352faa1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -188,6 +188,7 @@ module.exports = { 'valid-v-html': require('./rules/valid-v-html'), 'valid-v-if': require('./rules/valid-v-if'), 'valid-v-is': require('./rules/valid-v-is'), + 'valid-v-memo': require('./rules/valid-v-memo'), 'valid-v-model': require('./rules/valid-v-model'), 'valid-v-on': require('./rules/valid-v-on'), 'valid-v-once': require('./rules/valid-v-once'), diff --git a/lib/rules/valid-v-memo.js b/lib/rules/valid-v-memo.js new file mode 100644 index 000000000..b20ba49d4 --- /dev/null +++ b/lib/rules/valid-v-memo.js @@ -0,0 +1,120 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce valid `v-memo` directives', + // TODO Switch to `vue3-essential` in the major version. + // categories: ['vue3-essential'], + categories: undefined, + url: 'https://eslint.vuejs.org/rules/valid-v-memo.html' + }, + fixable: null, + schema: [], + messages: { + unexpectedArgument: "'v-memo' directives require no argument.", + unexpectedModifier: "'v-memo' directives require no modifier.", + expectedValue: "'v-memo' directives require that attribute value.", + expectedArray: + "'v-memo' directives require the attribute value to be an array.", + insideVFor: "'v-memo' directive does not work inside 'v-for'." + } + }, + /** @param {RuleContext} context */ + create(context) { + /** @type {VElement | null} */ + let vForElement = null + return utils.defineTemplateBodyVisitor(context, { + VElement(node) { + if (!vForElement && utils.hasDirective(node, 'for')) { + vForElement = node + } + }, + 'VElement:exit'(node) { + if (vForElement === node) { + vForElement = null + } + }, + /** @param {VDirective} node */ + "VAttribute[directive=true][key.name.name='memo']"(node) { + if (vForElement && vForElement !== node.parent.parent) { + context.report({ + node: node.key, + messageId: 'insideVFor' + }) + } + if (node.key.argument) { + context.report({ + node: node.key.argument, + messageId: 'unexpectedArgument' + }) + } + if (node.key.modifiers.length > 0) { + context.report({ + node, + loc: { + start: node.key.modifiers[0].loc.start, + end: node.key.modifiers[node.key.modifiers.length - 1].loc.end + }, + messageId: 'unexpectedModifier' + }) + } + if (!node.value || utils.isEmptyValueDirective(node, context)) { + context.report({ + node, + messageId: 'expectedValue' + }) + return + } + if (!node.value.expression) { + return + } + const expressions = [node.value.expression] + let expression + while ((expression = expressions.pop())) { + if ( + expression.type === 'ObjectExpression' || + expression.type === 'ClassExpression' || + expression.type === 'ArrowFunctionExpression' || + expression.type === 'FunctionExpression' || + expression.type === 'Literal' || + expression.type === 'TemplateLiteral' || + expression.type === 'UnaryExpression' || + expression.type === 'BinaryExpression' || + expression.type === 'UpdateExpression' + ) { + context.report({ + node: expression, + messageId: 'expectedArray' + }) + } else if (expression.type === 'AssignmentExpression') { + expressions.push(expression.right) + } else if (expression.type === 'TSAsExpression') { + expressions.push(expression.expression) + } else if (expression.type === 'SequenceExpression') { + expressions.push( + expression.expressions[expression.expressions.length - 1] + ) + } else if (expression.type === 'ConditionalExpression') { + expressions.push(expression.consequent, expression.alternate) + } + } + } + }) + } +} diff --git a/tests/lib/rules/valid-v-memo.js b/tests/lib/rules/valid-v-memo.js new file mode 100644 index 000000000..51f086213 --- /dev/null +++ b/tests/lib/rules/valid-v-memo.js @@ -0,0 +1,145 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/valid-v-memo') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2021 } +}) + +tester.run('valid-v-memo', rule, { + valid: [ + { + filename: 'test.js', + code: 'test' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + // parsing error + { + filename: 'parsing-error.vue', + code: '' + }, + // comment value (parsing error) + { + filename: 'parsing-error.vue', + code: '' + }, + // v-for + { + filename: 'test.vue', + code: '' + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + errors: ["'v-memo' directives require no argument."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'v-memo' directives require no modifier."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'v-memo' directives require that attribute value."] + }, + // empty value + { + filename: 'empty-value.vue', + code: '', + errors: ["'v-memo' directives require that attribute value."] + }, + { + filename: 'test.vue', + code: ` + `, + errors: [ + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 3, + column: 22 + }, + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 4, + column: 26 + }, + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 4, + column: 31 + }, + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 5, + column: 33 + }, + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 6, + column: 22 + }, + { + message: + "'v-memo' directives require the attribute value to be an array.", + line: 7, + column: 24 + } + ] + }, + // v-for + { + filename: 'test.vue', + code: ``, + errors: [ + { + message: "'v-memo' directive does not work inside 'v-for'.", + line: 1, + column: 40 + } + ] + } + ] +}) diff --git a/typings/eslint-plugin-vue/util-types/ast/ast.ts b/typings/eslint-plugin-vue/util-types/ast/ast.ts index 4356ebc55..68d6b8843 100644 --- a/typings/eslint-plugin-vue/util-types/ast/ast.ts +++ b/typings/eslint-plugin-vue/util-types/ast/ast.ts @@ -74,6 +74,16 @@ export type VNodeListenerMap = { | (V.VExpressionContainer & { expression: ES.Expression | null }) | null } + "VAttribute[directive=true][key.name.name='memo']": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } + "VAttribute[directive=true][key.name.name='memo']:exit": V.VDirective & { + value: + | (V.VExpressionContainer & { expression: ES.Expression | null }) + | null + } "VAttribute[directive=true][key.name.name='on']": V.VDirective & { value: | (V.VExpressionContainer & {