123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- 'use strict';
- const {upperFirst} = require('lodash');
- const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
- const messages = {
- [MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class',
- };
- const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
- const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
- const getConstructorMethod = className => `
- constructor() {
- super();
- this.name = '${className}';
- }
- `;
- const hasValidSuperClass = node => {
- if (!node.superClass) {
- return false;
- }
- let {name, type, property} = node.superClass;
- if (type === 'MemberExpression') {
- ({name} = property);
- }
- return nameRegexp.test(name);
- };
- const isSuperExpression = node =>
- node.type === 'ExpressionStatement'
- && node.expression.type === 'CallExpression'
- && node.expression.callee.type === 'Super';
- const isAssignmentExpression = (node, name) => {
- if (
- node.type !== 'ExpressionStatement'
- || node.expression.type !== 'AssignmentExpression'
- ) {
- return false;
- }
- const lhs = node.expression.left;
- if (!lhs.object || lhs.object.type !== 'ThisExpression') {
- return false;
- }
- return lhs.property.name === name;
- };
- const isPropertyDefinition = (node, name) =>
- node.type === 'PropertyDefinition'
- && !node.computed
- && node.key.type === 'Identifier'
- && node.key.name === name;
- function * customErrorDefinition(context, node) {
- if (!hasValidSuperClass(node)) {
- return;
- }
- if (node.id === null) {
- return;
- }
- const {name} = node.id;
- const className = getClassName(name);
- if (name !== className) {
- yield {
- node: node.id,
- message: `Invalid class name, use \`${className}\`.`,
- };
- }
- const {body, range} = node.body;
- const constructor = body.find(x => x.kind === 'constructor');
- if (!constructor) {
- yield {
- node,
- message: 'Add a constructor to your error.',
- fix: fixer => fixer.insertTextAfterRange([
- range[0],
- range[0] + 1,
- ], getConstructorMethod(name)),
- };
- return;
- }
- const constructorBodyNode = constructor.value.body;
- // Verify the constructor has a body (TypeScript)
- if (!constructorBodyNode) {
- return;
- }
- const constructorBody = constructorBodyNode.body;
- const superExpression = constructorBody.find(body => isSuperExpression(body));
- const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, 'message'));
- if (!superExpression) {
- yield {
- node: constructorBodyNode,
- message: 'Missing call to `super()` in constructor.',
- };
- } else if (messageExpressionIndex !== -1) {
- const expression = constructorBody[messageExpressionIndex];
- yield {
- node: superExpression,
- message: 'Pass the error message to `super()` instead of setting `this.message`.',
- * fix(fixer) {
- if (superExpression.expression.arguments.length === 0) {
- const rhs = expression.expression.right;
- yield fixer.insertTextAfterRange([
- superExpression.range[0],
- superExpression.range[0] + 6,
- ], rhs.raw || rhs.name);
- }
- yield fixer.removeRange([
- messageExpressionIndex === 0 ? constructorBodyNode.range[0] : constructorBody[messageExpressionIndex - 1].range[1],
- expression.range[1],
- ]);
- },
- };
- }
- const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
- if (!nameExpression) {
- const nameProperty = body.find(node => isPropertyDefinition(node, 'name'));
- if (!nameProperty?.value || nameProperty.value.value !== name) {
- yield {
- node: nameProperty?.value ?? constructorBodyNode,
- message: `The \`name\` property should be set to \`${name}\`.`,
- };
- }
- } else if (nameExpression.expression.right.value !== name) {
- yield {
- node: nameExpression?.expression.right ?? constructorBodyNode,
- message: `The \`name\` property should be set to \`${name}\`.`,
- };
- }
- }
- const customErrorExport = (context, node) => {
- const exportsName = node.left.property.name;
- const maybeError = node.right;
- if (maybeError.type !== 'ClassExpression') {
- return;
- }
- if (!hasValidSuperClass(maybeError)) {
- return;
- }
- if (!maybeError.id) {
- return;
- }
- // Assume rule has already fixed the error name
- const errorName = maybeError.id.name;
- if (exportsName === errorName) {
- return;
- }
- return {
- node: node.left.property,
- messageId: MESSAGE_ID_INVALID_EXPORT,
- fix: fixer => fixer.replaceText(node.left.property, errorName),
- };
- };
- /** @param {import('eslint').Rule.RuleContext} context */
- const create = context => ({
- ClassDeclaration: node => customErrorDefinition(context, node),
- 'AssignmentExpression[right.type="ClassExpression"]': node => customErrorDefinition(context, node.right),
- 'AssignmentExpression[left.type="MemberExpression"][left.object.type="Identifier"][left.object.name="exports"]': node => customErrorExport(context, node),
- });
- /** @type {import('eslint').Rule.RuleModule} */
- module.exports = {
- create,
- meta: {
- type: 'problem',
- docs: {
- description: 'Enforce correct `Error` subclassing.',
- },
- fixable: 'code',
- messages,
- },
- };
|