Skip to content

Commit d0aeae5

Browse files
authored
Merge pull request #7 from Xvezda/feature/fix-bugs
Fix false positive
2 parents 399758b + 0d6f892 commit d0aeae5

File tree

5 files changed

+517
-194
lines changed

5 files changed

+517
-194
lines changed

src/rules/no-implicit-propagation.js

Lines changed: 113 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ const utils = require('@typescript-eslint/type-utils');
44
const {
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*try\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*try\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

Comments
 (0)