Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix(eslint-plugin): [no-unnecessary-type-arguments] fix comparison of…
… types (#4555)
  • Loading branch information
bradzacher committed Feb 15, 2022
1 parent 877cc48 commit fc3936e
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 7 deletions.
41 changes: 34 additions & 7 deletions packages/eslint-plugin/src/rules/no-unnecessary-type-arguments.ts
Expand Up @@ -38,6 +38,22 @@ export default util.createRule<[], MessageIds>({
const parserServices = util.getParserServices(context);
const checker = parserServices.program.getTypeChecker();

function getTypeForComparison(type: ts.Type): {
type: ts.Type;
typeArguments: readonly ts.Type[];
} {
if (util.isTypeReferenceType(type)) {
return {
type: type.target,
typeArguments: util.getTypeArguments(type, checker),
};
}
return {
type,
typeArguments: [],
};
}

function checkTSArgsAndParameters(
esParameters: TSESTree.TSTypeParameterInstantiation,
typeParameters: readonly ts.TypeParameterDeclaration[],
Expand All @@ -49,19 +65,30 @@ export default util.createRule<[], MessageIds>({
if (!param?.default) {
return;
}

// TODO: would like checker.areTypesEquivalent. https://github.com/Microsoft/TypeScript/issues/13502
const defaultType = checker.getTypeAtLocation(param.default);
const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(arg);
const argType = checker.getTypeAtLocation(argTsNode);
if (!argType.aliasSymbol && !defaultType.aliasSymbol) {
if (argType.flags !== defaultType.flags) {
// this check should handle some of the most simple cases of like strings, numbers, etc
if (defaultType !== argType) {
// For more complex types (like aliases to generic object types) - TS won't always create a
// global shared type object for the type - so we need to resort to manually comparing the
// reference type and the passed type arguments.
// Also - in case there are aliases - we need to resolve them before we do checks
const defaultTypeResolved = getTypeForComparison(defaultType);
const argTypeResolved = getTypeForComparison(argType);
if (
// ensure the resolved type AND all the parameters are the same
defaultTypeResolved.type !== argTypeResolved.type ||
defaultTypeResolved.typeArguments.length !==
argTypeResolved.typeArguments.length ||
defaultTypeResolved.typeArguments.some(
(t, i) => t !== argTypeResolved.typeArguments[i],
)
) {
return;
}
} else if (
argType.aliasSymbol !== defaultType.aliasSymbol ||
argType.aliasTypeArguments !== defaultType.aliasTypeArguments
) {
return;
}

context.report({
Expand Down
Expand Up @@ -132,6 +132,19 @@ import { F } from './missing';
function bar<T = F>() {}
bar<F<number>>();
`,
`
type A<T = Element> = T;
type B = A<HTMLInputElement>;
`,
`
type A<T = Map<string, string>> = T;
type B = A<Map<string, number>>;
`,
`
type A = Map<string, string>;
type B<T = A> = T;
type C2 = B<Map<string, number>>;
`,
],
invalid: [
{
Expand Down Expand Up @@ -317,5 +330,101 @@ declare module 'bar' {
}
`,
},
{
code: `
type A<T = Map<string, string>> = T;
type B = A<Map<string, string>>;
`,
errors: [
{
line: 3,
messageId: 'unnecessaryTypeParameter',
},
],
output: `
type A<T = Map<string, string>> = T;
type B = A;
`,
},
{
code: `
type A = Map<string, string>;
type B<T = A> = T;
type C = B<A>;
`,
errors: [
{
line: 4,
messageId: 'unnecessaryTypeParameter',
},
],
output: `
type A = Map<string, string>;
type B<T = A> = T;
type C = B;
`,
},
{
code: `
type A = Map<string, string>;
type B<T = A> = T;
type C = B<Map<string, string>>;
`,
errors: [
{
line: 4,
messageId: 'unnecessaryTypeParameter',
},
],
output: `
type A = Map<string, string>;
type B<T = A> = T;
type C = B;
`,
},
{
code: `
type A = Map<string, string>;
type B = Map<string, string>;
type C<T = A> = T;
type D = C<B>;
`,
errors: [
{
line: 5,
messageId: 'unnecessaryTypeParameter',
},
],
output: `
type A = Map<string, string>;
type B = Map<string, string>;
type C<T = A> = T;
type D = C;
`,
},
{
code: `
type A = Map<string, string>;
type B = A;
type C = Map<string, string>;
type D = C;
type E<T = B> = T;
type F = E<D>;
`,
errors: [
{
line: 7,
messageId: 'unnecessaryTypeParameter',
},
],
output: `
type A = Map<string, string>;
type B = A;
type C = Map<string, string>;
type D = C;
type E<T = B> = T;
type F = E;
`,
},
],
});
18 changes: 18 additions & 0 deletions packages/type-utils/src/predicates.ts
Expand Up @@ -54,6 +54,24 @@ export function isTypeUnknownType(type: ts.Type): boolean {
return isTypeFlagSet(type, ts.TypeFlags.Unknown);
}

// https://github.com/microsoft/TypeScript/blob/42aa18bf442c4df147e30deaf27261a41cbdc617/src/compiler/types.ts#L5157
const Nullable = ts.TypeFlags.Undefined | ts.TypeFlags.Null;
// https://github.com/microsoft/TypeScript/blob/42aa18bf442c4df147e30deaf27261a41cbdc617/src/compiler/types.ts#L5187
const ObjectFlagsType =
ts.TypeFlags.Any |
Nullable |
ts.TypeFlags.Never |
ts.TypeFlags.Object |
ts.TypeFlags.Union |
ts.TypeFlags.Intersection;
export function isTypeReferenceType(type: ts.Type): type is ts.TypeReference {
if ((type.flags & ObjectFlagsType) === 0) {
return false;
}
const objectTypeFlags = (type as ts.ObjectType).objectFlags;
return (objectTypeFlags & ts.ObjectFlags.Reference) !== 0;
}

/**
* @returns true if the type is `any`
*/
Expand Down

0 comments on commit fc3936e

Please sign in to comment.