11// @ts -check
22const { ESLintUtils, AST_NODE_TYPES } = require ( '@typescript-eslint/utils' ) ;
3+ const utils = require ( '@typescript-eslint/type-utils' ) ;
4+ const ts = require ( 'typescript' ) ;
35const { hasThrowsTag } = require ( '../utils' ) ;
46const { findParent } = require ( '../utils' ) ;
57
@@ -19,6 +21,8 @@ module.exports = createRule({
1921 messages : {
2022 implicitPropagation :
2123 'Implicit propagation of exceptions is not allowed. Use try/catch to handle exceptions.' ,
24+ throwTypeMismatch :
25+ 'The type of the exception thrown does not match the type specified in the @throws (or @exception) tag.' ,
2226 } ,
2327 defaultOptions : [
2428 { tabLength : 4 } ,
@@ -41,10 +45,13 @@ module.exports = createRule({
4145
4246 const sourceCode = context . sourceCode ;
4347 const services = ESLintUtils . getParserServices ( context ) ;
48+ const checker = services . program . getTypeChecker ( ) ;
4449
4550 return {
4651 /** @param {import('@typescript-eslint/utils').TSESTree.ExpressionStatement } node */
4752 'FunctionDeclaration :not(TryStatement > BlockStatement) ExpressionStatement:has(> CallExpression)' ( node ) {
53+ if ( node . expression . type !== AST_NODE_TYPES . CallExpression ) return ;
54+
4855 const declaration =
4956 /** @type {import('@typescript-eslint/utils').TSESTree.FunctionDeclaration } */
5057 ( findParent ( node , ( n ) => n . type === AST_NODE_TYPES . FunctionDeclaration ) ) ;
@@ -56,12 +63,83 @@ module.exports = createRule({
5663 . map ( ( { value } ) => value )
5764 . some ( hasThrowsTag ) ;
5865
59- if ( isCommented ) return ;
66+ // TODO: Branching type checking or not
67+ if ( isCommented ) {
68+ const calleeDeclaration = services . getTypeAtLocation ( node . expression . callee ) . symbol . valueDeclaration ;
69+ if ( ! calleeDeclaration ) return ;
6070
61- if ( node . expression . type !== AST_NODE_TYPES . CallExpression ) return ;
71+ const calleeTags =
72+ /** @type {import('typescript').JSDocThrowsTag[] } */
73+ ( ts . getAllJSDocTagsOfKind ( calleeDeclaration , ts . SyntaxKind . JSDocThrowsTag ) ) ;
74+
75+ const calleeThrowTypeNodes =
76+ calleeTags
77+ . map ( ( tag ) => tag . typeExpression ?. type )
78+ . filter ( ( tag ) => ! ! tag ) ;
79+
80+ const tsDeclaration = services . getTypeAtLocation ( declaration ) . symbol . valueDeclaration ;
81+ if ( ! tsDeclaration ) return ;
82+
83+ const declarationTags =
84+ /** @type {import('typescript').JSDocThrowsTag[] } */
85+ ( ts . getAllJSDocTagsOfKind ( tsDeclaration , ts . SyntaxKind . JSDocThrowsTag ) ) ;
86+
87+ const declarationThrowTypeNodes =
88+ declarationTags
89+ . map ( ( tag ) => tag . typeExpression ?. type )
90+ . filter ( ( tag ) => ! ! tag ) ;
91+
92+ const calleeThrowTypes = calleeThrowTypeNodes
93+ . map ( ( node ) => checker . getTypeFromTypeNode ( node ) )
94+ . flatMap ( t => t . isUnion ( ) ? t . types : t ) ;
95+
96+ const declarationThrowTypes = declarationThrowTypeNodes
97+ . map ( ( node ) => checker . getTypeFromTypeNode ( node ) ) ;
98+
99+ const isAllCalleeThrowsAssignable = calleeThrowTypes
100+ . every ( ( t ) => declarationThrowTypes
101+ . some ( ( n ) => checker . isTypeAssignableTo ( t , n ) ) ) ;
102+
103+ if ( isAllCalleeThrowsAssignable ) return ;
104+
105+ context . report ( {
106+ node,
107+ messageId : 'throwTypeMismatch' ,
108+ fix ( fixer ) {
109+ const lastTagtypeNode =
110+ declarationThrowTypeNodes [ declarationThrowTypeNodes . length - 1 ] ;
111+
112+ if ( declarationTags . length > 1 ) {
113+ const lastTag = declarationTags [ declarationTags . length - 1 ] ;
114+ const notAssignableThrows = calleeThrowTypes
115+ . filter ( ( t ) => ! declarationThrowTypes
116+ . some ( ( n ) => checker . isTypeAssignableTo ( t , n ) ) ) ;
117+
118+ return fixer . replaceTextRange (
119+ [ lastTag . parent . getStart ( ) , lastTag . parent . getEnd ( ) ] ,
120+ notAssignableThrows
121+ . reduce ( ( acc , t ) =>
122+ acc . replace (
123+ / ( [ ^ * \n ] + ) ( \* + [ / ] ) / ,
124+ `$1* @throws {${ utils . getTypeName ( checker , t ) } }\n$1$2`
125+ ) ,
126+ lastTag . parent . getFullText ( )
127+ )
128+ ) ;
129+ }
130+
131+ return fixer . replaceTextRange (
132+ [ lastTagtypeNode . pos , lastTagtypeNode . end ] ,
133+ calleeThrowTypes . map ( t => utils . getTypeName ( checker , t ) ) . join ( ' | ' ) ,
134+ ) ;
135+ } ,
136+ } ) ;
137+ return ;
138+ }
62139
63140 const calleeType = services . getTypeAtLocation ( node . expression . callee ) ;
64141 if ( ! calleeType . symbol ) return ;
142+
65143 const calleeTags = calleeType . symbol . getJsDocTags ( ) ;
66144
67145 const isCalleeThrowable = calleeTags
@@ -71,9 +149,9 @@ module.exports = createRule({
71149
72150 const lines = sourceCode . getLines ( ) ;
73151 const currentLine = lines [ node . loc . start . line - 1 ] ;
152+ const prevLine = lines [ node . loc . start . line - 2 ] ;
74153 const indent = currentLine . match ( / ^ \s * / ) ?. [ 0 ] ?? '' ;
75154 const newIndent = indent + ' ' . repeat ( tabLength ) ;
76- const prevLine = lines [ node . loc . start . line - 2 ] ;
77155
78156 // TODO: Better way to handle this?
79157 if ( / ^ \s * t r y \s * \{ / . test ( prevLine ) ) return ;
0 commit comments