Skip to content

Commit 5e612a6

Browse files
authored
Merge pull request #27 from Xvezda/feature/refactor
Refactor
2 parents ef81384 + 7ccb155 commit 5e612a6

File tree

3 files changed

+64
-46
lines changed

3 files changed

+64
-46
lines changed

src/rules/no-implicit-propagation.js

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils');
33
const utils = require('@typescript-eslint/type-utils');
44
const {
55
getLast,
6+
getNodeID,
67
createRule,
78
isInHandledContext,
89
typesToUnionString,
@@ -11,7 +12,7 @@ const {
1112
getJSDocThrowsTagTypes,
1213
getDeclarationTSNodeOfESTreeNode,
1314
toFlattenedTypeArray,
14-
isTypesAssignableTo,
15+
groupTypesByCompatibility,
1516
findClosestFunctionNode,
1617
findNodeToComment,
1718
createInsertJSDocBeforeFixer,
@@ -147,8 +148,8 @@ module.exports = createRule({
147148

148149
/** @param {import('@typescript-eslint/utils').TSESTree.Expression} node */
149150
const visitExpression = (node) => {
150-
if (visitedNodes.has(node.range[0])) return;
151-
visitedNodes.add(node.range[0]);
151+
if (visitedNodes.has(getNodeID(node))) return;
152+
visitedNodes.add(getNodeID(node));
152153

153154
if (isInHandledContext(node)) return;
154155

@@ -178,29 +179,14 @@ module.exports = createRule({
178179

179180
const callerThrowsTypes = toFlattenedTypeArray(getJSDocThrowsTagTypes(checker, callerDeclarationTSNode));
180181

181-
if (
182-
isTypesAssignableTo(services.program, calleeThrowsTypes, callerThrowsTypes)
183-
) {
182+
const typeGroups = groupTypesByCompatibility(services.program, calleeThrowsTypes, callerThrowsTypes);
183+
if (!typeGroups.incompatible) {
184184
return;
185185
}
186186

187187
const lastThrowsTypeNode = getLast(callerThrowsTypeNodes);
188188
if (!lastThrowsTypeNode) return;
189189

190-
const notAssignableThrows = calleeThrowsTypes
191-
.filter((calleeType) => !callerThrowsTypes
192-
.some((callerType) => {
193-
if (
194-
utils.isErrorLike(services.program, callerType) &&
195-
utils.isErrorLike(services.program, calleeType)
196-
) {
197-
return utils.typeIsOrHasBaseType(calleeType, callerType);
198-
}
199-
return checker.isTypeAssignableTo(calleeType, callerType);
200-
}));
201-
202-
if (!notAssignableThrows.length) return;
203-
204190
context.report({
205191
node,
206192
messageId: 'throwTypeMismatch',
@@ -224,11 +210,14 @@ module.exports = createRule({
224210
jsdocString
225211
);
226212

213+
if (!typeGroups.incompatible) {
214+
return null;
215+
}
227216
return fixer.replaceTextRange(
228217
[callerJSDocTSNode.getStart(), callerJSDocTSNode.getEnd()],
229218
appendThrowsTags(
230219
callerJSDocTSNode.getFullText(),
231-
notAssignableThrows
220+
typeGroups.incompatible,
232221
)
233222
);
234223
}

src/rules/no-undocumented-throws.js

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const ts = require('typescript');
55
const {
66
getFirst,
77
getLast,
8+
getNodeID,
89
createRule,
910
findParent,
1011
hasJSDocThrowsTag,
@@ -13,7 +14,7 @@ const {
1314
getOptionsFromContext,
1415
getJSDocThrowsTags,
1516
getJSDocThrowsTagTypes,
16-
isTypesAssignableTo,
17+
groupTypesByCompatibility,
1718
findClosestFunctionNode,
1819
findNodeToComment,
1920
findIdentifierDeclaration,
@@ -60,14 +61,6 @@ module.exports = createRule({
6061

6162
const visitedNodes = new Set();
6263

63-
/**
64-
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
65-
* @returns {string}
66-
*/
67-
const getNodeID = (node) => {
68-
return `${node.type}:${node.loc.start.line}:${node.loc.start.column}`;
69-
};
70-
7164
/**
7265
* Group throw statements in functions
7366
* @type {Map<string, import('@typescript-eslint/utils').TSESTree.ThrowStatement[]>}
@@ -113,7 +106,12 @@ module.exports = createRule({
113106
.map(t => node.async ? checker.getAwaitedType(t) : t)
114107
.filter(t => !!t);
115108

116-
if (isTypesAssignableTo(services.program, throwTypes, throwsTagTypes)) return;
109+
const typeGroups = groupTypesByCompatibility(
110+
services.program,
111+
throwTypes,
112+
throwsTagTypes,
113+
);
114+
if (!typeGroups.incompatible) return;
117115

118116
const lastTagtypeNode = getLast(throwsTagTypeNodes);
119117
if (!lastTagtypeNode) return;

src/utils.js

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,26 @@ const { ESLintUtils, AST_NODE_TYPES } = require('@typescript-eslint/utils');
33
const utils = require('@typescript-eslint/type-utils');
44
const ts = require('typescript');
55

6+
// Object.groupBy clone
7+
/**
8+
* Groups an array of objects by a specified key or function.
9+
* @template T
10+
* @template {string} K
11+
* @param {T[]} arr - The array to group.
12+
* @param {((item: T) => K)} key
13+
* @return {Record<K, T[] | undefined>}
14+
*/
15+
const groupBy = (arr, key) => {
16+
return arr.reduce((acc, item) => {
17+
const groupKey = key(item);
18+
if (!acc[groupKey]) {
19+
acc[groupKey] = [];
20+
}
21+
acc[groupKey].push(item);
22+
return acc;
23+
}, /** @type {Record<string, T[]>} */(Object.create(null)));
24+
};
25+
626
/**
727
* @template {unknown} T
828
* @param {Readonly<T[]>} arr
@@ -32,6 +52,14 @@ const hasThrowsTag = comment =>
3252
comment.includes('@throws') ||
3353
comment.includes('@exception');
3454

55+
/**
56+
* @param {import('@typescript-eslint/utils').TSESTree.Node} node
57+
* @returns {string}
58+
*/
59+
const getNodeID = (node) => {
60+
return `${node.loc.start.line}:${node.loc.start.column}`;
61+
};
62+
3563
/**
3664
* Check if node has JSDoc comment with @throws or @exception tag.
3765
*
@@ -173,23 +201,25 @@ const toFlattenedTypeArray = (types) =>
173201
* @param {import('@typescript-eslint/utils').ParserServicesWithTypeInformation['program']} program
174202
* @param {import('typescript').Type[]} source
175203
* @param {import('typescript').Type[]} target
176-
* @returns {boolean}
204+
* @returns {{ compatible?: import('typescript').Type[]; incompatible?: import('typescript').Type[] }}
177205
*/
178-
const isTypesAssignableTo = (program, source, target) => {
206+
const groupTypesByCompatibility = (program, source, target) => {
179207
const checker = program.getTypeChecker();
180-
return source
181-
.every(sourceType =>
182-
target
183-
.some(targetType => {
184-
if (
185-
utils.isErrorLike(program, sourceType) &&
186-
utils.isErrorLike(program, targetType)
187-
) {
188-
return utils.typeIsOrHasBaseType(sourceType, targetType);
189-
}
190-
return checker.isTypeAssignableTo(sourceType, targetType);
191-
})
208+
209+
return groupBy(source, sourceType => {
210+
const isCompatible = target.some(targetType => {
211+
if (
212+
utils.isErrorLike(program, sourceType) &&
213+
utils.isErrorLike(program, targetType)
214+
) {
215+
return utils.typeIsOrHasBaseType(sourceType, targetType);
216+
}
217+
return checker.isTypeAssignableTo(sourceType, targetType);
218+
});
219+
return /** @type {'compatible'|'incompatible'} */(
220+
isCompatible ? 'compatible' : 'incompatible'
192221
);
222+
})
193223
}
194224

195225
/**
@@ -388,6 +418,7 @@ const createInsertJSDocBeforeFixer = (sourceCode, node, typeString) => {
388418
module.exports = {
389419
getFirst,
390420
getLast,
421+
getNodeID,
391422
createRule,
392423
hasThrowsTag,
393424
hasJSDocThrowsTag,
@@ -399,7 +430,7 @@ module.exports = {
399430
getJSDocThrowsTags,
400431
getJSDocThrowsTagTypes,
401432
toFlattenedTypeArray,
402-
isTypesAssignableTo,
433+
groupTypesByCompatibility,
403434
findClosestFunctionNode,
404435
findNodeToComment,
405436
findIdentifierDeclaration,

0 commit comments

Comments
 (0)