You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
365 lines
11 KiB
365 lines
11 KiB
/** |
|
* @author ItMaga |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
const { findVariable } = require('@eslint-community/eslint-utils') |
|
const utils = require('../utils') |
|
|
|
/** |
|
* @typedef {import('../utils').ComponentEmit} ComponentEmit |
|
* @typedef {import('../utils').VueObjectData} VueObjectData |
|
*/ |
|
|
|
/** |
|
* @typedef {object} SetupContext |
|
* @property {Set<Identifier>} contextReferenceIds |
|
* @property {Set<Identifier>} emitReferenceIds |
|
*/ |
|
|
|
/** |
|
* @typedef {object} NameWithLoc |
|
* @property {string} name |
|
* @property {SourceLocation} loc |
|
* @property {Range} range |
|
*/ |
|
|
|
/** |
|
* Get the name param node from the given CallExpression |
|
* @param {CallExpression} node CallExpression |
|
* @returns {NameWithLoc | null} |
|
*/ |
|
function getNameParamNode(node) { |
|
const nameLiteralNode = node.arguments[0] |
|
if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) { |
|
const name = utils.getStringLiteralValue(nameLiteralNode) |
|
if (name != null) { |
|
return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range } |
|
} |
|
} |
|
|
|
return null |
|
} |
|
|
|
/** |
|
* Check if the given node is a reference of setup context |
|
* @param {Expression | Super | SpreadElement} value |
|
* @param {SetupContext} setupContext |
|
* @returns {boolean} |
|
* */ |
|
function hasReferenceId(value, setupContext) { |
|
const { contextReferenceIds, emitReferenceIds } = setupContext |
|
return ( |
|
value.type === 'Identifier' && |
|
(emitReferenceIds.has(value) || contextReferenceIds.has(value)) |
|
) |
|
} |
|
|
|
/** |
|
* Check if the given name matches emitReferenceIds variable name |
|
* @param {string} name |
|
* @param {Set<Identifier>} emitReferenceIds |
|
* @returns {boolean} |
|
*/ |
|
function isEmitVariableName(name, emitReferenceIds) { |
|
if (emitReferenceIds.size === 0) return false |
|
const emitVariable = emitReferenceIds.values().next().value.name |
|
return emitVariable === name |
|
} |
|
|
|
module.exports = { |
|
meta: { |
|
type: 'suggestion', |
|
docs: { |
|
description: 'disallow unused emit declarations', |
|
categories: undefined, |
|
url: 'https://eslint.vuejs.org/rules/no-unused-emit-declarations.html' |
|
}, |
|
fixable: null, |
|
schema: [], |
|
messages: { |
|
unused: '`{{name}}` is defined as emit but never used.' |
|
} |
|
}, |
|
/** @param {RuleContext} context */ |
|
create(context) { |
|
/** @type {Map<string, ComponentEmit>} */ |
|
const emitDeclarations = new Map() |
|
/** @type {Map<string, Expression>} */ |
|
const emitCalls = new Map() |
|
/** @type {Map<ObjectExpression | Program, SetupContext>} */ |
|
const setupContexts = new Map() |
|
const programNode = context.getSourceCode().ast |
|
|
|
/** |
|
* @param {CallExpression} node |
|
* */ |
|
function addEmitCall(node) { |
|
const nameParamNode = getNameParamNode(node) |
|
if (nameParamNode) { |
|
emitCalls.set(nameParamNode.name, node) |
|
} |
|
} |
|
function clearEmits() { |
|
emitCalls.clear() |
|
emitDeclarations.clear() |
|
} |
|
|
|
/** |
|
* @param {Expression | SpreadElement} expression |
|
* @param {SetupContext} setupContext |
|
* @returns {boolean} |
|
* */ |
|
function checkExpressionReference(expression, setupContext) { |
|
if (expression.type === 'MemberExpression') { |
|
const memObject = utils.skipChainExpression(expression.object) |
|
if (hasReferenceId(memObject, setupContext)) { |
|
clearEmits() |
|
return true |
|
} |
|
} |
|
if (hasReferenceId(expression, setupContext)) { |
|
clearEmits() |
|
return true |
|
} |
|
return false |
|
} |
|
|
|
/** |
|
* |
|
* @param {Array<Expression | SpreadElement>} args |
|
* @param {SetupContext} setupContext |
|
* @returns {boolean} |
|
*/ |
|
function verifyArgumentsReferences(args, setupContext) { |
|
for (const argument of args) { |
|
if (argument.type === 'ObjectExpression') { |
|
for (const property of argument.properties) { |
|
if ( |
|
property.type === 'Property' && |
|
checkExpressionReference(property.value, setupContext) |
|
) { |
|
return true |
|
} |
|
} |
|
} |
|
|
|
if (argument.type === 'ArrayExpression') { |
|
for (const element of argument.elements) { |
|
if (!element) { |
|
continue |
|
} |
|
if (checkExpressionReference(element, setupContext)) { |
|
return true |
|
} |
|
} |
|
} |
|
|
|
if (checkExpressionReference(argument, setupContext)) { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
/** |
|
* @param {Expression | Super} callee |
|
* @param {Set<Identifier>} referenceIds |
|
* @param {CallExpression} node |
|
* */ |
|
function addEmitCallByReference(callee, referenceIds, node) { |
|
if (callee.type === 'Identifier' && referenceIds.has(callee)) { |
|
addEmitCall(node) |
|
} |
|
} |
|
|
|
const callVisitor = { |
|
/** |
|
* @param {CallExpression} node |
|
* @param {VueObjectData} [info] |
|
*/ |
|
CallExpression(node, info) { |
|
const callee = utils.skipChainExpression(node.callee) |
|
|
|
let emit = null |
|
if (callee.type === 'MemberExpression') { |
|
const name = utils.getStaticPropertyName(callee) |
|
if (name === 'emit' || name === '$emit') { |
|
emit = { name, member: callee } |
|
} |
|
} |
|
|
|
const vueDefineNode = info ? info.node : programNode |
|
const setupContext = setupContexts.get(vueDefineNode) |
|
if (setupContext) { |
|
if ( |
|
callee.parent.type === 'CallExpression' && |
|
callee.parent.arguments && |
|
verifyArgumentsReferences(callee.parent.arguments, setupContext) |
|
) { |
|
// skip if the emit passed as argument |
|
return |
|
} |
|
|
|
const { contextReferenceIds, emitReferenceIds } = setupContext |
|
|
|
// verify defineEmits variable in template |
|
if ( |
|
callee.type === 'Identifier' && |
|
isEmitVariableName(callee.name, emitReferenceIds) |
|
) { |
|
addEmitCall(node) |
|
} |
|
|
|
// verify setup(props,{emit}) {emit()} |
|
addEmitCallByReference(callee, emitReferenceIds, node) |
|
if (emit && emit.name === 'emit') { |
|
const memObject = utils.skipChainExpression(emit.member.object) |
|
// verify setup(props,context) {context.emit()} |
|
addEmitCallByReference(memObject, contextReferenceIds, node) |
|
} |
|
} |
|
|
|
if (emit && emit.name === '$emit') { |
|
const memObject = utils.skipChainExpression(emit.member.object) |
|
// verify this.$emit() |
|
if (utils.isThis(memObject, context)) { |
|
addEmitCall(node) |
|
} |
|
} |
|
|
|
// verify $emit() in template |
|
if (callee.type === 'Identifier' && callee.name === '$emit') { |
|
addEmitCall(node) |
|
} |
|
} |
|
} |
|
|
|
return utils.compositingVisitors( |
|
utils.defineTemplateBodyVisitor(context, callVisitor), |
|
utils.defineVueVisitor(context, { |
|
...callVisitor, |
|
onVueObjectEnter(node) { |
|
for (const emit of utils.getComponentEmitsFromOptions(node)) { |
|
if (emit.emitName) { |
|
emitDeclarations.set(emit.emitName, emit) |
|
} |
|
} |
|
}, |
|
onSetupFunctionEnter(node, { node: vueNode }) { |
|
const contextParam = node.params[1] |
|
if ( |
|
!contextParam || |
|
contextParam.type === 'RestElement' || |
|
contextParam.type === 'ArrayPattern' |
|
) { |
|
// no arguments or cannot check |
|
return |
|
} |
|
|
|
/** @type {Set<Identifier>} */ |
|
const contextReferenceIds = new Set() |
|
/** @type {Set<Identifier>} */ |
|
const emitReferenceIds = new Set() |
|
if (contextParam.type === 'ObjectPattern') { |
|
const emitProperty = utils.findAssignmentProperty( |
|
contextParam, |
|
'emit' |
|
) |
|
if (!emitProperty) { |
|
return |
|
} |
|
const emitParam = emitProperty.value |
|
// `setup(props, {emit})` |
|
const variable = |
|
emitParam.type === 'Identifier' |
|
? findVariable(utils.getScope(context, emitParam), emitParam) |
|
: null |
|
if (!variable) { |
|
return |
|
} |
|
for (const reference of variable.references) { |
|
emitReferenceIds.add(reference.identifier) |
|
} |
|
} else if (contextParam.type === 'Identifier') { |
|
// `setup(props, context)` |
|
const variable = findVariable( |
|
utils.getScope(context, contextParam), |
|
contextParam |
|
) |
|
for (const reference of variable.references) { |
|
contextReferenceIds.add(reference.identifier) |
|
} |
|
} |
|
|
|
setupContexts.set(vueNode, { |
|
contextReferenceIds, |
|
emitReferenceIds |
|
}) |
|
} |
|
}), |
|
utils.defineScriptSetupVisitor(context, { |
|
onDefineEmitsEnter(node, emits) { |
|
for (const emit of emits) { |
|
if (emit.emitName) { |
|
emitDeclarations.set(emit.emitName, emit) |
|
} |
|
} |
|
|
|
if ( |
|
!node.parent || |
|
node.parent.type !== 'VariableDeclarator' || |
|
node.parent.init !== node |
|
) { |
|
return |
|
} |
|
|
|
const emitParam = node.parent.id |
|
const variable = |
|
emitParam.type === 'Identifier' |
|
? findVariable(utils.getScope(context, emitParam), emitParam) |
|
: null |
|
if (!variable) { |
|
return |
|
} |
|
/** @type {Set<Identifier>} */ |
|
const emitReferenceIds = new Set() |
|
for (const reference of variable.references) { |
|
emitReferenceIds.add(reference.identifier) |
|
} |
|
setupContexts.set(programNode, { |
|
contextReferenceIds: new Set(), |
|
emitReferenceIds |
|
}) |
|
}, |
|
onDefineModelEnter(node, model) { |
|
if ( |
|
node.parent && |
|
node.parent.type === 'VariableDeclarator' && |
|
node.parent.init === node |
|
) { |
|
// If the return value of defineModel() is stored in a variable, we can mark the 'update:modelName' event as used if that that variable is used. |
|
// If that variable is unused, it will already be reported by `no-unused-var` rule. |
|
emitCalls.set(`update:${model.name.modelName}`, node) |
|
} |
|
}, |
|
...callVisitor |
|
}), |
|
{ |
|
'Program:exit'() { |
|
for (const [name, emit] of emitDeclarations) { |
|
if (!emitCalls.has(name) && emit.node) { |
|
context.report({ |
|
node: emit.node, |
|
loc: emit.node.loc, |
|
messageId: 'unused', |
|
data: { name } |
|
}) |
|
} |
|
} |
|
} |
|
} |
|
) |
|
} |
|
}
|
|
|