UNPKG

chrome-devtools-frontend

Version:
216 lines (186 loc) • 8.47 kB
// 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); } }, }; }, });