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.
307 lines
8.9 KiB
307 lines
8.9 KiB
/** |
|
* @author Yosuke Ota |
|
* See LICENSE file in root directory for full license. |
|
*/ |
|
'use strict' |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Requirements |
|
// ------------------------------------------------------------------------------ |
|
|
|
const { findVariable } = require('eslint-utils') |
|
const utils = require('../utils') |
|
const casing = require('../utils/casing') |
|
const { toRegExp } = require('../utils/regexp') |
|
|
|
/** |
|
* @typedef {import('../utils').VueObjectData} VueObjectData |
|
*/ |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Helpers |
|
// ------------------------------------------------------------------------------ |
|
|
|
const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase'] |
|
const DEFAULT_CASE = 'kebab-case' |
|
|
|
/** |
|
* Get the name param node from the given CallExpression |
|
* @param {CallExpression} node CallExpression |
|
* @returns { Literal & { value: string } | null } |
|
*/ |
|
function getNameParamNode(node) { |
|
const nameLiteralNode = node.arguments[0] |
|
if ( |
|
!nameLiteralNode || |
|
nameLiteralNode.type !== 'Literal' || |
|
typeof nameLiteralNode.value !== 'string' |
|
) { |
|
// cannot check |
|
return null |
|
} |
|
|
|
return /** @type {Literal & { value: string }} */ (nameLiteralNode) |
|
} |
|
/** |
|
* 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 |
|
} |
|
|
|
// ------------------------------------------------------------------------------ |
|
// Rule Definition |
|
// ------------------------------------------------------------------------------ |
|
|
|
const OBJECT_OPTION_SCHEMA = { |
|
type: 'object', |
|
properties: { |
|
ignores: { |
|
type: 'array', |
|
items: { type: 'string' }, |
|
uniqueItems: true, |
|
additionalItems: false |
|
} |
|
}, |
|
additionalProperties: false |
|
} |
|
module.exports = { |
|
meta: { |
|
type: 'suggestion', |
|
docs: { |
|
description: 'enforce specific casing for custom event name', |
|
categories: undefined, |
|
url: 'https://eslint.vuejs.org/rules/custom-event-name-casing.html' |
|
}, |
|
fixable: null, |
|
schema: { |
|
anyOf: [ |
|
{ |
|
type: 'array', |
|
items: [ |
|
{ |
|
enum: ALLOWED_CASE_OPTIONS |
|
}, |
|
OBJECT_OPTION_SCHEMA |
|
] |
|
}, |
|
// For backward compatibility |
|
{ |
|
type: 'array', |
|
items: [OBJECT_OPTION_SCHEMA] |
|
} |
|
] |
|
}, |
|
messages: { |
|
unexpected: "Custom event name '{{name}}' must be {{caseType}}." |
|
} |
|
}, |
|
/** @param {RuleContext} context */ |
|
create(context) { |
|
/** @type {Map<ObjectExpression|Program, {contextReferenceIds:Set<Identifier>,emitReferenceIds:Set<Identifier>}>} */ |
|
const setupContexts = new Map() |
|
const options = |
|
context.options.length === 1 && typeof context.options[0] !== 'string' |
|
? // For backward compatibility |
|
[undefined, context.options[0]] |
|
: context.options |
|
const caseType = options[0] || DEFAULT_CASE |
|
const objectOption = options[1] || {} |
|
const caseChecker = casing.getChecker(caseType) |
|
/** @type {RegExp[]} */ |
|
const ignores = (objectOption.ignores || []).map(toRegExp) |
|
|
|
/** |
|
* Check whether the given event name is valid. |
|
* @param {string} name The name to check. |
|
* @returns {boolean} `true` if the given event name is valid. |
|
*/ |
|
function isValidEventName(name) { |
|
return caseChecker(name) || name.startsWith('update:') |
|
} |
|
|
|
/** |
|
* @param { Literal & { value: string } } nameLiteralNode |
|
*/ |
|
function verify(nameLiteralNode) { |
|
const name = nameLiteralNode.value |
|
if (isValidEventName(name) || ignores.some((re) => re.test(name))) { |
|
return |
|
} |
|
context.report({ |
|
node: nameLiteralNode, |
|
messageId: 'unexpected', |
|
data: { |
|
name, |
|
caseType |
|
} |
|
}) |
|
} |
|
|
|
const programNode = context.getSourceCode().ast |
|
|
|
const callVisitor = { |
|
/** |
|
* @param {CallExpression} node |
|
* @param {VueObjectData} [info] |
|
*/ |
|
CallExpression(node, info) { |
|
const nameLiteralNode = getNameParamNode(node) |
|
if (!nameLiteralNode) { |
|
// cannot check |
|
return |
|
} |
|
|
|
// verify setup context |
|
const setupContext = setupContexts.get(info ? info.node : programNode) |
|
if (setupContext) { |
|
const { contextReferenceIds, emitReferenceIds } = setupContext |
|
if ( |
|
node.callee.type === 'Identifier' && |
|
emitReferenceIds.has(node.callee) |
|
) { |
|
// verify setup(props,{emit}) {emit()} |
|
verify(nameLiteralNode) |
|
} 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()} |
|
verify(nameLiteralNode) |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
return utils.defineTemplateBodyVisitor( |
|
context, |
|
{ |
|
CallExpression(node) { |
|
const callee = node.callee |
|
const nameLiteralNode = getNameParamNode(node) |
|
if (!nameLiteralNode) { |
|
// cannot check |
|
return |
|
} |
|
if (callee.type === 'Identifier' && callee.name === '$emit') { |
|
verify(nameLiteralNode) |
|
} |
|
} |
|
}, |
|
utils.compositingVisitors( |
|
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 variable = findVariable(context.getScope(), emitParam) |
|
if (!variable) { |
|
return |
|
} |
|
const emitReferenceIds = new Set() |
|
for (const reference of variable.references) { |
|
emitReferenceIds.add(reference.identifier) |
|
} |
|
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 |
|
} |
|
const emitParam = emitProperty.value |
|
// `setup(props, {emit})` |
|
const variable = findVariable(context.getScope(), emitParam) |
|
if (!variable) { |
|
return |
|
} |
|
for (const reference of variable.references) { |
|
emitReferenceIds.add(reference.identifier) |
|
} |
|
} else { |
|
// `setup(props, context)` |
|
const variable = findVariable(context.getScope(), contextParam) |
|
if (!variable) { |
|
return |
|
} |
|
for (const reference of variable.references) { |
|
contextReferenceIds.add(reference.identifier) |
|
} |
|
} |
|
setupContexts.set(vueNode, { |
|
contextReferenceIds, |
|
emitReferenceIds |
|
}) |
|
}, |
|
...callVisitor, |
|
onVueObjectExit(node) { |
|
setupContexts.delete(node) |
|
} |
|
}), |
|
{ |
|
CallExpression(node) { |
|
const nameLiteralNode = getNameParamNode(node) |
|
if (!nameLiteralNode) { |
|
// cannot check |
|
return |
|
} |
|
const emit = getCalleeMemberNode(node) |
|
// verify $emit |
|
if (emit && emit.name === '$emit') { |
|
// verify this.$emit() |
|
verify(nameLiteralNode) |
|
} |
|
} |
|
} |
|
) |
|
) |
|
} |
|
}
|
|
|