| // 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. |
| |
| 'use strict'; |
| module.exports = { |
| 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 be 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 |
| }, |
| create: function(context) { |
| function findConstructorAndSuperCallAndFirstArgumentToSuper(node) { |
| const foundNodes = { |
| constructor: undefined, |
| superExpression: undefined, |
| firstArgumentToSuper: undefined, |
| }; |
| const constructor = node.body.body.find(bodyNode => { |
| return bodyNode.type === 'MethodDefinition' && bodyNode.key?.name === 'constructor'; |
| }); |
| if (!constructor) { |
| return foundNodes; |
| } |
| |
| foundNodes.constructor = constructor; |
| |
| const superExpression = constructor.value.body.body.find(bodyNode => { |
| const isExpression = bodyNode.type === 'ExpressionStatement'; |
| if (!isExpression) { |
| return false; |
| } |
| return isExpression && bodyNode.expression.callee?.type === 'Super'; |
| }); |
| |
| if (!superExpression) { |
| return foundNodes; |
| } |
| |
| foundNodes.superExpression = superExpression; |
| |
| const firstArgumentToSuper = superExpression.expression.arguments[0]; |
| if (!firstArgumentToSuper) { |
| // This is invalid so bail. |
| return foundNodes; |
| } |
| foundNodes.firstArgumentToSuper = firstArgumentToSuper; |
| return foundNodes; |
| } |
| |
| function tryToAutoFixIfWeCan(fixer, node) { |
| // 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 nodeIsMissingEventName = node.body.body[0]?.key?.name !== 'eventName'; |
| const className = node.id.name; |
| |
| const {firstArgumentToSuper} = findConstructorAndSuperCallAndFirstArgumentToSuper(node); |
| if (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 = []; |
| if (nodeIsMissingEventName) { |
| fixes.push(fixer.insertTextBefore(node.body.body[0], `static readonly eventName = '${eventNameFromSuper}';`)); |
| } |
| |
| fixes.push(fixer.replaceText(firstArgumentToSuper, `${className}.eventName`)); |
| return fixes; |
| } |
| |
| return { |
| ClassDeclaration(node) { |
| if (!node.superClass) { |
| return; |
| } |
| if (node.superClass.name !== 'Event') { |
| return; |
| } |
| |
| 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]; |
| if (!firstBodyNode || firstBodyNode.key.name !== 'eventName') { |
| context.report({ |
| node, |
| messageId: 'missingEventName', |
| fix(fixer) { |
| return tryToAutoFixIfWeCan(fixer, node); |
| } |
| }); |
| return; |
| } |
| if (!firstBodyNode.readonly) { |
| context.report({node, messageId: 'eventNameNotReadonly'}); |
| } |
| if (!firstBodyNode.static) { |
| context.report({node, messageId: 'eventNameNotStatic'}); |
| return; |
| } |
| |
| // Now we know the static readonly eventName is defined, we check for |
| // the constructor and the super() call. |
| const {constructor, superExpression, firstArgumentToSuper} = |
| findConstructorAndSuperCallAndFirstArgumentToSuper(node); |
| |
| if (!constructor) { |
| context.report({node, messageId: 'noConstructorFound'}); |
| return; |
| } |
| if (!superExpression) { |
| context.report({node, messageId: 'noSuperCallFound'}); |
| return; |
| } |
| if (!firstArgumentToSuper) { |
| context.report({node, messageId: 'superEventNameWrong'}); |
| return; |
| } |
| |
| const customEventClassName = node.id.name; |
| if (firstArgumentToSuper.type !== 'MemberExpression') { |
| context.report({ |
| node, |
| messageId: 'superEventNameWrong', |
| fix(fixer) { |
| return tryToAutoFixIfWeCan(fixer, node); |
| } |
| }); |
| return; |
| } |
| if (firstArgumentToSuper.object.name !== customEventClassName || |
| firstArgumentToSuper.property.name !== 'eventName') { |
| context.report({node, messageId: 'superEventNameWrong'}); |
| return; |
| } |
| } |
| }; |
| } |
| }; |