@@ -4,13 +4,14 @@ const utils = require('@typescript-eslint/type-utils');
44const {
55 createRule,
66 hasThrowsTag,
7- findParent,
87 getOptionsFromContext,
98 getDeclarationTSNodeOfESTreeNode,
109 getJSDocThrowsTags,
1110 getJSDocThrowsTagTypes,
1211 toFlattenedTypeArray,
1312 isTypesAssignableTo,
13+ findClosestFunctionNode,
14+ findNodeToComment,
1415} = require ( '../utils' ) ;
1516
1617
@@ -52,134 +53,139 @@ module.exports = createRule({
5253 const services = ESLintUtils . getParserServices ( context ) ;
5354 const checker = services . program . getTypeChecker ( ) ;
5455
55- return {
56- /** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement } node */
57- 'FunctionDeclaration :not(TryStatement > BlockStatement) ExpressionStatement:has(> CallExpression)' ( node ) {
58- if ( node . expression . type !== AST_NODE_TYPES . CallExpression ) return ;
56+ /** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement } node */
57+ const visitExpressionStatement = ( node ) => {
58+ if ( node . expression . type !== AST_NODE_TYPES . CallExpression ) return ;
5959
60- const callerDeclaration =
61- /** @type {import('@typescript-eslint/utils').TSESTree.FunctionDeclaration } */
62- ( findParent ( node , ( n ) => n . type === AST_NODE_TYPES . FunctionDeclaration ) ) ;
60+ const callerDeclaration = findClosestFunctionNode ( node ) ;
61+ if ( ! callerDeclaration ) return ;
6362
64- const comments = sourceCode . getCommentsBefore ( callerDeclaration ) ;
65- const isCommented =
66- comments . length &&
67- comments
68- . map ( ( { value } ) => value )
69- . some ( hasThrowsTag ) ;
63+ const nodeToComment = findNodeToComment ( callerDeclaration ) ;
64+ if ( ! nodeToComment ) return ;
7065
71- // TODO: Branching type checking or not
72- if ( isCommented ) {
73- const calleeDeclarationTSNode =
74- getDeclarationTSNodeOfESTreeNode ( services , node . expression . callee ) ;
66+ const comments = sourceCode . getCommentsBefore ( nodeToComment ) ;
67+ const isCommented =
68+ comments . length &&
69+ comments
70+ . map ( ( { value } ) => value )
71+ . some ( hasThrowsTag ) ;
7572
76- if ( ! calleeDeclarationTSNode ) return ;
73+ // TODO: Branching type checking or not
74+ if ( isCommented ) {
75+ const calleeDeclarationTSNode =
76+ getDeclarationTSNodeOfESTreeNode ( services , node . expression . callee ) ;
7777
78- const callerDeclarationTSNode =
79- getDeclarationTSNodeOfESTreeNode ( services , callerDeclaration ) ;
78+ if ( ! calleeDeclarationTSNode ) return ;
8079
81- if ( ! callerDeclarationTSNode ) return ;
80+ const callerDeclarationTSNode =
81+ getDeclarationTSNodeOfESTreeNode ( services , callerDeclaration ) ;
8282
83- const calleeThrowsTypes =
84- toFlattenedTypeArray (
85- getJSDocThrowsTagTypes ( checker , calleeDeclarationTSNode )
86- ) ;
83+ if ( ! callerDeclarationTSNode ) return ;
84+
85+ const calleeThrowsTypes =
86+ toFlattenedTypeArray (
87+ getJSDocThrowsTagTypes ( checker , calleeDeclarationTSNode )
88+ ) ;
89+
90+ const callerThrowsTags = getJSDocThrowsTags ( callerDeclarationTSNode ) ;
91+ const callerThrowsTypeNodes =
92+ callerThrowsTags
93+ . map ( tag => tag . typeExpression ?. type )
94+ . filter ( tag => ! ! tag ) ;
95+
96+ const callerThrowsTypes = getJSDocThrowsTagTypes ( checker , callerDeclarationTSNode ) ;
97+
98+ if (
99+ isTypesAssignableTo ( checker , calleeThrowsTypes , callerThrowsTypes )
100+ ) {
101+ return ;
102+ }
87103
88- const callerThrowsTags = getJSDocThrowsTags ( callerDeclarationTSNode ) ;
89- const callerThrowsTypeNodes =
90- callerThrowsTags
91- . map ( tag => tag . typeExpression ?. type )
92- . filter ( tag => ! ! tag ) ;
93-
94- const callerThrowsTypes = getJSDocThrowsTagTypes ( checker , callerDeclarationTSNode ) ;
95-
96- if (
97- isTypesAssignableTo ( checker , calleeThrowsTypes , callerThrowsTypes )
98- ) {
99- return ;
100- }
101-
102- context . report ( {
103- node,
104- messageId : 'throwTypeMismatch' ,
105- fix ( fixer ) {
106- const lastThrowsTypeNode =
107- callerThrowsTypeNodes [ callerThrowsTypeNodes . length - 1 ] ;
108-
109- if ( callerThrowsTags . length > 1 ) {
110- const lastThrowsTag = callerThrowsTags [ callerThrowsTags . length - 1 ] ;
111- const notAssignableThrows = calleeThrowsTypes
112- . filter ( ( t ) => ! callerThrowsTypes
113- . some ( ( n ) => checker . isTypeAssignableTo ( t , n ) ) ) ;
114-
115- const callerJSDocTSNode = lastThrowsTag . parent ;
116- /**
117- * @param {string } jsdocString
118- * @param {import('typescript').Type[] } types
119- * @returns {string }
120- */
121- const appendThrowsTags = ( jsdocString , types ) =>
122- types . reduce ( ( acc , t ) =>
123- acc . replace (
124- / ( [ ^ * \n ] + ) ( \* + [ / ] ) / ,
125- `$1* @throws {${ utils . getTypeName ( checker , t ) } }\n$1$2`
126- ) ,
127- jsdocString
128- ) ;
129-
130- return fixer . replaceTextRange (
131- [ callerJSDocTSNode . getStart ( ) , callerJSDocTSNode . getEnd ( ) ] ,
132- appendThrowsTags (
133- callerJSDocTSNode . getFullText ( ) ,
134- notAssignableThrows
135- )
104+ context . report ( {
105+ node,
106+ messageId : 'throwTypeMismatch' ,
107+ fix ( fixer ) {
108+ const lastThrowsTypeNode =
109+ callerThrowsTypeNodes [ callerThrowsTypeNodes . length - 1 ] ;
110+
111+ if ( callerThrowsTags . length > 1 ) {
112+ const lastThrowsTag = callerThrowsTags [ callerThrowsTags . length - 1 ] ;
113+ const notAssignableThrows = calleeThrowsTypes
114+ . filter ( ( t ) => ! callerThrowsTypes
115+ . some ( ( n ) => checker . isTypeAssignableTo ( t , n ) ) ) ;
116+
117+ const callerJSDocTSNode = lastThrowsTag . parent ;
118+ /**
119+ * @param {string } jsdocString
120+ * @param {import('typescript').Type[] } types
121+ * @returns {string }
122+ */
123+ const appendThrowsTags = ( jsdocString , types ) =>
124+ types . reduce ( ( acc , t ) =>
125+ acc . replace (
126+ / ( [ ^ * \n ] + ) ( \* + [ / ] ) / ,
127+ `$1* @throws {${ utils . getTypeName ( checker , t ) } }\n$1$2`
128+ ) ,
129+ jsdocString
136130 ) ;
137- }
138131
139- // If there is only one throws tag, make it as a union type
140132 return fixer . replaceTextRange (
141- [ lastThrowsTypeNode . pos , lastThrowsTypeNode . end ] ,
142- calleeThrowsTypes
143- . map ( t => utils . getTypeName ( checker , t ) ) . join ( ' | ' ) ,
133+ [ callerJSDocTSNode . getStart ( ) , callerJSDocTSNode . getEnd ( ) ] ,
134+ appendThrowsTags (
135+ callerJSDocTSNode . getFullText ( ) ,
136+ notAssignableThrows
137+ )
144138 ) ;
145- } ,
146- } ) ;
139+ }
147140
148- return ;
149- }
141+ // If there is only one throws tag, make it as a union type
142+ return fixer . replaceTextRange (
143+ [ lastThrowsTypeNode . pos , lastThrowsTypeNode . end ] ,
144+ calleeThrowsTypes
145+ . map ( t => utils . getTypeName ( checker , t ) ) . join ( ' | ' ) ,
146+ ) ;
147+ } ,
148+ } ) ;
150149
151- const calleeType = services . getTypeAtLocation ( node . expression . callee ) ;
152- if ( ! calleeType . symbol ) return ;
150+ return ;
151+ }
153152
154- const calleeTags = calleeType . symbol . getJsDocTags ( ) ;
153+ const calleeType = services . getTypeAtLocation ( node . expression . callee ) ;
154+ if ( ! calleeType . symbol ) return ;
155155
156- const isCalleeThrowable = calleeTags
157- . some ( ( tag ) => tag . name === 'throws' || tag . name === 'exception' ) ;
156+ const calleeTags = calleeType . symbol . getJsDocTags ( ) ;
158157
159- if ( ! isCalleeThrowable ) return ;
158+ const isCalleeThrowable = calleeTags
159+ . some ( ( tag ) => tag . name === 'throws' || tag . name === 'exception' ) ;
160160
161- const lines = sourceCode . getLines ( ) ;
161+ if ( ! isCalleeThrowable ) return ;
162162
163- const currentLine = lines [ node . loc . start . line - 1 ] ;
164- const prevLine = lines [ node . loc . start . line - 2 ] ;
163+ const lines = sourceCode . getLines ( ) ;
165164
166- const indent = currentLine . match ( / ^ \s * / ) ?. [ 0 ] ?? '' ;
167- const newIndent = indent + ' ' . repeat ( options . tabLength ) ;
165+ const currentLine = lines [ node . loc . start . line - 1 ] ;
166+ const prevLine = lines [ node . loc . start . line - 2 ] ;
168167
169- // TODO: Better way to handle this?
170- if ( / ^ \s * t r y \s * \{ / . test ( prevLine ) ) return ;
168+ const indent = currentLine . match ( / ^ \s * / ) ?. [ 0 ] ?? '' ;
169+ const newIndent = indent + ' ' . repeat ( options . tabLength ) ;
171170
172- context . report ( {
173- node,
174- messageId : 'implicitPropagation' ,
175- fix ( fixer ) {
176- return [
177- fixer . insertTextBefore ( node , `try {\n${ newIndent } ` ) ,
178- fixer . insertTextAfter ( node , `\n${ indent } } catch {}` ) ,
179- ] ;
180- } ,
181- } ) ;
182- } ,
171+ // TODO: Better way to handle this?
172+ if ( / ^ \s * t r y \s * \{ / . test ( prevLine ) ) return ;
173+
174+ context . report ( {
175+ node,
176+ messageId : 'implicitPropagation' ,
177+ fix ( fixer ) {
178+ return [
179+ fixer . insertTextBefore ( node , `try {\n${ newIndent } ` ) ,
180+ fixer . insertTextAfter ( node , `\n${ indent } } catch {}` ) ,
181+ ] ;
182+ } ,
183+ } ) ;
184+ } ;
185+
186+ return {
187+ 'ArrowFunctionExpression :not(TryStatement[handler!=null]) ExpressionStatement:has(> CallExpression)' : visitExpressionStatement ,
188+ 'FunctionDeclaration :not(TryStatement[handler!=null]) ExpressionStatement:has(> CallExpression)' : visitExpressionStatement ,
183189 } ;
184190 } ,
185191 defaultOptions : [ { tabLength : 4 } ] ,
0 commit comments