From 732140a7513338267b105bf99d11147c3c57148d Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Fri, 25 Dec 2020 15:39:55 +0900 Subject: [PATCH 1/4] Add deepData option to `vue/no-unused-properties` --- docs/rules/no-unused-properties.md | 30 +- lib/rules/no-unused-properties.js | 695 ++++++++++++++---------- tests/lib/rules/no-unused-properties.js | 491 +++++++++++++++++ 3 files changed, 943 insertions(+), 273 deletions(-) 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..d79ffa3bf 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -10,6 +10,7 @@ const utils = require('../utils') const eslintUtils = require('eslint-utils') +const { getStaticPropertyName } = require('../utils') /** * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData @@ -17,17 +18,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,54 +90,192 @@ 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 | null} + */ +function findExpression(context, id) { + const variable = findVariable(context, id) + if (!variable) { + return null + } + 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 null +} + +/** + * @typedef { (context: RuleContext) => UsedProperties } UsedPropertiesTracker + * @typedef { { node: CallExpression, index: number } } CallAndParamIndex + */ + +/** + * Collects the property names used. + */ +class UsedProperties { /** - * @returns {IterableIterator} + * @param {object} [option] + * @param {boolean} [option.unknown] */ - names() { - return this.map.keys() + constructor(option) { + /** @type {Map} */ + this.map = new 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) { + return this.map.has(name) } + /** * @param {string} name - * @param {UsedPropsTracker} tracker + * @param {UsedPropertiesTracker | null} tracker */ - add(name, tracker) { + addUsed(name, tracker) { const list = this.map.get(name) if (list) { - list.push(tracker) + if (tracker) list.push(tracker) } else { - this.map.set(name, [tracker]) + this.map.set(name, tracker ? [tracker] : []) + } + } + + /** + * @returns {IterableIterator<{ name: string, tracker: UsedPropertiesTracker }>} + */ + *entries() { + for (const [name, trackers] of this.map.entries()) { + yield { name, tracker: this._mergePropsTracker(trackers) } } } + + /** + * @param {string} name + * @returns {UsedPropertiesTracker} + */ + getPropsTracker(name) { + const trackers = this.map.get(name) || [] + return this._mergePropsTracker(trackers) + } + /** - * @param {UsedNames} other + * @param {UsedProperties} other */ - addAll(other) { - other.map.forEach((trackers, name) => { - const list = this.map.get(name) - if (list) { - list.push(...trackers) + merge(other) { + other.map.forEach((otherTrackers, name) => { + const trackers = this.map.get(name) + if (trackers) { + trackers.push(...otherTrackers) } else { - this.map.set(name, trackers) + this.map.set(name, otherTrackers) } }) + this.unknown = this.unknown || other.unknown + this.calls.push(...other.calls) + } + + /** + * @param {UsedPropertiesTracker[]} trackers + * @returns {UsedPropertiesTracker} + */ + _mergePropsTracker(trackers) { + return (context) => { + const result = new UsedProperties() + for (const tracker of trackers) { + result.merge(tracker(context)) + } + return result + } + } +} + +/** + * Collects the property names used for one parameter of the function. + */ +class ParamUsedProperties extends UsedProperties { + /** + * @param {Pattern} paramNode + * @param {RuleContext} context + */ + constructor(paramNode, context) { + super() + while (paramNode.type === 'AssignmentPattern') { + paramNode = paramNode.left + } + if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { + // cannot check + return + } + if (paramNode.type === 'ObjectPattern') { + this.merge(extractObjectPatternProperties(paramNode)) + return + } + if (paramNode.type !== 'Identifier') { + return + } + const variable = findVariable(context, paramNode) + if (!variable) { + return + } + for (const reference of variable.references) { + const id = reference.identifier + this.merge(extractPatternOrThisProperties(id, context, false)) + } } } @@ -150,63 +284,46 @@ class UsedNames { * @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} + * @returns {UsedPropertiesTracker} */ 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 + return extractObjectPatternProperties(pattern) } } if (pattern.type === 'Identifier') { return (context) => { - const result = new UsedProps() + const result = new UsedProperties() 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) + result.merge(extractPatternOrThisProperties(id, context, false)) } return result } @@ -214,35 +331,36 @@ function getObjectPatternPropertyPatternTracker(pattern) { return getObjectPatternPropertyPatternTracker(pattern.left) } return () => { - const result = new UsedProps() - result.unknown = true - return result + return new UsedProperties({ unknown: true }) } } /** * @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) @@ -251,13 +369,7 @@ function extractPatternOrThisProperties(node, context) { } 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(extractPatternOrThisProperties(id, context, false)) } } } @@ -267,8 +379,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,98 +388,56 @@ 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 - } - if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { - // cannot check - return + function maybeExternalUsed(parentTarget) { + if ( + parentTarget.type === 'ReturnStatement' || + parentTarget.type === 'VExpressionContainer' + ) { + return true } - if (paramNode.type === 'ObjectPattern') { - const { usedNames, unknown } = extractObjectPatternProperties(paramNode) - this.usedNames.addAll(usedNames) - this.unknown = this.unknown || unknown - return + if (parentTarget.type === 'ArrayExpression') { + return maybeExternalUsed(parentTarget.parent) } - if (paramNode.type !== 'Identifier') { - return + if (parentTarget.type === 'Property') { + return maybeExternalUsed(parentTarget.parent.parent) } - 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 } } @@ -382,13 +452,13 @@ class ParamsUsedProps { constructor(node, context) { this.node = node this.context = context - /** @type {ParamUsedProps[]} */ + /** @type {ParamUsedProperties[]} */ this.params = [] } /** * @param {number} index - * @returns {ParamUsedProps | null} + * @returns {ParamUsedProperties | null} */ getParam(index) { const param = this.params[index] @@ -396,7 +466,7 @@ class ParamsUsedProps { return param } if (this.node.params[index]) { - return (this.params[index] = new ParamUsedProps( + return (this.params[index] = new ParamUsedProperties( this.node.params[index], this.context )) @@ -435,7 +505,8 @@ module.exports = { }, additionalItems: false, uniqueItems: true - } + }, + deepData: { type: 'boolean' } }, additionalProperties: false } @@ -448,12 +519,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 {TemplatePropertiesContainer} */ const templatePropertiesContainer = { - usedNames: new Set(), + usedProperties: new UsedProperties(), refNames: new Set() } /** @type {Map} */ @@ -481,37 +553,99 @@ 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') { + const expr = findExpression(context, targetExpr) + if (!expr) { + return + } + targetExpr = expr + } + if (targetExpr.type === 'ObjectExpression') { + const usedProperties = new UsedProperties() + for (const usedProps of iterateUsedProperties(baseUsedProperties, { + allowUnknownCall: true + })) { + if (usedProps.unknown) { + return + } + usedProperties.merge(usedProps) + } + for (const prop of targetExpr.properties) { + if (prop.type !== 'Property') { + continue + } + const name = 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 - continue - } - for (const property of container.properties) { - if ( - container.usedNames.has(property.name) || - templatePropertiesContainer.usedNames.has(property.name) - ) { - // used + const usedProperties = new UsedProperties() + const usedPropertiesForProps = new UsedProperties() + for (const usedProps of iterateUsedProperties( + container.usedProperties + )) { + if (usedProps.unknown) { continue } + usedProperties.merge(usedProps) + } + usedProperties.merge(templatePropertiesContainer.usedProperties) + + for (const usedProps of iterateUsedProperties( + container.usedPropertiesForProps + )) { + usedPropertiesForProps.merge(usedProps) + } + + for (const property of container.properties) { if ( property.groupName === 'props' && - (container.unknownProps || - container.usedPropsNames.has(property.name)) + (usedPropertiesForProps.unknown || + usedPropertiesForProps.isUsed(property.name)) ) { // used props continue @@ -523,6 +657,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 +686,53 @@ module.exports = { } /** - * @param {UsedProps} usedProps - * @param {Map>} already - * @returns {IterableIterator} + * @param {UsedProperties} usedProps + * @param {object} [options] + * @param {boolean} [options.allowUnknownCall] + * @returns {IterableIterator} */ - function* iterateUsedProps(usedProps, already = new Map()) { - yield usedProps - for (const call of usedProps.calls) { - let alreadyIndexes = already.get(call.node) - if (!alreadyIndexes) { - alreadyIndexes = new Set() - already.set(call.node, alreadyIndexes) - } - if (alreadyIndexes.has(call.index)) { - continue - } - alreadyIndexes.add(call.index) - const paramsUsedProps = getParamsUsedProps(call.node) - const paramUsedProps = paramsUsedProps.getParam(call.index) - if (!paramUsedProps) { - continue - } - yield paramUsedProps - yield* iterateUsedProps(paramUsedProps, already) - } - } + function* iterateUsedProperties(usedProps, options) { + const allowUnknownCall = options && options.allowUnknownCall + const already = new Map() - /** - * @param {VueComponentPropertiesContainer} container - * @param {UsedProps} baseUseProps - */ - function processParamPropsUsed(container, baseUseProps) { - for (const { usedNames, unknown } of iterateUsedProps(baseUseProps)) { - if (unknown) { - container.unknownProps = true - return - } - for (const name of usedNames.names()) { - container.usedPropsNames.add(name) - } - } - } + yield* iterate(usedProps) - /** - * @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) + /** + * @param {UsedProperties} usedProps + * @returns {IterableIterator} + */ + function* iterate(usedProps) { + 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 = getParamsUsedProps(fnNode) + const paramUsedProps = paramsUsedProps.getParam(call.index) + if (!paramUsedProps) { + continue + } + yield* iterate(paramUsedProps) } } } @@ -618,17 +761,33 @@ module.exports = { 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 +802,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]'( @@ -704,23 +858,21 @@ module.exports = { } const paramsUsedProps = getParamsUsedProps(node) - const usedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + const usedProps = /** @type {ParamUsedProperties} */ (paramsUsedProps.getParam( 0 )) - processUsed( - getVueComponentPropertiesContainer(vueData.node), - usedProps - ) + 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( + const paramUsedProps = /** @type {ParamUsedProperties} */ (paramsUsedProps.getParam( 0 )) - processParamPropsUsed(container, paramUsedProps) + container.usedPropertiesForProps.merge(paramUsedProps) } }, onRenderFunctionEnter(node, vueData) { @@ -728,12 +880,12 @@ module.exports = { if (node.params[0]) { // for Vue 3.x render const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + const paramUsedProps = /** @type {ParamUsedProperties} */ (paramsUsedProps.getParam( 0 )) - processParamPropsUsed(container, paramUsedProps) - if (container.unknownProps) { + container.usedPropertiesForProps.merge(paramUsedProps) + if (container.usedPropertiesForProps.unknown) { return } } @@ -741,23 +893,20 @@ module.exports = { if (vueData.functional && node.params[1]) { // for Vue 2.x render & functional const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = /** @type {ParamUsedProps} */ (paramsUsedProps.getParam( + const paramUsedProps = /** @type {ParamUsedProperties} */ (paramsUsedProps.getParam( 1 )) - for (const { usedNames, unknown } of iterateUsedProps( - paramUsedProps - )) { - if (unknown) { - container.unknownProps = true + for (const usedProps of iterateUsedProperties(paramUsedProps)) { + if (usedProps.unknown) { + container.usedPropertiesForProps.unknown = true return } - for (const usedPropsTracker of usedNames.get('props')) { - const propUsedProps = usedPropsTracker(context) - processParamPropsUsed(container, propUsedProps) - if (container.unknownProps) { - return - } + const usedPropsTracker = usedProps.getPropsTracker('props') + const propUsedProps = usedPropsTracker(context) + container.usedPropertiesForProps.merge(propUsedProps) + if (container.usedPropertiesForProps.unknown) { + return } } } @@ -771,9 +920,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 +939,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/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 + } + ] } ] }) From 3d81fd304acf4dabc81d9e1fd85f2beb8b5eecae Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 26 Dec 2020 01:21:59 +0900 Subject: [PATCH 2/4] update --- lib/rules/no-unused-properties.js | 378 ++++++++++++++---------------- lib/utils/index.js | 7 +- 2 files changed, 176 insertions(+), 209 deletions(-) diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index d79ffa3bf..305fa1f6a 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -131,12 +131,12 @@ function findFunction(context, id) { /** * @param {RuleContext} context * @param {Identifier} id - * @returns {Expression | null} + * @returns {Expression} */ function findExpression(context, id) { const variable = findVariable(context, id) if (!variable) { - return null + return id } if (variable.defs.length === 1) { const def = variable.defs[0] @@ -151,7 +151,7 @@ function findExpression(context, id) { return def.node.init } } - return null + return id } /** @@ -160,7 +160,7 @@ function findExpression(context, id) { */ /** - * Collects the property names used. + * Collects the used property names. */ class UsedProperties { /** @@ -179,6 +179,10 @@ class UsedProperties { * @param {string} name */ isUsed(name) { + if (this.unknown) { + // If it is unknown, it is considered used. + return true + } return this.map.has(name) } @@ -195,28 +199,35 @@ class UsedProperties { } } - /** - * @returns {IterableIterator<{ name: string, tracker: UsedPropertiesTracker }>} - */ - *entries() { - for (const [name, trackers] of this.map.entries()) { - yield { name, tracker: this._mergePropsTracker(trackers) } - } - } - /** * @param {string} name * @returns {UsedPropertiesTracker} */ getPropsTracker(name) { + if (this.unknown) { + return () => new UsedProperties({ unknown: true }) + } const trackers = this.map.get(name) || [] - return this._mergePropsTracker(trackers) + return (context) => { + const result = new UsedProperties() + for (const tracker of trackers) { + result.merge(tracker(context)) + } + return result + } } /** - * @param {UsedProperties} other + * @param {UsedProperties | null} other */ merge(other) { + if (!other) { + return + } + this.unknown = this.unknown || other.unknown + if (this.unknown) { + return + } other.map.forEach((otherTrackers, name) => { const trackers = this.map.get(name) if (trackers) { @@ -225,61 +236,80 @@ class UsedProperties { this.map.set(name, otherTrackers) } }) - this.unknown = this.unknown || other.unknown this.calls.push(...other.calls) } - - /** - * @param {UsedPropertiesTracker[]} trackers - * @returns {UsedPropertiesTracker} - */ - _mergePropsTracker(trackers) { - return (context) => { - const result = new UsedProperties() - for (const tracker of trackers) { - result.merge(tracker(context)) - } - return result - } - } } /** - * Collects the property names used for one parameter of the function. + * Collects the used property names for parameters of the function. */ -class ParamUsedProperties extends UsedProperties { +class ParamsUsedProperties { /** - * @param {Pattern} paramNode + * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {RuleContext} context */ - constructor(paramNode, context) { - super() - while (paramNode.type === 'AssignmentPattern') { - paramNode = paramNode.left - } - if (paramNode.type === 'RestElement' || paramNode.type === 'ArrayPattern') { - // cannot check - return - } - if (paramNode.type === 'ObjectPattern') { - this.merge(extractObjectPatternProperties(paramNode)) - return - } - if (paramNode.type !== 'Identifier') { - return - } - const variable = findVariable(context, paramNode) - if (!variable) { - return + 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 } - for (const reference of variable.references) { - const id = reference.identifier - this.merge(extractPatternOrThisProperties(id, context, false)) + 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} */ @@ -305,37 +335,25 @@ function extractObjectPatternProperties(node) { } /** - * @param {Pattern} pattern - * @returns {UsedPropertiesTracker} + * Extract the used property name from id. + * @param {Identifier} node + * @param {RuleContext} context + * @returns {UsedProperties} */ -function getObjectPatternPropertyPatternTracker(pattern) { - if (pattern.type === 'ObjectPattern') { - return () => { - return extractObjectPatternProperties(pattern) - } - } - if (pattern.type === 'Identifier') { - return (context) => { - const result = new UsedProperties() - const variable = findVariable(context, pattern) - if (!variable) { - return result - } - for (const reference of variable.references) { - const id = reference.identifier - result.merge(extractPatternOrThisProperties(id, context, false)) - } - return result - } - } else if (pattern.type === 'AssignmentPattern') { - return getObjectPatternPropertyPatternTracker(pattern.left) +function extractIdentifierProperties(node, context) { + const result = new UsedProperties() + const variable = findVariable(context, node) + if (!variable) { + return result } - return () => { - return new UsedProperties({ unknown: true }) + 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 @@ -363,14 +381,7 @@ function extractPatternOrThisProperties(node, context, withInTemplate) { 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 - result.merge(extractPatternOrThisProperties(id, context, false)) - } + result.merge(extractIdentifierProperties(parent.id, context)) } } return result @@ -442,37 +453,19 @@ function extractPatternOrThisProperties(node, context, withInTemplate) { } /** - * 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 {ParamUsedProperties[]} */ - this.params = [] +function getObjectPatternPropertyPatternTracker(pattern) { + if (pattern.type === 'ObjectPattern') { + return () => extractObjectPatternProperties(pattern) } - - /** - * @param {number} index - * @returns {ParamUsedProperties | null} - */ - getParam(index) { - const param = this.params[index] - if (param != null) { - return param - } - if (this.node.params[index]) { - return (this.params[index] = new ParamUsedProperties( - 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 }) } // ------------------------------------------------------------------------------ @@ -521,8 +514,8 @@ module.exports = { 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 = { usedProperties: new UsedProperties(), @@ -533,13 +526,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 } @@ -572,22 +565,16 @@ module.exports = { ) { let targetExpr = propertyValue if (targetExpr.type === 'Identifier') { - const expr = findExpression(context, targetExpr) - if (!expr) { - return - } - targetExpr = expr + targetExpr = findExpression(context, targetExpr) } if (targetExpr.type === 'ObjectExpression') { - const usedProperties = new UsedProperties() - for (const usedProps of iterateUsedProperties(baseUsedProperties, { + const usedProperties = resolvedUsedProperties(baseUsedProperties, { allowUnknownCall: true - })) { - if (usedProps.unknown) { - return - } - usedProperties.merge(usedProps) + }) + if (usedProperties.unknown) { + return } + for (const prop of targetExpr.properties) { if (prop.type !== 'Property') { continue @@ -623,29 +610,20 @@ module.exports = { */ function reportUnusedProperties() { for (const container of vueComponentPropertiesContainerMap.values()) { - const usedProperties = new UsedProperties() - const usedPropertiesForProps = new UsedProperties() - for (const usedProps of iterateUsedProperties( - container.usedProperties - )) { - if (usedProps.unknown) { - continue - } - usedProperties.merge(usedProps) - } + const usedProperties = resolvedUsedProperties(container.usedProperties) usedProperties.merge(templatePropertiesContainer.usedProperties) + if (usedProperties.unknown) { + continue + } - for (const usedProps of iterateUsedProperties( + const usedPropertiesForProps = resolvedUsedProperties( container.usedPropertiesForProps - )) { - usedPropertiesForProps.merge(usedProps) - } + ) for (const property of container.properties) { if ( property.groupName === 'props' && - (usedPropertiesForProps.unknown || - usedPropertiesForProps.isUsed(property.name)) + usedPropertiesForProps.isUsed(property.name) ) { // used props continue @@ -686,22 +664,32 @@ module.exports = { } /** - * @param {UsedProperties} usedProps + * @param {UsedProperties | null} usedProps * @param {object} [options] * @param {boolean} [options.allowUnknownCall] - * @returns {IterableIterator} + * @returns {UsedProperties} */ - function* iterateUsedProperties(usedProps, options) { + function resolvedUsedProperties(usedProps, options) { const allowUnknownCall = options && options.allowUnknownCall const already = new Map() - yield* iterate(usedProps) + const result = new UsedProperties() + for (const up of iterate(usedProps)) { + result.merge(up) + if (result.unknown) { + break + } + } + return result /** - * @param {UsedProperties} usedProps + * @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') { @@ -727,11 +715,8 @@ module.exports = { continue } alreadyIndexes.add(call.index) - const paramsUsedProps = getParamsUsedProps(fnNode) + const paramsUsedProps = getParamsUsedProperties(fnNode) const paramUsedProps = paramsUsedProps.getParam(call.index) - if (!paramUsedProps) { - continue - } yield* iterate(paramUsedProps) } } @@ -750,13 +735,13 @@ 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) { @@ -850,65 +835,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 {ParamUsedProperties} */ (paramsUsedProps.getParam( - 0 - )) + 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 {ParamUsedProperties} */ (paramsUsedProps.getParam( - 0 - )) - - container.usedPropertiesForProps.merge(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 {ParamUsedProperties} */ (paramsUsedProps.getParam( - 0 - )) - - container.usedPropertiesForProps.merge(paramUsedProps) - if (container.usedPropertiesForProps.unknown) { - return - } - } - if (vueData.functional && node.params[1]) { - // for Vue 2.x render & functional - const paramsUsedProps = getParamsUsedProps(node) - const paramUsedProps = /** @type {ParamUsedProperties} */ (paramsUsedProps.getParam( - 1 - )) + // Check for Vue 3.x render + const paramsUsedProps = getParamsUsedProperties(node) + const ctxUsedProps = paramsUsedProps.getParam(0) - for (const usedProps of iterateUsedProperties(paramUsedProps)) { - if (usedProps.unknown) { - container.usedPropertiesForProps.unknown = true - return - } - const usedPropsTracker = usedProps.getPropsTracker('props') - const propUsedProps = usedPropsTracker(context) - container.usedPropertiesForProps.merge(propUsedProps) - if (container.usedPropertiesForProps.unknown) { - return - } - } + container.usedPropertiesForProps.merge(ctxUsedProps) + if (container.usedPropertiesForProps.unknown) { + 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) } }, /** 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. From 443eb33f4a25e27e96ff8fa7f437fb5f33057d4a Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 26 Dec 2020 01:24:39 +0900 Subject: [PATCH 3/4] update --- lib/rules/no-unused-properties.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 305fa1f6a..0242c9b2a 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -10,7 +10,6 @@ const utils = require('../utils') const eslintUtils = require('eslint-utils') -const { getStaticPropertyName } = require('../utils') /** * @typedef {import('../utils').ComponentPropertyData} ComponentPropertyData @@ -579,7 +578,7 @@ module.exports = { if (prop.type !== 'Property') { continue } - const name = getStaticPropertyName(prop) + const name = utils.getStaticPropertyName(prop) if (name == null) { continue } From 53a57984774d102f338ec042e39c75ed3708be02 Mon Sep 17 00:00:00 2001 From: ota-meshi Date: Sat, 26 Dec 2020 01:32:13 +0900 Subject: [PATCH 4/4] update --- lib/rules/no-unused-properties.js | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/lib/rules/no-unused-properties.js b/lib/rules/no-unused-properties.js index 0242c9b2a..61ac8357f 100644 --- a/lib/rules/no-unused-properties.js +++ b/lib/rules/no-unused-properties.js @@ -167,8 +167,8 @@ class UsedProperties { * @param {boolean} [option.unknown] */ constructor(option) { - /** @type {Map} */ - this.map = new Map() + /** @type {Record} */ + this.map = {} /** @type {CallAndParamIndex[]} */ this.calls = [] this.unknown = (option && option.unknown) || false @@ -182,7 +182,7 @@ class UsedProperties { // If it is unknown, it is considered used. return true } - return this.map.has(name) + return Boolean(this.map[name]) } /** @@ -190,12 +190,8 @@ class UsedProperties { * @param {UsedPropertiesTracker | null} tracker */ addUsed(name, tracker) { - const list = this.map.get(name) - if (list) { - if (tracker) list.push(tracker) - } else { - this.map.set(name, tracker ? [tracker] : []) - } + const trackers = this.map[name] || (this.map[name] = []) + if (tracker) trackers.push(tracker) } /** @@ -206,7 +202,7 @@ class UsedProperties { if (this.unknown) { return () => new UsedProperties({ unknown: true }) } - const trackers = this.map.get(name) || [] + const trackers = this.map[name] || [] return (context) => { const result = new UsedProperties() for (const tracker of trackers) { @@ -227,14 +223,10 @@ class UsedProperties { if (this.unknown) { return } - other.map.forEach((otherTrackers, name) => { - const trackers = this.map.get(name) - if (trackers) { - trackers.push(...otherTrackers) - } else { - this.map.set(name, otherTrackers) - } - }) + 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) } }