Skip to content

Commit 23fbaaa

Browse files
authored
Merge pull request #4 from Xvezda/feature/arrow-functions
Arrow functions, function expression and method supports
2 parents 4c782d1 + 16053a7 commit 23fbaaa

File tree

2 files changed

+346
-71
lines changed

2 files changed

+346
-71
lines changed

src/rules/no-undocumented-throws.js

Lines changed: 159 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,64 @@ const {
1212
isTypesAssignableTo,
1313
} = require('../utils');
1414

15+
/**
16+
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
17+
* @returns {import('@typescript-eslint/utils').TSESTree.Node | null}
18+
*/
19+
const findParentFunctionNode = (node) => {
20+
return findParent(node, (n) =>
21+
n.type === AST_NODE_TYPES.FunctionDeclaration ||
22+
n.type === AST_NODE_TYPES.FunctionExpression ||
23+
n.type === AST_NODE_TYPES.ArrowFunctionExpression
24+
);
25+
};
26+
27+
/**
28+
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
29+
* @returns {import('@typescript-eslint/utils').TSESTree.Node | null}
30+
*/
31+
const findNodeToComment = (node) => {
32+
switch (node.type) {
33+
case AST_NODE_TYPES.FunctionDeclaration:
34+
return node;
35+
case AST_NODE_TYPES.FunctionExpression:
36+
case AST_NODE_TYPES.ArrowFunctionExpression:
37+
return (
38+
/**
39+
* @example
40+
* ```
41+
* class Klass {
42+
* // here
43+
* target() { ... }
44+
* }
45+
* ```
46+
*/
47+
findParent(node, (n) => n.type === AST_NODE_TYPES.MethodDefinition) ??
48+
/**
49+
* @example
50+
* ```
51+
* const obj = {
52+
* // here
53+
* target: () => { ... },
54+
* };
55+
* ```
56+
*/
57+
findParent(node, (n) => n.type === AST_NODE_TYPES.Property) ??
58+
/**
59+
* @example
60+
* ```
61+
* // here
62+
* const target = () => { ... };
63+
* ```
64+
*/
65+
findParent(node, (n) => n.type === AST_NODE_TYPES.VariableDeclaration)
66+
);
67+
default:
68+
break;
69+
}
70+
return null;
71+
};
72+
1573
module.exports = createRule({
1674
name: 'no-undocumented-throws',
1775
meta: {
@@ -59,99 +117,129 @@ module.exports = createRule({
59117
const typesToUnionString = (types) =>
60118
types.map(t => utils.getTypeName(checker, t)).join(' | ');
61119

62-
return {
63-
/** @param {import('@typescript-eslint/utils').TSESTree.ThrowStatement} node */
64-
'FunctionDeclaration :not(TryStatement > BlockStatement) ThrowStatement'(node) {
65-
const functionDeclaration =
66-
/** @type {import('@typescript-eslint/utils').TSESTree.FunctionDeclaration} */
67-
(findParent(node, (n) => n.type === AST_NODE_TYPES.FunctionDeclaration));
120+
/** @param {import('@typescript-eslint/utils').TSESTree.Node} node */
121+
const visitOnExit = (node) => {
122+
const nodeToComment = findNodeToComment(node);
68123

69-
// TODO: Use "SAFE" unique function identifier
70-
if (!throwStatements.has(functionDeclaration.range[0])) {
71-
throwStatements.set(functionDeclaration.range[0], []);
72-
}
73-
const throwStatementNodes =
74-
/** @type {import('@typescript-eslint/utils').TSESTree.ThrowStatement[]} */
75-
(throwStatements.get(functionDeclaration.range[0]));
124+
if (!nodeToComment) return;
76125

77-
throwStatementNodes.push(node);
78-
},
79-
/** @param {import('@typescript-eslint/utils').TSESTree.FunctionDeclaration} node */
80-
'FunctionDeclaration:has(:not(TryStatement > BlockStatement) ThrowStatement):exit'(node) {
81-
const throwStatementNodes = throwStatements.get(node.range[0]);
126+
const comments = sourceCode.getCommentsBefore(nodeToComment);
82127

83-
if (!throwStatementNodes) return;
128+
const throwStatementNodes = throwStatements.get(node.range[0]);
84129

85-
const comments = sourceCode.getCommentsBefore(node);
130+
if (!throwStatementNodes) return;
86131

87-
const isCommented =
88-
comments.length &&
89-
comments
90-
.map(({ value }) => value)
91-
.some(hasThrowsTag);
132+
const isCommented =
133+
comments.length &&
134+
comments
135+
.map(({ value }) => value)
136+
.some(hasThrowsTag);
92137

93-
/** @type {import('typescript').Type[]} */
94-
const throwTypes = throwStatementNodes
95-
.map(n => {
96-
const type = services.getTypeAtLocation(n.argument);
97-
const tsNode = services.esTreeNodeToTSNodeMap.get(n.argument);
138+
/** @type {import('typescript').Type[]} */
139+
const throwTypes = throwStatementNodes
140+
.map(n => {
141+
const type = services.getTypeAtLocation(n.argument);
142+
const tsNode = services.esTreeNodeToTSNodeMap.get(n.argument);
98143

99-
return options.useBaseTypeOfLiteral && ts.isLiteralTypeLiteral(tsNode)
100-
? checker.getBaseTypeOfLiteralType(type)
101-
: type;
102-
})
103-
.flatMap(t => t.isUnion() ? t.types : t);
144+
return options.useBaseTypeOfLiteral && ts.isLiteralTypeLiteral(tsNode)
145+
? checker.getBaseTypeOfLiteralType(type)
146+
: type;
147+
})
148+
.flatMap(t => t.isUnion() ? t.types : t);
104149

105-
if (isCommented) {
106-
if (!services.esTreeNodeToTSNodeMap.has(node)) return;
150+
if (isCommented) {
151+
if (!services.esTreeNodeToTSNodeMap.has(nodeToComment)) return;
107152

108-
const functionDeclarationTSNode = services.esTreeNodeToTSNodeMap.get(node);
153+
const functionDeclarationTSNode = services.esTreeNodeToTSNodeMap.get(node);
109154

110-
const throwsTags = getJSDocThrowsTags(functionDeclarationTSNode);
111-
const throwsTagTypeNodes = throwsTags
112-
.map(tag => tag.typeExpression?.type)
113-
.filter(tag => !!tag);
155+
const throwsTags = getJSDocThrowsTags(functionDeclarationTSNode);
156+
const throwsTagTypeNodes = throwsTags
157+
.map(tag => tag.typeExpression?.type)
158+
.filter(tag => !!tag);
114159

115-
if (!throwsTagTypeNodes.length) return;
160+
if (!throwsTagTypeNodes.length) return;
116161

117-
const throwsTagTypes = getJSDocThrowsTagTypes(checker, functionDeclarationTSNode);
162+
const throwsTagTypes = getJSDocThrowsTagTypes(checker, functionDeclarationTSNode);
118163

119-
if (isTypesAssignableTo(checker, throwTypes, throwsTagTypes)) return;
164+
if (isTypesAssignableTo(checker, throwTypes, throwsTagTypes)) return;
120165

121-
const lastTagtypeNode = throwsTagTypeNodes[throwsTagTypeNodes.length - 1];
122-
123-
context.report({
124-
node,
125-
messageId: 'throwTypeMismatch',
126-
fix(fixer) {
127-
return fixer.replaceTextRange(
128-
[lastTagtypeNode.pos, lastTagtypeNode.end],
129-
typesToUnionString(throwTypes)
130-
);
131-
},
132-
});
133-
return;
134-
}
166+
const lastTagtypeNode = throwsTagTypeNodes[throwsTagTypeNodes.length - 1];
135167

136168
context.report({
137169
node,
138-
messageId: 'missingThrowsTag',
170+
messageId: 'throwTypeMismatch',
139171
fix(fixer) {
140-
const lines = sourceCode.getLines();
141-
const currentLine = lines[node.loc.start.line - 1];
142-
const indent = currentLine.match(/^\s*/)?.[0] ?? '';
143-
return fixer
144-
.insertTextBefore(
145-
node,
146-
`/**\n` +
147-
`${indent} * @throws {${typesToUnionString(throwTypes)}}\n` +
148-
`${indent} */\n` +
149-
`${indent}`
150-
);
172+
return fixer.replaceTextRange(
173+
[lastTagtypeNode.pos, lastTagtypeNode.end],
174+
typesToUnionString(throwTypes)
175+
);
151176
},
152177
});
153178
return;
179+
}
180+
181+
context.report({
182+
node,
183+
messageId: 'missingThrowsTag',
184+
fix(fixer) {
185+
const lines = sourceCode.getLines();
186+
const currentLine = lines[nodeToComment.loc.start.line - 1];
187+
const indent = currentLine.match(/^\s*/)?.[0] ?? '';
188+
return fixer
189+
.insertTextBefore(
190+
nodeToComment,
191+
`/**\n` +
192+
`${indent} * @throws {${typesToUnionString(throwTypes)}}\n` +
193+
`${indent} */\n` +
194+
`${indent}`
195+
);
196+
},
197+
});
198+
return;
199+
};
200+
201+
const unhandledThrowStatementSelector =
202+
':not(TryStatement > BlockStatement) ThrowStatement';
203+
204+
const createExpressionVisitors = () =>
205+
['ArrowFunctionExpression', 'FunctionExpression']
206+
.reduce((acc, nodeType) => {
207+
const targetNodeSelector =
208+
`${nodeType}:has(${unhandledThrowStatementSelector})`;
209+
210+
return {
211+
...acc,
212+
[`VariableDeclaration > VariableDeclarator[id.type="Identifier"] > ${targetNodeSelector}:exit`]: visitOnExit,
213+
[`Property > ${targetNodeSelector}:exit`]: visitOnExit,
214+
[`MethodDefinition > ${targetNodeSelector}:exit`]: visitOnExit,
215+
};
216+
}, {});
217+
218+
return {
219+
/**
220+
* Collect and group throw statements in functions
221+
*
222+
* @param {import('@typescript-eslint/utils').TSESTree.ThrowStatement} node
223+
*/
224+
[unhandledThrowStatementSelector](node) {
225+
const functionDeclaration = findParentFunctionNode(node);
226+
227+
if (!functionDeclaration) return;
228+
229+
// TODO: Use "SAFE" unique function identifier
230+
if (!throwStatements.has(functionDeclaration.range[0])) {
231+
throwStatements.set(functionDeclaration.range[0], []);
232+
}
233+
const throwStatementNodes =
234+
/** @type {import('@typescript-eslint/utils').TSESTree.ThrowStatement[]} */
235+
(throwStatements.get(functionDeclaration.range[0]));
236+
237+
throwStatementNodes.push(node);
154238
},
239+
240+
[`FunctionDeclaration:has(${unhandledThrowStatementSelector}):exit`]: visitOnExit,
241+
242+
...createExpressionVisitors(),
155243
};
156244
},
157245
});

0 commit comments

Comments
 (0)