diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 8b6bfa132..722e95c31 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -11,10 +11,15 @@ const utils = require('../utils') const eslintUtils = require('eslint-utils') const { getStyleVariablesContext } = require('../utils/style-variables') +const { + definePropertyReferenceExtractor, + mergePropertyReferences +} = require('../utils/property-references') /** * @typedef {import('../utils').GroupName} GroupName * @typedef {import('../utils').VueObjectData} VueObjectData + * @typedef {import('../utils/property-references').IPropertyReferences} IPropertyReferences */ /** @@ -36,12 +41,12 @@ const { getStyleVariablesContext } = require('../utils/style-variables') /** * @typedef {object} TemplatePropertiesContainer - * @property {UsedProperties} usedProperties + * @property {IPropertyReferences[]} propertyReferences * @property {Set} refNames * @typedef {object} VueComponentPropertiesContainer * @property {ComponentPropertyData[]} properties - * @property {UsedProperties} usedProperties - * @property {UsedProperties} usedPropertiesForProps + * @property {IPropertyReferences[]} propertyReferences + * @property {IPropertyReferences[]} propertyReferencesForProps */ // ------------------------------------------------------------------------------ @@ -118,40 +123,6 @@ function getReferences(references) { return references.filter((ref) => ref.variable == null).map((ref) => ref.id) } -/** - * @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 @@ -178,312 +149,6 @@ function findExpression(context, id) { return id } -/** - * @typedef { (context: RuleContext) => UsedProperties } UsedPropertiesTracker - * @typedef { { node: CallExpression, index: number } } CallAndParamIndex - */ - -/** - * Collects the used property names. - */ -class UsedProperties { - /** - * @param {object} [option] - * @param {boolean} [option.unknown] - */ - constructor(option) { - /** @type {Record} */ - this.map = Object.create(null) - /** @type {CallAndParamIndex[]} */ - this.calls = [] - this.unknown = (option && option.unknown) || false - } - - /** - * @param {string} 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 {UsedPropertiesTracker | null} tracker - */ - addUsed(name, tracker) { - const trackers = this.map[name] || (this.map[name] = []) - if (tracker) trackers.push(tracker) - } - - /** - * @param {string} name - * @returns {UsedPropertiesTracker} - */ - 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] = extractParamOrVerProperties( - 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 extractParamOrVerProperties(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 result = new UsedProperties() - for (const prop of node.properties) { - if (prop.type === 'Property') { - const name = utils.getStaticPropertyName(prop) - if (name) { - result.addUsed(name, getObjectPatternPropertyPatternTracker(prop.value)) - } else { - // If cannot trace name, everything is used! - result.unknown = true - return result - } - } else { - // If use RestElement, everything is used! - result.unknown = true - return result - } - } - return result -} - -/** - * Extract the used property name from id. - * @param {Identifier} node - * @param {RuleContext} context - * @returns {UsedProperties} - */ -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 - * @param {boolean} withInTemplate - * @returns {UsedProperties} - */ -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)` - 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` - result.merge(extractObjectPatternProperties(parent.id)) - } else if (parent.id.type === 'Identifier') { - // `const foo = arg` - result.merge(extractIdentifierProperties(parent.id, context)) - } - } - return result - } else if (parent.type === 'MemberExpression') { - if (parent.object === node) { - // `arg.foo` - const name = utils.getStaticPropertyName(parent) - if (name) { - result.addUsed(name, () => - extractPatternOrThisProperties(parent, context, withInTemplate) - ) - } else { - result.unknown = true - } - } - return result - } else if (parent.type === 'CallExpression') { - if (withInTemplate) { - return result - } - const argIndex = parent.arguments.indexOf(node) - if (argIndex > -1) { - // `foo(arg)` - result.calls.push({ - node: parent, - index: argIndex - }) - } - } else if (parent.type === 'ChainExpression') { - result.merge( - extractPatternOrThisProperties(parent, context, withInTemplate) - ) - } 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 - - /** - * @param {ASTNode} parentTarget - * @returns {boolean} - */ - function maybeExternalUsed(parentTarget) { - if ( - parentTarget.type === 'ReturnStatement' || - parentTarget.type === 'VExpressionContainer' - ) { - return true - } - if (parentTarget.type === 'ArrayExpression') { - return maybeExternalUsed(parentTarget.parent) - } - if (parentTarget.type === 'Property') { - return maybeExternalUsed(parentTarget.parent.parent) - } - if (parentTarget.type === 'ArrowFunctionExpression') { - return parentTarget.body === node - } - return false - } -} - -/** - * @param {Pattern} pattern - * @returns {UsedPropertiesTracker} - */ -function getObjectPatternPropertyPatternTracker(pattern) { - if (pattern.type === 'ObjectPattern') { - return () => extractObjectPatternProperties(pattern) - } - if (pattern.type === 'Identifier') { - return (context) => extractIdentifierProperties(pattern, context) - } else if (pattern.type === 'AssignmentPattern') { - return getObjectPatternPropertyPatternTracker(pattern.left) - } - return () => new UsedProperties({ unknown: true }) -} - /** * Check if the given component property is marked as `@public` in JSDoc comments. * @param {ComponentPropertyData} property @@ -617,29 +282,16 @@ module.exports = { const deepData = Boolean(options.deepData) const ignorePublicMembers = Boolean(options.ignorePublicMembers) - /** @type {Map} */ - const paramsUsedPropertiesMap = new Map() + const propertyReferenceExtractor = definePropertyReferenceExtractor(context) + /** @type {TemplatePropertiesContainer} */ const templatePropertiesContainer = { - usedProperties: new UsedProperties(), + propertyReferences: [], refNames: new Set() } /** @type {Map} */ const vueComponentPropertiesContainerMap = new Map() - /** - * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node - * @returns {ParamsUsedProperties} - */ - function getParamsUsedProperties(node) { - let usedProps = paramsUsedPropertiesMap.get(node) - if (!usedProps) { - usedProps = new ParamsUsedProperties(node, context) - paramsUsedPropertiesMap.set(node, usedProps) - } - return usedProps - } - /** * @param {ASTNode} node * @returns {VueComponentPropertiesContainer} @@ -649,8 +301,8 @@ module.exports = { if (!container) { container = { properties: [], - usedProperties: new UsedProperties(), - usedPropertiesForProps: new UsedProperties() + propertyReferences: [], + propertyReferencesForProps: [] } vueComponentPropertiesContainerMap.set(node, container) } @@ -659,25 +311,18 @@ module.exports = { /** * @param {string[]} segments * @param {Expression} propertyValue - * @param {UsedProperties} baseUsedProperties + * @param {IPropertyReferences} propertyReferences */ function verifyDataOptionDeepProperties( segments, propertyValue, - baseUsedProperties + propertyReferences ) { 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 @@ -686,7 +331,9 @@ module.exports = { if (name == null) { continue } - if (!usedProperties.isUsed(name)) { + if ( + !propertyReferences.hasProperty(name, { unknownCallAsAny: true }) + ) { // report context.report({ node: prop.key, @@ -702,7 +349,7 @@ module.exports = { verifyDataOptionDeepProperties( [...segments, name], prop.value, - usedProperties.getPropsTracker(name)(context) + propertyReferences.getNest(name) ) } } @@ -713,20 +360,19 @@ module.exports = { */ function reportUnusedProperties() { for (const container of vueComponentPropertiesContainerMap.values()) { - const usedProperties = resolvedUsedProperties(container.usedProperties) - usedProperties.merge(templatePropertiesContainer.usedProperties) - if (usedProperties.unknown) { - continue - } + const propertyReferences = mergePropertyReferences([ + ...container.propertyReferences, + ...templatePropertiesContainer.propertyReferences + ]) - const usedPropertiesForProps = resolvedUsedProperties( - container.usedPropertiesForProps + const propertyReferencesForProps = mergePropertyReferences( + container.propertyReferencesForProps ) for (const property of container.properties) { if ( property.groupName === 'props' && - usedPropertiesForProps.isUsed(property.name) + propertyReferencesForProps.hasProperty(property.name) ) { // used props continue @@ -744,7 +390,7 @@ module.exports = { ) { continue } - if (usedProperties.isUsed(property.name)) { + if (propertyReferences.hasProperty(property.name)) { // used if ( deepData && @@ -755,7 +401,7 @@ module.exports = { verifyDataOptionDeepProperties( [property.name], property.property.value, - usedProperties.getPropsTracker(property.name)(context) + propertyReferences.getNest(property.name) ) } continue @@ -772,65 +418,6 @@ module.exports = { } } - /** - * @param {UsedProperties | null} usedProps - * @param {object} [options] - * @param {boolean} [options.allowUnknownCall] - * @returns {UsedProperties} - */ - 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 - } - } - return result - - /** - * @param {UsedProperties | null} usedProps - * @returns {IterableIterator} - */ - function* iterate(usedProps) { - if (!usedProps) { - return - } - 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 - } - - 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) - } - } - } - /** * @param {Expression} node * @returns {Property|null} @@ -899,8 +486,9 @@ module.exports = { } const pattern = target.parent.id - const usedProps = extractParamOrVerProperties(pattern, context) - container.usedPropertiesForProps.merge(usedProps) + const propertyReferences = + propertyReferenceExtractor.extractFromPattern(pattern) + container.propertyReferencesForProps.push(propertyReferences) } }), utils.defineVueVisitor(context, { @@ -914,27 +502,9 @@ module.exports = { if (watcherOrExpose.groupName === GROUP_WATCHER) { const watcher = watcherOrExpose // Process `watch: { foo /* <- this */ () {} }` - 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 - } - } - }) + container.propertyReferences.push( + propertyReferenceExtractor.extractFromPath(watcher.name) + ) // Process `watch: { x: 'foo' /* <- this */ }` if (watcher.type === 'object') { @@ -949,7 +519,9 @@ module.exports = { ) { const name = utils.getStringLiteralValue(handlerValueNode) if (name != null) { - container.usedProperties.addUsed(name, null) + container.propertyReferences.push( + propertyReferenceExtractor.extractFromName(name) + ) } } } @@ -957,7 +529,9 @@ module.exports = { } } else if (watcherOrExpose.groupName === GROUP_EXPOSE) { const expose = watcherOrExpose - container.usedProperties.addUsed(expose.name, null) + container.propertyReferences.push( + propertyReferenceExtractor.extractFromName(expose.name) + ) } } container.properties.push(...utils.iterateProperties(node, groups)) @@ -1008,37 +582,33 @@ module.exports = { } } - const paramsUsedProps = getParamsUsedProperties(node) - const usedProps = paramsUsedProps.getParam(0) + const propertyReferences = + propertyReferenceExtractor.extractFromFunctionParam(node, 0) const container = getVueComponentPropertiesContainer(vueData.node) - container.usedProperties.merge(usedProps) + container.propertyReferences.push(propertyReferences) }, onSetupFunctionEnter(node, vueData) { const container = getVueComponentPropertiesContainer(vueData.node) - const paramsUsedProps = getParamsUsedProperties(node) - const paramUsedProps = paramsUsedProps.getParam(0) - container.usedPropertiesForProps.merge(paramUsedProps) + const propertyReferences = + propertyReferenceExtractor.extractFromFunctionParam(node, 0) + container.propertyReferencesForProps.push(propertyReferences) }, onRenderFunctionEnter(node, vueData) { const container = getVueComponentPropertiesContainer(vueData.node) // Check for Vue 3.x render - const paramsUsedProps = getParamsUsedProperties(node) - const ctxUsedProps = paramsUsedProps.getParam(0) - - container.usedPropertiesForProps.merge(ctxUsedProps) - if (container.usedPropertiesForProps.unknown) { - return - } + const propertyReferences = + propertyReferenceExtractor.extractFromFunctionParam(node, 0) + container.propertyReferencesForProps.push(propertyReferences) if (vueData.functional) { // Check for Vue 2.x render & functional - const contextUsedProps = resolvedUsedProperties( - paramsUsedProps.getParam(1) + const propertyReferencesForV2 = + propertyReferenceExtractor.extractFromFunctionParam(node, 1) + + container.propertyReferencesForProps.push( + propertyReferencesForV2.getNest('props') ) - const tracker = contextUsedProps.getPropsTracker('props') - const propUsedProps = tracker(context) - container.usedPropertiesForProps.merge(propUsedProps) } }, /** @@ -1050,8 +620,9 @@ module.exports = { return } const container = getVueComponentPropertiesContainer(vueData.node) - const usedProps = extractPatternOrThisProperties(node, context, false) - container.usedProperties.merge(usedProps) + const propertyReferences = + propertyReferenceExtractor.extractFromExpression(node, false) + container.propertyReferences.push(propertyReferences) } }), { @@ -1060,9 +631,10 @@ module.exports = { const styleVars = getStyleVariablesContext(context) if (styleVars) { for (const { id } of styleVars.references) { - templatePropertiesContainer.usedProperties.addUsed( - id.name, - (context) => extractPatternOrThisProperties(id, context, true) + templatePropertiesContainer.propertyReferences.push( + propertyReferenceExtractor.extractFromName(id.name, () => + propertyReferenceExtractor.extractFromExpression(id, true) + ) ) } } @@ -1079,9 +651,10 @@ module.exports = { */ VExpressionContainer(node) { for (const id of getReferences(node.references)) { - templatePropertiesContainer.usedProperties.addUsed( - id.name, - (context) => extractPatternOrThisProperties(id, context, true) + templatePropertiesContainer.propertyReferences.push( + propertyReferenceExtractor.extractFromName(id.name, () => + propertyReferenceExtractor.extractFromExpression(id, true) + ) ) } }, diff --git a/lib/utils/property-references.js b/lib/utils/property-references.js new file mode 100644 index 000000000..2291ea717 --- /dev/null +++ b/lib/utils/property-references.js @@ -0,0 +1,611 @@ +/** + * @author Yosuke Ota + * @copyright 2021 Yosuke Ota. All rights reserved. + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('./index') +const eslintUtils = require('eslint-utils') + +/** + * @typedef {'props' | 'data' | 'computed' | 'methods' | 'setup'} Group + */ +/** + * @typedef {object} IHasPropertyOption + * @property {boolean} [unknownCallAsAny] + */ +/** + * @typedef {object} IPropertyReferences + * @property { (name: string, option?: IHasPropertyOption) => boolean } hasProperty + * @property { (name: string) => IPropertyReferences } getNest + */ + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** @type {IPropertyReferences} */ +const ANY = { + hasProperty: () => true, + getNest: () => ANY +} + +/** @type {IPropertyReferences} */ +const NEVER = { + hasProperty: () => false, + getNest: () => NEVER +} + +/** + * Find the variable of a given name. + * @param {RuleContext} context The rule context + * @param {Identifier} node The variable name to find. + * @returns {Variable|null} The found variable or null. + */ +function findVariable(context, node) { + return eslintUtils.findVariable(getScope(context, node), node) +} +/** + * Gets the scope for the current node + * @param {RuleContext} context The rule context + * @param {ESNode} 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 + + /** @type {ESNode | null} */ + let node = currentNode + for (; node; node = /** @type {ESNode | null} */ (node.parent)) { + const scope = scopeManager.acquire(node, inner) + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] + } + return scope + } + } + + return scopeManager.scopes[0] +} + +/** + * @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 +} + +// ------------------------------------------------------------------------------ +// Public +// ------------------------------------------------------------------------------ + +module.exports = { + definePropertyReferenceExtractor, + mergePropertyReferences +} + +/** + * @param {RuleContext} context The rule context. + */ +function definePropertyReferenceExtractor(context) { + /** @type {Map} */ + const cacheForExpression = new Map() + /** @type {Map} */ + const cacheForPattern = new Map() + /** @type {Map>} */ + const cacheForFunction = new Map() + /** @type {{ toRefNodes: Set, toRefsNodes: Set} | null} */ + let toRefSet = null + + function getToRefSet() { + if (toRefSet) { + return toRefSet + } + const tracker = new eslintUtils.ReferenceTracker( + context.getSourceCode().scopeManager.scopes[0] + ) + const toRefNodes = new Set() + for (const { node } of tracker.iterateEsmReferences( + utils.createCompositionApiTraceMap({ + [eslintUtils.ReferenceTracker.ESM]: true, + toRef: { + [eslintUtils.ReferenceTracker.CALL]: true + } + }) + )) { + toRefNodes.add(node) + } + const toRefsNodes = new Set() + for (const { node } of tracker.iterateEsmReferences( + utils.createCompositionApiTraceMap({ + [eslintUtils.ReferenceTracker.ESM]: true, + toRefs: { + [eslintUtils.ReferenceTracker.CALL]: true + } + }) + )) { + toRefsNodes.add(node) + } + + return (toRefSet = { toRefNodes, toRefsNodes }) + } + + /** + * Collects the property references for member expr. + * @implements IPropertyReferences + */ + class PropertyReferencesForMember { + /** + * + * @param {MemberExpression} node + * @param {string} name + * @param {boolean} withInTemplate + */ + constructor(node, name, withInTemplate) { + this.node = node + this.name = name + this.withInTemplate = withInTemplate + } + + /** + * @param {string} name + */ + hasProperty(name) { + return name === this.name + } + + /** + * @param {string} name + * @returns {IPropertyReferences} + */ + getNest(name) { + return name === this.name + ? extractFromExpression(this.node, this.withInTemplate) + : NEVER + } + } + /** + * Collects the property references for object. + * @implements IPropertyReferences + */ + class PropertyReferencesForObject { + constructor() { + /** @type {Record} */ + this.properties = Object.create(null) + } + + /** + * @param {string} name + */ + hasProperty(name) { + return Boolean(this.properties[name]) + } + + /** + * @param {string} name + * @returns {IPropertyReferences} + */ + getNest(name) { + const properties = this.properties[name] + return properties + ? mergePropertyReferences( + properties.map((property) => getNestFromPattern(property.value)) + ) + : NEVER + + /** + * @param {Pattern} pattern + * @returns {IPropertyReferences} + */ + function getNestFromPattern(pattern) { + if (pattern.type === 'ObjectPattern') { + return extractFromObjectPattern(pattern) + } + if (pattern.type === 'Identifier') { + return extractFromIdentifier(pattern) + } else if (pattern.type === 'AssignmentPattern') { + return getNestFromPattern(pattern.left) + } + return ANY + } + } + } + + /** + * Extract the property references from Expression. + * @param {Identifier | MemberExpression | ChainExpression | ThisExpression | CallExpression} node + * @param {boolean} withInTemplate + * @returns {IPropertyReferences} + */ + function extractFromExpression(node, withInTemplate) { + const ref = cacheForExpression.get(node) + if (ref) { + return ref + } + cacheForExpression.set(node, ANY) + const result = extractWithoutCache() + cacheForExpression.set(node, result) + return result + + function extractWithoutCache() { + const parent = node.parent + if (parent.type === 'AssignmentExpression') { + if (withInTemplate) { + return NEVER + } + if (parent.right === node) { + // `({foo} = arg)` + return extractFromPattern(parent.left) + } + return NEVER + } else if (parent.type === 'VariableDeclarator') { + if (withInTemplate) { + return NEVER + } + if (parent.init === node) { + // `const {foo} = arg` + // `const foo = arg` + return extractFromPattern(parent.id) + } + return NEVER + } else if (parent.type === 'MemberExpression') { + if (parent.object === node) { + // `arg.foo` + const name = utils.getStaticPropertyName(parent) + if (name) { + return new PropertyReferencesForMember(parent, name, withInTemplate) + } else { + return ANY + } + } + return NEVER + } else if (parent.type === 'CallExpression') { + if (withInTemplate) { + return NEVER + } + const argIndex = parent.arguments.indexOf(node) + if (argIndex > -1) { + // `foo(arg)` + return extractFromCall(parent, argIndex) + } + } else if (parent.type === 'ChainExpression') { + return extractFromExpression(parent, withInTemplate) + } else if ( + parent.type === 'ArrowFunctionExpression' || + parent.type === 'ReturnStatement' || + parent.type === 'VExpressionContainer' || + parent.type === 'Property' || + parent.type === 'ArrayExpression' + ) { + // Maybe used externally. + if (maybeExternalUsed(parent)) { + return ANY + } + } + return NEVER + } + + /** + * @param {ASTNode} parentTarget + * @returns {boolean} + */ + function maybeExternalUsed(parentTarget) { + if ( + parentTarget.type === 'ReturnStatement' || + parentTarget.type === 'VExpressionContainer' + ) { + return true + } + if (parentTarget.type === 'ArrayExpression') { + return maybeExternalUsed(parentTarget.parent) + } + if (parentTarget.type === 'Property') { + return maybeExternalUsed(parentTarget.parent.parent) + } + if (parentTarget.type === 'ArrowFunctionExpression') { + return parentTarget.body === node + } + return false + } + } + + /** + * Extract the property references from one parameter of the function. + * @param {Pattern} node + * @returns {IPropertyReferences} + */ + function extractFromPattern(node) { + const ref = cacheForPattern.get(node) + if (ref) { + return ref + } + cacheForPattern.set(node, ANY) + const result = extractWithoutCache() + cacheForPattern.set(node, result) + return result + + function extractWithoutCache() { + while (node.type === 'AssignmentPattern') { + node = node.left + } + if (node.type === 'RestElement' || node.type === 'ArrayPattern') { + // cannot check + return NEVER + } + if (node.type === 'ObjectPattern') { + return extractFromObjectPattern(node) + } + if (node.type === 'Identifier') { + return extractFromIdentifier(node) + } + return NEVER + } + } + + /** + * Extract the property references from ObjectPattern. + * @param {ObjectPattern} node + * @returns {IPropertyReferences} + */ + function extractFromObjectPattern(node) { + const refs = new PropertyReferencesForObject() + for (const prop of node.properties) { + if (prop.type === 'Property') { + const name = utils.getStaticPropertyName(prop) + if (name) { + const list = refs.properties[name] || (refs.properties[name] = []) + list.push(prop) + } else { + // If cannot trace name, everything is used! + return ANY + } + } else { + // If use RestElement, everything is used! + return ANY + } + } + return refs + } + + /** + * Extract the property references from id. + * @param {Identifier} node + * @returns {IPropertyReferences} + */ + function extractFromIdentifier(node) { + const variable = findVariable(context, node) + if (!variable) { + return NEVER + } + return mergePropertyReferences( + variable.references.map((reference) => { + const id = reference.identifier + return extractFromExpression(id, false) + }) + ) + } + + /** + * Extract the property references from call. + * @param {CallExpression} node + * @param {number} argIndex + * @returns {IPropertyReferences} + */ + function extractFromCall(node, argIndex) { + if (node.callee.type !== 'Identifier') { + return { + hasProperty(_name, options) { + return Boolean(options && options.unknownCallAsAny) + }, + getNest: () => ANY + } + } + + const fnNode = findFunction(context, node.callee) + if (!fnNode) { + if (argIndex === 0) { + if (getToRefSet().toRefNodes.has(node)) { + return extractFromToRef(node) + } else if (getToRefSet().toRefsNodes.has(node)) { + return extractFromToRefs(node) + } + } + return { + hasProperty(_name, options) { + return Boolean(options && options.unknownCallAsAny) + }, + getNest: () => ANY + } + } + + return extractFromFunctionParam(fnNode, argIndex) + } + + /** + * Extract the property references from function param. + * @param {FunctionExpression | ArrowFunctionExpression | FunctionDeclaration} node + * @param {number} argIndex + * @returns {IPropertyReferences} + */ + function extractFromFunctionParam(node, argIndex) { + let cacheForIndexes = cacheForFunction.get(node) + if (!cacheForIndexes) { + cacheForIndexes = new Map() + cacheForFunction.set(node, cacheForIndexes) + } + const ref = cacheForIndexes.get(argIndex) + if (ref) { + return ref + } + cacheForIndexes.set(argIndex, NEVER) + const arg = node.params[argIndex] + if (!arg) { + return NEVER + } + const result = extractFromPattern(arg) + cacheForIndexes.set(argIndex, result) + return result + } + + /** + * Extract the property references from path. + * @param {string} pathString + * @returns {IPropertyReferences} + */ + function extractFromPath(pathString) { + return extractFromSegments(pathString.split('.')) + + /** + * @param {string[]} segments + * @returns {IPropertyReferences} + */ + function extractFromSegments(segments) { + if (!segments.length) { + return ANY + } + const segmentName = segments[0] + return { + hasProperty: (name) => name === segmentName, + getNest: (name) => + name === segmentName ? extractFromSegments(segments.slice(1)) : NEVER + } + } + } + + /** + * Extract the property references from name. + * @param {string} referenceName + * @param { () => IPropertyReferences } [getNest] + * @returns {IPropertyReferences} + */ + function extractFromName(referenceName, getNest) { + return { + hasProperty: (name) => name === referenceName, + getNest: (name) => + name === referenceName ? (getNest ? getNest() : ANY) : NEVER + } + } + + /** + * Extract the property references from toRef call. + * @param {CallExpression} node + * @returns {IPropertyReferences} + */ + function extractFromToRef(node) { + const nameNode = node.arguments[1] + const refName = + nameNode && + (nameNode.type === 'Literal' || nameNode.type === 'TemplateLiteral') + ? utils.getStringLiteralValue(nameNode) + : null + if (!refName) { + // unknown name + return ANY + } + return extractFromName(refName, () => { + return extractFromExpression(node, false).getNest('value') + }) + } + + /** + * Extract the property references from toRefs call. + * @param {CallExpression} node + * @returns {IPropertyReferences} + */ + function extractFromToRefs(node) { + const base = extractFromExpression(node, false) + return { + hasProperty: (name, option) => base.hasProperty(name, option), + getNest: (name) => base.getNest(name).getNest('value') + } + } + return { + extractFromExpression, + extractFromPattern, + extractFromFunctionParam, + extractFromPath, + extractFromName + } +} + +/** + * @param {IPropertyReferences[]} references + * @returns {IPropertyReferences} + */ +function mergePropertyReferences(references) { + if (references.length === 0) { + return NEVER + } + if (references.length === 1) { + return references[0] + } + return new PropertyReferencesForMerge(references) +} + +/** + * Collects the property references for merge. + * @implements IPropertyReferences + */ +class PropertyReferencesForMerge { + /** + * @param {IPropertyReferences[]} references + */ + constructor(references) { + this.references = references + } + + /** + * @param {string} name + * @param {IHasPropertyOption} [option] + */ + hasProperty(name, option) { + return this.references.some((ref) => ref.hasProperty(name, option)) + } + + /** + * @param {string} name + * @returns {IPropertyReferences} + */ + getNest(name) { + /** @type {IPropertyReferences[]} */ + const nest = [] + for (const ref of this.references) { + if (ref.hasProperty(name)) { + nest.push(ref.getNest(name)) + } + } + return mergePropertyReferences(nest) + } +} diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js index 164e562ab..de535bd25 100644 --- a/tests/lib/rules/no-unused-properties.js +++ b/tests/lib/rules/no-unused-properties.js @@ -1594,6 +1594,70 @@ tester.run('no-unused-properties', rule, { } ` + }, + + // toRefs + { + // https://github.com/vuejs/eslint-plugin-vue/issues/1643 + filename: 'test.vue', + parserOptions: { + parser: '@typescript-eslint/parser' + }, + code: ` + + + + + ` } ], @@ -2507,6 +2571,145 @@ tester.run('no-unused-properties', rule, { "'f' of computed property found, but never used.", "'h' of method found, but never used." ] + }, + + // toRef, toRefs + { + 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: ` + + `, + options: deepDataOptions, + errors: ["'foo.baz' of data found, but never used."] + }, + { + filename: 'test.vue', + code: ` + + `, + options: deepDataOptions, + errors: [ + "'foo.bar.b' of data found, but never used.", + "'foo.baz.a' of data found, but never used." + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: deepDataOptions, + errors: [ + "'foo.bar.b' of data found, but never used.", + "'foo.baz' of data found, but never used." + ] } ] })