From fb6a5b65a4844634673164230a7af7b1fa33d71e Mon Sep 17 00:00:00 2001 From: Michaela Robosova Date: Tue, 9 Apr 2019 17:49:51 +0200 Subject: [PATCH 1/3] New: add vue/no-unused-properties rule (#631) --- docs/rules/no-unused-properties.md | 129 +++++ lib/rules/no-unused-properties.js | 167 ++++++ tests/lib/rules/no-unused-properties.js | 646 ++++++++++++++++++++++++ 3 files changed, 942 insertions(+) create mode 100644 docs/rules/no-unused-properties.md create mode 100644 lib/rules/no-unused-properties.js create mode 100644 tests/lib/rules/no-unused-properties.js diff --git a/docs/rules/no-unused-properties.md b/docs/rules/no-unused-properties.md new file mode 100644 index 000000000..4f6b56da8 --- /dev/null +++ b/docs/rules/no-unused-properties.md @@ -0,0 +1,129 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/no-unused-properties +description: disallow unused properties, data and computed properties +--- +# vue/no-unused-properties +> disallow unused properties, data and computed properties + +## :book: Rule Details + +This rule disallows any unused properties, data and computed properties. + +```vue +/* ✓ GOOD */ + + + + +``` + +```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 + } + ] + } + ] +}) From 21ed973d7bd220539a41ae297246b1c89897bc51 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Tue, 19 May 2020 22:37:02 +0900 Subject: [PATCH 2/3] Update --- docs/rules/README.md | 1 + docs/rules/no-unused-properties.md | 79 +++-- lib/index.js | 1 + lib/rules/no-unused-properties.js | 317 +++++++++++++------ tests/lib/rules/no-unused-properties.js | 395 ++++++++++++++++++++++-- 5 files changed, 651 insertions(+), 142 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 83eda5692..23ba5a897 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -290,6 +290,7 @@ For example: | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" | | | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates | | | [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/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: | | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | | diff --git a/docs/rules/no-unused-properties.md b/docs/rules/no-unused-properties.md index 4f6b56da8..438187060 100644 --- a/docs/rules/no-unused-properties.md +++ b/docs/rules/no-unused-properties.md @@ -2,22 +2,26 @@ pageClass: rule-details sidebarDepth: 0 title: vue/no-unused-properties -description: disallow unused properties, data and computed properties +description: disallow unused properties --- # vue/no-unused-properties -> disallow unused properties, data and computed properties +> disallow unused properties ## :book: Rule Details -This rule disallows any unused properties, data and computed properties. +This rule is aimed at eliminating unused properties. -```vue -/* ✓ GOOD */ +::: warning Note +This rule cannot be checked for use in other components (e.g. `mixins`, Property access via `$refs`) and use in places where the scope cannot be determined. +::: + + +```vue + - ``` -```vue -/* ✗ BAD (`count` property not used) */ + + + +```vue + - ``` -```vue -/* ✓ GOOD */ + + +## :wrench: Options + +```json +{ + "vue/no-unused-properties": ["error", { + "groups": ["props"] + }] +} +``` + +- `"groups"` (`string[]`) Array of groups to search for properties. Default is `["props"]`. The value of the array is some of the following strings: + - `"props"` + - `"data"` + - `"computed"` + - `"methods"` + - `"setup"` + +### `"groups": ["props", "data"]` + + +```vue + ``` -```vue -/* ✓ BAD (`count` data not used) */ + + + +```vue + ``` -```vue -/* ✓ GOOD */ + + +### `"groups": ["props", "computed"]` + + +```vue + - ``` -```vue -/* ✓ BAD (`reversedMessage` computed property not used) */ + + + +```vue + - ``` -## :wrench: Options - -None. + ## :mag: Implementation diff --git a/lib/index.js b/lib/index.js index 6adbbecd4..47fb57681 100644 --- a/lib/index.js +++ b/lib/index.js @@ -81,6 +81,7 @@ module.exports = { 'no-unregistered-components': require('./rules/no-unregistered-components'), 'no-unsupported-features': require('./rules/no-unsupported-features'), 'no-unused-components': require('./rules/no-unused-components'), + 'no-unused-properties': require('./rules/no-unused-properties'), 'no-unused-vars': require('./rules/no-unused-vars'), 'no-use-v-if-with-v-for': require('./rules/no-use-v-if-with-v-for'), 'no-v-html': require('./rules/no-v-html'), diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 7897788b2..449e4dacc 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -8,8 +8,8 @@ // Requirements // ------------------------------------------------------------------------------ -const remove = require('lodash/remove') const utils = require('../utils') +const { findVariable } = require('eslint-utils') // ------------------------------------------------------------------------------ // Constants @@ -18,12 +18,16 @@ const utils = require('../utils') const GROUP_PROPERTY = 'props' const GROUP_DATA = 'data' const GROUP_COMPUTED_PROPERTY = 'computed' +const GROUP_METHODS = 'methods' +const GROUP_SETUP = 'setup' const GROUP_WATCHER = 'watch' const PROPERTY_LABEL = { [GROUP_PROPERTY]: 'property', [GROUP_DATA]: 'data', - [GROUP_COMPUTED_PROPERTY]: 'computed property' + [GROUP_COMPUTED_PROPERTY]: 'computed property', + [GROUP_METHODS]: 'method', + [GROUP_SETUP]: 'property returned from `setup()`' } // ------------------------------------------------------------------------------ @@ -34,33 +38,9 @@ const PROPERTY_LABEL = { * 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}"` - }) - }) + return references + .filter(ref => ref.variable == null) + .map(ref => ref.id.name) } // ------------------------------------------------------------------------------ @@ -71,97 +51,242 @@ module.exports = { meta: { type: 'suggestion', docs: { - description: 'disallow unused properties, data and computed properties', - category: undefined, + description: 'disallow unused properties', + categories: undefined, url: 'https://eslint.vuejs.org/rules/no-unused-properties.html' }, fixable: null, - schema: [] + schema: [ + { + type: 'object', + properties: { + groups: { + type: 'array', + items: { + enum: [ + GROUP_PROPERTY, + GROUP_DATA, + GROUP_COMPUTED_PROPERTY, + GROUP_METHODS, + GROUP_SETUP + ] + }, + additionalItems: false, + uniqueItems: true + } + }, + additionalProperties: false + } + ], + messages: { + unused: "'{{name}}' of {{group}} found, but never used." + } }, 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.' - }) + const options = context.options[0] || {} + const groups = new Set(options.groups || [GROUP_PROPERTY]) + + /** + * @typedef {import('vue-eslint-parser').AST.Node} ASTNode + * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern + * @typedef { { node: ASTNode } } VueData + * @typedef { { name: string, groupName: string, node: ASTNode } } PropertyData + * @typedef { { + * usedNames: Set, + * } } BasePropertiesContainer + * @typedef { BasePropertiesContainer } TemplatePropertiesContainer + * @typedef { BasePropertiesContainer & { + * ignore: boolean, + * properties: Array, + * usedPropsNames: Set, + * propsReferenceIds: Set, + * } } VueComponentPropertiesContainer + * @typedef { {node: ASTNode, upper: VueDataStack} } VueDataStack + */ + + /** @type {TemplatePropertiesContainer} */ + const templatePropertiesContainer = { + usedNames: new Set() + } + /** @type {Map} */ + const vueComponentPropertiesContainers = new Map() + /** + * @param {ASTNode} node + * @returns {VueComponentPropertiesContainer} + */ + function getVueComponentPropertiesContainer (node) { + const key = node + + let container = vueComponentPropertiesContainers.get(key) + if (!container) { + container = { + properties: [], + usedNames: new Set(), + usedPropsNames: new Set(), + propsReferenceIds: new Set(), + ignore: false + } + vueComponentPropertiesContainers.set(key, container) + } + return container + } + + /** + * @param {ObjectPattern} node + * @param {Set} usedNames + * @param {VueComponentPropertiesContainer} vueComponentPropertiesContainer + */ + function extractObjectPatternProperties (node, usedNames, vueComponentPropertiesContainer) { + for (const prop of node.properties) { + if (prop.type === 'Property') { + usedNames.add(utils.getStaticPropertyName(prop)) + } else { + // If use RestElement, everything is used! + vueComponentPropertiesContainer.ignore = true return } + } + } + + /** + * @param {ASTNode} node + * @param {VueComponentPropertiesContainer} vueComponentPropertiesContainer + * @returns {Set | null} + */ + function getPropertyNamesSet (node, vueComponentPropertiesContainer) { + if (utils.isThis(node, context)) { + return vueComponentPropertiesContainer.usedNames + } + if (vueComponentPropertiesContainer.propsReferenceIds.has(node)) { + return vueComponentPropertiesContainer.usedPropsNames + } + return null + } - hasTemplate = Boolean(node.templateBody) + /** + * Report all unused properties. + */ + function reportUnusedProperties () { + for (const container of vueComponentPropertiesContainers.values()) { + if (container.ignore) { + continue + } + for (const property of container.properties) { + if (container.usedNames.has(property.name) || templatePropertiesContainer.usedNames.has(property.name)) { + continue + } + if (property.groupName === 'props' && container.usedPropsNames.has(property.name)) { + continue + } + context.report({ + node: property.node, + messageId: 'unused', + data: { + group: PROPERTY_LABEL[property.groupName], + name: property.name + } + }) + } } } const scriptVisitor = Object.assign( {}, + utils.defineVueVisitor(context, { + ObjectExpression (node, vueData) { + if (node !== vueData.node) { + return + } + + const container = getVueComponentPropertiesContainer(vueData.node) + const watcherNames = new Set() + for (const watcher of utils.iterateProperties(node, new Set([GROUP_WATCHER]))) { + watcherNames.add(watcher.name) + } + for (const prop of utils.iterateProperties(node, groups)) { + if (watcherNames.has(prop.name)) { + continue + } + container.properties.push(prop) + } + }, + 'Property[value.type=/^(Arrow)?FunctionExpression$/]' (node, vueData) { + if (node.parent !== vueData.node) { + return + } + if (utils.getStaticPropertyName(node) !== 'setup') { + return + } + const container = getVueComponentPropertiesContainer(vueData.node) + const propsParam = node.value.params[0] + if (!propsParam) { + // no arguments + return + } + if (propsParam.type === 'RestElement' || propsParam.type === 'ArrayPattern') { + // cannot check + return + } + if (propsParam.type === 'ObjectPattern') { + extractObjectPatternProperties(propsParam, container.usedPropsNames, container) + return + } + const variable = findVariable(context.getScope(), propsParam) + if (!variable) { + return + } + for (const reference of variable.references) { + container.propsReferenceIds.add(reference.identifier) + } + }, + MemberExpression (node, vueData) { + const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) + const usedNames = getPropertyNamesSet(node.object, vueComponentPropertiesContainer) + if (!usedNames) { + return + } + usedNames.add(utils.getStaticPropertyName(node)) + }, + 'VariableDeclarator > ObjectPattern' (node, vueData) { + const decl = node.parent + const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) + const usedNames = getPropertyNamesSet(decl.init, vueComponentPropertiesContainer) + if (!usedNames) { + return + } + extractObjectPatternProperties(node, usedNames, vueComponentPropertiesContainer) + }, + 'AssignmentExpression > ObjectPattern' (node, vueData) { + const assign = node.parent + const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) + const usedNames = getPropertyNamesSet(assign.right, vueComponentPropertiesContainer) + if (!usedNames) { + return + } + extractObjectPatternProperties(node, usedNames, vueComponentPropertiesContainer) + } + }), { - 'MemberExpression[object.type="ThisExpression"][property.type="Identifier"][property.name]' ( - node - ) { - thisExpressionsVariablesNames.push(node.property.name) + 'Program:exit' (node) { + if (!node.templateBody) { + reportUnusedProperties() + } } }, - 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 + 'VExpressionContainer' (node) { + for (const name of getReferencesNames(node.references)) { + templatePropertiesContainer.usedNames.add(name) } - - rootTemplateEnd = node.loc.end }, - "VElement[name='template']:exit" (node) { - if (node.loc.end !== rootTemplateEnd) { - return - } - - if (unusedProperties.length) { - reportUnusedProperties(context, unusedProperties) - } + "VElement[parent.type!='VElement']:exit" () { + reportUnusedProperties() } } - return Object.assign( - {}, - initialize, - utils.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor) - ) + return utils.defineTemplateBodyVisitor(context, templateVisitor, scriptVisitor) } } diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index e80216468..4ca80dab2 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -8,13 +8,15 @@ const RuleTester = require('eslint').RuleTester const rule = require('../../../lib/rules/no-unused-properties') const tester = new RuleTester({ - parser: 'vue-eslint-parser', + parser: require.resolve('vue-eslint-parser'), parserOptions: { - ecmaVersion: 2018, + ecmaVersion: 2020, sourceType: 'module' } }) +const allOptions = [{ groups: ['props', 'computed', 'data', 'methods', 'setup'] }] + tester.run('no-unused-properties', rule, { valid: [ // a property used in a script expression @@ -31,6 +33,41 @@ tester.run('no-unused-properties', rule, { ` }, + // default options + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, // a property being watched { @@ -184,7 +221,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data being watched @@ -205,7 +243,8 @@ tester.run('no-unused-properties', rule, { }, }; - ` + `, + options: allOptions }, // data used as a template identifier @@ -224,7 +263,8 @@ tester.run('no-unused-properties', rule, { } } - ` + `, + options: allOptions }, // data used in a template expression @@ -244,7 +284,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data used in v-if @@ -263,7 +304,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data used in v-for @@ -282,7 +324,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data used in v-html @@ -301,7 +344,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data used in v-model @@ -320,7 +364,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data passed in a component @@ -339,7 +384,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // data used in v-on @@ -358,7 +404,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // computed property used in a script expression @@ -377,7 +424,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // computed property being watched @@ -398,7 +446,8 @@ tester.run('no-unused-properties', rule, { }, }; - ` + `, + options: allOptions }, // computed property used as a template identifier @@ -417,7 +466,8 @@ tester.run('no-unused-properties', rule, { } } - ` + `, + options: allOptions }, // computed properties used in a template expression @@ -439,7 +489,8 @@ tester.run('no-unused-properties', rule, { } } - ` + `, + options: allOptions }, // computed property used in v-if @@ -458,7 +509,8 @@ tester.run('no-unused-properties', rule, { } } - ` + `, + options: allOptions }, // computed property used in v-for @@ -477,7 +529,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // computed property used in v-html @@ -496,7 +549,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // computed property used in v-model @@ -528,7 +582,8 @@ tester.run('no-unused-properties', rule, { } }; - ` + `, + options: allOptions }, // computed property passed in a component @@ -547,7 +602,8 @@ tester.run('no-unused-properties', rule, { } } - ` + `, + options: allOptions }, // ignores unused data when marked with eslint-disable @@ -567,6 +623,92 @@ tester.run('no-unused-properties', rule, { } }; + `, + options: allOptions + }, + + // trace this + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` + }, + // use rest + { + filename: 'test.vue', + code: ` + + ` } ], @@ -587,7 +729,7 @@ tester.run('no-unused-properties', rule, { `, errors: [ { - message: 'Unused property found: "count"', + message: "'count' of property found, but never used.", line: 7 } ] @@ -610,9 +752,10 @@ tester.run('no-unused-properties', rule, { }; `, + options: [{ groups: ['props', 'computed', 'data'] }], errors: [ { - message: 'Unused data found: "count"', + message: "'count' of data found, but never used.", line: 9 } ] @@ -635,12 +778,216 @@ tester.run('no-unused-properties', rule, { }; `, + options: [{ groups: ['props', 'computed', 'data'] }], errors: [ { - message: 'Unused computed property found: "count"', + message: "'count' of computed property found, but never used.", line: 8 } ] + }, + + // all options + { + filename: 'test.vue', + code: ` + + + `, + options: allOptions, + errors: [ + { + message: "'a' of property found, but never used.", + line: 7 + }, + { + message: "'b' of data found, but never used.", + line: 9 + }, + { + message: "'c' of computed property found, but never used.", + line: 12 + }, + { + message: "'d' of method found, but never used.", + line: 17 + }, + { + message: "'e' of property returned from `setup()` found, but never used.", + line: 20 + } + ] + }, + + // trace this + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + { + message: "'count' of property found, but never used.", + line: 4 + } + ] + }, + + // setup + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] } ] }) From 2db7504bf28068b604e2ea277d6e64db22e0e153 Mon Sep 17 00:00:00 2001 From: yosuke ota Date: Wed, 20 May 2020 11:25:57 +0900 Subject: [PATCH 3/3] Updated to trace function calls. --- lib/rules/no-unused-properties.js | 402 ++++++++++++++++++------ lib/utils/index.js | 11 + tests/lib/rules/no-unused-properties.js | 252 +++++++++++++++ 3 files changed, 567 insertions(+), 98 deletions(-) diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 449e4dacc..2462446e4 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -9,7 +9,27 @@ // ------------------------------------------------------------------------------ const utils = require('../utils') -const { findVariable } = require('eslint-utils') +const eslintUtils = require('eslint-utils') + +/** + * @typedef {import('vue-eslint-parser').AST.Node} Node + * @typedef {import('vue-eslint-parser').AST.ESLintNode} ASTNode + * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern + * @typedef {import('vue-eslint-parser').AST.ESLintIdentifier} Identifier + * @typedef {import('vue-eslint-parser').AST.ESLintThisExpression} ThisExpression + * @typedef {import('vue-eslint-parser').AST.ESLintFunctionExpression} FunctionExpression + * @typedef {import('vue-eslint-parser').AST.ESLintArrowFunctionExpression} ArrowFunctionExpression + * @typedef {import('vue-eslint-parser').AST.ESLintFunctionDeclaration} FunctionDeclaration + * @typedef {import('eslint').Scope.Variable} Variable + * @typedef {import('eslint').Rule.RuleContext} RuleContext + */ +/** + * @typedef { { name: string, groupName: string, node: ASTNode } } PropertyData + * @typedef { { usedNames: Set } } TemplatePropertiesContainer + * @typedef { { properties: Array, usedNames: Set, unknown: boolean, usedPropsNames: Set, unknownProps: boolean } } VueComponentPropertiesContainer + * @typedef { { node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, index: number } } CallIdAndParamIndex + * @typedef { { usedNames: Set, unknown: boolean } } UsedProperties + */ // ------------------------------------------------------------------------------ // Constants @@ -34,15 +54,222 @@ const PROPERTY_LABEL = { // Helpers // ------------------------------------------------------------------------------ +/** + * Find the variable of a given name. + * @param {RuleContext} context The rule context + * @param {ASTNode} node The variable name to find. + * @returns {Variable|null} The found variable or null. + */ +function findVariable (context, node) { + // @ts-ignore + return eslintUtils.findVariable(getScope(context, node), node) +} +/** + * Gets the scope for the current node + * @param {RuleContext} context The rule context + * @param {ASTNode} currentNode The node to get the scope of + * @returns { import('eslint-scope').Scope } The scope information for this node + */ +function getScope (context, currentNode) { + // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. + const inner = currentNode.type !== 'Program' + const scopeManager = context.getSourceCode().scopeManager + + // @ts-ignore + for (let node = currentNode; node; node = node.parent) { + // @ts-ignore + const scope = scopeManager.acquire(node, inner) + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] + } + return scope + } + } + + return scopeManager.scopes[0] +} + /** * Extract names from references objects. */ -const getReferencesNames = references => { +function getReferencesNames (references) { return references .filter(ref => ref.variable == null) .map(ref => ref.id.name) } +/** + * @param {ObjectPattern} node + * @returns {UsedProperties} + */ +function extractObjectPatternProperties (node) { + const usedNames = new Set() + for (const prop of node.properties) { + if (prop.type === 'Property') { + usedNames.add(utils.getStaticPropertyName(prop)) + } else { + // If use RestElement, everything is used! + return { + usedNames, + unknown: true + } + } + } + return { + usedNames, + unknown: false + } +} + +/** + * @param {Identifier | ThisExpression} node + * @param {RuleContext} context + * @returns {UsedProps} + */ +function extractIdOrThisProperties (node, context) { + /** @type {UsedProps} */ + const result = new UsedProps() + const parent = node.parent + if (parent.type === 'AssignmentExpression') { + if (parent.right === node && parent.left.type === 'ObjectPattern') { + // `({foo} = arg)` + const { usedNames, unknown } = extractObjectPatternProperties(parent.left) + usedNames.forEach(name => result.usedNames.add(name)) + result.unknown = result.unknown || unknown + } + } else if (parent.type === 'VariableDeclarator') { + if (parent.init === node && parent.id.type === 'ObjectPattern') { + // `const {foo} = arg` + const { usedNames, unknown } = extractObjectPatternProperties(parent.id) + usedNames.forEach(name => result.usedNames.add(name)) + result.unknown = result.unknown || unknown + } + } else if (parent.type === 'MemberExpression') { + if (parent.object === node) { + // `arg.foo` + const name = utils.getStaticPropertyName(parent) + if (name) { + result.usedNames.add(name) + } else { + result.unknown = true + } + } + } else if (parent.type === 'CallExpression') { + const argIndex = parent.arguments.indexOf(node) + if (argIndex > -1 && parent.callee.type === 'Identifier') { + // `foo(arg)` + const calleeVariable = findVariable(context, parent.callee) + if (!calleeVariable) { + return result + } + if (calleeVariable.defs.length === 1) { + const def = calleeVariable.defs[0] + if ( + def.type === 'Variable' && + def.parent && + def.parent.kind === 'const' && + (def.node.init.type === 'FunctionExpression' || def.node.init.type === 'ArrowFunctionExpression') + ) { + result.calls.push({ + // @ts-ignore + node: def.node.init, + index: argIndex + }) + } else if (def.node.type === 'FunctionDeclaration') { + result.calls.push({ + node: def.node, + index: argIndex + }) + } + } + } + } + return result +} + +/** + * Collects the property names used. + */ +class UsedProps { + constructor () { + /** @type {Set} */ + this.usedNames = new Set() + /** @type {CallIdAndParamIndex[]} */ + this.calls = [] + this.unknown = false + } +} + +/** + * Collects the property names used for one parameter of the function. + */ +class ParamUsedProps extends UsedProps { + /** + * @param {ASTNode} paramNode + * @param {RuleContext} context + */ + constructor (paramNode, context) { + super() + + if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { + // cannot check + return + } + if (paramNode.type === 'ObjectPattern') { + const { usedNames, unknown } = extractObjectPatternProperties(paramNode) + usedNames.forEach(name => this.usedNames.add(name)) + this.unknown = this.unknown || unknown + return + } + const variable = findVariable(context, paramNode) + if (!variable) { + return + } + for (const reference of variable.references) { + /** @type {Identifier} */ + // @ts-ignore + const id = reference.identifier + const { usedNames, unknown, calls } = extractIdOrThisProperties(id, context) + usedNames.forEach(name => this.usedNames.add(name)) + this.unknown = this.unknown || unknown + this.calls.push(...calls) + } + } +} + +/** + * Collects the property names used for parameters of the function. + */ +class ParamsUsedProps { + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @param {RuleContext} context + */ + constructor (node, context) { + this.node = node + this.context = context + /** @type {ParamUsedProps[]} */ + this.params = [] + } + + /** + * @param {number} index + * @returns {ParamUsedProps} + */ + getParam (index) { + const param = this.params[index] + if (param != null) { + return param + } + if (this.node.params[index]) { + return (this.params[index] = new ParamUsedProps(this.node.params[index], this.context)) + } + return null + } +} + // ------------------------------------------------------------------------------ // Rule Definition // ------------------------------------------------------------------------------ @@ -87,96 +314,60 @@ module.exports = { const options = context.options[0] || {} const groups = new Set(options.groups || [GROUP_PROPERTY]) - /** - * @typedef {import('vue-eslint-parser').AST.Node} ASTNode - * @typedef {import('vue-eslint-parser').AST.ESLintObjectPattern} ObjectPattern - * @typedef { { node: ASTNode } } VueData - * @typedef { { name: string, groupName: string, node: ASTNode } } PropertyData - * @typedef { { - * usedNames: Set, - * } } BasePropertiesContainer - * @typedef { BasePropertiesContainer } TemplatePropertiesContainer - * @typedef { BasePropertiesContainer & { - * ignore: boolean, - * properties: Array, - * usedPropsNames: Set, - * propsReferenceIds: Set, - * } } VueComponentPropertiesContainer - * @typedef { {node: ASTNode, upper: VueDataStack} } VueDataStack - */ - + /** @type {Map} */ + const paramsUsedPropsMap = new Map() /** @type {TemplatePropertiesContainer} */ const templatePropertiesContainer = { usedNames: new Set() } /** @type {Map} */ - const vueComponentPropertiesContainers = new Map() + const vueComponentPropertiesContainerMap = new Map() + + /** + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node + * @returns {ParamsUsedProps} + */ + function getParamsUsedProps (node) { + let usedProps = paramsUsedPropsMap.get(node) + if (!usedProps) { + usedProps = new ParamsUsedProps(node, context) + paramsUsedPropsMap.set(node, usedProps) + } + return usedProps + } + /** * @param {ASTNode} node * @returns {VueComponentPropertiesContainer} */ function getVueComponentPropertiesContainer (node) { - const key = node - - let container = vueComponentPropertiesContainers.get(key) + let container = vueComponentPropertiesContainerMap.get(node) if (!container) { container = { properties: [], usedNames: new Set(), usedPropsNames: new Set(), - propsReferenceIds: new Set(), - ignore: false + unknown: false, + unknownProps: false } - vueComponentPropertiesContainers.set(key, container) + vueComponentPropertiesContainerMap.set(node, container) } return container } - /** - * @param {ObjectPattern} node - * @param {Set} usedNames - * @param {VueComponentPropertiesContainer} vueComponentPropertiesContainer - */ - function extractObjectPatternProperties (node, usedNames, vueComponentPropertiesContainer) { - for (const prop of node.properties) { - if (prop.type === 'Property') { - usedNames.add(utils.getStaticPropertyName(prop)) - } else { - // If use RestElement, everything is used! - vueComponentPropertiesContainer.ignore = true - return - } - } - } - - /** - * @param {ASTNode} node - * @param {VueComponentPropertiesContainer} vueComponentPropertiesContainer - * @returns {Set | null} - */ - function getPropertyNamesSet (node, vueComponentPropertiesContainer) { - if (utils.isThis(node, context)) { - return vueComponentPropertiesContainer.usedNames - } - if (vueComponentPropertiesContainer.propsReferenceIds.has(node)) { - return vueComponentPropertiesContainer.usedPropsNames - } - return null - } - /** * Report all unused properties. */ function reportUnusedProperties () { - for (const container of vueComponentPropertiesContainers.values()) { - if (container.ignore) { + for (const container of vueComponentPropertiesContainerMap.values()) { + if (container.unknown) { continue } for (const property of container.properties) { if (container.usedNames.has(property.name) || templatePropertiesContainer.usedNames.has(property.name)) { continue } - if (property.groupName === 'props' && container.usedPropsNames.has(property.name)) { + if (property.groupName === 'props' && (container.unknownProps || container.usedPropsNames.has(property.name))) { continue } context.report({ @@ -191,6 +382,33 @@ module.exports = { } } + /** + * @param {UsedProps} usedProps + * @param {Map>} already + * @returns {Generator} + */ + function * iterateUsedProps (usedProps, already = new Map()) { + yield usedProps + for (const call of usedProps.calls) { + let alreadyIndexes = already.get(call.node) + if (!alreadyIndexes) { + alreadyIndexes = new Set() + already.set(call.node, alreadyIndexes) + } + if (alreadyIndexes.has(call.index)) { + continue + } + alreadyIndexes.add(call.index) + const paramsUsedProps = getParamsUsedProps(call.node) + const paramUsedProps = paramsUsedProps.getParam(call.index) + if (!paramUsedProps) { + continue + } + yield paramUsedProps + yield * iterateUsedProps(paramUsedProps, already) + } + } + const scriptVisitor = Object.assign( {}, utils.defineVueVisitor(context, { @@ -224,47 +442,35 @@ module.exports = { // no arguments return } - if (propsParam.type === 'RestElement' || propsParam.type === 'ArrayPattern') { - // cannot check - return - } - if (propsParam.type === 'ObjectPattern') { - extractObjectPatternProperties(propsParam, container.usedPropsNames, container) - return - } - const variable = findVariable(context.getScope(), propsParam) - if (!variable) { - return - } - for (const reference of variable.references) { - container.propsReferenceIds.add(reference.identifier) - } - }, - MemberExpression (node, vueData) { - const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) - const usedNames = getPropertyNamesSet(node.object, vueComponentPropertiesContainer) - if (!usedNames) { - return + const paramsUsedProps = getParamsUsedProps(node.value) + const paramUsedProps = paramsUsedProps.getParam(0) + + for (const { usedNames, unknown } of iterateUsedProps(paramUsedProps)) { + if (unknown) { + container.unknownProps = true + return + } + for (const name of usedNames) { + container.usedPropsNames.add(name) + } } - usedNames.add(utils.getStaticPropertyName(node)) }, - 'VariableDeclarator > ObjectPattern' (node, vueData) { - const decl = node.parent - const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) - const usedNames = getPropertyNamesSet(decl.init, vueComponentPropertiesContainer) - if (!usedNames) { + 'ThisExpression, Identifier' (node, vueData) { + if (!utils.isThis(node, context)) { return } - extractObjectPatternProperties(node, usedNames, vueComponentPropertiesContainer) - }, - 'AssignmentExpression > ObjectPattern' (node, vueData) { - const assign = node.parent - const vueComponentPropertiesContainer = getVueComponentPropertiesContainer(vueData.node) - const usedNames = getPropertyNamesSet(assign.right, vueComponentPropertiesContainer) - if (!usedNames) { - return + const container = getVueComponentPropertiesContainer(vueData.node) + const usedProps = extractIdOrThisProperties(node, context) + + for (const { usedNames, unknown } of iterateUsedProps(usedProps)) { + if (unknown) { + container.unknown = true + return + } + for (const name of usedNames) { + container.usedNames.add(name) + } } - extractObjectPatternProperties(node, usedNames, vueComponentPropertiesContainer) } }), { diff --git a/lib/utils/index.js b/lib/utils/index.js index c31ed0e6e..4eb2dda84 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -870,6 +870,17 @@ module.exports = { if (node.type !== 'Identifier') { return false } + const parent = node.parent + if (parent.type === 'MemberExpression') { + if (parent.property === node) { + return false + } + } else if (parent.type === 'Property') { + if (parent.key === node && !parent.computed) { + return false + } + } + const variable = findVariable(context.getScope(), node) if (variable != null && variable.defs.length === 1) { diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index 4ca80dab2..a17aff8b9 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -710,6 +710,45 @@ tester.run('no-unused-properties', rule, { }; ` + }, + + // function trace + { + filename: 'test.vue', + code: ` + + ` + }, + { + filename: 'test.vue', + code: ` + + ` } ], @@ -988,6 +1027,219 @@ tester.run('no-unused-properties', rule, { "'foo' of property found, but never used.", "'bar' of property found, but never used." ] + }, + + // function trace + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + errors: [ + "'foo' of property found, but never used.", + "'bar' of property found, but never used.", + "'baz' of property found, but never used." + ] } ] })