Skip to content

Commit c64f9f6

Browse files
fix: remove tsutils and tsutils-etc (#3)
- Remove unmaintained `tsutils` and `tsutils-etc`. - `tsutils` has been replaced by the community with `ts-api-utils`. See ajafff/tsutils#145. Replacing it was fairly simple. - `tsutils-etc` relies on `tsutils`, and I haven't seen a community-chosen replacement, so we're re-implementing the utilities into this repo, similar to how we re-implemented `eslint-etc` utilities. So utilities now exist in `ts-api-utils`, but `couldBeType` and `couldBeFunction` had to be ported over. - Pull in a dev dependency on `@typescript/vfs` to unit test `couldBeType` based on the `tsutils-etc` unit tests for that function. - Bump the minimum versions of TypeScript to >=4.2.0 to align with `ts-api-utils` and `tslib` to ^2.1.0 to align with `rxjs`.
1 parent 804812c commit c64f9f6

10 files changed

+342
-131
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,21 @@
5353
"@typescript-eslint/utils": "^8.12.2",
5454
"common-tags": "^1.8.0",
5555
"decamelize": "^5.0.0 || ^6.0.0",
56-
"tslib": "^2.0.0",
57-
"tsutils": "^3.0.0",
58-
"tsutils-etc": "^1.4.2"
56+
"ts-api-utils": "^1.3.0",
57+
"tslib": "^2.1.0"
5958
},
6059
"peerDependencies": {
6160
"eslint": "^8.57.0 || ^9.0.0",
6261
"rxjs": ">=7.0.0",
63-
"typescript": ">=4.0.0"
62+
"typescript": ">=4.2.0"
6463
},
6564
"devDependencies": {
6665
"@eslint/js": "^9.13.0",
6766
"@stylistic/eslint-plugin": "^2.10.1",
6867
"@types/common-tags": "^1.8.4",
6968
"@types/node": "^18.18.0",
7069
"@typescript-eslint/rule-tester": "^8.12.2",
70+
"@typescript/vfs": "^1.6.0",
7171
"@vitest/coverage-v8": "^2.1.4",
7272
"@vitest/eslint-plugin": "^1.1.7",
7373
"bumpp": "^9.8.0",

src/etc/could-be-function.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as ts from 'typescript';
2+
import { couldBeType } from './could-be-type';
3+
4+
export function couldBeFunction(type: ts.Type): boolean {
5+
return (
6+
type.getCallSignatures().length > 0
7+
|| couldBeType(type, 'Function')
8+
|| couldBeType(type, 'ArrowFunction')
9+
|| couldBeType(type, ts.InternalSymbolName.Function)
10+
);
11+
}

src/etc/could-be-type.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import * as tsutils from 'ts-api-utils';
2+
import * as ts from 'typescript';
3+
4+
export function couldBeType(
5+
type: ts.Type,
6+
name: string | RegExp,
7+
qualified?: {
8+
name: RegExp;
9+
typeChecker: ts.TypeChecker;
10+
},
11+
): boolean {
12+
if (tsutils.isTypeReference(type)) {
13+
type = type.target;
14+
}
15+
16+
if (isType(type, name, qualified)) {
17+
return true;
18+
}
19+
20+
if (tsutils.isUnionOrIntersectionType(type)) {
21+
return type.types.some(t => couldBeType(t, name, qualified));
22+
}
23+
24+
const baseTypes = type.getBaseTypes();
25+
if (baseTypes?.some(t => couldBeType(t, name, qualified))) {
26+
return true;
27+
}
28+
29+
if (couldImplement(type, name, qualified)) {
30+
return true;
31+
}
32+
33+
return false;
34+
}
35+
36+
function isType(
37+
type: ts.Type,
38+
name: string | RegExp,
39+
qualified?: {
40+
name: RegExp;
41+
typeChecker: ts.TypeChecker;
42+
},
43+
): boolean {
44+
if (!type.symbol) {
45+
return false;
46+
}
47+
if (
48+
qualified
49+
&& !qualified.name.test(
50+
qualified.typeChecker.getFullyQualifiedName(type.symbol),
51+
)
52+
) {
53+
return false;
54+
}
55+
return typeof name === 'string'
56+
? type.symbol.name === name
57+
: Boolean(type.symbol.name.match(name));
58+
}
59+
60+
function couldImplement(
61+
type: ts.Type,
62+
name: string | RegExp,
63+
qualified?: {
64+
name: RegExp;
65+
typeChecker: ts.TypeChecker;
66+
},
67+
): boolean {
68+
const { symbol } = type;
69+
if (symbol) {
70+
const { valueDeclaration } = symbol;
71+
if (valueDeclaration && ts.isClassDeclaration(valueDeclaration)) {
72+
const { heritageClauses } = valueDeclaration;
73+
if (heritageClauses) {
74+
const implemented = heritageClauses.some(
75+
({ token, types }) =>
76+
token === ts.SyntaxKind.ImplementsKeyword
77+
&& types.some(node => isMatchingNode(node, name, qualified)),
78+
);
79+
if (implemented) {
80+
return true;
81+
}
82+
}
83+
}
84+
}
85+
return false;
86+
}
87+
88+
function isMatchingNode(
89+
node: ts.ExpressionWithTypeArguments,
90+
name: string | RegExp,
91+
qualified?: {
92+
name: RegExp;
93+
typeChecker: ts.TypeChecker;
94+
},
95+
): boolean {
96+
const { expression } = node;
97+
if (qualified) {
98+
const type = qualified.typeChecker.getTypeAtLocation(expression);
99+
if (type) {
100+
const qualifiedName = qualified.typeChecker.getFullyQualifiedName(
101+
type.symbol,
102+
);
103+
if (!qualified.name.test(qualifiedName)) {
104+
return false;
105+
}
106+
}
107+
}
108+
const text = expression.getText();
109+
return typeof name === 'string' ? text === name : Boolean(text.match(name));
110+
}

src/etc/get-type-services.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';
2-
import * as tsutils from 'tsutils-etc';
2+
import * as tsutils from 'ts-api-utils';
33
import * as ts from 'typescript';
4+
import { couldBeFunction } from './could-be-function';
5+
import { couldBeType as tsutilsEtcCouldBeType } from './could-be-type';
46
import { isArrowFunctionExpression, isFunctionDeclaration } from './is';
57

68
export function getTypeServices<
@@ -17,7 +19,7 @@ export function getTypeServices<
1719
qualified?: { name: RegExp },
1820
): boolean => {
1921
const type = getType(node);
20-
return tsutils.couldBeType(
22+
return tsutilsEtcCouldBeType(
2123
type,
2224
name,
2325
qualified ? { ...qualified, typeChecker } : undefined,
@@ -46,7 +48,7 @@ export function getTypeServices<
4648
}
4749
return Boolean(
4850
tsTypeNode
49-
&& tsutils.couldBeType(
51+
&& tsutilsEtcCouldBeType(
5052
typeChecker.getTypeAtLocation(tsTypeNode),
5153
name,
5254
qualified ? { ...qualified, typeChecker } : undefined,
@@ -66,7 +68,7 @@ export function getTypeServices<
6668
if (isArrowFunctionExpression(node) || isFunctionDeclaration(node)) {
6769
return true;
6870
}
69-
return tsutils.couldBeFunction(getType(node));
71+
return couldBeFunction(getType(node));
7072
},
7173
couldBeMonoTypeOperatorFunction: (node: TSESTree.Node) =>
7274
couldBeType(node, 'MonoTypeOperatorFunction'),
@@ -78,9 +80,9 @@ export function getTypeServices<
7880
couldReturnType(node, 'Observable'),
7981
couldReturnType,
8082
getType,
81-
isAny: (node: TSESTree.Node) => tsutils.isAny(getType(node)),
82-
isReferenceType: (node: TSESTree.Node) => tsutils.isReferenceType(getType(node)),
83-
isUnknown: (node: TSESTree.Node) => tsutils.isUnknown(getType(node)),
83+
isAny: (node: TSESTree.Node) => tsutils.isIntrinsicAnyType(getType(node)),
84+
isReferenceType: (node: TSESTree.Node) => tsutils.isTypeReference(getType(node)),
85+
isUnknown: (node: TSESTree.Node) => tsutils.isIntrinsicUnknownType(getType(node)),
8486
typeChecker,
8587
};
8688
}

src/etc/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
export * from './could-be-function';
2+
export * from './could-be-type';
13
export * from './find-parent';
24
export * from './get-loc';
35
export * from './get-type-services';

src/rules/no-unsafe-subject-next.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { TSESTree as es } from '@typescript-eslint/utils';
2-
import * as tsutils from 'tsutils';
3-
import { couldBeType, isReferenceType, isUnionType } from 'tsutils-etc';
2+
import * as tsutils from 'ts-api-utils';
43
import * as ts from 'typescript';
54
import {
5+
couldBeType,
66
getTypeServices,
77
isMemberExpression } from '../etc';
88
import { ruleCreator } from '../utils';
@@ -30,7 +30,7 @@ export const noUnsafeSubjectNext = ruleCreator({
3030
) => {
3131
if (node.arguments.length === 0 && isMemberExpression(node.callee)) {
3232
const type = getType(node.callee.object);
33-
if (isReferenceType(type) && couldBeType(type, 'Subject')) {
33+
if (tsutils.isTypeReference(type) && couldBeType(type, 'Subject')) {
3434
const [typeArg] = typeChecker.getTypeArguments(type);
3535
if (tsutils.isTypeFlagSet(typeArg, ts.TypeFlags.Any)) {
3636
return;
@@ -42,7 +42,7 @@ export const noUnsafeSubjectNext = ruleCreator({
4242
return;
4343
}
4444
if (
45-
isUnionType(typeArg)
45+
tsutils.isUnionType(typeArg)
4646
&& typeArg.types.some((t) =>
4747
tsutils.isTypeFlagSet(t, ts.TypeFlags.Void),
4848
)

src/rules/throw-error.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { TSESTree as es, ESLintUtils } from '@typescript-eslint/utils';
2-
import { couldBeFunction, couldBeType, isAny, isUnknown } from 'tsutils-etc';
2+
import * as tsutils from 'ts-api-utils';
33
import * as ts from 'typescript';
4-
import { getTypeServices } from '../etc';
4+
import { couldBeFunction, couldBeType, getTypeServices } from '../etc';
55
import { ruleCreator } from '../utils';
66

77
export const throwErrorRule = ruleCreator({
@@ -32,8 +32,8 @@ export const throwErrorRule = ruleCreator({
3232
type = program.getTypeChecker().getTypeAtLocation(annotation ?? body);
3333
}
3434
if (
35-
!isAny(type)
36-
&& !isUnknown(type)
35+
!tsutils.isIntrinsicAnyType(type)
36+
&& !tsutils.isIntrinsicUnknownType(type)
3737
&& !couldBeType(type, /^(Error|DOMException)$/)
3838
) {
3939
context.report({

tests/etc/could-be-type.test.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import * as ts from 'typescript';
2+
import { couldBeType } from '../../src/etc/could-be-type';
3+
import { createSourceFileAndTypeChecker } from './create-source-file-and-type-checker';
4+
5+
describe('couldBeType', () => {
6+
it('should match a specific type', () => {
7+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
8+
`
9+
class A {}
10+
let a: A;
11+
`,
12+
);
13+
const node = (sourceFile.statements[1] as ts.VariableStatement).declarationList.declarations[0];
14+
const type = typeChecker.getTypeAtLocation(node);
15+
16+
expect(couldBeType(type, 'A')).toBe(true);
17+
});
18+
19+
it('should not match different types', () => {
20+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
21+
`
22+
class A {}
23+
class B {}
24+
let b: B;
25+
`,
26+
);
27+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
28+
const type = typeChecker.getTypeAtLocation(node);
29+
30+
expect(couldBeType(type, 'A')).toBe(false);
31+
expect(couldBeType(type, 'B')).toBe(true);
32+
});
33+
34+
it('should match a base type', () => {
35+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
36+
`
37+
class A {}
38+
class B extends A {}
39+
let b: B;
40+
`,
41+
);
42+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
43+
const type = typeChecker.getTypeAtLocation(node);
44+
45+
expect(couldBeType(type, 'A')).toBe(true);
46+
expect(couldBeType(type, 'B')).toBe(true);
47+
});
48+
49+
it('should match an implemented interface', () => {
50+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
51+
`
52+
interface A { name: string; }
53+
class B implements A { name = ""; }
54+
let b: B;
55+
`,
56+
);
57+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
58+
const type = typeChecker.getTypeAtLocation(node);
59+
60+
expect(couldBeType(type, 'A')).toBe(true);
61+
expect(couldBeType(type, 'B')).toBe(true);
62+
});
63+
64+
it('should match an implemented generic interface', () => {
65+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
66+
`
67+
interface A<T> { value: T; }
68+
class B<T> implements A<T> { constructor(public value: T) {} }
69+
let b = new B<string>("B");
70+
`,
71+
);
72+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
73+
const type = typeChecker.getTypeAtLocation(node);
74+
75+
expect(couldBeType(type, 'A')).toBe(true);
76+
expect(couldBeType(type, 'B')).toBe(true);
77+
});
78+
79+
it('should match an intersection type', () => {
80+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
81+
`
82+
class A {}
83+
class B {}
84+
let ab: A & B;
85+
`,
86+
);
87+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
88+
const type = typeChecker.getTypeAtLocation(node);
89+
90+
expect(couldBeType(type, 'A')).toBe(true);
91+
expect(couldBeType(type, 'B')).toBe(true);
92+
});
93+
94+
it('should match a union type', () => {
95+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
96+
`
97+
class A {}
98+
class B {}
99+
let ab: A | B;
100+
`,
101+
);
102+
const node = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
103+
const type = typeChecker.getTypeAtLocation(node);
104+
105+
expect(couldBeType(type, 'A')).toBe(true);
106+
expect(couldBeType(type, 'B')).toBe(true);
107+
});
108+
109+
it.todo('should support fully-qualified types', () => {
110+
// TODO: This test is disabled because we're failing to import from other files using @typescript/vfs. See env.languageService.getSemanticDiagnostics(fileName) for error message.
111+
const { sourceFile, typeChecker } = createSourceFileAndTypeChecker(
112+
`
113+
import { A } from "./a";
114+
class B {}
115+
let a: A;
116+
let b: B;
117+
`,
118+
);
119+
const nodeA = (sourceFile.statements[2] as ts.VariableStatement).declarationList.declarations[0];
120+
const nodeB = (sourceFile.statements[3] as ts.VariableStatement).declarationList.declarations[0];
121+
const typeA = typeChecker.getTypeAtLocation(nodeA);
122+
const typeB = typeChecker.getTypeAtLocation(nodeB);
123+
124+
expect(
125+
couldBeType(typeA, 'A', {
126+
name: /"a"/,
127+
typeChecker,
128+
}),
129+
).toBe(true);
130+
expect(
131+
couldBeType(typeB, 'B', {
132+
name: /"b"/,
133+
typeChecker,
134+
}),
135+
).toBe(false);
136+
});
137+
});

0 commit comments

Comments
 (0)