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
+
{{ count }}
-
```
-```vue
-/* ✗ BAD (`count` property not used) */
+
+
+
+```vue
+
{{ cnt }}
-
```
-```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
+
{{ reversedMessage }}
-
```
-```vue
-/* ✓ BAD (`reversedMessage` computed property not used) */
+
+
+
+```vue
+
{{ message }}
-
```
-## :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: `
+
+ {{ foo }}
+
+
`
}
],
@@ -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: `
+
+ {{ foo }}
+
+
+ `,
+ 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."
+ ]
}
]
})