UNPKG

eslint-plugin-vue

Version:

Official ESLint plugin for Vue.js

328 lines (306 loc) 9.55 kB
/** * @author Yosuke Ota * See LICENSE file in root directory for full license. */ 'use strict' const { findVariable } = require('@eslint-community/eslint-utils') const { extractRefObjectReferences } = require('../utils/ref-object-references') const utils = require('../utils') /** * @typedef {import('../utils/ref-object-references').RefObjectReferences} RefObjectReferences * @typedef {import('../utils/ref-object-references').RefObjectReferenceForIdentifier} RefObjectReferenceForIdentifier */ /** * Checks whether the given identifier reference has been initialized with a ref object. * @param {RefObjectReferenceForIdentifier | null} data * @returns {data is RefObjectReferenceForIdentifier} */ function isRefInit(data) { const init = data && data.variableDeclarator && data.variableDeclarator.init if (!init) { return false } return data.defineChain.includes(/** @type {any} */ (init)) } /** * Get the callee member node from the given CallExpression * @param {CallExpression} node CallExpression */ 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 } } } // cannot check return null } /** * Get the callee member node from the given CallExpression * @param {CallExpression} node CallExpression */ function getCalleeMemberNode(node) { const callee = utils.skipChainExpression(node.callee) if (callee.type === 'MemberExpression') { const name = utils.getStaticPropertyName(callee) if (name) { return { name, member: callee } } } return null } module.exports = { meta: { type: 'suggestion', docs: { description: 'disallow use of value wrapped by `ref()` (Composition API) as an operand', categories: ['vue3-essential', 'vue2-essential'], url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html' }, fixable: 'code', schema: [], messages: { requireDotValue: 'Must use `.value` to read or write the value wrapped by `{{method}}()`.' } }, /** @param {RuleContext} context */ create(context) { /** @type {RefObjectReferences} */ let refReferences const setupContexts = new Map() /** * Collect identifier id * @param {Identifier} node * @param {Set<Identifier>} referenceIds */ function collectReferenceIds(node, referenceIds) { const variable = findVariable(utils.getScope(context, node), node) if (!variable) { return } for (const reference of variable.references) { referenceIds.add(reference.identifier) } } /** * @param {Identifier} node */ function reportIfRefWrapped(node) { const data = refReferences.get(node) if (!isRefInit(data)) { return } context.report({ node, messageId: 'requireDotValue', data: { method: data.method }, fix(fixer) { return fixer.insertTextAfter(node, '.value') } }) } /** * @param {CallExpression} node */ function reportWrappedIdentifiers(node) { const nodes = node.arguments.filter((node) => node.type === 'Identifier') for (const node of nodes) { reportIfRefWrapped(node) } } const programNode = context.getSourceCode().ast const callVisitor = { /** * @param {CallExpression} node * @param {import('../utils').VueObjectData} [info] */ CallExpression(node, info) { const nameWithLoc = getNameParamNode(node) if (!nameWithLoc) { // cannot check return } // verify setup context const setupContext = setupContexts.get(info ? info.node : programNode) if (!setupContext) { return } const { contextReferenceIds, emitReferenceIds } = setupContext if ( node.callee.type === 'Identifier' && emitReferenceIds.has(node.callee) ) { // verify setup(props,{emit}) {emit()} reportWrappedIdentifiers(node) } else { const emit = getCalleeMemberNode(node) if ( emit && emit.name === 'emit' && emit.member.object.type === 'Identifier' && contextReferenceIds.has(emit.member.object) ) { // verify setup(props,context) {context.emit()} reportWrappedIdentifiers(node) } } } } return utils.compositingVisitors( { Program() { refReferences = extractRefObjectReferences(context) }, // if (refValue) /** @param {Identifier} node */ 'IfStatement>Identifier'(node) { reportIfRefWrapped(node) }, // switch (refValue) /** @param {Identifier} node */ 'SwitchStatement>Identifier'(node) { reportIfRefWrapped(node) }, // -refValue, +refValue, !refValue, ~refValue, typeof refValue /** @param {Identifier} node */ 'UnaryExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue++, refValue-- /** @param {Identifier} node */ 'UpdateExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue+1, refValue-1 /** @param {Identifier} node */ 'BinaryExpression>Identifier'(node) { reportIfRefWrapped(node) }, // refValue+=1, refValue-=1, foo+=refValue, foo-=refValue /** @param {Identifier & {parent: AssignmentExpression}} node */ 'AssignmentExpression>Identifier'(node) { if (node.parent.operator === '=' && node.parent.left !== node) { return } reportIfRefWrapped(node) }, // refValue || other, refValue && other. ignore: other || refValue /** @param {Identifier & {parent: LogicalExpression}} node */ 'LogicalExpression>Identifier'(node) { if (node.parent.left !== node) { return } // Report only constants. const data = refReferences.get(node) if ( !data || !data.variableDeclaration || data.variableDeclaration.kind !== 'const' ) { return } reportIfRefWrapped(node) }, // refValue ? x : y /** @param {Identifier & {parent: ConditionalExpression}} node */ 'ConditionalExpression>Identifier'(node) { if (node.parent.test !== node) { return } reportIfRefWrapped(node) }, // `${refValue}` /** @param {Identifier} node */ 'TemplateLiteral>Identifier'(node) { reportIfRefWrapped(node) }, // refValue.x /** @param {Identifier & {parent: MemberExpression}} node */ 'MemberExpression>Identifier'(node) { if (node.parent.object !== node) { return } const name = utils.getStaticPropertyName(node.parent) if ( name === 'value' || name == null || // WritableComputedRef name === 'effect' ) { return } reportIfRefWrapped(node) } }, utils.defineScriptSetupVisitor(context, { onDefineEmitsEnter(node) { if ( !node.parent || node.parent.type !== 'VariableDeclarator' || node.parent.init !== node ) { return } const emitParam = node.parent.id if (emitParam.type !== 'Identifier') { return } // const emit = defineEmits() const emitReferenceIds = new Set() collectReferenceIds(emitParam, emitReferenceIds) setupContexts.set(programNode, { contextReferenceIds: new Set(), emitReferenceIds }) }, ...callVisitor }), utils.defineVueVisitor(context, { onSetupFunctionEnter(node, { node: vueNode }) { const contextParam = utils.skipDefaultParamValue(node.params[1]) if (!contextParam) { // no arguments return } if ( contextParam.type === 'RestElement' || contextParam.type === 'ArrayPattern' ) { // cannot check return } const contextReferenceIds = new Set() const emitReferenceIds = new Set() if (contextParam.type === 'ObjectPattern') { const emitProperty = utils.findAssignmentProperty( contextParam, 'emit' ) if (!emitProperty || emitProperty.value.type !== 'Identifier') { return } // `setup(props, {emit})` collectReferenceIds(emitProperty.value, emitReferenceIds) } else { // `setup(props, context)` collectReferenceIds(contextParam, contextReferenceIds) } setupContexts.set(vueNode, { contextReferenceIds, emitReferenceIds }) }, ...callVisitor, onVueObjectExit(node) { setupContexts.delete(node) } }) ) } }