diff --git a/src/rules/no-undocumented-throws.js b/src/rules/no-undocumented-throws.js index 19a7076..1362b3d 100644 --- a/src/rules/no-undocumented-throws.js +++ b/src/rules/no-undocumented-throws.js @@ -157,6 +157,12 @@ module.exports = createRule({ } break; } + // If getter/setter both exist and is used in assignment, + // only setter throw types are collected. + case AST_NODE_TYPES.MemberExpression: + if (node.parent?.type === AST_NODE_TYPES.AssignmentExpression) { + return; + } default: break; } diff --git a/src/utils.js b/src/utils.js index 46c92e8..5208c25 100644 --- a/src/utils.js +++ b/src/utils.js @@ -336,6 +336,44 @@ const getCallee = (node) => { * @return {import('typescript').Declaration | null} */ const getCalleeDeclaration = (services, node) => { + /** + * Return type of setter when assigning + * + * @example + * ``` + * foo.bar = 'baz'; + * // ^ This can be a setter + * ``` + */ + if (node.type === AST_NODE_TYPES.AssignmentExpression) { + /** @type {import('@typescript-eslint/utils').TSESTree.Node | null} */ + const calleeNode = getCallee(node); + if (!calleeNode) return null; + + const type = services.getTypeAtLocation(calleeNode); + for ( + const declaration of + type.symbol?.declarations ?? + services + .getSymbolAtLocation(calleeNode) + ?.declarations ?? + [] + ) { + if (!services.tsNodeToESTreeNodeMap.has(declaration)) continue; + + const declarationNode = + services.tsNodeToESTreeNodeMap.get(declaration); + + const isSetter = isAccessorNode(declarationNode) && + declarationNode.kind === 'set'; + + if (isSetter) { + return declaration; + } + } + return null; + } + /** @type {import('typescript').Declaration | null} */ let declaration = null; if ( @@ -371,24 +409,6 @@ const getCalleeDeclaration = (services, node) => { if (!declaration) return null; switch (node.type) { - /** - * Return type of setter when assigning - * - * @example - * ``` - * foo.bar = 'baz'; - * // ^ This can be a setter - * ``` - */ - case AST_NODE_TYPES.AssignmentExpression: { - const declarationNode = - services.tsNodeToESTreeNodeMap.get(declaration); - - const isSetter = isAccessorNode(declarationNode) && - declarationNode.kind === 'set'; - - return isSetter ? declaration : null; - } /** * Return type of getter when accessing * diff --git a/tests/rules/check-throws-tag-type.test.js b/tests/rules/check-throws-tag-type.test.js index 7151079..ac84639 100644 --- a/tests/rules/check-throws-tag-type.test.js +++ b/tests/rules/check-throws-tag-type.test.js @@ -1676,6 +1676,72 @@ ruleTester.run( { messageId: 'throwTypeMismatch' }, ], }, + { + code: ` + const foo = { + /** + * @throws {RangeError} + */ + get bar() { + throw new RangeError('baz'); + }, + /** + * @throws {TypeError} + */ + set bar(value) { + throw new TypeError('baz'); + }, + }; + + /** + * @throws {TypeError} + */ + function baz() { + foo.bar; + } + + /** + * @throws {RangeError} + */ + function qux() { + foo.bar = 'quux'; + } + `, + output: ` + const foo = { + /** + * @throws {RangeError} + */ + get bar() { + throw new RangeError('baz'); + }, + /** + * @throws {TypeError} + */ + set bar(value) { + throw new TypeError('baz'); + }, + }; + + /** + * @throws {RangeError} + */ + function baz() { + foo.bar; + } + + /** + * @throws {TypeError} + */ + function qux() { + foo.bar = 'quux'; + } + `, + errors: [ + { messageId: 'throwTypeMismatch' }, + { messageId: 'throwTypeMismatch' }, + ], + }, ], }, ); diff --git a/tests/rules/no-undocumented-throws.test.js b/tests/rules/no-undocumented-throws.test.js index 3989fde..16d4f83 100644 --- a/tests/rules/no-undocumented-throws.test.js +++ b/tests/rules/no-undocumented-throws.test.js @@ -4023,6 +4023,66 @@ ruleTester.run( { messageId: 'missingThrowsTag' }, ], }, + { + code: ` + const foo = { + /** + * @throws {RangeError} + */ + get bar() { + throw new RangeError('baz'); + }, + /** + * @throws {TypeError} + */ + set bar(value) { + throw new TypeError('baz'); + }, + }; + + function baz() { + foo.bar; + } + + function qux() { + foo.bar = 'quux'; + } + `, + output: ` + const foo = { + /** + * @throws {RangeError} + */ + get bar() { + throw new RangeError('baz'); + }, + /** + * @throws {TypeError} + */ + set bar(value) { + throw new TypeError('baz'); + }, + }; + + /** + * @throws {RangeError} + */ + function baz() { + foo.bar; + } + + /** + * @throws {TypeError} + */ + function qux() { + foo.bar = 'quux'; + } + `, + errors: [ + { messageId: 'missingThrowsTag' }, + { messageId: 'missingThrowsTag' }, + ], + }, ], }, );