diff --git a/docs/rules/README.md b/docs/rules/README.md index 3577668f4..05565d3e6 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -320,6 +320,7 @@ For example: | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties | | | [vue/no-unused-refs](./no-unused-refs.md) | disallow unused refs | | +| [vue/no-use-computed-property-like-method](./no-use-computed-property-like-method.md) | disallow use computed property like method | | | [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | | [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | @@ -365,7 +366,7 @@ The following rules extend the rules provided by ESLint itself and apply them to | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | | [vue/no-sparse-arrays](./no-sparse-arrays.md) | disallow sparse arrays | | | [vue/no-useless-concat](./no-useless-concat.md) | disallow unnecessary concatenation of literals or template literals | | -| [vue/object-curly-newline](./object-curly-newline.md) | enforce consistent line breaks after opening and before closing braces | :wrench: | +| [vue/object-curly-newline](./object-curly-newline.md) | enforce consistent line breaks inside braces | :wrench: | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | | [vue/object-property-newline](./object-property-newline.md) | enforce placing object properties on separate lines | :wrench: | | [vue/operator-linebreak](./operator-linebreak.md) | enforce consistent linebreak style for operators | :wrench: | diff --git a/docs/rules/no-use-computed-property-like-method.md b/docs/rules/no-use-computed-property-like-method.md new file mode 100644 index 000000000..d776cd7eb --- /dev/null +++ b/docs/rules/no-use-computed-property-like-method.md @@ -0,0 +1,340 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-use-computed-property-like-method +description: disallow use computed property like method +--- +# vue/no-use-computed-property-like-method + +> disallow use computed property like method + +- :exclamation: ***This rule has not been released yet.*** + +## :book: Rule Details + +This rule disallows to use computed property like method. + + + +```vue + +``` + + + +This rule can't check if props is used as array: + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-use-computed-property-like-method.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-use-computed-property-like-method.js) diff --git a/docs/rules/object-curly-newline.md b/docs/rules/object-curly-newline.md index 52e3d6ae4..cc4831c12 100644 --- a/docs/rules/object-curly-newline.md +++ b/docs/rules/object-curly-newline.md @@ -2,12 +2,12 @@ pageClass: rule-details sidebarDepth: 0 title: vue/object-curly-newline -description: enforce consistent line breaks after opening and before closing braces +description: enforce consistent line breaks inside braces since: v7.0.0 --- # vue/object-curly-newline -> enforce consistent line breaks after opening and before closing braces +> enforce consistent line breaks inside braces - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule. diff --git a/lib/index.js b/lib/index.js index 8bf6cad45..88ea06c18 100644 --- a/lib/index.js +++ b/lib/index.js @@ -122,6 +122,7 @@ module.exports = { 'no-unused-properties': require('./rules/no-unused-properties'), 'no-unused-refs': require('./rules/no-unused-refs'), 'no-unused-vars': require('./rules/no-unused-vars'), + 'no-use-computed-property-like-method': require('./rules/no-use-computed-property-like-method'), 'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'), 'no-useless-concat': require('./rules/no-useless-concat'), 'no-useless-mustaches': require('./rules/no-useless-mustaches'), diff --git a/lib/rules/no-use-computed-property-like-method.js b/lib/rules/no-use-computed-property-like-method.js new file mode 100644 index 000000000..5b0371b8f --- /dev/null +++ b/lib/rules/no-use-computed-property-like-method.js @@ -0,0 +1,257 @@ +/** + * @author tyankatsu + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ +const eslintUtils = require('eslint-utils') +const utils = require('../utils') + +/** + * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData + * @typedef {import('../utils').ComponentObjectPropertyData} ComponentObjectPropertyData + * + * @typedef {{[key: string]: ComponentPropertyData & { valueType: { type: string | null } }}} PropertyMap + */ +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +/** + * Get type of props item. + * Can't consider array props like: props: {propsA: [String, Number, Function]} + * @param {ComponentObjectPropertyData} property + * @return {string | null} + * + * @example + * props: { + * propA: String, // => String + * propB: { + * type: Number // => String + * }, + * } + */ +const getComponentPropsType = (property) => { + /** + * Check basic props `props: { basicProps: ... }` + */ + if (property.property.value.type === 'Identifier') { + return property.property.value.name + } + /** + * Check object props `props: { objectProps: {...} }` + */ + if (property.property.value.type === 'ObjectExpression') { + const typeProperty = utils.findProperty(property.property.value, 'type') + if (typeProperty == null) return null + + if (typeProperty.value.type === 'Identifier') return typeProperty.value.name + } + return null +} + +/** + * + * @param {any} obj + */ +const getPrototypeType = (obj) => + Object.prototype.toString.call(obj).slice(8, -1) + +/** + * Get return type of property. + * @param {{ property: ComponentPropertyData, propertyMap?: PropertyMap }} args + * @returns {{type: string | null | 'ReturnStatementHasNotArgument'}} + */ +const getValueType = ({ property, propertyMap }) => { + if (property.type === 'array') { + return { + type: null + } + } + + if (property.type === 'object') { + if (property.groupName === 'props') { + return { + type: getComponentPropsType(property) + } + } + + if (property.groupName === 'computed' || property.groupName === 'methods') { + if ( + property.property.value.type === 'FunctionExpression' && + property.property.value.body.type === 'BlockStatement' + ) { + const blockStatement = property.property.value.body + + /** + * Only check return statement inside computed and methods + */ + const returnStatement = blockStatement.body.find( + (b) => b.type === 'ReturnStatement' + ) + + if (!returnStatement || returnStatement.type !== 'ReturnStatement') + return { + type: null + } + + if (returnStatement.argument === null) + return { + type: 'ReturnStatementHasNotArgument' + } + + if ( + property.groupName === 'computed' && + propertyMap && + propertyMap[property.name] && + returnStatement.argument + ) { + if ( + returnStatement.argument.type === 'MemberExpression' && + returnStatement.argument.object.type === 'ThisExpression' && + returnStatement.argument.property.type === 'Identifier' + ) + return { + type: propertyMap[returnStatement.argument.property.name] + .valueType.type + } + + if ( + returnStatement.argument.type === 'CallExpression' && + returnStatement.argument.callee.type === 'MemberExpression' && + returnStatement.argument.callee.object.type === 'ThisExpression' && + returnStatement.argument.callee.property.type === 'Identifier' + ) + return { + type: propertyMap[returnStatement.argument.callee.property.name] + .valueType.type + } + } + + /** + * Use value as Object even if object includes method + */ + if ( + property.groupName === 'computed' && + returnStatement.argument.type === 'ObjectExpression' + ) { + return { + type: 'Object' + } + } + + const evaluated = eslintUtils.getStaticValue(returnStatement.argument) + + if (evaluated) { + return { + type: getPrototypeType(evaluated.value) + } + } + } + } + + const evaluated = eslintUtils.getStaticValue(property.property.value) + + if (evaluated) { + return { + type: getPrototypeType(evaluated.value) + } + } + } + return { + type: null + } +} + +/** + * @param {Set} groups + * @param {ObjectExpression} vueNodeMap + * @param {PropertyMap} propertyMap + */ +const addPropertyMap = (groups, vueNodeMap, propertyMap) => { + const properties = utils.iterateProperties(vueNodeMap, groups) + for (const property of properties) { + propertyMap[property.name] = { + ...propertyMap[property.name], + ...property, + valueType: getValueType({ property, propertyMap }) + } + } +} + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'disallow use computed property like method', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html' + }, + fixable: null, + schema: [], + messages: { + unexpected: 'Use {{ likeProperty }} instead of {{ likeMethod }}.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const GROUP_NAMES = ['data', 'props', 'computed', 'methods'] + const groups = new Set(GROUP_NAMES) + + /** @type PropertyMap */ + const propertyMap = {} + + /**@type ObjectExpression */ + let vueNodeMap = {} + + return utils.defineVueVisitor(context, { + onVueObjectEnter(node) { + vueNodeMap = node + const properties = utils.iterateProperties(node, groups) + + for (const property of properties) { + propertyMap[property.name] = { + ...propertyMap[property.name], + ...property, + valueType: getValueType({ property }) + } + } + }, + + /** @param {ThisExpression} node */ + 'CallExpression > MemberExpression > ThisExpression'(node) { + addPropertyMap(groups, vueNodeMap, propertyMap) + + if (node.parent.type !== 'MemberExpression') return + if (node.parent.property.type !== 'Identifier') return + + const thisMember = node.parent.property.name + + if ( + !propertyMap[thisMember] || + !propertyMap[thisMember].valueType || + !propertyMap[thisMember].valueType.type + ) + return + + if ( + propertyMap[thisMember].groupName === 'computed' && + propertyMap[thisMember].valueType.type !== 'Function' + ) { + context.report({ + node: node.parent.parent, + loc: node.parent.parent.loc, + messageId: 'unexpected', + data: { + likeProperty: `this.${thisMember}`, + likeMethod: `this.${thisMember}()` + } + }) + } + } + }) + } +} diff --git a/tests/lib/rules/no-use-computed-property-like-method.js b/tests/lib/rules/no-use-computed-property-like-method.js new file mode 100644 index 000000000..b6e5e6958 --- /dev/null +++ b/tests/lib/rules/no-use-computed-property-like-method.js @@ -0,0 +1,703 @@ +/** + * @author tyankatsu + * See LICENSE file in root directory for full license. + */ + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-use-computed-property-like-method') + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { ecmaVersion: 2015, sourceType: 'module' } +}) + +tester.run('no-use-computed-property-like-method', 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: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnDataString instead of this.computedReturnDataString().', + 'Use this.computedReturnDataNumber instead of this.computedReturnDataNumber().', + 'Use this.computedReturnDataObject instead of this.computedReturnDataObject().', + 'Use this.computedReturnDataArray instead of this.computedReturnDataArray().', + 'Use this.computedReturnDataBoolean instead of this.computedReturnDataBoolean().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnPropsString instead of this.computedReturnPropsString().', + 'Use this.computedReturnPropsNumber instead of this.computedReturnPropsNumber().', + 'Use this.computedReturnPropsObject instead of this.computedReturnPropsObject().', + 'Use this.computedReturnPropsArray instead of this.computedReturnPropsArray().', + 'Use this.computedReturnPropsBoolean instead of this.computedReturnPropsBoolean().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnPropsString instead of this.computedReturnPropsString().', + 'Use this.computedReturnPropsNumber instead of this.computedReturnPropsNumber().', + 'Use this.computedReturnPropsObject instead of this.computedReturnPropsObject().', + 'Use this.computedReturnPropsArray instead of this.computedReturnPropsArray().', + 'Use this.computedReturnPropsBoolean instead of this.computedReturnPropsBoolean().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnString instead of this.computedReturnString().', + 'Use this.computedReturnNumber instead of this.computedReturnNumber().', + 'Use this.computedReturnObject instead of this.computedReturnObject().', + 'Use this.computedReturnArray instead of this.computedReturnArray().', + 'Use this.computedReturnBoolean instead of this.computedReturnBoolean().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnComputedReturnString instead of this.computedReturnComputedReturnString().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnObject instead of this.computedReturnObject().' + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + 'Use this.computedReturnNothing instead of this.computedReturnNothing().' + ] + } + ] +}) diff --git a/typings/eslint-utils/index.d.ts b/typings/eslint-utils/index.d.ts index 46d0f5abe..25e9ba8d4 100644 --- a/typings/eslint-utils/index.d.ts +++ b/typings/eslint-utils/index.d.ts @@ -8,6 +8,11 @@ export function findVariable( nameOrNode: VAST.Identifier | string ): eslint.Scope.Variable +export function getStaticValue( + node: VAST.ESNode, + initialScope?: eslint.Scope.Scope +): { value: any } | null + export function isParenthesized( num: number, node: VAST.ESNode,