Skip to content

Commit 55bd794

Browse files
fix(no-mixed-types): handle more than just property signatures, check the type of type references (#793)
fix #734
1 parent fc4aacc commit 55bd794

File tree

5 files changed

+260
-239
lines changed

5 files changed

+260
-239
lines changed

docs/rules/no-mixed-types.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Foo = {
2727

2828
### ✅ Correct
2929

30+
<!-- eslint-skip -->
31+
3032
```ts
3133
/* eslint functional/no-mixed-types: "error" */
3234

@@ -36,12 +38,14 @@ type Foo = {
3638
};
3739
```
3840

41+
<!-- eslint-skip -->
42+
3943
```ts
4044
/* eslint functional/no-mixed-types: "error" */
4145

4246
type Foo = {
4347
prop1: () => string;
44-
prop2: () => () => number;
48+
prop2(): number;
4549
};
4650
```
4751

src/rules/no-mixed-types.ts

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1-
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
1+
import { type TSESTree } from "@typescript-eslint/utils";
22
import { type JSONSchema4 } from "@typescript-eslint/utils/json-schema";
33
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";
44

55
import { ruleNameScope } from "#eslint-plugin-functional/utils/misc";
66
import {
77
createRuleUsingFunction,
8+
getTypeOfNode,
89
type NamedCreateRuleCustomMeta,
910
type RuleResult,
1011
} from "#eslint-plugin-functional/utils/rule";
1112
import {
13+
isFunctionLikeType,
1214
isIdentifier,
15+
isTSCallSignatureDeclaration,
16+
isTSConstructSignatureDeclaration,
17+
isTSFunctionType,
18+
isTSIndexSignature,
19+
isTSMethodSignature,
1320
isTSPropertySignature,
1421
isTSTypeLiteral,
1522
isTSTypeReference,
@@ -92,42 +99,21 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
9299
*/
93100
function hasTypeElementViolations(
94101
typeElements: TSESTree.TypeElement[],
102+
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
95103
): boolean {
96-
type CarryType = {
97-
readonly prevMemberType: AST_NODE_TYPES | undefined;
98-
readonly prevMemberTypeAnnotation: AST_NODE_TYPES | undefined;
99-
readonly violations: boolean;
100-
};
101-
102-
const typeElementsTypeInfo = typeElements.map((member) => ({
103-
type: member.type,
104-
typeAnnotation:
105-
isTSPropertySignature(member) && member.typeAnnotation !== undefined
106-
? member.typeAnnotation.typeAnnotation.type
107-
: undefined,
108-
}));
109-
110-
return typeElementsTypeInfo.reduce<CarryType>(
111-
(carry, member) => ({
112-
prevMemberType: member.type,
113-
prevMemberTypeAnnotation: member.typeAnnotation,
114-
violations:
115-
// Not the first property in the interface.
116-
carry.prevMemberType !== undefined &&
117-
// And different property type to previous property.
118-
(carry.prevMemberType !== member.type ||
119-
// Or annotated with a different type annotation.
120-
(carry.prevMemberTypeAnnotation !== member.typeAnnotation &&
121-
// Where one of the properties is a annotated as a function.
122-
(carry.prevMemberTypeAnnotation === AST_NODE_TYPES.TSFunctionType ||
123-
member.typeAnnotation === AST_NODE_TYPES.TSFunctionType))),
124-
}),
125-
{
126-
prevMemberType: undefined,
127-
prevMemberTypeAnnotation: undefined,
128-
violations: false,
129-
},
130-
).violations;
104+
return !typeElements
105+
.map((member) => {
106+
return (
107+
isTSMethodSignature(member) ||
108+
isTSCallSignatureDeclaration(member) ||
109+
isTSConstructSignatureDeclaration(member) ||
110+
((isTSPropertySignature(member) || isTSIndexSignature(member)) &&
111+
member.typeAnnotation !== undefined &&
112+
(isTSFunctionType(member.typeAnnotation.typeAnnotation) ||
113+
isFunctionLikeType(getTypeOfNode(member, context))))
114+
);
115+
})
116+
.every((isFunction, _, array) => array[0] === isFunction);
131117
}
132118

133119
/**
@@ -140,7 +126,7 @@ function checkTSInterfaceDeclaration(
140126
): RuleResult<keyof typeof errorMessages, Options> {
141127
return {
142128
context,
143-
descriptors: hasTypeElementViolations(node.body.body)
129+
descriptors: hasTypeElementViolations(node.body.body, context)
144130
? [{ node, messageId: "generic" }]
145131
: [],
146132
};
@@ -159,15 +145,16 @@ function checkTSTypeAliasDeclaration(
159145
descriptors:
160146
// TypeLiteral.
161147
(isTSTypeLiteral(node.typeAnnotation) &&
162-
hasTypeElementViolations(node.typeAnnotation.members)) ||
148+
hasTypeElementViolations(node.typeAnnotation.members, context)) ||
163149
// TypeLiteral inside `Readonly<>`.
164150
(isTSTypeReference(node.typeAnnotation) &&
165151
isIdentifier(node.typeAnnotation.typeName) &&
166-
node.typeAnnotation.typeParameters !== undefined &&
167-
node.typeAnnotation.typeParameters.params.length === 1 &&
168-
isTSTypeLiteral(node.typeAnnotation.typeParameters.params[0]!) &&
152+
node.typeAnnotation.typeArguments !== undefined &&
153+
node.typeAnnotation.typeArguments.params.length === 1 &&
154+
isTSTypeLiteral(node.typeAnnotation.typeArguments.params[0]!) &&
169155
hasTypeElementViolations(
170-
node.typeAnnotation.typeParameters.params[0].members,
156+
node.typeAnnotation.typeArguments.params[0].members,
157+
context,
171158
))
172159
? [{ node, messageId: "generic" }]
173160
: [],

src/utils/type-guards.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,24 @@ export function isTSIndexSignature(
255255
return node.type === AST_NODE_TYPES.TSIndexSignature;
256256
}
257257

258+
export function isTSMethodSignature(
259+
node: TSESTree.Node,
260+
): node is TSESTree.TSMethodSignature {
261+
return node.type === AST_NODE_TYPES.TSMethodSignature;
262+
}
263+
264+
export function isTSCallSignatureDeclaration(
265+
node: TSESTree.Node,
266+
): node is TSESTree.TSCallSignatureDeclaration {
267+
return node.type === AST_NODE_TYPES.TSCallSignatureDeclaration;
268+
}
269+
270+
export function isTSConstructSignatureDeclaration(
271+
node: TSESTree.Node,
272+
): node is TSESTree.TSConstructSignatureDeclaration {
273+
return node.type === AST_NODE_TYPES.TSConstructSignatureDeclaration;
274+
}
275+
258276
export function isTSInterfaceBody(
259277
node: TSESTree.Node,
260278
): node is TSESTree.TSInterfaceBody {
@@ -412,3 +430,7 @@ export function isObjectConstructorType(type: Type | null): boolean {
412430
(isUnionType(type) && type.types.some(isObjectConstructorType)))
413431
);
414432
}
433+
434+
export function isFunctionLikeType(type: Type | null): boolean {
435+
return type !== null && type.getCallSignatures().length > 0;
436+
}
Lines changed: 104 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
2-
import dedent from "dedent";
3-
41
import { type rule } from "#eslint-plugin-functional/rules/no-mixed-types";
52
import {
63
type InvalidTestCaseSet,
@@ -11,110 +8,110 @@ import {
118
const tests: Array<
129
InvalidTestCaseSet<MessagesOf<typeof rule>, OptionsOf<typeof rule>>
1310
> = [
14-
// Mixing properties and methods (MethodSignature) should produce failures.
15-
{
16-
code: dedent`
17-
type Foo = {
18-
bar: string;
19-
zoo(): number;
20-
};
21-
`,
22-
optionsSet: [[], [{ checkInterfaces: false }]],
23-
errors: [
24-
{
25-
messageId: "generic",
26-
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
27-
line: 1,
28-
column: 1,
29-
},
30-
],
31-
},
32-
{
33-
code: dedent`
34-
type Foo = Readonly<{
35-
bar: string;
36-
zoo(): number;
37-
}>;
38-
`,
39-
optionsSet: [[], [{ checkInterfaces: false }]],
40-
errors: [
41-
{
42-
messageId: "generic",
43-
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
44-
line: 1,
45-
column: 1,
46-
},
47-
],
48-
},
49-
{
50-
code: dedent`
51-
interface Foo {
52-
bar: string;
53-
zoo(): number;
54-
}
55-
`,
56-
optionsSet: [[], [{ checkTypeLiterals: false }]],
57-
errors: [
58-
{
59-
messageId: "generic",
60-
type: AST_NODE_TYPES.TSInterfaceDeclaration,
61-
line: 1,
62-
column: 1,
63-
},
64-
],
65-
},
66-
// Mixing properties and functions (PropertySignature) should produce failures.
67-
{
68-
code: dedent`
69-
type Foo = {
70-
bar: string;
71-
zoo: () => number;
72-
};
73-
`,
74-
optionsSet: [[], [{ checkInterfaces: false }]],
75-
errors: [
76-
{
77-
messageId: "generic",
78-
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
79-
line: 1,
80-
column: 1,
81-
},
82-
],
83-
},
84-
{
85-
code: dedent`
86-
type Foo = Readonly<{
87-
bar: string;
88-
zoo: () => number;
89-
}>;
90-
`,
91-
optionsSet: [[], [{ checkInterfaces: false }]],
92-
errors: [
93-
{
94-
messageId: "generic",
95-
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
96-
line: 1,
97-
column: 1,
98-
},
99-
],
100-
},
101-
{
102-
code: dedent`
103-
interface Foo {
104-
bar: string;
105-
zoo: () => number;
106-
}
107-
`,
108-
optionsSet: [[], [{ checkTypeLiterals: false }]],
109-
errors: [
110-
{
111-
messageId: "generic",
112-
type: AST_NODE_TYPES.TSInterfaceDeclaration,
113-
line: 1,
114-
column: 1,
115-
},
116-
],
117-
},
11+
// // Mixing properties and methods (MethodSignature) should produce failures.
12+
// {
13+
// code: dedent`
14+
// type Foo = {
15+
// bar: string;
16+
// zoo(): number;
17+
// };
18+
// `,
19+
// optionsSet: [[], [{ checkInterfaces: false }]],
20+
// errors: [
21+
// {
22+
// messageId: "generic",
23+
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
24+
// line: 1,
25+
// column: 1,
26+
// },
27+
// ],
28+
// },
29+
// {
30+
// code: dedent`
31+
// type Foo = Readonly<{
32+
// bar: string;
33+
// zoo(): number;
34+
// }>;
35+
// `,
36+
// optionsSet: [[], [{ checkInterfaces: false }]],
37+
// errors: [
38+
// {
39+
// messageId: "generic",
40+
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
41+
// line: 1,
42+
// column: 1,
43+
// },
44+
// ],
45+
// },
46+
// {
47+
// code: dedent`
48+
// interface Foo {
49+
// bar: string;
50+
// zoo(): number;
51+
// }
52+
// `,
53+
// optionsSet: [[], [{ checkTypeLiterals: false }]],
54+
// errors: [
55+
// {
56+
// messageId: "generic",
57+
// type: AST_NODE_TYPES.TSInterfaceDeclaration,
58+
// line: 1,
59+
// column: 1,
60+
// },
61+
// ],
62+
// },
63+
// // Mixing properties and functions (PropertySignature) should produce failures.
64+
// {
65+
// code: dedent`
66+
// type Foo = {
67+
// bar: string;
68+
// zoo: () => number;
69+
// };
70+
// `,
71+
// optionsSet: [[], [{ checkInterfaces: false }]],
72+
// errors: [
73+
// {
74+
// messageId: "generic",
75+
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
76+
// line: 1,
77+
// column: 1,
78+
// },
79+
// ],
80+
// },
81+
// {
82+
// code: dedent`
83+
// type Foo = Readonly<{
84+
// bar: string;
85+
// zoo: () => number;
86+
// }>;
87+
// `,
88+
// optionsSet: [[], [{ checkInterfaces: false }]],
89+
// errors: [
90+
// {
91+
// messageId: "generic",
92+
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
93+
// line: 1,
94+
// column: 1,
95+
// },
96+
// ],
97+
// },
98+
// {
99+
// code: dedent`
100+
// interface Foo {
101+
// bar: string;
102+
// zoo: () => number;
103+
// }
104+
// `,
105+
// optionsSet: [[], [{ checkTypeLiterals: false }]],
106+
// errors: [
107+
// {
108+
// messageId: "generic",
109+
// type: AST_NODE_TYPES.TSInterfaceDeclaration,
110+
// line: 1,
111+
// column: 1,
112+
// },
113+
// ],
114+
// },
118115
];
119116

120117
export default tests;

0 commit comments

Comments
 (0)