eslint-plugin-san
Version:
Official ESLint plugin for San
203 lines (189 loc) • 7.83 kB
JavaScript
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict';
/* eslint-disable */
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const {findVariable} = require('eslint-utils');
const utils = require('../utils');
const {isKebabCase} = require('../utils/casing');
// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
/**
* 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 isKebabCase(name) || name.startsWith('update:');
}
/**
* 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
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce custom event names always use "kebab-case"',
categories: ['essential'],
url: 'https://ecomfe.github.io/eslint-plugin-san/rules/custom-event-name-casing.html'
},
fixable: null,
schema: [],
messages: {
unexpected: "Custom event name '{{name}}' must be kebab-case."
}
},
/** @param {RuleContext} context */
create(context) {
const setupContexts = new Map();
/**
* @param { Literal & { value: string } } nameLiteralNode
*/
function verify(nameLiteralNode) {
const name = nameLiteralNode.value;
if (isValidEventName(name)) {
return;
}
context.report({
node: nameLiteralNode,
messageId: 'unexpected',
data: {
name
}
});
}
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 === 'fire') {
verify(nameLiteralNode);
}
}
},
utils.compositingVisitors(
utils.defineSanVisitor(context, {
onSetupFunctionEnter(node, {node: sanNode}) {
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(sanNode, {
contextReferenceIds,
emitReferenceIds
});
},
CallExpression(node, {node: sanNode}) {
const nameLiteralNode = getNameParamNode(node);
if (!nameLiteralNode) {
// cannot check
return;
}
// verify setup context
const setupContext = setupContexts.get(sanNode);
if (setupContext) {
const {contextReferenceIds, emitReferenceIds} = setupContext;
if (emitReferenceIds.has(node.callee)) {
// verify setup(props,{emit}) {emit()}
verify(nameLiteralNode);
} else {
const emit = getCalleeMemberNode(node);
if (emit && emit.name === 'emit' && contextReferenceIds.has(emit.member.object)) {
// verify setup(props,context) {context.emit()}
verify(nameLiteralNode);
}
}
}
},
onSanObjectExit(node) {
setupContexts.delete(node);
}
}),
{
CallExpression(node) {
const nameLiteralNode = getNameParamNode(node);
if (!nameLiteralNode) {
// cannot check
return;
}
const emit = getCalleeMemberNode(node);
// verify fire
if (emit && emit.name === 'fire') {
// verify this.fire()
verify(nameLiteralNode);
}
}
}
)
);
}
};