chrome-devtools-frontend
Version:
Chrome DevTools UI
216 lines (186 loc) • 8.47 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {TSESLint, TSESTree} from '@typescript-eslint/utils';
import {createRule} from './utils/ruleCreator.ts';
// Define specific node types for clarity
type ClassDeclaration = TSESTree.ClassDeclaration;
type MethodDefinition = TSESTree.MethodDefinition;
type ExpressionStatement = TSESTree.ExpressionStatement;
type MemberExpression = TSESTree.MemberExpression;
type Literal = TSESTree.Literal;
type Identifier = TSESTree.Identifier;
type RuleFix = TSESLint.RuleFix;
type RuleFixer = TSESLint.RuleFixer;
// Define message IDs type based on meta.messages
// Define the structure for the return value of findConstructorAndSuperCallAndFirstArgumentToSuper
interface FoundNodes {
constructor: MethodDefinition|undefined;
superExpression: ExpressionStatement|undefined;
firstArgumentToSuper: MemberExpression|Literal|Identifier|undefined;
}
export default createRule({
name: 'static-custom-event-names',
meta: {
type: 'problem',
docs: {
description: 'Enforce event naming convention',
category: 'Possible Errors',
},
fixable: 'code',
messages: {
missingEventName:
'Event is missing a static readonly eventName property containing the event name, or the eventName property was not found as the first node in the class body.',
eventNameNotReadonly: 'The eventName property must be marked as readonly.',
eventNameNotStatic: 'The eventName property must a static property.',
superEventNameWrong: 'The first argument to super() must be EventClass.eventName.',
noSuperCallFound: 'Could not find a super() call in the constructor.',
noConstructorFound: 'Could not find a constructor for the custom event.',
},
schema: [], // no options
},
defaultOptions: [],
create: function(context) {
function findConstructorAndSuperCallAndFirstArgumentToSuper(node: ClassDeclaration): FoundNodes {
const foundNodes: FoundNodes = {
constructor: undefined,
superExpression: undefined,
firstArgumentToSuper: undefined,
};
const constructor = node.body.body.find((bodyNode): bodyNode is MethodDefinition => {
return bodyNode.type === 'MethodDefinition' && bodyNode.kind === 'constructor' &&
bodyNode.key?.type === 'Identifier' && bodyNode.key?.name === 'constructor';
});
if (!constructor?.value.body) {
return foundNodes;
}
foundNodes.constructor = constructor;
const superExpression = constructor.value.body.body.find((bodyNode): bodyNode is ExpressionStatement => {
return bodyNode.type === 'ExpressionStatement' && bodyNode.expression.type === 'CallExpression' &&
bodyNode.expression.callee?.type === 'Super';
});
if (!superExpression || superExpression.expression.type !== 'CallExpression') {
return foundNodes;
}
foundNodes.superExpression = superExpression;
const firstArgumentToSuper = superExpression.expression.arguments[0];
if (!firstArgumentToSuper ||
(firstArgumentToSuper.type !== 'MemberExpression' && firstArgumentToSuper.type !== 'Literal' &&
firstArgumentToSuper.type !== 'Identifier')) {
// This is invalid or unexpected type, so bail.
return foundNodes;
}
foundNodes.firstArgumentToSuper = firstArgumentToSuper;
return foundNodes;
}
function tryToAutoFixIfWeCan(fixer: RuleFixer, node: ClassDeclaration): RuleFix[] {
// We can autofix nodes when either:
// 1. They do not have the static declaration, and there is a super('fooevent') call
// => in this case, we can define static readonly eventName = 'fooevent' and update the super() call's first argument.
// 2. There is a staticEventName declared, but the super() call is using the literal. In this case, we can update the super() call's first argument.
// Note: if we cannot fix, we return an empty array, which signifies to
// ESLint that there's no fixes we'd like to apply.
const firstBodyNode = node.body.body[0];
const nodeIsMissingEventName =
!(firstBodyNode?.type === 'PropertyDefinition' && firstBodyNode.key?.type === 'Identifier' &&
firstBodyNode.key?.name === 'eventName');
const className = node.id?.name;
if (!className) {
// Cannot fix anonymous classes or classes without names
return [];
}
const {firstArgumentToSuper} = findConstructorAndSuperCallAndFirstArgumentToSuper(node);
if (!firstArgumentToSuper || firstArgumentToSuper.type !== 'Literal') {
// Either it's OK, or it's some value that isn't a string literal, so we should bail.
return [];
}
const eventNameFromSuper = firstArgumentToSuper.value;
const fixes: RuleFix[] = [];
if (nodeIsMissingEventName) {
fixes.push(fixer.insertTextBefore(node.body.body[0], `static readonly eventName = '${eventNameFromSuper}';`));
}
fixes.push(fixer.replaceText(firstArgumentToSuper, `${className}.eventName`));
return fixes;
}
function lintClassNode(node: ClassDeclaration) {
const bodyMembersOfClass = node.body.body;
// Look for the static readonly eventName line.
// We purposefully look at the first body node as it should be defined first
const firstBodyNode = bodyMembersOfClass[0];
const className = node.id?.name; // Get class name for later use
// Check for static readonly eventName property as the first member
if (!(firstBodyNode?.type === 'PropertyDefinition' && firstBodyNode.key?.type === 'Identifier' &&
firstBodyNode.key.name === 'eventName')) {
context.report({
node,
messageId: 'missingEventName',
fix(fixer) {
return tryToAutoFixIfWeCan(fixer, node);
},
});
return;
}
if (!firstBodyNode.readonly) {
context.report({node: firstBodyNode, messageId: 'eventNameNotReadonly'});
}
if (!firstBodyNode.static) {
context.report({node: firstBodyNode, messageId: 'eventNameNotStatic'});
return;
}
// Now check the constructor and super() call.
const {constructor, superExpression, firstArgumentToSuper} =
findConstructorAndSuperCallAndFirstArgumentToSuper(node);
if (!constructor) {
context.report({node, messageId: 'noConstructorFound'});
return;
}
if (!superExpression) {
context.report({node: constructor, messageId: 'noSuperCallFound'});
return;
}
if (!firstArgumentToSuper) {
context.report({node: superExpression, messageId: 'superEventNameWrong'});
return;
}
// Check if the first argument to super() is ClassName.eventName
if (firstArgumentToSuper.type !== 'MemberExpression' || firstArgumentToSuper.object.type !== 'Identifier' ||
firstArgumentToSuper.property.type !== 'Identifier' || firstArgumentToSuper.object.name !== className ||
firstArgumentToSuper.property.name !== 'eventName') {
context.report({
node: firstArgumentToSuper,
messageId: 'superEventNameWrong',
fix(fixer) {
return tryToAutoFixIfWeCan(fixer, node);
},
});
}
}
let foundLocalEventClassDeclaration = false;
const classDeclarationsToLint: ClassDeclaration[] = [];
return {
ClassDeclaration(node) {
// If we find a local class defined called Event, we do not apply this
// check, as we have some instances where a local Event class is used
// which is not the builtin Event class that represents DOM emitted
// events.
if (node.id?.name === 'Event') {
foundLocalEventClassDeclaration = true;
return;
}
// Check if the class extends the global 'Event'
if (!node.superClass || node.superClass.type !== 'Identifier' || node.superClass.name !== 'Event') {
return;
}
classDeclarationsToLint.push(node);
},
'Program:exit'() {
if (foundLocalEventClassDeclaration) {
return;
}
for (const node of classDeclarationsToLint) {
lintClassNode(node);
}
},
};
},
});