diff --git a/docs/rules/README.md b/docs/rules/README.md index 97117319f..777c6caf6 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -155,6 +155,7 @@ For example: | [vue/no-deprecated-scope-attribute](./no-deprecated-scope-attribute.md) | disallow deprecated `scope` attribute (in Vue.js 2.5.0+) | :wrench: | | [vue/no-empty-pattern](./no-empty-pattern.md) | disallow empty destructuring patterns | | | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax | | +| [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties, data and computed properties | | | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | | [vue/script-indent](./script-indent.md) | enforce consistent indentation in ` +``` + +```vue +/* ✗ BAD (`count` property not used) */ + + + + +``` + +```vue +/* ✓ GOOD */ + + +``` + +```vue +/* ✓ BAD (`count` data not used) */ + + +``` + +```vue +/* ✓ GOOD */ + + + + +``` + +```vue +/* ✓ BAD (`reversedMessage` computed property not used) */ + + + + +``` + +## :wrench: Options + +None. + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-unused-properties.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-unused-properties.js) diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js new file mode 100644 index 000000000..7897788b2 --- /dev/null +++ b/lib/rules/no-unused-properties.js @@ -0,0 +1,167 @@ +/** + * @fileoverview Disallow unused properties, data and computed properties. + * @author Learning Equality + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const remove = require('lodash/remove') +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Constants +// ------------------------------------------------------------------------------ + +const GROUP_PROPERTY = 'props' +const GROUP_DATA = 'data' +const GROUP_COMPUTED_PROPERTY = 'computed' +const GROUP_WATCHER = 'watch' + +const PROPERTY_LABEL = { + [GROUP_PROPERTY]: 'property', + [GROUP_DATA]: 'data', + [GROUP_COMPUTED_PROPERTY]: 'computed property' +} + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Extract names from references objects. + */ +const getReferencesNames = references => { + if (!references || !references.length) { + return [] + } + + return references.map(reference => { + if (!reference.id || !reference.id.name) { + return + } + + return reference.id.name + }) +} + +/** + * Report all unused properties. + */ +const reportUnusedProperties = (context, properties) => { + if (!properties || !properties.length) { + return + } + + properties.forEach(property => { + context.report({ + node: property.node, + message: `Unused ${PROPERTY_LABEL[property.groupName]} found: "${property.name}"` + }) + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow unused properties, data and computed properties', + category: undefined, + url: 'https://eslint.vuejs.org/rules/no-unused-properties.html' + }, + fixable: null, + schema: [] + }, + + create (context) { + let hasTemplate + let rootTemplateEnd + let unusedProperties = [] + const thisExpressionsVariablesNames = [] + + const initialize = { + Program (node) { + if (context.parserServices.getTemplateBodyTokenStore == null) { + context.report({ + loc: { line: 1, column: 0 }, + message: + 'Use the latest vue-eslint-parser. See also https://vuejs.github.io/eslint-plugin-vue/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error.' + }) + return + } + + hasTemplate = Boolean(node.templateBody) + } + } + + const scriptVisitor = Object.assign( + {}, + { + 'MemberExpression[object.type="ThisExpression"][property.type="Identifier"][property.name]' ( + node + ) { + thisExpressionsVariablesNames.push(node.property.name) + } + }, + utils.executeOnVue(context, obj => { + unusedProperties = Array.from( + utils.iterateProperties(obj, new Set([GROUP_PROPERTY, GROUP_DATA, GROUP_COMPUTED_PROPERTY])) + ) + + const watchers = Array.from(utils.iterateProperties(obj, new Set([GROUP_WATCHER]))) + const watchersNames = watchers.map(watcher => watcher.name) + + remove(unusedProperties, property => { + return ( + thisExpressionsVariablesNames.includes(property.name) || + watchersNames.includes(property.name) + ) + }) + + if (!hasTemplate && unusedProperties.length) { + reportUnusedProperties(context, unusedProperties) + } + }) + ) + + const templateVisitor = { + 'VExpressionContainer[expression!=null][references]' (node) { + const referencesNames = getReferencesNames(node.references) + + remove(unusedProperties, property => { + return referencesNames.includes(property.name) + }) + }, + // save root template end location - just a helper to be used + // for a decision if a parser reached the end of the root template + "VElement[name='template']" (node) { + if (rootTemplateEnd) { + return + } + + rootTemplateEnd = node.loc.end + }, + "VElement[name='template']:exit" (node) { + if (node.loc.end !== rootTemplateEnd) { + return + } + + if (unusedProperties.length) { + reportUnusedProperties(context, unusedProperties) + } + } + } + + return Object.assign( + {}, + initialize, + utils.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor) + ) + } +} diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js new file mode 100644 index 000000000..e80216468 --- /dev/null +++ b/tests/lib/rules/no-unused-properties.js @@ -0,0 +1,646 @@ +/** + * @fileoverview Disallow unused properties, data and computed properties. + * @author Learning Equality + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/no-unused-properties') + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module' + } +}) + +tester.run('no-unused-properties', rule, { + valid: [ + // a property used in a script expression + { + filename: 'test.vue', + code: ` + + ` + }, + + // a property being watched + { + filename: 'test.vue', + code: ` + + ` + }, + + // a property used as a template identifier + { + filename: 'test.vue', + code: ` + + + ` + }, + + // properties used in a template expression + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-if + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-for + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-html + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property passed in a component + { + filename: 'test.vue', + code: ` + + + ` + }, + + // a property used in v-on + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in a script expression + { + filename: 'test.vue', + code: ` + + ` + }, + + // data being watched + { + filename: 'test.vue', + code: ` + + ` + }, + + // data used as a template identifier + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in a template expression + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in v-if + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in v-for + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in v-html + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in v-model + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data passed in a component + { + filename: 'test.vue', + code: ` + + + ` + }, + + // data used in v-on + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property used in a script expression + { + filename: 'test.vue', + code: ` + + ` + }, + + // computed property being watched + { + filename: 'test.vue', + code: ` + + ` + }, + + // computed property used as a template identifier + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed properties used in a template expression + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property used in v-if + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property used in v-for + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property used in v-html + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property used in v-model + { + filename: 'test.vue', + code: ` + + + ` + }, + + // computed property passed in a component + { + filename: 'test.vue', + code: ` + + + ` + }, + + // ignores unused data when marked with eslint-disable + { + filename: 'test.vue', + code: ` + + + ` + } + ], + + invalid: [ + // unused property + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unused property found: "count"', + line: 7 + } + ] + }, + + // unused data + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unused data found: "count"', + line: 9 + } + ] + }, + + // unused computed property + { + filename: 'test.vue', + code: ` + + + `, + errors: [ + { + message: 'Unused computed property found: "count"', + line: 8 + } + ] + } + ] +})