diff --git a/docs/rules/README.md b/docs/rules/README.md index ab1f5b220..72faa9698 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -160,6 +160,7 @@ For example: | [vue/no-deprecated-slot-scope-attribute](./no-deprecated-slot-scope-attribute.md) | disallow deprecated `slot-scope` attribute (in Vue.js 2.6.0+) | :wrench: | | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | | | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace | | +| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | | | [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | | diff --git a/docs/rules/no-ref-as-operand.md b/docs/rules/no-ref-as-operand.md new file mode 100644 index 000000000..e6d0b94b3 --- /dev/null +++ b/docs/rules/no-ref-as-operand.md @@ -0,0 +1,58 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-ref-as-operand +description: disallow use of value wrapped by `ref()` (Composition API) as an operand +--- +# vue/no-ref-as-operand +> disallow use of value wrapped by `ref()` (Composition API) as an operand + +## :book: Rule Details + +This rule reports cases where a ref is used incorrectly as an operand. + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further reading + +- [Vue RFCs - 0013-composition-api](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-ref-as-operand.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-ref-as-operand.js) diff --git a/lib/index.js b/lib/index.js index 02e65ba96..023e499c0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -48,6 +48,7 @@ module.exports = { 'no-irregular-whitespace': require('./rules/no-irregular-whitespace'), 'no-multi-spaces': require('./rules/no-multi-spaces'), 'no-parsing-error': require('./rules/no-parsing-error'), + 'no-ref-as-operand': require('./rules/no-ref-as-operand'), 'no-reserved-component-names': require('./rules/no-reserved-component-names'), 'no-reserved-keys': require('./rules/no-reserved-keys'), 'no-restricted-syntax': require('./rules/no-restricted-syntax'), diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js new file mode 100644 index 000000000..ea57d9da4 --- /dev/null +++ b/lib/rules/no-ref-as-operand.js @@ -0,0 +1,124 @@ +/** + * @author Yosuke Ota + * See LICENSE file in root directory for full license. + */ +'use strict' +const { ReferenceTracker, findVariable } = require('eslint-utils') + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow use of value wrapped by `ref()` (Composition API) as an operand', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html' + }, + fixable: null, + schema: [], + messages: { + requireDotValue: 'Must use `.value` to read or write the value wrapped by `ref()`.' + } + }, + create (context) { + const refReferenceIds = new Map() + + function reportIfRefWrapped (node) { + if (!refReferenceIds.has(node)) { + return + } + context.report({ + node, + messageId: 'requireDotValue' + }) + } + return { + 'Program' () { + const tracker = new ReferenceTracker(context.getScope()) + const traceMap = { + vue: { + [ReferenceTracker.ESM]: true, + ref: { + [ReferenceTracker.CALL]: true + } + } + } + + for (const { node } of tracker.iterateEsmReferences(traceMap)) { + const variableDeclarator = node.parent + if ( + !variableDeclarator || + variableDeclarator.type !== 'VariableDeclarator' || + variableDeclarator.id.type !== 'Identifier' + ) { + continue + } + const variable = findVariable(context.getScope(), variableDeclarator.id) + if (!variable) { + continue + } + const variableDeclaration = ( + variableDeclarator.parent && + variableDeclarator.parent.type === 'VariableDeclaration' && + variableDeclarator.parent + ) || null + for (const reference of variable.references) { + if (!reference.isRead()) { + continue + } + + refReferenceIds.set(reference.identifier, { + variableDeclarator, + variableDeclaration + }) + } + } + }, + // if (refValue) + 'IfStatement>Identifier' (node) { + reportIfRefWrapped(node) + }, + // switch (refValue) + 'SwitchStatement>Identifier' (node) { + reportIfRefWrapped(node) + }, + // -refValue, +refValue, !refValue, ~refValue, typeof refValue + 'UnaryExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue++, refValue-- + 'UpdateExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue+1, refValue-1 + 'BinaryExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue + 'AssignmentExpression>Identifier' (node) { + reportIfRefWrapped(node) + }, + // refValue || other, refValue && other. ignore: other || refValue + 'LogicalExpression>Identifier' (node) { + if (node.parent.left !== node) { + return + } + // Report only constants. + const info = refReferenceIds.get(node) + if (!info) { + return + } + if (!info.variableDeclaration || info.variableDeclaration.kind !== 'const') { + return + } + reportIfRefWrapped(node) + }, + // refValue ? x : y + 'ConditionalExpression>Identifier' (node) { + if (node.parent.test !== node) { + return + } + reportIfRefWrapped(node) + } + } + } +} diff --git a/package.json b/package.json index 1744009a5..099b182a4 100644 --- a/package.json +++ b/package.json @@ -47,9 +47,10 @@ "eslint": "^5.0.0 || ^6.0.0" }, "dependencies": { + "eslint-utils": "^2.0.0", "natural-compare": "^1.4.0", - "vue-eslint-parser": "^7.0.0", - "semver": "^5.6.0" + "semver": "^5.6.0", + "vue-eslint-parser": "^7.0.0" }, "devDependencies": { "@types/node": "^4.2.16", diff --git a/tests/lib/rules/no-ref-as-operand.js b/tests/lib/rules/no-ref-as-operand.js new file mode 100644 index 000000000..b8632c88b --- /dev/null +++ b/tests/lib/rules/no-ref-as-operand.js @@ -0,0 +1,334 @@ +/** + * @author Yosuke Ota + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-ref-as-operand') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2019, sourceType: 'module' } +}) + +tester.run('no-ref-as-operand', rule, { + valid: [ + ` + import { ref } from 'vue' + const count = ref(0) + console.log(count.value) // 0 + + count.value++ + console.log(count.value) // 1 + `, + ` + + `, + ` + import { ref } from 'vue' + const count = ref(0) + if (count.value) {} + switch (count.value) {} + var foo = -count.value + var foo = +count.value + count.value++ + count.value-- + count.value + 1 + 1 - count.value + count.value || other + count.value && other + var foo = count.value ? x : y + `, + ` + import { ref } from 'vue' + const foo = ref(true) + if (bar) foo + `, + ` + import { ref } from 'vue' + const foo = ref(true) + var a = other || foo // ignore + var b = other && foo // ignore + + let bar = ref(true) + var a = bar || other + var b = bar || other + `, + ` + import { ref } from 'vue' + let count = not_ref(0) + + count++ + `, + ` + import { ref } from 'vue' + const foo = ref(0) + const bar = ref(0) + var baz = x ? foo : bar + `, + ` + import { ref } from 'vue' + // Probably wrong, but not checked by this rule. + const {value} = ref(0) + value++ + `, + ` + import { ref } from 'vue' + const count = ref(0) + function foo() { + let count = 0 + count++ + } + `, + ` + import { ref } from 'unknown' + const count = ref(0) + count++ + `, + ` + import { ref } from 'vue' + const count = ref + count++ + ` + ], + invalid: [ + { + code: ` + import { ref } from 'vue' + let count = ref(0) + + count++ // error + console.log(count + 1) // error + console.log(1 + count) // error + `, + errors: [ + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 5, + column: 7, + endLine: 5, + endColumn: 12 + }, + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 6, + column: 19, + endLine: 6, + endColumn: 24 + }, + { + message: 'Must use `.value` to read or write the value wrapped by `ref()`.', + line: 7, + column: 23, + endLine: 7, + endColumn: 28 + } + ] + }, + { + code: ` + + `, + errors: [ + { + messageId: 'requireDotValue', + line: 8, + column: 13, + endLine: 8, + endColumn: 18 + }, + { + messageId: 'requireDotValue', + line: 9, + column: 25, + endLine: 9, + endColumn: 30 + }, + { + messageId: 'requireDotValue', + line: 10, + column: 29, + endLine: 10, + endColumn: 34 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + if (foo) { + // + } + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + switch (foo) { + // + } + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(0) + var a = -foo + var b = +foo + var c = !foo + var d = ~foo + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + }, + { + messageId: 'requireDotValue', + line: 6 + }, + { + messageId: 'requireDotValue', + line: 7 + } + ] + }, + { + code: ` + import { ref } from 'vue' + let foo = ref(0) + foo += 1 + foo -= 1 + baz += foo + baz -= foo + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + }, + { + messageId: 'requireDotValue', + line: 6 + }, + { + messageId: 'requireDotValue', + line: 7 + } + ] + }, + { + code: ` + import { ref } from 'vue' + const foo = ref(true) + var a = foo || other + var b = foo && other + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + }, + { + messageId: 'requireDotValue', + line: 5 + } + ] + }, + { + code: ` + import { ref } from 'vue' + let foo = ref(true) + var a = foo ? x : y + `, + errors: [ + { + messageId: 'requireDotValue', + line: 4 + } + ] + }, + { + code: ` + + `, + errors: [ + { + messageId: 'requireDotValue', + line: 7 + }, + { + messageId: 'requireDotValue', + line: 8 + }, + { + messageId: 'requireDotValue', + line: 9 + } + ] + } + ] +})