diff --git a/docs/rules/README.md b/docs/rules/README.md index 481f39099..f45d9c68a 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -166,6 +166,7 @@ For example: | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: | | [vue/v-on-function-call](./v-on-function-call.md) | enforce or forbid parentheses after method calls without arguments in `v-on` directives | :wrench: | | [vue/v-slot-style](./v-slot-style.md) | enforce `v-slot` directive style | :wrench: | +| [vue/valid-v-bind-sync](./valid-v-bind-sync.md) | enforce valid `.sync` modifier on `v-bind` directives | | | [vue/valid-v-slot](./valid-v-slot.md) | enforce valid `v-slot` directives | | ## Deprecated diff --git a/docs/rules/valid-v-bind-sync.md b/docs/rules/valid-v-bind-sync.md new file mode 100644 index 000000000..6e8d9504e --- /dev/null +++ b/docs/rules/valid-v-bind-sync.md @@ -0,0 +1,70 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/valid-v-bind-sync +description: enforce valid `.sync` modifier on `v-bind` directives +--- +# vue/valid-v-bind-sync +> enforce valid `.sync` modifier on `v-bind` directives + +This rule checks whether every `.sync` modifier on `v-bind` directives is valid. + +## :book: Rule Details + +This rule reports `.sync` modifier on `v-bind` directives in the following cases: + +- The `.sync` modifier does not have the attribute value which is valid as LHS. E.g. `` +- The `.sync` modifier is on non Vue-components. E.g. `` +- The `.sync` modifier's reference is iteration variables. E.g. `
` + + + +```vue + +``` + + + +::: warning Note +This rule does not check syntax errors in directives because it's checked by [no-parsing-error] rule. +::: + +## :wrench: Options + +Nothing. + +## :couple: Related rules + +- [no-parsing-error] + +[no-parsing-error]: no-parsing-error.md + +## :books: Further reading + +- [Guide - `.sync` Modifier]([https://vuejs.org/v2/guide/list.html#v-for-with-a-Component](https://vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-v-bind-sync.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-v-bind-sync.js) diff --git a/lib/index.js b/lib/index.js index 28343da6d..9b6c34693 100644 --- a/lib/index.js +++ b/lib/index.js @@ -81,6 +81,7 @@ module.exports = { 'v-on-style': require('./rules/v-on-style'), 'v-slot-style': require('./rules/v-slot-style'), 'valid-template-root': require('./rules/valid-template-root'), + 'valid-v-bind-sync': require('./rules/valid-v-bind-sync'), 'valid-v-bind': require('./rules/valid-v-bind'), 'valid-v-cloak': require('./rules/valid-v-cloak'), 'valid-v-else-if': require('./rules/valid-v-else-if'), diff --git a/lib/rules/valid-v-bind-sync.js b/lib/rules/valid-v-bind-sync.js new file mode 100644 index 000000000..727d0390e --- /dev/null +++ b/lib/rules/valid-v-bind-sync.js @@ -0,0 +1,113 @@ +/** + * @fileoverview enforce valid `.sync` modifier on `v-bind` directives + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Check whether the given node is valid or not. + * @param {ASTNode} node The element node to check. + * @returns {boolean} `true` if the node is valid. + */ +function isValidElement (node) { + if ( + (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) || + utils.isHtmlWellKnownElementName(node.rawName) || + utils.isSvgWellKnownElementName(node.rawName) + ) { + // non Vue-component + return false + } + return true +} + +/** + * Check whether the given node can be LHS. + * @param {ASTNode} node The node to check. + * @returns {boolean} `true` if the node can be LHS. + */ +function isLhs (node) { + return Boolean(node) && ( + node.type === 'Identifier' || + node.type === 'MemberExpression' + ) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'enforce valid `.sync` modifier on `v-bind` directives', + category: undefined, + url: 'https://eslint.vuejs.org/rules/valid-v-bind-sync.html' + }, + fixable: null, + schema: [], + messages: { + unexpectedInvalidElement: "'.sync' modifiers aren't supported on <{{name}}> non Vue-components.", + unexpectedNonLhsExpression: "'.sync' modifiers require the attribute value which is valid as LHS.", + unexpectedUpdateIterationVariable: "'.sync' modifiers cannot update the iteration variable '{{varName}}' itself." + } + }, + + create (context) { + return utils.defineTemplateBodyVisitor(context, { + "VAttribute[directive=true][key.name.name='bind']" (node) { + if (!node.key.modifiers.map(mod => mod.name).includes('sync')) { + return + } + const element = node.parent.parent + const name = element.name + + if (!isValidElement(element)) { + context.report({ + node, + loc: node.loc, + messageId: 'unexpectedInvalidElement', + data: { name } + }) + } + + if (node.value) { + if (!isLhs(node.value.expression)) { + context.report({ + node, + loc: node.loc, + messageId: 'unexpectedNonLhsExpression' + }) + } + + for (const reference of node.value.references) { + const id = reference.id + if (id.parent.type !== 'VExpressionContainer') { + continue + } + const variable = reference.variable + if (variable) { + context.report({ + node, + loc: node.loc, + messageId: 'unexpectedUpdateIterationVariable', + data: { varName: id.name } + }) + } + } + } + } + }) + } +} diff --git a/tests/lib/rules/valid-v-bind-sync.js b/tests/lib/rules/valid-v-bind-sync.js new file mode 100644 index 000000000..97fb33e9d --- /dev/null +++ b/tests/lib/rules/valid-v-bind-sync.js @@ -0,0 +1,292 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/valid-v-bind-sync') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { ecmaVersion: 2015 } +}) + +tester.run('valid-v-bind-sync', rule, { + valid: [ + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + // not .sync + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + // does not report + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + }, + { + filename: 'test.vue', + code: '' + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 41 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 47 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers aren't supported on non Vue-components.", + line: 3, + column: 18, + endColumn: 33 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 41 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers require the attribute value which is valid as LHS.", + line: 3, + column: 24, + endColumn: 46 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers aren't supported on non Vue-components.", + line: 3, + column: 18, + endColumn: 39 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers cannot update the iteration variable 'x' itself.", + line: 4, + column: 26, + endColumn: 39 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers cannot update the iteration variable 'e' itself.", + line: 4, + column: 26, + endColumn: 45 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers cannot update the iteration variable 'e1' itself.", + line: 6 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [{ + message: "'.sync' modifiers cannot update the iteration variable 'index' itself.", + line: 4 + }] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: ["'.sync' modifiers aren't supported on
non Vue-components."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'.sync' modifiers require the attribute value which is valid as LHS."] + }, + { + filename: 'test.vue', + code: '', + errors: ["'.sync' modifiers require the attribute value which is valid as LHS."] + } + ] +})