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: ` + + + `, + 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 + }, + { + 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: ` + + + `, + 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 + } + ] } ] })