UNPKG

5.22 kBJavaScriptView Raw
1'use strict';
2const {upperFirst} = require('lodash');
3const getDocumentationUrl = require('./utils/get-documentation-url');
4
5const MESSAGE_ID_INVALID_EXPORT = 'invalidExport';
6
7const nameRegexp = /^(?:[A-Z][\da-z]*)*Error$/;
8
9const getClassName = name => upperFirst(name).replace(/(?:error|)$/i, 'Error');
10
11const getConstructorMethod = className => `
12 constructor() {
13 super();
14 this.name = '${className}';
15 }
16`;
17
18const 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
32const isSuperExpression = node =>
33 node.type === 'ExpressionStatement' &&
34 node.expression.type === 'CallExpression' &&
35 node.expression.callee.type === 'Super';
36
37const 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
54const 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
68const 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 if (superExpression.expression.arguments.length === 0) {
127 const rhs = expression.expression.right;
128 yield fixer.insertTextAfterRange([
129 superExpression.range[0],
130 superExpression.range[0] + 6
131 ], rhs.raw || rhs.name);
132 }
133
134 yield fixer.removeRange([
135 messageExpressionIndex === 0 ? constructorBodyNode.range[0] : constructorBody[messageExpressionIndex - 1].range[1],
136 expression.range[1]
137 ]);
138 }
139 });
140 }
141
142 const nameExpression = constructorBody.find(x => isAssignmentExpression(x, 'name'));
143 if (!nameExpression) {
144 const nameProperty = node.body.body.find(node => isClassProperty(node, 'name'));
145
146 if (!nameProperty || !nameProperty.value || nameProperty.value.value !== name) {
147 context.report({
148 node: nameProperty && nameProperty.value ? nameProperty.value : constructorBodyNode,
149 message: `The \`name\` property should be set to \`${name}\`.`
150 });
151 }
152 } else if (nameExpression.expression.right.value !== name) {
153 context.report({
154 node: nameExpression ? nameExpression.expression.right : constructorBodyNode,
155 message: `The \`name\` property should be set to \`${name}\`.`
156 });
157 }
158};
159
160const customErrorExport = (context, node) => {
161 if (!node.left.object || node.left.object.name !== 'exports') {
162 return;
163 }
164
165 if (!node.left.property) {
166 return;
167 }
168
169 const exportsName = node.left.property.name;
170
171 const maybeError = node.right;
172
173 if (maybeError.type !== 'ClassExpression') {
174 return;
175 }
176
177 if (!hasValidSuperClass(maybeError)) {
178 return;
179 }
180
181 if (!maybeError.id) {
182 return;
183 }
184
185 // Assume rule has already fixed the error name
186 const errorName = maybeError.id.name;
187
188 if (exportsName === errorName) {
189 return;
190 }
191
192 context.report({
193 node: node.left.property,
194 messageId: MESSAGE_ID_INVALID_EXPORT,
195 fix: fixer => fixer.replaceText(node.left.property, errorName)
196 });
197};
198
199const create = context => {
200 return {
201 ClassDeclaration: node => customErrorDefinition(context, node),
202 'AssignmentExpression[right.type="ClassExpression"]': node => customErrorDefinition(context, node.right),
203 'AssignmentExpression[left.type="MemberExpression"]': node => customErrorExport(context, node)
204 };
205};
206
207module.exports = {
208 create,
209 meta: {
210 type: 'problem',
211 docs: {
212 url: getDocumentationUrl(__filename)
213 },
214 fixable: 'code',
215 messages: {
216 [MESSAGE_ID_INVALID_EXPORT]: 'Exported error name should match error class'
217 }
218 }
219};