Skip to content

Commit

Permalink
fix(no-mixed-types): handle more than just property signatures, check…
Browse files Browse the repository at this point in the history
… the type of type references (#793)

fix #734
  • Loading branch information
RebeccaStevens authored Mar 25, 2024
1 parent fc4aacc commit 55bd794
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 239 deletions.
6 changes: 5 additions & 1 deletion docs/rules/no-mixed-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type Foo = {

### ✅ Correct

<!-- eslint-skip -->

```ts
/* eslint functional/no-mixed-types: "error" */

Expand All @@ -36,12 +38,14 @@ type Foo = {
};
```

<!-- eslint-skip -->

```ts
/* eslint functional/no-mixed-types: "error" */

type Foo = {
prop1: () => string;
prop2: () => () => number;
prop2(): number;
};
```

Expand Down
71 changes: 29 additions & 42 deletions src/rules/no-mixed-types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
import { type TSESTree } from "@typescript-eslint/utils";
import { type JSONSchema4 } from "@typescript-eslint/utils/json-schema";
import { type RuleContext } from "@typescript-eslint/utils/ts-eslint";

import { ruleNameScope } from "#eslint-plugin-functional/utils/misc";
import {
createRuleUsingFunction,
getTypeOfNode,
type NamedCreateRuleCustomMeta,
type RuleResult,
} from "#eslint-plugin-functional/utils/rule";
import {
isFunctionLikeType,
isIdentifier,
isTSCallSignatureDeclaration,
isTSConstructSignatureDeclaration,
isTSFunctionType,
isTSIndexSignature,
isTSMethodSignature,
isTSPropertySignature,
isTSTypeLiteral,
isTSTypeReference,
Expand Down Expand Up @@ -92,42 +99,21 @@ const meta: NamedCreateRuleCustomMeta<keyof typeof errorMessages, Options> = {
*/
function hasTypeElementViolations(
typeElements: TSESTree.TypeElement[],
context: Readonly<RuleContext<keyof typeof errorMessages, Options>>,
): boolean {
type CarryType = {
readonly prevMemberType: AST_NODE_TYPES | undefined;
readonly prevMemberTypeAnnotation: AST_NODE_TYPES | undefined;
readonly violations: boolean;
};

const typeElementsTypeInfo = typeElements.map((member) => ({
type: member.type,
typeAnnotation:
isTSPropertySignature(member) && member.typeAnnotation !== undefined
? member.typeAnnotation.typeAnnotation.type
: undefined,
}));

return typeElementsTypeInfo.reduce<CarryType>(
(carry, member) => ({
prevMemberType: member.type,
prevMemberTypeAnnotation: member.typeAnnotation,
violations:
// Not the first property in the interface.
carry.prevMemberType !== undefined &&
// And different property type to previous property.
(carry.prevMemberType !== member.type ||
// Or annotated with a different type annotation.
(carry.prevMemberTypeAnnotation !== member.typeAnnotation &&
// Where one of the properties is a annotated as a function.
(carry.prevMemberTypeAnnotation === AST_NODE_TYPES.TSFunctionType ||
member.typeAnnotation === AST_NODE_TYPES.TSFunctionType))),
}),
{
prevMemberType: undefined,
prevMemberTypeAnnotation: undefined,
violations: false,
},
).violations;
return !typeElements
.map((member) => {
return (
isTSMethodSignature(member) ||
isTSCallSignatureDeclaration(member) ||
isTSConstructSignatureDeclaration(member) ||
((isTSPropertySignature(member) || isTSIndexSignature(member)) &&
member.typeAnnotation !== undefined &&
(isTSFunctionType(member.typeAnnotation.typeAnnotation) ||
isFunctionLikeType(getTypeOfNode(member, context))))
);
})
.every((isFunction, _, array) => array[0] === isFunction);
}

/**
Expand All @@ -140,7 +126,7 @@ function checkTSInterfaceDeclaration(
): RuleResult<keyof typeof errorMessages, Options> {
return {
context,
descriptors: hasTypeElementViolations(node.body.body)
descriptors: hasTypeElementViolations(node.body.body, context)
? [{ node, messageId: "generic" }]
: [],
};
Expand All @@ -159,15 +145,16 @@ function checkTSTypeAliasDeclaration(
descriptors:
// TypeLiteral.
(isTSTypeLiteral(node.typeAnnotation) &&
hasTypeElementViolations(node.typeAnnotation.members)) ||
hasTypeElementViolations(node.typeAnnotation.members, context)) ||
// TypeLiteral inside `Readonly<>`.
(isTSTypeReference(node.typeAnnotation) &&
isIdentifier(node.typeAnnotation.typeName) &&
node.typeAnnotation.typeParameters !== undefined &&
node.typeAnnotation.typeParameters.params.length === 1 &&
isTSTypeLiteral(node.typeAnnotation.typeParameters.params[0]!) &&
node.typeAnnotation.typeArguments !== undefined &&
node.typeAnnotation.typeArguments.params.length === 1 &&
isTSTypeLiteral(node.typeAnnotation.typeArguments.params[0]!) &&
hasTypeElementViolations(
node.typeAnnotation.typeParameters.params[0].members,
node.typeAnnotation.typeArguments.params[0].members,
context,
))
? [{ node, messageId: "generic" }]
: [],
Expand Down
22 changes: 22 additions & 0 deletions src/utils/type-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,24 @@ export function isTSIndexSignature(
return node.type === AST_NODE_TYPES.TSIndexSignature;
}

export function isTSMethodSignature(
node: TSESTree.Node,
): node is TSESTree.TSMethodSignature {
return node.type === AST_NODE_TYPES.TSMethodSignature;
}

export function isTSCallSignatureDeclaration(
node: TSESTree.Node,
): node is TSESTree.TSCallSignatureDeclaration {
return node.type === AST_NODE_TYPES.TSCallSignatureDeclaration;
}

export function isTSConstructSignatureDeclaration(
node: TSESTree.Node,
): node is TSESTree.TSConstructSignatureDeclaration {
return node.type === AST_NODE_TYPES.TSConstructSignatureDeclaration;
}

export function isTSInterfaceBody(
node: TSESTree.Node,
): node is TSESTree.TSInterfaceBody {
Expand Down Expand Up @@ -412,3 +430,7 @@ export function isObjectConstructorType(type: Type | null): boolean {
(isUnionType(type) && type.types.some(isObjectConstructorType)))
);
}

export function isFunctionLikeType(type: Type | null): boolean {
return type !== null && type.getCallSignatures().length > 0;
}
211 changes: 104 additions & 107 deletions tests/rules/no-mixed-type/ts/invalid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
import dedent from "dedent";

import { type rule } from "#eslint-plugin-functional/rules/no-mixed-types";
import {
type InvalidTestCaseSet,
Expand All @@ -11,110 +8,110 @@ import {
const tests: Array<
InvalidTestCaseSet<MessagesOf<typeof rule>, OptionsOf<typeof rule>>
> = [
// Mixing properties and methods (MethodSignature) should produce failures.
{
code: dedent`
type Foo = {
bar: string;
zoo(): number;
};
`,
optionsSet: [[], [{ checkInterfaces: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
line: 1,
column: 1,
},
],
},
{
code: dedent`
type Foo = Readonly<{
bar: string;
zoo(): number;
}>;
`,
optionsSet: [[], [{ checkInterfaces: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
line: 1,
column: 1,
},
],
},
{
code: dedent`
interface Foo {
bar: string;
zoo(): number;
}
`,
optionsSet: [[], [{ checkTypeLiterals: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSInterfaceDeclaration,
line: 1,
column: 1,
},
],
},
// Mixing properties and functions (PropertySignature) should produce failures.
{
code: dedent`
type Foo = {
bar: string;
zoo: () => number;
};
`,
optionsSet: [[], [{ checkInterfaces: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
line: 1,
column: 1,
},
],
},
{
code: dedent`
type Foo = Readonly<{
bar: string;
zoo: () => number;
}>;
`,
optionsSet: [[], [{ checkInterfaces: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSTypeAliasDeclaration,
line: 1,
column: 1,
},
],
},
{
code: dedent`
interface Foo {
bar: string;
zoo: () => number;
}
`,
optionsSet: [[], [{ checkTypeLiterals: false }]],
errors: [
{
messageId: "generic",
type: AST_NODE_TYPES.TSInterfaceDeclaration,
line: 1,
column: 1,
},
],
},
// // Mixing properties and methods (MethodSignature) should produce failures.
// {
// code: dedent`
// type Foo = {
// bar: string;
// zoo(): number;
// };
// `,
// optionsSet: [[], [{ checkInterfaces: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
// {
// code: dedent`
// type Foo = Readonly<{
// bar: string;
// zoo(): number;
// }>;
// `,
// optionsSet: [[], [{ checkInterfaces: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
// {
// code: dedent`
// interface Foo {
// bar: string;
// zoo(): number;
// }
// `,
// optionsSet: [[], [{ checkTypeLiterals: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSInterfaceDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
// // Mixing properties and functions (PropertySignature) should produce failures.
// {
// code: dedent`
// type Foo = {
// bar: string;
// zoo: () => number;
// };
// `,
// optionsSet: [[], [{ checkInterfaces: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
// {
// code: dedent`
// type Foo = Readonly<{
// bar: string;
// zoo: () => number;
// }>;
// `,
// optionsSet: [[], [{ checkInterfaces: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSTypeAliasDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
// {
// code: dedent`
// interface Foo {
// bar: string;
// zoo: () => number;
// }
// `,
// optionsSet: [[], [{ checkTypeLiterals: false }]],
// errors: [
// {
// messageId: "generic",
// type: AST_NODE_TYPES.TSInterfaceDeclaration,
// line: 1,
// column: 1,
// },
// ],
// },
];

export default tests;
Loading

0 comments on commit 55bd794

Please sign in to comment.