@@ -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+
1573module . 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