1 | 'use strict';
|
2 | const {upperFirst} = require('lodash');
|
3 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
4 |
|
5 | const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
|
6 |
|
7 | const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
|
8 |
|
9 | const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
|
10 |
|
11 | const getConstructorMethod = className => `
|
12 | constructor() {
|
13 | super();
|
14 | this.name = '${className}';
|
15 | }
|
16 | `;
|
17 |
|
18 | const hasValidSuperClass = node => {
|
19 | if (!node.superClass) {
|
20 | return false;
|
21 | }
|
22 |
|
23 | let {name} = node.superClass;
|
24 |
|
25 | if (node.superClass.type === 'MemberExpression') {
|
26 | ({name} = node.superClass.property);
|
27 | }
|
28 |
|
29 | return nameRegexp.test(name);
|
30 | };
|
31 |
|
32 | const isSuperExpression = node =>
|
33 | node.type === 'ExpressionStatement' &&
|
34 | node.expression.type === 'CallExpression' &&
|
35 | node.expression.callee.type === 'Super';
|
36 |
|
37 | const isAssignmentExpression = (node, name) => {
|
38 | if (
|
39 | node.type !== 'ExpressionStatement' ||
|
40 | node.expression.type !== 'AssignmentExpression'
|
41 | ) {
|
42 | return false;
|
43 | }
|
44 |
|
45 | const lhs = node.expression.left;
|
46 |
|
47 | if (!lhs.object || lhs.object.type !== 'ThisExpression') {
|
48 | return false;
|
49 | }
|
50 |
|
51 | return lhs.property.name === name;
|
52 | };
|
53 |
|
54 | const isClassProperty = (node, name) => {
|
55 | if (node.type !== 'ClassProperty' || node.computed) {
|
56 | return false;
|
57 | }
|
58 |
|
59 | const {key} = node;
|
60 |
|
61 | if (key.type !== 'Identifier') {
|
62 | return false;
|
63 | }
|
64 |
|
65 | return key.name === name;
|
66 | };
|
67 |
|
68 | const customErrorDefinition = (context, node) => {
|
69 | if (!hasValidSuperClass(node)) {
|
70 | return;
|
71 | }
|
72 |
|
73 | if (node.id === null) {
|
74 | return;
|
75 | }
|
76 |
|
77 | const {name} = node.id;
|
78 | const className = getClassName(name);
|
79 |
|
80 | if (name !== className) {
|
81 | context.report({
|
82 | node: node.id,
|
83 | message: `Invalid class name, use \`${className}\`.`
|
84 | });
|
85 | }
|
86 |
|
87 | const {body} = node.body;
|
88 | const constructor = body.find(x => x.kind === 'constructor');
|
89 |
|
90 | if (!constructor) {
|
91 | context.report({
|
92 | node,
|
93 | message: 'Add a constructor to your error.',
|
94 | fix: fixer => fixer.insertTextAfterRange([
|
95 | node.body.range[0],
|
96 | node.body.range[0] + 1
|
97 | ], getConstructorMethod(name))
|
98 | });
|
99 | return;
|
100 | }
|
101 |
|
102 | const constructorBodyNode = constructor.value.body;
|
103 |
|
104 | // Verify the constructor has a body (TypeScript)
|
105 | if (!constructorBodyNode) {
|
106 | return;
|
107 | }
|
108 |
|
109 | const constructorBody = constructorBodyNode.body;
|
110 |
|
111 | const superExpression = constructorBody.find(body => isSuperExpression(body));
|
112 | const messageExpressionIndex = constructorBody.findIndex(x => isAssignmentExpression(x, 'message'));
|
113 |
|
114 | if (!superExpression) {
|
115 | context.report({
|
116 | node: constructorBodyNode,
|
117 | message: 'Missing call to `super()` in constructor.'
|
118 | });
|
119 | } else if (messageExpressionIndex !== -1) {
|
120 | const expression = constructorBody[messageExpressionIndex];
|
121 |
|
122 | context.report({
|
123 | node: superExpression,
|
124 | message: 'Pass the error message to `super()` instead of setting `this.message`.',
|
125 | fix: fixer => {
|
126 | const fixings = [];
|
127 | if (superExpression.expression.arguments.length === 0) {
|
128 | const rhs = expression.expression.right;
|
129 | fixings.push(
|
130 | fixer.insertTextAfterRange([
|
131 | superExpression.range[0],
|
132 | superExpression.range[0] + 6
|
133 | ], rhs.raw || rhs.name)
|
134 | );
|
135 | }
|
136 |
|
137 | fixings.push(
|
138 | fixer.removeRange([
|
139 | messageExpressionIndex === 0 ? constructorBodyNode.range[0] : constructorBody[messageExpressionIndex - 1].range[1],
|
140 | expression.range[1]
|
141 | ])
|
142 | );
|
143 | return fixings;
|
144 | }
|
145 | });
|
146 | }
|
147 |
|
148 | const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
|
149 | if (!nameExpression) {
|
150 | const nameProperty = node.body.body.find(node => isClassProperty(node, 'name'));
|
151 |
|
152 | if (!nameProperty || !nameProperty.value || nameProperty.value.value !== name) {
|
153 | context.report({
|
154 | node: nameProperty && nameProperty.value ? nameProperty.value : constructorBodyNode,
|
155 | message: `The \`name\` property should be set to \`${name}\`.`
|
156 | });
|
157 | }
|
158 | } else if (nameExpression.expression.right.value !== name) {
|
159 | context.report({
|
160 | node: nameExpression ? nameExpression.expression.right : constructorBodyNode,
|
161 | message: `The \`name\` property should be set to \`${name}\`.`
|
162 | });
|
163 | }
|
164 | };
|
165 |
|
166 | const customErrorExport = (context, node) => {
|
167 | if (!node.left.object || node.left.object.name !== 'exports') {
|
168 | return;
|
169 | }
|
170 |
|
171 | if (!node.left.property) {
|
172 | return;
|
173 | }
|
174 |
|
175 | const exportsName = node.left.property.name;
|
176 |
|
177 | const maybeError = node.right;
|
178 |
|
179 | if (maybeError.type !== 'ClassExpression') {
|
180 | return;
|
181 | }
|
182 |
|
183 | if (!hasValidSuperClass(maybeError)) {
|
184 | return;
|
185 | }
|
186 |
|
187 | if (!maybeError.id) {
|
188 | return;
|
189 | }
|
190 |
|
191 |
|
192 | const errorName = maybeError.id.name;
|
193 |
|
194 | if (exportsName === errorName) {
|
195 | return;
|
196 | }
|
197 |
|
198 | context.report({
|
199 | node: node.left.property,
|
200 | messageId: MESSAGE_ID_INVALID_EXPORT,
|
201 | fix: fixer => fixer.replaceText(node.left.property, errorName)
|
202 | });
|
203 | };
|
204 |
|
205 | const create = context => {
|
206 | return {
|
207 | ClassDeclaration: node => customErrorDefinition(context, node),
|
208 | 'AssignmentExpression[right.type="ClassExpression"]': node => customErrorDefinition(context, node.right),
|
209 | 'AssignmentExpression[left.type="MemberExpression"]': node => customErrorExport(context, node)
|
210 | };
|
211 | };
|
212 |
|
213 | module.exports = {
|
214 | create,
|
215 | meta: {
|
216 | type: 'problem',
|
217 | docs: {
|
218 | url: getDocumentationUrl(__filename)
|
219 | },
|
220 | fixable: 'code',
|
221 | messages: {
|
222 | [MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class'
|
223 | }
|
224 | }
|
225 | };
|