diff --git a/docs/rules/no-unused-properties.md b/docs/rules/no-unused-properties.md
index e0e39eedb..932fb9563 100644
--- a/docs/rules/no-unused-properties.md
+++ b/docs/rules/no-unused-properties.md
@@ -54,7 +54,8 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
```json
{
"vue/no-unused-properties": ["error", {
- "groups": ["props"]
+ "groups": ["props"],
+ "deepData": false
}]
}
```
@@ -65,6 +66,7 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
- `"computed"`
- `"methods"`
- `"setup"`
+- `"deepData"` (`boolean`) If `true`, the object of the property defined in `data` will be searched deeply. Default is `false`. Include `"data"` in `groups` to use this option.
### `"groups": ["props", "data"]`
@@ -108,6 +110,32 @@ This rule cannot be checked for use in other components (e.g. `mixins`, Property
+### `{ "groups": ["props", "data"], "deepData": true }`
+
+
+
+```vue
+
+
+
+
+```
+
+
+
### `"groups": ["props", "computed"]`
diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js
index a405c8e60..61ac8357f 100644
--- a/lib/rules/no-unused-properties.js
+++ b/lib/rules/no-unused-properties.js
@@ -17,17 +17,12 @@ const eslintUtils = require('eslint-utils')
*/
/**
* @typedef {object} TemplatePropertiesContainer
- * @property {Set} usedNames
+ * @property {UsedProperties} usedProperties
* @property {Set} refNames
* @typedef {object} VueComponentPropertiesContainer
* @property {ComponentPropertyData[]} properties
- * @property {Set} usedNames
- * @property {boolean} unknown
- * @property {Set} usedPropsNames
- * @property {boolean} unknownProps
- * @typedef { { node: FunctionExpression | ArrowFunctionExpression | FunctionDeclaration, index: number } } CallIdAndParamIndex
- * @typedef { { usedNames: UsedNames, unknown: boolean } } UsedProperties
- * @typedef { (context: RuleContext) => UsedProps } UsedPropsTracker
+ * @property {UsedProperties} usedProperties
+ * @property {UsedProperties} usedPropertiesForProps
*/
// ------------------------------------------------------------------------------
@@ -94,171 +89,290 @@ function getScope(context, currentNode) {
* Extract names from references objects.
* @param {VReference[]} references
*/
-function getReferencesNames(references) {
- return references
- .filter((ref) => ref.variable == null)
- .map((ref) => ref.id.name)
+function getReferences(references) {
+ return references.filter((ref) => ref.variable == null).map((ref) => ref.id)
}
-class UsedNames {
- constructor() {
- /** @type {Map} */
- this.map = new Map()
+/**
+ * @param {RuleContext} context
+ * @param {Identifier} id
+ * @returns {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration | null}
+ */
+function findFunction(context, id) {
+ const calleeVariable = findVariable(context, id)
+ if (!calleeVariable) {
+ return null
+ }
+ if (calleeVariable.defs.length === 1) {
+ const def = calleeVariable.defs[0]
+ if (def.node.type === 'FunctionDeclaration') {
+ return def.node
+ }
+ if (
+ def.type === 'Variable' &&
+ def.parent.kind === 'const' &&
+ def.node.init
+ ) {
+ if (
+ def.node.init.type === 'FunctionExpression' ||
+ def.node.init.type === 'ArrowFunctionExpression'
+ ) {
+ return def.node.init
+ }
+ if (def.node.init.type === 'Identifier') {
+ return findFunction(context, def.node.init)
+ }
+ }
+ }
+ return null
+}
+
+/**
+ * @param {RuleContext} context
+ * @param {Identifier} id
+ * @returns {Expression}
+ */
+function findExpression(context, id) {
+ const variable = findVariable(context, id)
+ if (!variable) {
+ return id
}
+ if (variable.defs.length === 1) {
+ const def = variable.defs[0]
+ if (
+ def.type === 'Variable' &&
+ def.parent.kind === 'const' &&
+ def.node.init
+ ) {
+ if (def.node.init.type === 'Identifier') {
+ return findExpression(context, def.node.init)
+ }
+ return def.node.init
+ }
+ }
+ return id
+}
+
+/**
+ * @typedef { (context: RuleContext) => UsedProperties } UsedPropertiesTracker
+ * @typedef { { node: CallExpression, index: number } } CallAndParamIndex
+ */
+
+/**
+ * Collects the used property names.
+ */
+class UsedProperties {
/**
- * @returns {IterableIterator}
+ * @param {object} [option]
+ * @param {boolean} [option.unknown]
*/
- names() {
- return this.map.keys()
+ constructor(option) {
+ /** @type {Record} */
+ this.map = {}
+ /** @type {CallAndParamIndex[]} */
+ this.calls = []
+ this.unknown = (option && option.unknown) || false
}
+
/**
* @param {string} name
- * @returns {UsedPropsTracker[]}
*/
- get(name) {
- return this.map.get(name) || []
+ isUsed(name) {
+ if (this.unknown) {
+ // If it is unknown, it is considered used.
+ return true
+ }
+ return Boolean(this.map[name])
}
+
/**
* @param {string} name
- * @param {UsedPropsTracker} tracker
+ * @param {UsedPropertiesTracker | null} tracker
*/
- add(name, tracker) {
- const list = this.map.get(name)
- if (list) {
- list.push(tracker)
- } else {
- this.map.set(name, [tracker])
- }
+ addUsed(name, tracker) {
+ const trackers = this.map[name] || (this.map[name] = [])
+ if (tracker) trackers.push(tracker)
}
+
/**
- * @param {UsedNames} other
+ * @param {string} name
+ * @returns {UsedPropertiesTracker}
*/
- addAll(other) {
- other.map.forEach((trackers, name) => {
- const list = this.map.get(name)
- if (list) {
- list.push(...trackers)
- } else {
- this.map.set(name, trackers)
+ getPropsTracker(name) {
+ if (this.unknown) {
+ return () => new UsedProperties({ unknown: true })
+ }
+ const trackers = this.map[name] || []
+ return (context) => {
+ const result = new UsedProperties()
+ for (const tracker of trackers) {
+ result.merge(tracker(context))
}
- })
+ return result
+ }
+ }
+
+ /**
+ * @param {UsedProperties | null} other
+ */
+ merge(other) {
+ if (!other) {
+ return
+ }
+ this.unknown = this.unknown || other.unknown
+ if (this.unknown) {
+ return
+ }
+ for (const [name, otherTrackers] of Object.entries(other.map)) {
+ const trackers = this.map[name] || (this.map[name] = [])
+ trackers.push(...otherTrackers)
+ }
+ this.calls.push(...other.calls)
}
}
/**
+ * Collects the used property names for parameters of the function.
+ */
+class ParamsUsedProperties {
+ /**
+ * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
+ * @param {RuleContext} context
+ */
+ constructor(node, context) {
+ this.node = node
+ this.context = context
+ /** @type {UsedProperties[]} */
+ this.params = []
+ }
+
+ /**
+ * @param {number} index
+ * @returns {UsedProperties | null}
+ */
+ getParam(index) {
+ const param = this.params[index]
+ if (param != null) {
+ return param
+ }
+ if (this.node.params[index]) {
+ return (this.params[index] = extractParamProperties(
+ this.node.params[index],
+ this.context
+ ))
+ }
+ return null
+ }
+}
+/**
+ * Extract the used property name from one parameter of the function.
+ * @param {Pattern} node
+ * @param {RuleContext} context
+ * @returns {UsedProperties}
+ */
+function extractParamProperties(node, context) {
+ const result = new UsedProperties()
+
+ while (node.type === 'AssignmentPattern') {
+ node = node.left
+ }
+ if (node.type === 'RestElement' || node.type === 'ArrayPattern') {
+ // cannot check
+ return result
+ }
+ if (node.type === 'ObjectPattern') {
+ result.merge(extractObjectPatternProperties(node))
+ return result
+ }
+ if (node.type !== 'Identifier') {
+ return result
+ }
+ const variable = findVariable(context, node)
+ if (!variable) {
+ return result
+ }
+ for (const reference of variable.references) {
+ const id = reference.identifier
+ result.merge(extractPatternOrThisProperties(id, context, false))
+ }
+
+ return result
+}
+
+/**
+ * Extract the used property name from ObjectPattern.
* @param {ObjectPattern} node
* @returns {UsedProperties}
*/
function extractObjectPatternProperties(node) {
- const usedNames = new UsedNames()
+ const result = new UsedProperties()
for (const prop of node.properties) {
if (prop.type === 'Property') {
const name = utils.getStaticPropertyName(prop)
if (name) {
- usedNames.add(name, getObjectPatternPropertyPatternTracker(prop.value))
+ result.addUsed(name, getObjectPatternPropertyPatternTracker(prop.value))
} else {
// If cannot trace name, everything is used!
- return {
- usedNames,
- unknown: true
- }
+ result.unknown = true
+ return result
}
} else {
// If use RestElement, everything is used!
- return {
- usedNames,
- unknown: true
- }
+ result.unknown = true
+ return result
}
}
- return {
- usedNames,
- unknown: false
- }
+ return result
}
/**
- * @param {Pattern} pattern
- * @returns {UsedPropsTracker}
+ * Extract the used property name from id.
+ * @param {Identifier} node
+ * @param {RuleContext} context
+ * @returns {UsedProperties}
*/
-function getObjectPatternPropertyPatternTracker(pattern) {
- if (pattern.type === 'ObjectPattern') {
- return () => {
- const result = new UsedProps()
- const { usedNames, unknown } = extractObjectPatternProperties(pattern)
- result.usedNames.addAll(usedNames)
- result.unknown = unknown
- return result
- }
- }
- if (pattern.type === 'Identifier') {
- return (context) => {
- const result = new UsedProps()
- const variable = findVariable(context, pattern)
- if (!variable) {
- return result
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
- }
- return result
- }
- } else if (pattern.type === 'AssignmentPattern') {
- return getObjectPatternPropertyPatternTracker(pattern.left)
- }
- return () => {
- const result = new UsedProps()
- result.unknown = true
+function extractIdentifierProperties(node, context) {
+ const result = new UsedProperties()
+ const variable = findVariable(context, node)
+ if (!variable) {
return result
}
+ for (const reference of variable.references) {
+ const id = reference.identifier
+ result.merge(extractPatternOrThisProperties(id, context, false))
+ }
+ return result
}
-
/**
+ * Extract the used property name from pattern or `this`.
* @param {Identifier | MemberExpression | ChainExpression | ThisExpression} node
* @param {RuleContext} context
- * @returns {UsedProps}
+ * @param {boolean} withInTemplate
+ * @returns {UsedProperties}
*/
-function extractPatternOrThisProperties(node, context) {
- const result = new UsedProps()
+function extractPatternOrThisProperties(node, context, withInTemplate) {
+ const result = new UsedProperties()
const parent = node.parent
if (parent.type === 'AssignmentExpression') {
+ if (withInTemplate) {
+ return result
+ }
if (parent.right === node && parent.left.type === 'ObjectPattern') {
// `({foo} = arg)`
- const { usedNames, unknown } = extractObjectPatternProperties(parent.left)
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
+ result.merge(extractObjectPatternProperties(parent.left))
}
return result
} else if (parent.type === 'VariableDeclarator') {
+ if (withInTemplate) {
+ return result
+ }
if (parent.init === node) {
if (parent.id.type === 'ObjectPattern') {
// `const {foo} = arg`
- const { usedNames, unknown } = extractObjectPatternProperties(parent.id)
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
+ result.merge(extractObjectPatternProperties(parent.id))
} else if (parent.id.type === 'Identifier') {
// `const foo = arg`
- const variable = findVariable(context, parent.id)
- if (!variable) {
- return result
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
- }
+ result.merge(extractIdentifierProperties(parent.id, context))
}
}
return result
@@ -267,8 +381,8 @@ function extractPatternOrThisProperties(node, context) {
// `arg.foo`
const name = utils.getStaticPropertyName(parent)
if (name) {
- result.usedNames.add(name, () =>
- extractPatternOrThisProperties(parent, context)
+ result.addUsed(name, () =>
+ extractPatternOrThisProperties(parent, context, withInTemplate)
)
} else {
result.unknown = true
@@ -276,133 +390,73 @@ function extractPatternOrThisProperties(node, context) {
}
return result
} else if (parent.type === 'CallExpression') {
+ if (withInTemplate) {
+ return result
+ }
const argIndex = parent.arguments.indexOf(node)
- if (argIndex > -1 && parent.callee.type === 'Identifier') {
+ if (argIndex > -1) {
// `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.kind === 'const' &&
- def.node.init &&
- (def.node.init.type === 'FunctionExpression' ||
- def.node.init.type === 'ArrowFunctionExpression')
- ) {
- result.calls.push({
- node: def.node.init,
- index: argIndex
- })
- } else if (def.node.type === 'FunctionDeclaration') {
- result.calls.push({
- node: def.node,
- index: argIndex
- })
- }
- }
+ result.calls.push({
+ node: parent,
+ index: argIndex
+ })
}
} else if (parent.type === 'ChainExpression') {
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- parent,
- context
+ result.merge(
+ extractPatternOrThisProperties(parent, context, withInTemplate)
)
- result.usedNames.addAll(usedNames)
- result.unknown = result.unknown || unknown
- result.calls.push(...calls)
+ } else if (
+ parent.type === 'ArrowFunctionExpression' ||
+ parent.type === 'ReturnStatement' ||
+ parent.type === 'VExpressionContainer' ||
+ parent.type === 'Property' ||
+ parent.type === 'ArrayExpression'
+ ) {
+ // Maybe used externally.
+ if (maybeExternalUsed(parent)) {
+ result.unknown = true
+ }
}
return result
-}
-/**
- * Collects the property names used.
- */
-class UsedProps {
- constructor() {
- this.usedNames = new UsedNames()
- /** @type {CallIdAndParamIndex[]} */
- this.calls = []
- this.unknown = false
- }
-}
-
-/**
- * Collects the property names used for one parameter of the function.
- */
-class ParamUsedProps extends UsedProps {
/**
- * @param {Pattern} paramNode
- * @param {RuleContext} context
+ * @param {ASTNode} parentTarget
+ * @returns {boolean}
*/
- constructor(paramNode, context) {
- super()
- while (paramNode.type === 'AssignmentPattern') {
- paramNode = paramNode.left
+ function maybeExternalUsed(parentTarget) {
+ if (
+ parentTarget.type === 'ReturnStatement' ||
+ parentTarget.type === 'VExpressionContainer'
+ ) {
+ return true
}
- if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') {
- // cannot check
- return
+ if (parentTarget.type === 'ArrayExpression') {
+ return maybeExternalUsed(parentTarget.parent)
}
- if (paramNode.type === 'ObjectPattern') {
- const { usedNames, unknown } = extractObjectPatternProperties(paramNode)
- this.usedNames.addAll(usedNames)
- this.unknown = this.unknown || unknown
- return
+ if (parentTarget.type === 'Property') {
+ return maybeExternalUsed(parentTarget.parent.parent)
}
- if (paramNode.type !== 'Identifier') {
- return
- }
- const variable = findVariable(context, paramNode)
- if (!variable) {
- return
- }
- for (const reference of variable.references) {
- const id = reference.identifier
- const { usedNames, unknown, calls } = extractPatternOrThisProperties(
- id,
- context
- )
- this.usedNames.addAll(usedNames)
- this.unknown = this.unknown || unknown
- this.calls.push(...calls)
+ if (parentTarget.type === 'ArrowFunctionExpression') {
+ return parentTarget.body === node
}
+ return false
}
}
/**
- * Collects the property names used for parameters of the function.
+ * @param {Pattern} pattern
+ * @returns {UsedPropertiesTracker}
*/
-class ParamsUsedProps {
- /**
- * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
- * @param {RuleContext} context
- */
- constructor(node, context) {
- this.node = node
- this.context = context
- /** @type {ParamUsedProps[]} */
- this.params = []
+function getObjectPatternPropertyPatternTracker(pattern) {
+ if (pattern.type === 'ObjectPattern') {
+ return () => extractObjectPatternProperties(pattern)
}
-
- /**
- * @param {number} index
- * @returns {ParamUsedProps | null}
- */
- 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
+ if (pattern.type === 'Identifier') {
+ return (context) => extractIdentifierProperties(pattern, context)
+ } else if (pattern.type === 'AssignmentPattern') {
+ return getObjectPatternPropertyPatternTracker(pattern.left)
}
+ return () => new UsedProperties({ unknown: true })
}
// ------------------------------------------------------------------------------
@@ -435,7 +489,8 @@ module.exports = {
},
additionalItems: false,
uniqueItems: true
- }
+ },
+ deepData: { type: 'boolean' }
},
additionalProperties: false
}
@@ -448,12 +503,13 @@ module.exports = {
create(context) {
const options = context.options[0] || {}
const groups = new Set(options.groups || [GROUP_PROPERTY])
+ const deepData = Boolean(options.deepData)
- /** @type {Map} */
- const paramsUsedPropsMap = new Map()
+ /** @type {Map} */
+ const paramsUsedPropertiesMap = new Map()
/** @type {TemplatePropertiesContainer} */
const templatePropertiesContainer = {
- usedNames: new Set(),
+ usedProperties: new UsedProperties(),
refNames: new Set()
}
/** @type {Map} */
@@ -461,13 +517,13 @@ module.exports = {
/**
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
- * @returns {ParamsUsedProps}
+ * @returns {ParamsUsedProperties}
*/
- function getParamsUsedProps(node) {
- let usedProps = paramsUsedPropsMap.get(node)
+ function getParamsUsedProperties(node) {
+ let usedProps = paramsUsedPropertiesMap.get(node)
if (!usedProps) {
- usedProps = new ParamsUsedProps(node, context)
- paramsUsedPropsMap.set(node, usedProps)
+ usedProps = new ParamsUsedProperties(node, context)
+ paramsUsedPropertiesMap.set(node, usedProps)
}
return usedProps
}
@@ -481,37 +537,84 @@ module.exports = {
if (!container) {
container = {
properties: [],
- usedNames: new Set(),
- usedPropsNames: new Set(),
- unknown: false,
- unknownProps: false
+ usedProperties: new UsedProperties(),
+ usedPropertiesForProps: new UsedProperties()
}
vueComponentPropertiesContainerMap.set(node, container)
}
return container
}
+ /**
+ * @param {string[]} segments
+ * @param {Expression} propertyValue
+ * @param {UsedProperties} baseUsedProperties
+ */
+ function verifyDataOptionDeepProperties(
+ segments,
+ propertyValue,
+ baseUsedProperties
+ ) {
+ let targetExpr = propertyValue
+ if (targetExpr.type === 'Identifier') {
+ targetExpr = findExpression(context, targetExpr)
+ }
+ if (targetExpr.type === 'ObjectExpression') {
+ const usedProperties = resolvedUsedProperties(baseUsedProperties, {
+ allowUnknownCall: true
+ })
+ if (usedProperties.unknown) {
+ return
+ }
+
+ for (const prop of targetExpr.properties) {
+ if (prop.type !== 'Property') {
+ continue
+ }
+ const name = utils.getStaticPropertyName(prop)
+ if (name == null) {
+ continue
+ }
+ if (!usedProperties.isUsed(name)) {
+ // report
+ context.report({
+ node: prop.key,
+ messageId: 'unused',
+ data: {
+ group: PROPERTY_LABEL.data,
+ name: [...segments, name].join('.')
+ }
+ })
+ continue
+ }
+ // next
+ verifyDataOptionDeepProperties(
+ [...segments, name],
+ prop.value,
+ usedProperties.getPropsTracker(name)(context)
+ )
+ }
+ }
+ }
/**
* Report all unused properties.
*/
function reportUnusedProperties() {
for (const container of vueComponentPropertiesContainerMap.values()) {
- if (container.unknown) {
- // unknown
+ const usedProperties = resolvedUsedProperties(container.usedProperties)
+ usedProperties.merge(templatePropertiesContainer.usedProperties)
+ if (usedProperties.unknown) {
continue
}
+
+ const usedPropertiesForProps = resolvedUsedProperties(
+ container.usedPropertiesForProps
+ )
+
for (const property of container.properties) {
- if (
- container.usedNames.has(property.name) ||
- templatePropertiesContainer.usedNames.has(property.name)
- ) {
- // used
- continue
- }
if (
property.groupName === 'props' &&
- (container.unknownProps ||
- container.usedPropsNames.has(property.name))
+ usedPropertiesForProps.isUsed(property.name)
) {
// used props
continue
@@ -523,6 +626,22 @@ module.exports = {
// used template refs
continue
}
+ if (usedProperties.isUsed(property.name)) {
+ // used
+ if (
+ deepData &&
+ property.groupName === 'data' &&
+ property.type === 'object'
+ ) {
+ // Check the deep properties of the data option.
+ verifyDataOptionDeepProperties(
+ [property.name],
+ property.property.value,
+ usedProperties.getPropsTracker(property.name)(context)
+ )
+ }
+ continue
+ }
context.report({
node: property.node,
messageId: 'unused',
@@ -536,60 +655,60 @@ module.exports = {
}
/**
- * @param {UsedProps} usedProps
- * @param {Map>} already
- * @returns {IterableIterator}
+ * @param {UsedProperties | null} usedProps
+ * @param {object} [options]
+ * @param {boolean} [options.allowUnknownCall]
+ * @returns {UsedProperties}
*/
- 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)
+ function resolvedUsedProperties(usedProps, options) {
+ const allowUnknownCall = options && options.allowUnknownCall
+ const already = new Map()
+
+ const result = new UsedProperties()
+ for (const up of iterate(usedProps)) {
+ result.merge(up)
+ if (result.unknown) {
+ break
}
- 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)
}
- }
+ return result
- /**
- * @param {VueComponentPropertiesContainer} container
- * @param {UsedProps} baseUseProps
- */
- function processParamPropsUsed(container, baseUseProps) {
- for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
- if (unknown) {
- container.unknownProps = true
+ /**
+ * @param {UsedProperties | null} usedProps
+ * @returns {IterableIterator}
+ */
+ function* iterate(usedProps) {
+ if (!usedProps) {
return
}
- for (const name of usedNames.names()) {
- container.usedPropsNames.add(name)
- }
- }
- }
+ yield usedProps
+ for (const call of usedProps.calls) {
+ if (call.node.callee.type !== 'Identifier') {
+ if (allowUnknownCall) {
+ yield new UsedProperties({ unknown: true })
+ }
+ continue
+ }
+ const fnNode = findFunction(context, call.node.callee)
+ if (!fnNode) {
+ if (allowUnknownCall) {
+ yield new UsedProperties({ unknown: true })
+ }
+ continue
+ }
- /**
- * @param {VueComponentPropertiesContainer} container
- * @param {UsedProps} baseUseProps
- */
- function processUsed(container, baseUseProps) {
- for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) {
- if (unknown) {
- container.unknown = true
- return
- }
- for (const name of usedNames.names()) {
- container.usedNames.add(name)
+ let alreadyIndexes = already.get(fnNode)
+ if (!alreadyIndexes) {
+ alreadyIndexes = new Set()
+ already.set(fnNode, alreadyIndexes)
+ }
+ if (alreadyIndexes.has(call.index)) {
+ continue
+ }
+ alreadyIndexes.add(call.index)
+ const paramsUsedProps = getParamsUsedProperties(fnNode)
+ const paramUsedProps = paramsUsedProps.getParam(call.index)
+ yield* iterate(paramUsedProps)
}
}
}
@@ -607,28 +726,44 @@ module.exports = {
return null
}
const property = node.parent
- if (!property.parent || property.parent.type !== 'ObjectExpression') {
+ if (!utils.isProperty(property)) {
return null
}
- return /** @type {Property} */ (property)
+ return property
}
- const scriptVisitor = Object.assign(
+ const scriptVisitor = utils.compositingVisitors(
{},
utils.defineVueVisitor(context, {
onVueObjectEnter(node) {
const container = getVueComponentPropertiesContainer(node)
- const watcherUsedProperties = new Set()
+
for (const watcher of utils.iterateProperties(
node,
new Set([GROUP_WATCHER])
)) {
// Process `watch: { foo /* <- this */ () {} }`
- let path
- for (const seg of watcher.name.split('.')) {
- path = path ? `${path}.${seg}` : seg
- watcherUsedProperties.add(path)
- }
+ const segments = watcher.name.split('.')
+ container.usedProperties.addUsed(segments[0], (context) => {
+ return buildChainTracker(segments)(context)
+ /**
+ * @param {string[]} baseSegments
+ * @returns {UsedPropertiesTracker}
+ */
+ function buildChainTracker(baseSegments) {
+ return () => {
+ const subSegments = baseSegments.slice(1)
+ const usedProps = new UsedProperties()
+ if (subSegments.length) {
+ usedProps.addUsed(
+ subSegments[0],
+ buildChainTracker(subSegments)
+ )
+ }
+ return usedProps
+ }
+ }
+ })
// Process `watch: { x: 'foo' /* <- this */ }`
if (watcher.type === 'object') {
@@ -643,19 +778,14 @@ module.exports = {
) {
const name = utils.getStringLiteralValue(handlerValueNode)
if (name != null) {
- watcherUsedProperties.add(name)
+ container.usedProperties.addUsed(name, null)
}
}
}
}
}
}
- for (const prop of utils.iterateProperties(node, groups)) {
- if (watcherUsedProperties.has(prop.name)) {
- continue
- }
- container.properties.push(prop)
- }
+ container.properties.push(...utils.iterateProperties(node, groups))
},
/** @param { (FunctionExpression | ArrowFunctionExpression) & { parent: Property }} node */
'ObjectExpression > Property > :function[params.length>0]'(
@@ -696,70 +826,44 @@ module.exports = {
) {
return
}
- // check { computed: { foo: { get: () => vm.prop } } }
+ // check { computed: { foo: { get: (vm) => vm.prop } } }
} else {
return
}
}
}
- const paramsUsedProps = getParamsUsedProps(node)
- const usedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
- processUsed(
- getVueComponentPropertiesContainer(vueData.node),
- usedProps
- )
+ const paramsUsedProps = getParamsUsedProperties(node)
+ const usedProps = paramsUsedProps.getParam(0)
+ const container = getVueComponentPropertiesContainer(vueData.node)
+ container.usedProperties.merge(usedProps)
},
onSetupFunctionEnter(node, vueData) {
const container = getVueComponentPropertiesContainer(vueData.node)
- if (node.params[0]) {
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
-
- processParamPropsUsed(container, paramUsedProps)
- }
+ const paramsUsedProps = getParamsUsedProperties(node)
+ const paramUsedProps = paramsUsedProps.getParam(0)
+ container.usedPropertiesForProps.merge(paramUsedProps)
},
onRenderFunctionEnter(node, vueData) {
const container = getVueComponentPropertiesContainer(vueData.node)
- if (node.params[0]) {
- // for Vue 3.x render
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 0
- ))
-
- processParamPropsUsed(container, paramUsedProps)
- if (container.unknownProps) {
- return
- }
+
+ // Check for Vue 3.x render
+ const paramsUsedProps = getParamsUsedProperties(node)
+ const ctxUsedProps = paramsUsedProps.getParam(0)
+
+ container.usedPropertiesForProps.merge(ctxUsedProps)
+ if (container.usedPropertiesForProps.unknown) {
+ return
}
- if (vueData.functional && node.params[1]) {
- // for Vue 2.x render & functional
- const paramsUsedProps = getParamsUsedProps(node)
- const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam(
- 1
- ))
-
- for (const { usedNames, unknown } of iterateUsedProps(
- paramUsedProps
- )) {
- if (unknown) {
- container.unknownProps = true
- return
- }
- for (const usedPropsTracker of usedNames.get('props')) {
- const propUsedProps = usedPropsTracker(context)
- processParamPropsUsed(container, propUsedProps)
- if (container.unknownProps) {
- return
- }
- }
- }
+ if (vueData.functional) {
+ // Check for Vue 2.x render & functional
+ const contextUsedProps = resolvedUsedProperties(
+ paramsUsedProps.getParam(1)
+ )
+ const tracker = contextUsedProps.getPropsTracker('props')
+ const propUsedProps = tracker(context)
+ container.usedPropertiesForProps.merge(propUsedProps)
}
},
/**
@@ -771,9 +875,8 @@ module.exports = {
return
}
const container = getVueComponentPropertiesContainer(vueData.node)
- const usedProps = extractPatternOrThisProperties(node, context)
-
- processUsed(container, usedProps)
+ const usedProps = extractPatternOrThisProperties(node, context, false)
+ container.usedProperties.merge(usedProps)
}
}),
{
@@ -791,8 +894,11 @@ module.exports = {
* @param {VExpressionContainer} node
*/
VExpressionContainer(node) {
- for (const name of getReferencesNames(node.references)) {
- templatePropertiesContainer.usedNames.add(name)
+ for (const id of getReferences(node.references)) {
+ templatePropertiesContainer.usedProperties.addUsed(
+ id.name,
+ (context) => extractPatternOrThisProperties(id, context, true)
+ )
}
},
/**
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 3df90418a..b6ed05900 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -1652,11 +1652,14 @@ function findAssignmentProperty(node, name, filter) {
/**
* Checks whether the given node is Property.
- * @param {Property | SpreadElement} node
+ * @param {Property | SpreadElement | AssignmentProperty} node
* @returns {node is Property}
*/
function isProperty(node) {
- return node.type === 'Property'
+ if (node.type !== 'Property') {
+ return false
+ }
+ return !node.parent || node.parent.type === 'ObjectExpression'
}
/**
* Checks whether the given node is AssignmentProperty.
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index 1419add66..89478e06b 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -18,6 +18,7 @@ const tester = new RuleTester({
const allOptions = [
{ groups: ['props', 'computed', 'data', 'methods', 'setup'] }
]
+const deepDataOptions = [{ groups: ['data'], deepData: true }]
tester.run('no-unused-properties', rule, {
valid: [
@@ -1143,6 +1144,273 @@ tester.run('no-unused-properties', rule, {
};
`
+ },
+
+ // deep data
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ foo.baz.b }}
+
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ foo.bar }}
+ {{ foo.baz }}
+
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+
+
+
+
+ `,
+ options: deepDataOptions
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions
}
],
@@ -1686,6 +1954,229 @@ tester.run('no-unused-properties', rule, {
`,
errors: ["'x' of property found, but never used."]
+ },
+
+ // deep data
+ {
+ filename: 'test.vue',
+ code: `
+
+ {{ foo.baz.a }}
+
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ {
+ message: "'foo.baz.b' of data found, but never used.",
+ line: 14,
+ column: 21
+ }
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ "'foo.bar' of data found, but never used.",
+ "'foo.baz.b' of data found, but never used."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: ["'foo.bar.a' of data found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ "'foo.bar.a' of data found, but never used.",
+ "'foo.baz.b' of data found, but never used."
+ ]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: ["'foo.bar.a' of data found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: ["'foo.bar.a' of data found, but never used."]
+ },
+ {
+ filename: 'test.vue',
+ code: `
+
+ `,
+ options: deepDataOptions,
+ errors: [
+ {
+ message: "'foo.bar.a' of data found, but never used.",
+ line: 6,
+ column: 17
+ },
+ {
+ message: "'foo.baz.a' of data found, but never used.",
+ line: 9,
+ column: 17
+ }
+ ]
}
]
})