UNPKG

5.31 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 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
166const 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 // Assume rule has already fixed the error name
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
205const 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
213module.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};