From 6ee8df726ca268991bc8825e2699ea396c0d1465 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 1 Oct 2022 19:23:22 +0900 Subject: [PATCH 01/50] feat: add rule code --- packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 3 + ...plicate-type-union-intersection-members.ts | 122 ++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 8742d36b2089..9799e08cc97b 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -54,6 +54,7 @@ export = { '@typescript-eslint/no-dupe-class-members': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', 'no-duplicate-imports': 'off', + '@typescript-eslint/no-duplicate-type-union-intersection-members': 'error', '@typescript-eslint/no-dynamic-delete': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 29a47ec2384a..4390ca2d61d1 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -35,6 +35,7 @@ import noConfusingVoidExpression from './no-confusing-void-expression'; import noDupeClassMembers from './no-dupe-class-members'; import noDuplicateEnumValues from './no-duplicate-enum-values'; import noDuplicateImports from './no-duplicate-imports'; +import noDuplicateTypeUnionIntersectionMembers from './no-duplicate-type-union-intersection-members'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; @@ -164,6 +165,8 @@ export default { 'no-dupe-class-members': noDupeClassMembers, 'no-duplicate-enum-values': noDuplicateEnumValues, 'no-duplicate-imports': noDuplicateImports, + 'no-duplicate-type-union-intersection-members': + noDuplicateTypeUnionIntersectionMembers, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts new file mode 100644 index 000000000000..10463c7a5bb0 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts @@ -0,0 +1,122 @@ +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import * as util from '../util'; + +export type Options = [ + { + checkIntersections?: boolean; + checkUnions?: boolean; + }, +]; +export type MessageIds = 'duplicate' | 'suggestFix'; + +export default util.createRule({ + name: 'no-duplicate-type-union-intersection-members', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow duplicate union/intersection type members', + recommended: false, + }, + fixable: 'code', + hasSuggestions: true, + messages: { + duplicate: '{{type}} type member {{name}} is duplicated.', + suggestFix: 'Delete duplicated members of type (removes all comments).', + }, + schema: [ + { + type: 'object', + properties: { + checkIntersections: { + type: 'boolean', + }, + checkUnions: { + type: 'boolean', + }, + }, + }, + ], + }, + defaultOptions: [ + { + checkIntersections: true, + checkUnions: true, + }, + ], + create(context, [{ checkIntersections, checkUnions }]) { + const sourceCode = context.getSourceCode(); + function checkDuplicate( + node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, + ): void { + const duplicateMemberTexts: string[] = []; + const uniqueMemberTexts = new Set(); + + const source = node.types.map(type => { + return { + node: type, + text: sourceCode.getText(type), + }; + }); + + const hasComments = node.types.some(type => { + const count = + sourceCode.getCommentsBefore(type).length + + sourceCode.getCommentsAfter(type).length; + return count > 0; + }); + + const fix: TSESLint.ReportFixFunction = fixer => { + const result = [...uniqueMemberTexts].join( + node.type === AST_NODE_TYPES.TSIntersectionType ? ' & ' : ' | ', + ); + return fixer.replaceText(node, result); + }; + + source.forEach(({ text }) => { + if (uniqueMemberTexts.has(text)) { + duplicateMemberTexts.push(text); + } + uniqueMemberTexts.add(text); + }); + + duplicateMemberTexts.forEach(duplicateMemberName => { + context.report({ + data: { + name: duplicateMemberName, + type: + node.type === AST_NODE_TYPES.TSIntersectionType + ? 'Intersection' + : 'Union', + }, + messageId: 'duplicate', + node, + // don't autofix if any of the types have leading/trailing comments + ...(hasComments + ? { + suggest: [ + { + fix, + messageId: 'suggestFix', + }, + ], + } + : { fix }), + }); + }); + } + return { + ...(checkIntersections && { + TSIntersectionType(node): void { + checkDuplicate(node); + }, + }), + ...(checkUnions && { + TSUnionType(node): void { + checkDuplicate(node); + }, + }), + }; + }, +}); From 90b5e9694bf6462bf34af363187d019cdc4e72ae Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 1 Oct 2022 19:26:29 +0900 Subject: [PATCH 02/50] test: add test for rule --- ...te-type-union-intersection-members.test.ts | 422 ++++++++++++++++++ 1 file changed, 422 insertions(+) create mode 100644 packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts new file mode 100644 index 000000000000..aa5041ca13b1 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts @@ -0,0 +1,422 @@ +import type { TSESLint } from '@typescript-eslint/utils'; + +import type { + MessageIds, + Options, +} from '../../src/rules/no-duplicate-type-union-intersection-members'; +import rule from '../../src/rules/no-duplicate-type-union-intersection-members'; +import { noFormat, RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ + { + code: `type T = A ${operator} B;`, + }, + { + code: `const a : A ${operator} B = "A";`, + }, + { + code: `type T = A ${operator} /* comment */ B;`, + }, + { + code: `type T = 'A' ${operator} 'B';`, + }, + { + code: `type T = 1 ${operator} 2;`, + }, + { + code: `type T = 1 ${operator} '1';`, + }, + { + code: `type T = "1" ${operator} '1';`, + }, + { + code: `type T = true ${operator} boolean;`, + }, + { + code: `type T = null ${operator} undefined;`, + }, + { + code: `type T = any ${operator} unknown;`, + }, + { + code: noFormat`type T = (A) ${operator} (B);`, + }, + { + code: `type T = { a: string } ${operator} { b: string };`, + }, + { + code: `type T = { a: string ${operator} number };`, + }, + { + code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, + }, + { + code: `type T = (() => string) ${operator} (() => void);`, + }, + { + code: `type T = () => string ${operator} void;`, + }, + { + code: `type T = () => null ${operator} undefined;`, + }, + { + code: `type T = (arg : string ${operator} number) => void;`, + }, + { + code: `type T = A ${operator} B ${operator} C;`, + }, + { + code: `type T = readonly string[] ${operator} string[];`, + }, + { + code: `type T = (A | B) ${operator} (A | C);`, + }, + { + code: `type T = (A | B) ${operator} (A & B);`, + }, + { + code: `type T = Record;`, + }, +]; +const invalid = ( + operator: '|' | '&', +): TSESLint.InvalidTestCase[] => { + const type = operator === '|' ? 'Union' : 'Intersection'; + return [ + { + code: `type T = A ${operator} A;`, + output: `type T = A;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `const a : A ${operator} A = 'A';`, + output: `const a : A = 'A';`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type T = 1 ${operator} 1;`, + output: `type T = 1;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '1', + }, + }, + ], + }, + { + code: `type T = true ${operator} true;`, + output: `type T = true;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'true', + }, + }, + ], + }, + { + code: `type T = null ${operator} null;`, + output: `type T = null;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'null', + }, + }, + ], + }, + { + code: `type T = any ${operator} any;`, + output: `type T = any;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'any', + }, + }, + ], + }, + { + code: `type T = 'A' ${operator} 'A';`, + output: `type T = 'A';`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: "'A'", + }, + }, + ], + }, + { + code: noFormat`type T = (A) ${operator} (A);`, + output: noFormat`type T = A;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type T = { a: string } ${operator} { a: string };`, + output: `type T = { a: string };`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '{ a: string }', + }, + }, + ], + }, + { + code: `type T = { a: string ${operator} string };`, + output: `type T = { a: string };`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'string', + }, + }, + ], + }, + { + code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, + output: `type T = [1, 2, 3];`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '[1, 2, 3]', + }, + }, + ], + }, + { + code: `type T = (() => string) ${operator} (() => string);`, + output: `type T = () => string;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '() => string', + }, + }, + ], + }, + { + code: `type T = () => string ${operator} string;`, + output: `type T = () => string;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'string', + }, + }, + ], + }, + { + code: `type T = () => null ${operator} null;`, + output: `type T = () => null;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'null', + }, + }, + ], + }, + { + code: `type T = (arg : string ${operator} string) => void;`, + output: `type T = (arg : string) => void;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'string', + }, + }, + ], + }, + { + code: `type T = A ${operator} /* comment */ A;`, + output: null, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + suggestions: [ + { + messageId: 'suggestFix', + output: `type T = A;`, + }, + ], + }, + ], + }, + { + code: `type T = A ${operator} B ${operator} A;`, + output: `type T = A ${operator} B;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type T = A ${operator} B ${operator} A ${operator} B;`, + output: `type T = A ${operator} B;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + { + messageId: 'duplicate', + data: { + type, + name: 'B', + }, + }, + ], + }, + { + code: `type T = A ${operator} B ${operator} A ${operator} A;`, + output: `type T = A ${operator} B;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type T = A ${operator} B ${operator} A ${operator} C;`, + output: `type T = A ${operator} B ${operator} C;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type T = (A | B) ${operator} (A | B);`, + output: `type T = A | B;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A | B', + }, + }, + ], + }, + { + code: `type T = Record;`, + output: `type T = Record;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + ]; +}; + +ruleTester.run('disallow-duplicate-union-intersection', rule, { + valid: [ + ...valid('|'), + { + code: 'type T = A | A;', + options: [ + { + checkUnions: false, + }, + ], + }, + + ...valid('&'), + { + code: 'type T = A & A;', + options: [ + { + checkIntersections: false, + }, + ], + }, + ], + invalid: [...invalid('|'), ...invalid('&')], +}); From 1a3a620088aff16e14ad545080413e1535759d7a Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 1 Oct 2022 19:27:32 +0900 Subject: [PATCH 03/50] docs: add docs of new rule --- ...plicate-type-union-intersection-members.md | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md new file mode 100644 index 000000000000..afc0f811a25a --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md @@ -0,0 +1,49 @@ +--- +description: 'Disallow duplicate union/intersection type members.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-duplicate-type-union-intersection-members** for documentation. + +Although TypeScript supports duplicate union and intersection member values, people usually expect members to have unique values within the same intersection and union. Duplicate values make the code redundant and generally reduce readability. + +## Rule Details + +This rule disallows duplicate union or intersection type members. It only checks duplication on the notation. Members with the same value but different names are not marked duplicates. + + + +### ❌ Incorrect + +```ts +type T1 = A | A | B; + +type T2 = { a: string } & { a: string }; + +type T3 = [1, 2, 3] & [1, 2, 3]; + +type T4 = () => string | string; +``` + +### ✅ Correct + +```ts +type T1 = A | B | C; + +type T2 = { a: string } & { b: string }; + +type T3 = [1, 2, 3] & [1, 2, 3, 4]; + +type T4 = () => string | number; +``` + +## Options + +### `checkIntersections` + +When set to false, duplicate checks on intersection type members are ignored. + +### `checkUnions` + +When set to false, duplicate checks on union type members are ignored. From 29874a8019c2f36de192a08cd6ac25515dff30f4 Mon Sep 17 00:00:00 2001 From: sajikix Date: Wed, 5 Oct 2022 23:38:45 +0900 Subject: [PATCH 04/50] refactor: make method definitions more concise --- .../rules/no-duplicate-type-union-intersection-members.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts index 10463c7a5bb0..ec579f18ec1d 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts @@ -108,14 +108,10 @@ export default util.createRule({ } return { ...(checkIntersections && { - TSIntersectionType(node): void { - checkDuplicate(node); - }, + TSIntersectionType: checkDuplicate, }), ...(checkUnions && { - TSUnionType(node): void { - checkDuplicate(node); - }, + TSUnionType: checkDuplicate, }), }; }, From e03a7e848e4f4b100c72e9cda1a75edc7cfcbe7c Mon Sep 17 00:00:00 2001 From: sajikix Date: Thu, 6 Oct 2022 00:11:07 +0900 Subject: [PATCH 05/50] fix: change check option to ignore option --- ...uplicate-type-union-intersection-members.md | 8 ++++---- ...uplicate-type-union-intersection-members.ts | 18 +++++++++--------- ...ate-type-union-intersection-members.test.ts | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md index afc0f811a25a..b89b7523876a 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md @@ -40,10 +40,10 @@ type T4 = () => string | number; ## Options -### `checkIntersections` +### `ignoreIntersections` -When set to false, duplicate checks on intersection type members are ignored. +When set to true, duplicate checks on intersection type members are ignored. -### `checkUnions` +### `ignoreUnions` -When set to false, duplicate checks on union type members are ignored. +When set to true, duplicate checks on union type members are ignored. diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts index ec579f18ec1d..f0ac15363d85 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts @@ -5,8 +5,8 @@ import * as util from '../util'; export type Options = [ { - checkIntersections?: boolean; - checkUnions?: boolean; + ignoreIntersections?: boolean; + ignoreUnions?: boolean; }, ]; export type MessageIds = 'duplicate' | 'suggestFix'; @@ -29,10 +29,10 @@ export default util.createRule({ { type: 'object', properties: { - checkIntersections: { + ignoreIntersections: { type: 'boolean', }, - checkUnions: { + ignoreUnions: { type: 'boolean', }, }, @@ -41,11 +41,11 @@ export default util.createRule({ }, defaultOptions: [ { - checkIntersections: true, - checkUnions: true, + ignoreIntersections: false, + ignoreUnions: false, }, ], - create(context, [{ checkIntersections, checkUnions }]) { + create(context, [{ ignoreIntersections, ignoreUnions }]) { const sourceCode = context.getSourceCode(); function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, @@ -107,10 +107,10 @@ export default util.createRule({ }); } return { - ...(checkIntersections && { + ...(!ignoreIntersections && { TSIntersectionType: checkDuplicate, }), - ...(checkUnions && { + ...(!ignoreUnions && { TSUnionType: checkDuplicate, }), }; diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts index aa5041ca13b1..c0bd90bbb363 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts @@ -403,7 +403,7 @@ ruleTester.run('disallow-duplicate-union-intersection', rule, { code: 'type T = A | A;', options: [ { - checkUnions: false, + ignoreUnions: true, }, ], }, @@ -413,7 +413,7 @@ ruleTester.run('disallow-duplicate-union-intersection', rule, { code: 'type T = A & A;', options: [ { - checkIntersections: false, + ignoreIntersections: true, }, ], }, From 95afec81ebfc663de230fdf716120dcae01cb2f0 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 8 Oct 2022 13:36:13 +0900 Subject: [PATCH 06/50] refactor: rename to type-constituents --- .../rules/no-duplicate-type-union-intersection-members.md | 2 +- packages/eslint-plugin/src/configs/all.ts | 2 +- packages/eslint-plugin/src/rules/index.ts | 5 ++--- ...rsection-members.ts => no-duplicate-type-constituents.ts} | 2 +- ...embers.test.ts => no-duplicate-type-constituents.test.ts} | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) rename packages/eslint-plugin/src/rules/{no-duplicate-type-union-intersection-members.ts => no-duplicate-type-constituents.ts} (98%) rename packages/eslint-plugin/tests/rules/{no-duplicate-type-union-intersection-members.test.ts => no-duplicate-type-constituents.test.ts} (98%) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md index b89b7523876a..ee47cea8b9aa 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md @@ -4,7 +4,7 @@ description: 'Disallow duplicate union/intersection type members.' > 🛑 This file is source code, not the primary documentation location! 🛑 > -> See **https://typescript-eslint.io/rules/no-duplicate-type-union-intersection-members** for documentation. +> See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation. Although TypeScript supports duplicate union and intersection member values, people usually expect members to have unique values within the same intersection and union. Duplicate values make the code redundant and generally reduce readability. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 9799e08cc97b..4c6f3ee906e7 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -54,7 +54,7 @@ export = { '@typescript-eslint/no-dupe-class-members': 'error', '@typescript-eslint/no-duplicate-enum-values': 'error', 'no-duplicate-imports': 'off', - '@typescript-eslint/no-duplicate-type-union-intersection-members': 'error', + '@typescript-eslint/no-duplicate-type-constituents': 'error', '@typescript-eslint/no-dynamic-delete': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 4390ca2d61d1..145cc863936c 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -35,7 +35,7 @@ import noConfusingVoidExpression from './no-confusing-void-expression'; import noDupeClassMembers from './no-dupe-class-members'; import noDuplicateEnumValues from './no-duplicate-enum-values'; import noDuplicateImports from './no-duplicate-imports'; -import noDuplicateTypeUnionIntersectionMembers from './no-duplicate-type-union-intersection-members'; +import noDuplicateTypeConstituents from './no-duplicate-type-constituents'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; @@ -165,8 +165,7 @@ export default { 'no-dupe-class-members': noDupeClassMembers, 'no-duplicate-enum-values': noDuplicateEnumValues, 'no-duplicate-imports': noDuplicateImports, - 'no-duplicate-type-union-intersection-members': - noDuplicateTypeUnionIntersectionMembers, + 'no-duplicate-type-constituents': noDuplicateTypeConstituents, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts similarity index 98% rename from packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts rename to packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index f0ac15363d85..2fbdb78d44d0 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-union-intersection-members.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -12,7 +12,7 @@ export type Options = [ export type MessageIds = 'duplicate' | 'suggestFix'; export default util.createRule({ - name: 'no-duplicate-type-union-intersection-members', + name: 'no-duplicate-type-constituents', meta: { type: 'suggestion', docs: { diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts similarity index 98% rename from packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts rename to packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index c0bd90bbb363..3cde275d0bbe 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-union-intersection-members.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -3,8 +3,8 @@ import type { TSESLint } from '@typescript-eslint/utils'; import type { MessageIds, Options, -} from '../../src/rules/no-duplicate-type-union-intersection-members'; -import rule from '../../src/rules/no-duplicate-type-union-intersection-members'; +} from '../../src/rules/no-duplicate-type-constituents'; +import rule from '../../src/rules/no-duplicate-type-constituents'; import { noFormat, RuleTester } from '../RuleTester'; const ruleTester = new RuleTester({ From 1e7cbb10dd73c8f4d770403a208e53d9cc270917 Mon Sep 17 00:00:00 2001 From: sajikix Date: Tue, 11 Oct 2022 01:31:14 +0900 Subject: [PATCH 07/50] refactor: use recursive type-node checker --- .../rules/no-duplicate-type-constituents.ts | 140 +++++++++++++++--- 1 file changed, 120 insertions(+), 20 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 2fbdb78d44d0..a9fd80949b26 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -3,6 +3,110 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import * as util from '../util'; +const nodeTypesAreEqual = ( + actualTypeNode: TSESTree.Node, + expectedTypeNode: T, +): actualTypeNode is T => { + return actualTypeNode.type === expectedTypeNode.type; +}; + +const constituentsAreEqual = ( + actualTypeNode: TSESTree.TypeNode, + expectedTypeNode: TSESTree.TypeNode, +): boolean => { + switch (expectedTypeNode.type) { + // These types should never occur as constituents of a union/intersection + case AST_NODE_TYPES.TSAbstractKeyword: + case AST_NODE_TYPES.TSAsyncKeyword: + case AST_NODE_TYPES.TSDeclareKeyword: + case AST_NODE_TYPES.TSExportKeyword: + case AST_NODE_TYPES.TSNamedTupleMember: + case AST_NODE_TYPES.TSOptionalType: + case AST_NODE_TYPES.TSPrivateKeyword: + case AST_NODE_TYPES.TSProtectedKeyword: + case AST_NODE_TYPES.TSPublicKeyword: + case AST_NODE_TYPES.TSReadonlyKeyword: + case AST_NODE_TYPES.TSRestType: + case AST_NODE_TYPES.TSStaticKeyword: + case AST_NODE_TYPES.TSTypePredicate: + throw new Error(`Unexpected Type ${expectedTypeNode.type}`); + + default: + if (nodeTypesAreEqual(actualTypeNode, expectedTypeNode)) { + return astNodesAreEquals(actualTypeNode, expectedTypeNode); + } + return false; + } +}; + +const astIgnoreKeys = ['range', 'loc', 'parent']; + +const astNodesAreEquals = ( + actualNode: unknown, + expectedNode: unknown, +): boolean => { + if (actualNode === expectedNode) { + return true; + } + if ( + actualNode && + expectedNode && + typeof actualNode == 'object' && + typeof expectedNode == 'object' + ) { + if (actualNode.constructor !== expectedNode.constructor) { + return false; + } + if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { + if (actualNode.length != expectedNode.length) { + return false; + } + return !actualNode.some( + (nodeEle, index) => !astNodesAreEquals(nodeEle, expectedNode[index]), + ); + } + if (!isRecordType(actualNode) || !isRecordType(expectedNode)) { + return false; + } + const actualNodeKeys = Object.keys(actualNode).filter( + key => !astIgnoreKeys.includes(key), + ); + const expectedNodeKeys = Object.keys(expectedNode).filter( + key => !astIgnoreKeys.includes(key), + ); + if (actualNodeKeys.length !== expectedNodeKeys.length) { + return false; + } + if ( + actualNodeKeys.some( + actualNodeKey => + !Object.prototype.hasOwnProperty.call(expectedNode, actualNodeKey), + ) + ) { + return false; + } + if ( + actualNodeKeys.some( + actualNodeKey => + !astNodesAreEquals( + actualNode[actualNodeKey], + expectedNode[actualNodeKey], + ), + ) + ) { + return false; + } + return true; + } + return false; +}; + +const isRecordType = ( + object: object, +): object is Record => { + return object.constructor === Object; +}; + export type Options = [ { ignoreIntersections?: boolean; @@ -50,15 +154,18 @@ export default util.createRule({ function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const duplicateMemberTexts: string[] = []; - const uniqueMemberTexts = new Set(); + const duplicateMembers: TSESTree.TypeNode[] = []; - const source = node.types.map(type => { - return { - node: type, - text: sourceCode.getText(type), - }; - }); + const uniqueMembers = node.types.reduce( + (acc, cur) => { + if (acc.some(ele => constituentsAreEqual(ele, cur))) { + duplicateMembers.push(cur); + return acc; + } + return [...acc, cur]; + }, + [], + ); const hasComments = node.types.some(type => { const count = @@ -68,23 +175,16 @@ export default util.createRule({ }); const fix: TSESLint.ReportFixFunction = fixer => { - const result = [...uniqueMemberTexts].join( - node.type === AST_NODE_TYPES.TSIntersectionType ? ' & ' : ' | ', - ); + const result = [ + ...uniqueMembers.map(member => sourceCode.getText(member)), + ].join(node.type === AST_NODE_TYPES.TSIntersectionType ? ' & ' : ' | '); return fixer.replaceText(node, result); }; - source.forEach(({ text }) => { - if (uniqueMemberTexts.has(text)) { - duplicateMemberTexts.push(text); - } - uniqueMemberTexts.add(text); - }); - - duplicateMemberTexts.forEach(duplicateMemberName => { + duplicateMembers.forEach(duplicateMember => { context.report({ data: { - name: duplicateMemberName, + name: sourceCode.getText(duplicateMember), type: node.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' From 39f18bc11902204d3b16fa4111ae60a18e6197ac Mon Sep 17 00:00:00 2001 From: sajikix Date: Tue, 11 Oct 2022 16:40:19 +0900 Subject: [PATCH 08/50] fix: rename doc filename and test title --- ...ntersection-members.md => no-duplicate-type-constituents.md} | 0 .../tests/rules/no-duplicate-type-constituents.test.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/eslint-plugin/docs/rules/{no-duplicate-type-union-intersection-members.md => no-duplicate-type-constituents.md} (100%) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md similarity index 100% rename from packages/eslint-plugin/docs/rules/no-duplicate-type-union-intersection-members.md rename to packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 3cde275d0bbe..915d2a0b09d3 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -396,7 +396,7 @@ const invalid = ( ]; }; -ruleTester.run('disallow-duplicate-union-intersection', rule, { +ruleTester.run('no-duplicate-type-constituents', rule, { valid: [ ...valid('|'), { From f430871aff8c549edda33aaaaf3fc0e938c76d94 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 15 Oct 2022 19:10:39 +0900 Subject: [PATCH 09/50] refactor: use removeRage instead of replaceText --- .../rules/no-duplicate-type-constituents.ts | 102 +++++++++--------- .../no-duplicate-type-constituents.test.ts | 25 ++++- 2 files changed, 70 insertions(+), 57 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index a9fd80949b26..a67cfe0a3239 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -29,6 +29,7 @@ const constituentsAreEqual = ( case AST_NODE_TYPES.TSRestType: case AST_NODE_TYPES.TSStaticKeyword: case AST_NODE_TYPES.TSTypePredicate: + /* istanbul ignore next */ throw new Error(`Unexpected Type ${expectedTypeNode.type}`); default: @@ -54,9 +55,6 @@ const astNodesAreEquals = ( typeof actualNode == 'object' && typeof expectedNode == 'object' ) { - if (actualNode.constructor !== expectedNode.constructor) { - return false; - } if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { if (actualNode.length != expectedNode.length) { return false; @@ -65,38 +63,23 @@ const astNodesAreEquals = ( (nodeEle, index) => !astNodesAreEquals(nodeEle, expectedNode[index]), ); } - if (!isRecordType(actualNode) || !isRecordType(expectedNode)) { - return false; - } - const actualNodeKeys = Object.keys(actualNode).filter( - key => !astIgnoreKeys.includes(key), - ); - const expectedNodeKeys = Object.keys(expectedNode).filter( - key => !astIgnoreKeys.includes(key), - ); - if (actualNodeKeys.length !== expectedNodeKeys.length) { - return false; - } - if ( - actualNodeKeys.some( - actualNodeKey => - !Object.prototype.hasOwnProperty.call(expectedNode, actualNodeKey), - ) - ) { - return false; - } - if ( - actualNodeKeys.some( - actualNodeKey => - !astNodesAreEquals( - actualNode[actualNodeKey], - expectedNode[actualNodeKey], - ), - ) - ) { - return false; + if (isRecordType(actualNode) && isRecordType(expectedNode)) { + const actualNodeKeys = Object.keys(actualNode).filter( + key => !astIgnoreKeys.includes(key), + ); + const expectedNodeKeys = Object.keys(expectedNode).filter( + key => !astIgnoreKeys.includes(key), + ); + if (actualNodeKeys.length === expectedNodeKeys.length) { + return !actualNodeKeys.some( + actualNodeKey => + !astNodesAreEquals( + actualNode[actualNodeKey], + expectedNode[actualNodeKey], + ), + ); + } } - return true; } return false; }; @@ -113,6 +96,7 @@ export type Options = [ ignoreUnions?: boolean; }, ]; + export type MessageIds = 'duplicate' | 'suggestFix'; export default util.createRule({ @@ -120,14 +104,15 @@ export default util.createRule({ meta: { type: 'suggestion', docs: { - description: 'Disallow duplicate union/intersection type members', + description: 'Disallow duplicate union/intersection type constituents', recommended: false, }, fixable: 'code', hasSuggestions: true, messages: { duplicate: '{{type}} type member {{name}} is duplicated.', - suggestFix: 'Delete duplicated members of type (removes all comments).', + suggestFix: + 'Delete duplicated constituents of type (removes all comments).', }, schema: [ { @@ -154,18 +139,18 @@ export default util.createRule({ function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const duplicateMembers: TSESTree.TypeNode[] = []; + const duplicateConstituents: { + node: TSESTree.TypeNode; + index: number; + }[] = []; - const uniqueMembers = node.types.reduce( - (acc, cur) => { - if (acc.some(ele => constituentsAreEqual(ele, cur))) { - duplicateMembers.push(cur); - return acc; - } - return [...acc, cur]; - }, - [], - ); + node.types.reduce((acc, cur, index) => { + if (acc.some(ele => constituentsAreEqual(ele, cur))) { + duplicateConstituents.push({ node: cur, index }); + return acc; + } + return [...acc, cur]; + }, []); const hasComments = node.types.some(type => { const count = @@ -175,16 +160,23 @@ export default util.createRule({ }); const fix: TSESLint.ReportFixFunction = fixer => { - const result = [ - ...uniqueMembers.map(member => sourceCode.getText(member)), - ].join(node.type === AST_NODE_TYPES.TSIntersectionType ? ' & ' : ' | '); - return fixer.replaceText(node, result); + return duplicateConstituents.map(duplicateConstituent => { + const parent = duplicateConstituent.node.parent as + | TSESTree.TSUnionType + | TSESTree.TSIntersectionType; + const delteRangeStart = + parent.types[duplicateConstituent.index - 1].range[1]; + return fixer.removeRange([ + delteRangeStart, + duplicateConstituent.node.range[1], + ]); + }); }; - duplicateMembers.forEach(duplicateMember => { + duplicateConstituents.forEach(duplicateConstituent => { context.report({ data: { - name: sourceCode.getText(duplicateMember), + name: sourceCode.getText(duplicateConstituent.node), type: node.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' @@ -202,7 +194,9 @@ export default util.createRule({ }, ], } - : { fix }), + : { + fix, + }), }); }); } diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 915d2a0b09d3..4e97dbc72af7 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -48,12 +48,18 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ { code: `type T = { a: string } ${operator} { b: string };`, }, + { + code: `type T = { a: string, b: number } ${operator} { b: number, a: string };`, + }, { code: `type T = { a: string ${operator} number };`, }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, }, + { + code: `type T = [1, 2, 3] ${operator} [1, 2, 3, 4];`, + }, { code: `type T = (() => string) ${operator} (() => void);`, }, @@ -180,7 +186,7 @@ const invalid = ( }, { code: noFormat`type T = (A) ${operator} (A);`, - output: noFormat`type T = A;`, + output: noFormat`type T = (A);`, errors: [ { messageId: 'duplicate', @@ -204,6 +210,19 @@ const invalid = ( }, ], }, + { + code: `type T = { a: string, b: number } ${operator} { a: string, b: number };`, + output: `type T = { a: string, b: number };`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '{ a: string, b: number }', + }, + }, + ], + }, { code: `type T = { a: string ${operator} string };`, output: `type T = { a: string };`, @@ -232,7 +251,7 @@ const invalid = ( }, { code: `type T = (() => string) ${operator} (() => string);`, - output: `type T = () => string;`, + output: `type T = (() => string);`, errors: [ { messageId: 'duplicate', @@ -369,7 +388,7 @@ const invalid = ( }, { code: `type T = (A | B) ${operator} (A | B);`, - output: `type T = A | B;`, + output: `type T = (A | B);`, errors: [ { messageId: 'duplicate', From 49e27bea831c0fe49d4c29cc82f4c46e98cad105 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 15 Oct 2022 23:35:13 +0900 Subject: [PATCH 10/50] refactor: narrows node comparison function argument type --- .../rules/no-duplicate-type-constituents.ts | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index a67cfe0a3239..f7ceee0716b5 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -42,9 +42,21 @@ const constituentsAreEqual = ( const astIgnoreKeys = ['range', 'loc', 'parent']; -const astNodesAreEquals = ( - actualNode: unknown, - expectedNode: unknown, +type NodeElementsType = + | number + | string + | bigint + | boolean + | null + | undefined + | RegExp + | TSESTree.BaseNode + | TSESTree.BaseNode[] + | [number, number]; + +const astNodesAreEquals = ( + actualNode: T, + expectedNode: T, ): boolean => { if (actualNode === expectedNode) { return true; @@ -63,7 +75,7 @@ const astNodesAreEquals = ( (nodeEle, index) => !astNodesAreEquals(nodeEle, expectedNode[index]), ); } - if (isRecordType(actualNode) && isRecordType(expectedNode)) { + if (isAstNodeType(actualNode) && isAstNodeType(expectedNode)) { const actualNodeKeys = Object.keys(actualNode).filter( key => !astIgnoreKeys.includes(key), ); @@ -84,10 +96,8 @@ const astNodesAreEquals = ( return false; }; -const isRecordType = ( - object: object, -): object is Record => { - return object.constructor === Object; +const isAstNodeType = (node: object): node is Record => { + return node.constructor === Object; }; export type Options = [ From b368abb523fb367c313248d9de41c1d827cfbc6d Mon Sep 17 00:00:00 2001 From: sajikix Date: Sun, 16 Oct 2022 12:03:21 +0900 Subject: [PATCH 11/50] fix: doc description --- .../src/rules/no-duplicate-type-constituents.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index f7ceee0716b5..fd8472fb7a0d 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -114,15 +114,14 @@ export default util.createRule({ meta: { type: 'suggestion', docs: { - description: 'Disallow duplicate union/intersection type constituents', + description: 'Disallow duplicate union/intersection type members', recommended: false, }, fixable: 'code', hasSuggestions: true, messages: { duplicate: '{{type}} type member {{name}} is duplicated.', - suggestFix: - 'Delete duplicated constituents of type (removes all comments).', + suggestFix: 'Delete duplicated members of type (removes all comments).', }, schema: [ { From f1f507d874b3d62050f394113e30dee2bb5779d8 Mon Sep 17 00:00:00 2001 From: sajikix Date: Thu, 3 Nov 2022 17:31:19 +0900 Subject: [PATCH 12/50] refactor: update hasComments logic --- .../src/rules/no-duplicate-type-constituents.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index fd8472fb7a0d..1a160465ac6d 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -162,10 +162,10 @@ export default util.createRule({ }, []); const hasComments = node.types.some(type => { - const count = - sourceCode.getCommentsBefore(type).length + - sourceCode.getCommentsAfter(type).length; - return count > 0; + return ( + sourceCode.getCommentsBefore(type).length || + sourceCode.getCommentsAfter(type).length + ); }); const fix: TSESLint.ReportFixFunction = fixer => { From 9e75e07d1f2e1102d4d5fb001bc9503d0a7e50db Mon Sep 17 00:00:00 2001 From: sajikix Date: Thu, 3 Nov 2022 17:33:22 +0900 Subject: [PATCH 13/50] fix: remove cases that never occur --- .../rules/no-duplicate-type-constituents.ts | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 1a160465ac6d..c30a40bc8d4b 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -14,30 +14,10 @@ const constituentsAreEqual = ( actualTypeNode: TSESTree.TypeNode, expectedTypeNode: TSESTree.TypeNode, ): boolean => { - switch (expectedTypeNode.type) { - // These types should never occur as constituents of a union/intersection - case AST_NODE_TYPES.TSAbstractKeyword: - case AST_NODE_TYPES.TSAsyncKeyword: - case AST_NODE_TYPES.TSDeclareKeyword: - case AST_NODE_TYPES.TSExportKeyword: - case AST_NODE_TYPES.TSNamedTupleMember: - case AST_NODE_TYPES.TSOptionalType: - case AST_NODE_TYPES.TSPrivateKeyword: - case AST_NODE_TYPES.TSProtectedKeyword: - case AST_NODE_TYPES.TSPublicKeyword: - case AST_NODE_TYPES.TSReadonlyKeyword: - case AST_NODE_TYPES.TSRestType: - case AST_NODE_TYPES.TSStaticKeyword: - case AST_NODE_TYPES.TSTypePredicate: - /* istanbul ignore next */ - throw new Error(`Unexpected Type ${expectedTypeNode.type}`); - - default: - if (nodeTypesAreEqual(actualTypeNode, expectedTypeNode)) { - return astNodesAreEquals(actualTypeNode, expectedTypeNode); - } - return false; + if (nodeTypesAreEqual(actualTypeNode, expectedTypeNode)) { + return astNodesAreEquals(actualTypeNode, expectedTypeNode); } + return false; }; const astIgnoreKeys = ['range', 'loc', 'parent']; From bec198c201d69c0de42a2f60fa3d117f026988b4 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 4 Nov 2022 23:40:57 +0900 Subject: [PATCH 14/50] refactor: use type checker --- .../rules/no-duplicate-type-constituents.ts | 131 +++++------------- 1 file changed, 31 insertions(+), 100 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index c30a40bc8d4b..8deb7b92a870 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -1,85 +1,9 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; +import type { Type } from 'typescript'; import * as util from '../util'; -const nodeTypesAreEqual = ( - actualTypeNode: TSESTree.Node, - expectedTypeNode: T, -): actualTypeNode is T => { - return actualTypeNode.type === expectedTypeNode.type; -}; - -const constituentsAreEqual = ( - actualTypeNode: TSESTree.TypeNode, - expectedTypeNode: TSESTree.TypeNode, -): boolean => { - if (nodeTypesAreEqual(actualTypeNode, expectedTypeNode)) { - return astNodesAreEquals(actualTypeNode, expectedTypeNode); - } - return false; -}; - -const astIgnoreKeys = ['range', 'loc', 'parent']; - -type NodeElementsType = - | number - | string - | bigint - | boolean - | null - | undefined - | RegExp - | TSESTree.BaseNode - | TSESTree.BaseNode[] - | [number, number]; - -const astNodesAreEquals = ( - actualNode: T, - expectedNode: T, -): boolean => { - if (actualNode === expectedNode) { - return true; - } - if ( - actualNode && - expectedNode && - typeof actualNode == 'object' && - typeof expectedNode == 'object' - ) { - if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { - if (actualNode.length != expectedNode.length) { - return false; - } - return !actualNode.some( - (nodeEle, index) => !astNodesAreEquals(nodeEle, expectedNode[index]), - ); - } - if (isAstNodeType(actualNode) && isAstNodeType(expectedNode)) { - const actualNodeKeys = Object.keys(actualNode).filter( - key => !astIgnoreKeys.includes(key), - ); - const expectedNodeKeys = Object.keys(expectedNode).filter( - key => !astIgnoreKeys.includes(key), - ); - if (actualNodeKeys.length === expectedNodeKeys.length) { - return !actualNodeKeys.some( - actualNodeKey => - !astNodesAreEquals( - actualNode[actualNodeKey], - expectedNode[actualNodeKey], - ), - ); - } - } - } - return false; -}; - -const isAstNodeType = (node: object): node is Record => { - return node.constructor === Object; -}; - export type Options = [ { ignoreIntersections?: boolean; @@ -125,21 +49,25 @@ export default util.createRule({ ], create(context, [{ ignoreIntersections, ignoreUnions }]) { const sourceCode = context.getSourceCode(); + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const duplicateConstituents: { - node: TSESTree.TypeNode; - index: number; - }[] = []; + const uniqConstituentTypes = new Set(); + const duplicateConstituentNodes: TSESTree.TypeNode[] = []; - node.types.reduce((acc, cur, index) => { - if (acc.some(ele => constituentsAreEqual(ele, cur))) { - duplicateConstituents.push({ node: cur, index }); - return acc; + node.types.forEach(memberNode => { + const type = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(memberNode), + ); + if (uniqConstituentTypes.has(type)) { + duplicateConstituentNodes.push(memberNode); + } else { + uniqConstituentTypes.add(type); } - return [...acc, cur]; - }, []); + }); const hasComments = node.types.some(type => { return ( @@ -149,23 +77,26 @@ export default util.createRule({ }); const fix: TSESLint.ReportFixFunction = fixer => { - return duplicateConstituents.map(duplicateConstituent => { - const parent = duplicateConstituent.node.parent as - | TSESTree.TSUnionType - | TSESTree.TSIntersectionType; - const delteRangeStart = - parent.types[duplicateConstituent.index - 1].range[1]; - return fixer.removeRange([ - delteRangeStart, - duplicateConstituent.node.range[1], - ]); - }); + return duplicateConstituentNodes + .map(duplicateConstituentNode => { + const fixes: TSESLint.RuleFix[] = []; + const unionOrIntersectionToken = sourceCode.getTokenBefore( + duplicateConstituentNode, + ); + + if (unionOrIntersectionToken) { + fixes.push(fixer.remove(unionOrIntersectionToken)); + } + fixes.push(fixer.remove(duplicateConstituentNode)); + return fixes; + }) + .flat(); }; - duplicateConstituents.forEach(duplicateConstituent => { + duplicateConstituentNodes.forEach(duplicateConstituentNode => { context.report({ data: { - name: sourceCode.getText(duplicateConstituent.node), + name: sourceCode.getText(duplicateConstituentNode), type: node.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' From 71afc2ee15cfc320d4c4d0a793ec6f4aad3aa4cd Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 4 Nov 2022 23:42:50 +0900 Subject: [PATCH 15/50] fix: do not change fixer behavior with comments --- .../rules/no-duplicate-type-constituents.ts | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 8deb7b92a870..44ad8b6b4df6 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -11,7 +11,7 @@ export type Options = [ }, ]; -export type MessageIds = 'duplicate' | 'suggestFix'; +export type MessageIds = 'duplicate'; export default util.createRule({ name: 'no-duplicate-type-constituents', @@ -22,10 +22,8 @@ export default util.createRule({ recommended: false, }, fixable: 'code', - hasSuggestions: true, messages: { duplicate: '{{type}} type member {{name}} is duplicated.', - suggestFix: 'Delete duplicated members of type (removes all comments).', }, schema: [ { @@ -69,13 +67,6 @@ export default util.createRule({ } }); - const hasComments = node.types.some(type => { - return ( - sourceCode.getCommentsBefore(type).length || - sourceCode.getCommentsAfter(type).length - ); - }); - const fix: TSESLint.ReportFixFunction = fixer => { return duplicateConstituentNodes .map(duplicateConstituentNode => { @@ -104,19 +95,7 @@ export default util.createRule({ }, messageId: 'duplicate', node, - // don't autofix if any of the types have leading/trailing comments - ...(hasComments - ? { - suggest: [ - { - fix, - messageId: 'suggestFix', - }, - ], - } - : { - fix, - }), + fix, }); }); } From 7c79e8a49ab85c0ec3f7629454040e630550a9aa Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 5 Nov 2022 16:02:49 +0900 Subject: [PATCH 16/50] fix: delete bracket with fixer --- .../rules/no-duplicate-type-constituents.ts | 26 ++++++- .../no-duplicate-type-constituents.test.ts | 76 +++++++++++-------- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 44ad8b6b4df6..b4801542cc2e 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -71,14 +71,32 @@ export default util.createRule({ return duplicateConstituentNodes .map(duplicateConstituentNode => { const fixes: TSESLint.RuleFix[] = []; - const unionOrIntersectionToken = sourceCode.getTokenBefore( + const beforeTokens = sourceCode.getTokensBefore( duplicateConstituentNode, ); + const afterTokens = sourceCode.getTokensAfter( + duplicateConstituentNode, + ); + const beforeUnionOrIntersectionToken = beforeTokens + .reverse() + .find(token => token.value === '|' || token.value === '&'); - if (unionOrIntersectionToken) { - fixes.push(fixer.remove(unionOrIntersectionToken)); + if (beforeUnionOrIntersectionToken) { + const bracketBeforeTokens = sourceCode.getTokensBetween( + beforeUnionOrIntersectionToken, + duplicateConstituentNode, + ); + const bracketAfterTokens = afterTokens.slice( + 0, + bracketBeforeTokens.length, + ); + [ + beforeUnionOrIntersectionToken, + ...bracketBeforeTokens, + duplicateConstituentNode, + ...bracketAfterTokens, + ].forEach(token => fixes.push(fixer.remove(token))); } - fixes.push(fixer.remove(duplicateConstituentNode)); return fixes; }) .flat(); diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 4e97dbc72af7..2d376d931f01 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -5,10 +5,16 @@ import type { Options, } from '../../src/rules/no-duplicate-type-constituents'; import rule from '../../src/rules/no-duplicate-type-constituents'; -import { noFormat, RuleTester } from '../RuleTester'; +import { getFixturesRootDir, noFormat, RuleTester } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); const ruleTester = new RuleTester({ parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, }); const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ @@ -30,9 +36,6 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ { code: `type T = 1 ${operator} '1';`, }, - { - code: `type T = "1" ${operator} '1';`, - }, { code: `type T = true ${operator} boolean;`, }, @@ -95,7 +98,7 @@ const invalid = ( return [ { code: `type T = A ${operator} A;`, - output: `type T = A;`, + output: `type T = A ;`, errors: [ { messageId: 'duplicate', @@ -108,7 +111,7 @@ const invalid = ( }, { code: `const a : A ${operator} A = 'A';`, - output: `const a : A = 'A';`, + output: `const a : A = 'A';`, errors: [ { messageId: 'duplicate', @@ -121,7 +124,7 @@ const invalid = ( }, { code: `type T = 1 ${operator} 1;`, - output: `type T = 1;`, + output: `type T = 1 ;`, errors: [ { messageId: 'duplicate', @@ -134,7 +137,7 @@ const invalid = ( }, { code: `type T = true ${operator} true;`, - output: `type T = true;`, + output: `type T = true ;`, errors: [ { messageId: 'duplicate', @@ -147,7 +150,7 @@ const invalid = ( }, { code: `type T = null ${operator} null;`, - output: `type T = null;`, + output: `type T = null ;`, errors: [ { messageId: 'duplicate', @@ -160,7 +163,7 @@ const invalid = ( }, { code: `type T = any ${operator} any;`, - output: `type T = any;`, + output: `type T = any ;`, errors: [ { messageId: 'duplicate', @@ -172,21 +175,34 @@ const invalid = ( ], }, { - code: `type T = 'A' ${operator} 'A';`, - output: `type T = 'A';`, + code: `type T = 'A' ${operator} "A";`, + output: `type T = 'A' ;`, errors: [ { messageId: 'duplicate', data: { type, - name: "'A'", + name: '"A"', }, }, ], }, { code: noFormat`type T = (A) ${operator} (A);`, - output: noFormat`type T = (A);`, + output: noFormat`type T = (A) ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: noFormat`type T = (A) ${operator} ((A));`, + output: noFormat`type T = (A) ;`, errors: [ { messageId: 'duplicate', @@ -225,7 +241,7 @@ const invalid = ( }, { code: `type T = { a: string ${operator} string };`, - output: `type T = { a: string };`, + output: `type T = { a: string };`, errors: [ { messageId: 'duplicate', @@ -238,7 +254,7 @@ const invalid = ( }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, - output: `type T = [1, 2, 3];`, + output: `type T = [1, 2, 3] ;`, errors: [ { messageId: 'duplicate', @@ -251,7 +267,7 @@ const invalid = ( }, { code: `type T = (() => string) ${operator} (() => string);`, - output: `type T = (() => string);`, + output: `type T = (() => string) ;`, errors: [ { messageId: 'duplicate', @@ -264,7 +280,7 @@ const invalid = ( }, { code: `type T = () => string ${operator} string;`, - output: `type T = () => string;`, + output: `type T = () => string ;`, errors: [ { messageId: 'duplicate', @@ -277,7 +293,7 @@ const invalid = ( }, { code: `type T = () => null ${operator} null;`, - output: `type T = () => null;`, + output: `type T = () => null ;`, errors: [ { messageId: 'duplicate', @@ -290,7 +306,7 @@ const invalid = ( }, { code: `type T = (arg : string ${operator} string) => void;`, - output: `type T = (arg : string) => void;`, + output: `type T = (arg : string ) => void;`, errors: [ { messageId: 'duplicate', @@ -303,7 +319,7 @@ const invalid = ( }, { code: `type T = A ${operator} /* comment */ A;`, - output: null, + output: `type T = A /* comment */ ;`, errors: [ { messageId: 'duplicate', @@ -311,18 +327,12 @@ const invalid = ( type, name: 'A', }, - suggestions: [ - { - messageId: 'suggestFix', - output: `type T = A;`, - }, - ], }, ], }, { code: `type T = A ${operator} B ${operator} A;`, - output: `type T = A ${operator} B;`, + output: `type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -335,7 +345,7 @@ const invalid = ( }, { code: `type T = A ${operator} B ${operator} A ${operator} B;`, - output: `type T = A ${operator} B;`, + output: `type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -355,7 +365,7 @@ const invalid = ( }, { code: `type T = A ${operator} B ${operator} A ${operator} A;`, - output: `type T = A ${operator} B;`, + output: `type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -375,7 +385,7 @@ const invalid = ( }, { code: `type T = A ${operator} B ${operator} A ${operator} C;`, - output: `type T = A ${operator} B ${operator} C;`, + output: `type T = A ${operator} B ${operator} C;`, errors: [ { messageId: 'duplicate', @@ -388,7 +398,7 @@ const invalid = ( }, { code: `type T = (A | B) ${operator} (A | B);`, - output: `type T = (A | B);`, + output: `type T = (A | B) ;`, errors: [ { messageId: 'duplicate', @@ -401,7 +411,7 @@ const invalid = ( }, { code: `type T = Record;`, - output: `type T = Record;`, + output: `type T = Record;`, errors: [ { messageId: 'duplicate', From af57de9fd597043dff613ee3077d435c28e2dff2 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sun, 6 Nov 2022 23:37:43 +0900 Subject: [PATCH 17/50] fix: fix test cases and meta data --- .../rules/no-duplicate-type-constituents.ts | 7 +- .../no-duplicate-type-constituents.test.ts | 158 ++++++++++-------- 2 files changed, 91 insertions(+), 74 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index b4801542cc2e..5f3845ca0b73 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -20,6 +20,7 @@ export default util.createRule({ docs: { description: 'Disallow duplicate union/intersection type members', recommended: false, + requiresTypeChecking: true, }, fixable: 'code', messages: { @@ -56,12 +57,12 @@ export default util.createRule({ const uniqConstituentTypes = new Set(); const duplicateConstituentNodes: TSESTree.TypeNode[] = []; - node.types.forEach(memberNode => { + node.types.forEach(constituentNode => { const type = checker.getTypeAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(memberNode), + parserServices.esTreeNodeToTSNodeMap.get(constituentNode), ); if (uniqConstituentTypes.has(type)) { - duplicateConstituentNodes.push(memberNode); + duplicateConstituentNodes.push(constituentNode); } else { uniqConstituentTypes.add(type); } diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 2d376d931f01..043dd315667b 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -19,16 +19,24 @@ const ruleTester = new RuleTester({ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ { - code: `type T = A ${operator} B;`, + code: `type A = "A"; +type B = "B"; +type T = A ${operator} B;`, }, { - code: `const a : A ${operator} B = "A";`, + code: `type A = "A"; +type B = "B"; +const a : A ${operator} B = "A";`, }, { - code: `type T = A ${operator} /* comment */ B;`, + code: `type A = "A"; +type B = "B"; +type T = A ${operator} /* comment */ B;`, }, { - code: `type T = 'A' ${operator} 'B';`, + code: `type A = "A"; +type B = "B"; +type T = 'A' ${operator} 'B';`, }, { code: `type T = 1 ${operator} 2;`, @@ -46,7 +54,9 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ code: `type T = any ${operator} unknown;`, }, { - code: noFormat`type T = (A) ${operator} (B);`, + code: noFormat`type A = "A"; +type B = "B"; +type T = (A) ${operator} (B);`, }, { code: `type T = { a: string } ${operator} { b: string };`, @@ -76,19 +86,31 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ code: `type T = (arg : string ${operator} number) => void;`, }, { - code: `type T = A ${operator} B ${operator} C;`, + code: `type A = "A"; +type B = "B"; +type C = "C"; +type T = A ${operator} B ${operator} C;`, }, { code: `type T = readonly string[] ${operator} string[];`, }, { - code: `type T = (A | B) ${operator} (A | C);`, + code: `type A = "A"; +type B = "B"; +type C = "C"; +type D = "D"; +type T = (A | B) ${operator} (C | D); +`, }, { - code: `type T = (A | B) ${operator} (A & B);`, + code: `type A = "A"; +type B = "B"; +type T = (A | B) ${operator} (A & B);`, }, { - code: `type T = Record;`, + code: `type A = "A"; +type B = "B"; +type T = Record;`, }, ]; const invalid = ( @@ -97,8 +119,10 @@ const invalid = ( const type = operator === '|' ? 'Union' : 'Intersection'; return [ { - code: `type T = A ${operator} A;`, - output: `type T = A ;`, + code: `type A = "A"; +type T = A ${operator} A;`, + output: `type A = "A"; +type T = A ;`, errors: [ { messageId: 'duplicate', @@ -110,8 +134,10 @@ const invalid = ( ], }, { - code: `const a : A ${operator} A = 'A';`, - output: `const a : A = 'A';`, + code: `type A = "A"; +const a : A ${operator} A = 'A';`, + output: `type A = "A"; +const a : A = 'A';`, errors: [ { messageId: 'duplicate', @@ -188,8 +214,10 @@ const invalid = ( ], }, { - code: noFormat`type T = (A) ${operator} (A);`, - output: noFormat`type T = (A) ;`, + code: noFormat`type A = "A"; +type T = (A) ${operator} (A);`, + output: noFormat`type A = "A"; +type T = (A) ;`, errors: [ { messageId: 'duplicate', @@ -201,8 +229,10 @@ const invalid = ( ], }, { - code: noFormat`type T = (A) ${operator} ((A));`, - output: noFormat`type T = (A) ;`, + code: noFormat`type A = "A"; +type T = (A) ${operator} ((A));`, + output: noFormat`type A = "A"; +type T = (A) ;`, errors: [ { messageId: 'duplicate', @@ -213,32 +243,6 @@ const invalid = ( }, ], }, - { - code: `type T = { a: string } ${operator} { a: string };`, - output: `type T = { a: string };`, - errors: [ - { - messageId: 'duplicate', - data: { - type, - name: '{ a: string }', - }, - }, - ], - }, - { - code: `type T = { a: string, b: number } ${operator} { a: string, b: number };`, - output: `type T = { a: string, b: number };`, - errors: [ - { - messageId: 'duplicate', - data: { - type, - name: '{ a: string, b: number }', - }, - }, - ], - }, { code: `type T = { a: string ${operator} string };`, output: `type T = { a: string };`, @@ -265,19 +269,6 @@ const invalid = ( }, ], }, - { - code: `type T = (() => string) ${operator} (() => string);`, - output: `type T = (() => string) ;`, - errors: [ - { - messageId: 'duplicate', - data: { - type, - name: '() => string', - }, - }, - ], - }, { code: `type T = () => string ${operator} string;`, output: `type T = () => string ;`, @@ -318,8 +309,10 @@ const invalid = ( ], }, { - code: `type T = A ${operator} /* comment */ A;`, - output: `type T = A /* comment */ ;`, + code: `type A = "A"; +type T = A ${operator} /* comment */ A;`, + output: `type A = "A"; +type T = A /* comment */ ;`, errors: [ { messageId: 'duplicate', @@ -331,8 +324,12 @@ const invalid = ( ], }, { - code: `type T = A ${operator} B ${operator} A;`, - output: `type T = A ${operator} B ;`, + code: `type A = "A"; +type B = "B"; +type T = A ${operator} B ${operator} A;`, + output: `type A = "A"; +type B = "B"; +type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -344,8 +341,12 @@ const invalid = ( ], }, { - code: `type T = A ${operator} B ${operator} A ${operator} B;`, - output: `type T = A ${operator} B ;`, + code: `type A = "A"; +type B = "B"; +type T = A ${operator} B ${operator} A ${operator} B;`, + output: `type A = "A"; +type B = "B"; +type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -364,8 +365,12 @@ const invalid = ( ], }, { - code: `type T = A ${operator} B ${operator} A ${operator} A;`, - output: `type T = A ${operator} B ;`, + code: `type A = "A"; +type B = "B"; +type T = A ${operator} B ${operator} A ${operator} A;`, + output: `type A = "A"; +type B = "B"; +type T = A ${operator} B ;`, errors: [ { messageId: 'duplicate', @@ -384,8 +389,14 @@ const invalid = ( ], }, { - code: `type T = A ${operator} B ${operator} A ${operator} C;`, - output: `type T = A ${operator} B ${operator} C;`, + code: `type A = "A"; +type B = "B"; +type C = "C"; +type T = A ${operator} B ${operator} A ${operator} C;`, + output: `type A = "A"; +type B = "B"; +type C = "C"; +type T = A ${operator} B ${operator} C;`, errors: [ { messageId: 'duplicate', @@ -397,8 +408,12 @@ const invalid = ( ], }, { - code: `type T = (A | B) ${operator} (A | B);`, - output: `type T = (A | B) ;`, + code: `type A = "A"; +type B = "B"; +type T = (A | B) ${operator} (A | B);`, + output: `type A = "A"; +type B = "B"; +type T = (A | B) ;`, errors: [ { messageId: 'duplicate', @@ -410,8 +425,10 @@ const invalid = ( ], }, { - code: `type T = Record;`, - output: `type T = Record;`, + code: `type A = "A"; +type T = Record;`, + output: `type A = "A"; +type T = Record;`, errors: [ { messageId: 'duplicate', @@ -436,7 +453,6 @@ ruleTester.run('no-duplicate-type-constituents', rule, { }, ], }, - ...valid('&'), { code: 'type T = A & A;', From f15047d9f785880df4ae971abbb646cca61a6a7c Mon Sep 17 00:00:00 2001 From: sajikix Date: Sun, 13 Nov 2022 23:25:49 +0900 Subject: [PATCH 18/50] refactor : also use ast node checker --- .../rules/no-duplicate-type-constituents.ts | 92 +++++++++++++++++-- .../no-duplicate-type-constituents.test.ts | 64 +++++++++++++ 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 5f3845ca0b73..e7299f2d59af 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -13,6 +13,71 @@ export type Options = [ export type MessageIds = 'duplicate'; +const isRecordType = ( + object: object, +): object is Record => { + return object.constructor === Object; +}; + +const astIgnoreKeys = ['range', 'loc', 'parent']; + +const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { + if (actualNode === expectedNode) { + return true; + } + if ( + actualNode && + expectedNode && + typeof actualNode == 'object' && + typeof expectedNode == 'object' + ) { + if (actualNode.constructor !== expectedNode.constructor) { + return false; + } + if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { + if (actualNode.length != expectedNode.length) { + return false; + } + return !actualNode.some( + (nodeEle, index) => !isSameAstNode(nodeEle, expectedNode[index]), + ); + } + if (!isRecordType(actualNode) || !isRecordType(expectedNode)) { + return false; + } + const actualNodeKeys = Object.keys(actualNode).filter( + key => !astIgnoreKeys.includes(key), + ); + const expectedNodeKeys = Object.keys(expectedNode).filter( + key => !astIgnoreKeys.includes(key), + ); + if (actualNodeKeys.length !== expectedNodeKeys.length) { + return false; + } + if ( + actualNodeKeys.some( + actualNodeKey => + !Object.prototype.hasOwnProperty.call(expectedNode, actualNodeKey), + ) + ) { + return false; + } + if ( + actualNodeKeys.some( + actualNodeKey => + !isSameAstNode( + actualNode[actualNodeKey], + expectedNode[actualNodeKey], + ), + ) + ) { + return false; + } + return true; + } + return false; +}; + export default util.createRule({ name: 'no-duplicate-type-constituents', meta: { @@ -57,16 +122,25 @@ export default util.createRule({ const uniqConstituentTypes = new Set(); const duplicateConstituentNodes: TSESTree.TypeNode[] = []; - node.types.forEach(constituentNode => { - const type = checker.getTypeAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(constituentNode), - ); - if (uniqConstituentTypes.has(type)) { - duplicateConstituentNodes.push(constituentNode); - } else { + node.types.reduce( + (uniqConstituentNodes, constituentNode) => { + const type = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(constituentNode), + ); + if ( + uniqConstituentNodes.some(ele => + isSameAstNode(ele, constituentNode), + ) || + uniqConstituentTypes.has(type) + ) { + duplicateConstituentNodes.push(constituentNode); + return uniqConstituentNodes; + } uniqConstituentTypes.add(type); - } - }); + return [...uniqConstituentNodes, constituentNode]; + }, + [], + ); const fix: TSESLint.ReportFixFunction = fixer => { return duplicateConstituentNodes diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 043dd315667b..320b6db22f14 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -67,6 +67,9 @@ type T = (A) ${operator} (B);`, { code: `type T = { a: string ${operator} number };`, }, + { + code: `type T = Set ${operator} Set;`, + }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, }, @@ -256,6 +259,45 @@ type T = (A) ;`, }, ], }, + { + code: `type T = { a : string } ${operator} { a : string };`, + output: `type T = { a : string } ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '{ a : string }', + }, + }, + ], + }, + { + code: `type T = { a : string, b : number } ${operator} { a : string, b : number };`, + output: `type T = { a : string, b : number } ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: '{ a : string, b : number }', + }, + }, + ], + }, + { + code: `type T = Set ${operator} Set;`, + output: `type T = Set ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'Set', + }, + }, + ], + }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, output: `type T = [1, 2, 3] ;`, @@ -426,6 +468,28 @@ type T = (A | B) ;`, }, { code: `type A = "A"; +type T = A ${operator} (A ${operator} A);`, + output: `type A = "A"; +type T = A ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: `A ${operator} A`, + }, + }, + { + messageId: 'duplicate', + data: { + type, + name: 'A', + }, + }, + ], + }, + { + code: `type A = "A"; type T = Record;`, output: `type A = "A"; type T = Record;`, From 4571a23e2f03596cc2003f80fa38fab90c785e50 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 19 Nov 2022 18:23:28 +0900 Subject: [PATCH 19/50] refactor : organize test cases --- .../no-duplicate-type-constituents.test.ts | 183 ++++++++++-------- 1 file changed, 100 insertions(+), 83 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 320b6db22f14..b8fa470f28ba 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -18,26 +18,6 @@ const ruleTester = new RuleTester({ }); const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ - { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} B;`, - }, - { - code: `type A = "A"; -type B = "B"; -const a : A ${operator} B = "A";`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} /* comment */ B;`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = 'A' ${operator} 'B';`, - }, { code: `type T = 1 ${operator} 2;`, }, @@ -53,11 +33,6 @@ type T = 'A' ${operator} 'B';`, { code: `type T = any ${operator} unknown;`, }, - { - code: noFormat`type A = "A"; -type B = "B"; -type T = (A) ${operator} (B);`, - }, { code: `type T = { a: string } ${operator} { b: string };`, }, @@ -91,6 +66,31 @@ type T = (A) ${operator} (B);`, { code: `type A = "A"; type B = "B"; +type T = A ${operator} B;`, + }, + { + code: `type A = "A"; +type B = "B"; +const a : A ${operator} B = "A";`, + }, + { + code: `type A = "A"; +type B = "B"; +type T = A ${operator} /* comment */ B;`, + }, + { + code: `type A = "A"; +type B = "B"; +type T = 'A' ${operator} 'B';`, + }, + { + code: noFormat`type A = "A"; +type B = "B"; +type T = (A) ${operator} (B);`, + }, + { + code: `type A = "A"; +type B = "B"; type C = "C"; type T = A ${operator} B ${operator} C;`, }, @@ -122,133 +122,125 @@ const invalid = ( const type = operator === '|' ? 'Union' : 'Intersection'; return [ { - code: `type A = "A"; -type T = A ${operator} A;`, - output: `type A = "A"; -type T = A ;`, + code: `type T = 1 ${operator} 1;`, + output: `type T = 1 ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'A', + name: '1', }, }, ], }, { - code: `type A = "A"; -const a : A ${operator} A = 'A';`, - output: `type A = "A"; -const a : A = 'A';`, + code: `type T = true ${operator} true;`, + output: `type T = true ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'A', + name: 'true', }, }, ], }, { - code: `type T = 1 ${operator} 1;`, - output: `type T = 1 ;`, + code: `type T = null ${operator} null;`, + output: `type T = null ;`, errors: [ { messageId: 'duplicate', data: { type, - name: '1', + name: 'null', }, }, ], }, { - code: `type T = true ${operator} true;`, - output: `type T = true ;`, + code: `type T = any ${operator} any;`, + output: `type T = any ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'true', + name: 'any', }, }, ], }, { - code: `type T = null ${operator} null;`, - output: `type T = null ;`, + code: `type T = { a: string ${operator} string };`, + output: `type T = { a: string };`, errors: [ { messageId: 'duplicate', data: { type, - name: 'null', + name: 'string', }, }, ], }, { - code: `type T = any ${operator} any;`, - output: `type T = any ;`, + code: `type T = { a : string } ${operator} { a : string };`, + output: `type T = { a : string } ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'any', + name: '{ a : string }', }, }, ], }, { - code: `type T = 'A' ${operator} "A";`, - output: `type T = 'A' ;`, + code: `type T = { a : string, b : number } ${operator} { a : string, b : number };`, + output: `type T = { a : string, b : number } ;`, errors: [ { messageId: 'duplicate', data: { type, - name: '"A"', + name: '{ a : string, b : number }', }, }, ], }, { - code: noFormat`type A = "A"; -type T = (A) ${operator} (A);`, - output: noFormat`type A = "A"; -type T = (A) ;`, + code: `type T = Set ${operator} Set;`, + output: `type T = Set ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'A', + name: 'Set', }, }, ], }, { - code: noFormat`type A = "A"; -type T = (A) ${operator} ((A));`, - output: noFormat`type A = "A"; -type T = (A) ;`, + code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, + output: `type T = [1, 2, 3] ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'A', + name: '[1, 2, 3]', }, }, ], }, { - code: `type T = { a: string ${operator} string };`, - output: `type T = { a: string };`, + code: `type T = () => string ${operator} string;`, + output: `type T = () => string ;`, errors: [ { messageId: 'duplicate', @@ -260,92 +252,100 @@ type T = (A) ;`, ], }, { - code: `type T = { a : string } ${operator} { a : string };`, - output: `type T = { a : string } ;`, + code: `type T = () => null ${operator} null;`, + output: `type T = () => null ;`, errors: [ { messageId: 'duplicate', data: { type, - name: '{ a : string }', + name: 'null', }, }, ], }, { - code: `type T = { a : string, b : number } ${operator} { a : string, b : number };`, - output: `type T = { a : string, b : number } ;`, + code: `type T = (arg : string ${operator} string) => void;`, + output: `type T = (arg : string ) => void;`, errors: [ { messageId: 'duplicate', data: { type, - name: '{ a : string, b : number }', + name: 'string', }, }, ], }, { - code: `type T = Set ${operator} Set;`, - output: `type T = Set ;`, + code: `type T = 'A' ${operator} "A";`, + output: `type T = 'A' ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'Set', + name: '"A"', }, }, ], }, { - code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, - output: `type T = [1, 2, 3] ;`, + code: `type A = "A"; +type T = A ${operator} A;`, + output: `type A = "A"; +type T = A ;`, errors: [ { messageId: 'duplicate', data: { type, - name: '[1, 2, 3]', + name: 'A', }, }, ], }, { - code: `type T = () => string ${operator} string;`, - output: `type T = () => string ;`, + code: `type A = "A"; +const a : A ${operator} A = 'A';`, + output: `type A = "A"; +const a : A = 'A';`, errors: [ { messageId: 'duplicate', data: { type, - name: 'string', + name: 'A', }, }, ], }, { - code: `type T = () => null ${operator} null;`, - output: `type T = () => null ;`, + code: noFormat`type A = "A"; +type T = (A) ${operator} (A);`, + output: noFormat`type A = "A"; +type T = (A) ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'null', + name: 'A', }, }, ], }, { - code: `type T = (arg : string ${operator} string) => void;`, - output: `type T = (arg : string ) => void;`, + code: noFormat`type A = "A"; +type T = (A) ${operator} ((A));`, + output: noFormat`type A = "A"; +type T = (A) ;`, errors: [ { messageId: 'duplicate', data: { type, - name: 'string', + name: 'A', }, }, ], @@ -365,6 +365,23 @@ type T = A /* comment */ ;`, }, ], }, + { + code: `type A1 = "A"; +type A2 = "A"; +type T = A1 ${operator} A2;`, + output: `type A1 = "A"; +type A2 = "A"; +type T = A1 ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'A2', + }, + }, + ], + }, { code: `type A = "A"; type B = "B"; From aa31c673bed46df5a8a22e731406b5db961e4f4d Mon Sep 17 00:00:00 2001 From: sajikix Date: Wed, 23 Nov 2022 18:09:31 +0900 Subject: [PATCH 20/50] fix: fix rule description --- .../eslint-plugin/docs/rules/no-duplicate-type-constituents.md | 2 +- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index ee47cea8b9aa..5657156ad92b 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -1,5 +1,5 @@ --- -description: 'Disallow duplicate union/intersection type members.' +description: 'Disallow duplicate constituents of union or intersection types.' --- > 🛑 This file is source code, not the primary documentation location! 🛑 diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index e7299f2d59af..0210c29c7e9b 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -83,7 +83,8 @@ export default util.createRule({ meta: { type: 'suggestion', docs: { - description: 'Disallow duplicate union/intersection type members', + description: + 'Disallow duplicate constituents of union or intersection types', recommended: false, requiresTypeChecking: true, }, From fc9536e97ca59ba954b6e1aadb11dc6357007d78 Mon Sep 17 00:00:00 2001 From: sajikix Date: Wed, 23 Nov 2022 19:13:51 +0900 Subject: [PATCH 21/50] fix: modify Rule Details to match implementation --- .../rules/no-duplicate-type-constituents.md | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index 5657156ad92b..0c67083b5f37 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -10,32 +10,47 @@ Although TypeScript supports duplicate union and intersection member values, peo ## Rule Details -This rule disallows duplicate union or intersection type members. It only checks duplication on the notation. Members with the same value but different names are not marked duplicates. +This rule disallows duplicate union or intersection constituents. + +It determines whether two types are equivalent in the following way. + +1. whether the syntax is exactly the same. +2. whether TypeScript treats them as the same type. + +If either of the two conditions is satisfied, It treats the two types as duplicates. ### ❌ Incorrect ```ts -type T1 = A | A | B; +type T1 = 'A' | 'A'; -type T2 = { a: string } & { a: string }; +type T2 = A | A | B; -type T3 = [1, 2, 3] & [1, 2, 3]; +type T3 = { a: string } & { a: string }; -type T4 = () => string | string; +type T4 = [1, 2, 3] & [1, 2, 3]; + +type StringA = string; +type StringB = string; +type T5 = StringA | StringB; ``` ### ✅ Correct ```ts -type T1 = A | B | C; +type T1 = 'A' | 'B'; + +type T2 = A | B | C; -type T2 = { a: string } & { b: string }; +type T3 = { a: string } & { b: string }; -type T3 = [1, 2, 3] & [1, 2, 3, 4]; +type T4 = [1, 2, 3] & [1, 2, 3, 4]; -type T4 = () => string | number; +type StringA = string; +type NumberB = string; +type T5 = StringA | NumberB; ``` ## Options From 56da0d4708e4a620042078bc7f85a70f252ecc6c Mon Sep 17 00:00:00 2001 From: sajikix Date: Wed, 23 Nov 2022 21:05:23 +0900 Subject: [PATCH 22/50] refactor: add uniq set in each case --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 0210c29c7e9b..794a978a5537 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -135,6 +135,7 @@ export default util.createRule({ uniqConstituentTypes.has(type) ) { duplicateConstituentNodes.push(constituentNode); + uniqConstituentTypes.add(type); return uniqConstituentNodes; } uniqConstituentTypes.add(type); From 2f2bbad92dfbd0828d5b7e87db71551843fad288 Mon Sep 17 00:00:00 2001 From: sajikix Date: Mon, 5 Dec 2022 10:24:44 +0900 Subject: [PATCH 23/50] refactor: delete type guard --- .../src/rules/no-duplicate-type-constituents.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 794a978a5537..df3982f7e71f 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -13,12 +13,6 @@ export type Options = [ export type MessageIds = 'duplicate'; -const isRecordType = ( - object: object, -): object is Record => { - return object.constructor === Object; -}; - const astIgnoreKeys = ['range', 'loc', 'parent']; const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { @@ -42,9 +36,6 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { (nodeEle, index) => !isSameAstNode(nodeEle, expectedNode[index]), ); } - if (!isRecordType(actualNode) || !isRecordType(expectedNode)) { - return false; - } const actualNodeKeys = Object.keys(actualNode).filter( key => !astIgnoreKeys.includes(key), ); @@ -66,8 +57,8 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { actualNodeKeys.some( actualNodeKey => !isSameAstNode( - actualNode[actualNodeKey], - expectedNode[actualNodeKey], + actualNode[actualNodeKey as keyof typeof actualNode], + expectedNode[actualNodeKey as keyof typeof expectedNode], ), ) ) { From f5e6ce3bdb58a746cdc33b78e9248a7667d079cb Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 10 Dec 2022 14:50:51 +0900 Subject: [PATCH 24/50] refactor: add test case --- .../no-duplicate-type-constituents.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index b8fa470f28ba..a2f601b5966a 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -45,6 +45,15 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ { code: `type T = Set ${operator} Set;`, }, + { + code: `type T = Class ${operator} Class;`, + }, + { + code: `type T = string[] ${operator} number[];`, + }, + { + code: `type T = string[][] ${operator} string[];`, + }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, }, @@ -225,6 +234,45 @@ const invalid = ( }, ], }, + { + code: `type T = Class ${operator} Class;`, + output: `type T = Class ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'Class', + }, + }, + ], + }, + { + code: `type T = string[] ${operator} string[];`, + output: `type T = string[] ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'string[]', + }, + }, + ], + }, + { + code: `type T = string[][] ${operator} string[][];`, + output: `type T = string[][] ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + name: 'string[][]', + }, + }, + ], + }, { code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, output: `type T = [1, 2, 3] ;`, From 2d7021274dffb4a9d9888062fabe879992743986 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 10 Dec 2022 22:16:14 +0900 Subject: [PATCH 25/50] refactor: delete unnecessary comparison logic --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index df3982f7e71f..14e0221b6670 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -25,9 +25,6 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { typeof actualNode == 'object' && typeof expectedNode == 'object' ) { - if (actualNode.constructor !== expectedNode.constructor) { - return false; - } if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { if (actualNode.length != expectedNode.length) { return false; From fea730eddfaf4adb31ddc99b763166eeefedf880 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 10 Dec 2022 22:16:35 +0900 Subject: [PATCH 26/50] refactor: add test-case --- .../tests/rules/no-duplicate-type-constituents.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index a2f601b5966a..fdf7dac9874a 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -60,6 +60,9 @@ const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ { code: `type T = [1, 2, 3] ${operator} [1, 2, 3, 4];`, }, + { + code: `type T = "A" ${operator} string[];`, + }, { code: `type T = (() => string) ${operator} (() => void);`, }, From f6a9e323aa09490b3d5104c3c13bfa1f1a3153ef Mon Sep 17 00:00:00 2001 From: sajikix Date: Mon, 12 Dec 2022 00:42:05 +0900 Subject: [PATCH 27/50] feat: show which the previous type is duplicating --- .../rules/no-duplicate-type-constituents.ts | 69 ++++++----- .../no-duplicate-type-constituents.test.ts | 112 +++++++++++++----- 2 files changed, 118 insertions(+), 63 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 14e0221b6670..106ee8a82c68 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -78,7 +78,8 @@ export default util.createRule({ }, fixable: 'code', messages: { - duplicate: '{{type}} type member {{name}} is duplicated.', + duplicate: + '{{type}} type member {{duplicated}} is duplicated with {{previous}}.', }, schema: [ { @@ -108,39 +109,42 @@ export default util.createRule({ function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const uniqConstituentTypes = new Set(); - const duplicateConstituentNodes: TSESTree.TypeNode[] = []; + const duplicateConstituents: { + duplicated: TSESTree.TypeNode; + duplicatePrevious: TSESTree.TypeNode; + }[] = []; - node.types.reduce( - (uniqConstituentNodes, constituentNode) => { - const type = checker.getTypeAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(constituentNode), - ); - if ( - uniqConstituentNodes.some(ele => - isSameAstNode(ele, constituentNode), - ) || - uniqConstituentTypes.has(type) - ) { - duplicateConstituentNodes.push(constituentNode); - uniqConstituentTypes.add(type); - return uniqConstituentNodes; - } - uniqConstituentTypes.add(type); - return [...uniqConstituentNodes, constituentNode]; - }, - [], - ); + node.types.reduce< + { + node: TSESTree.TypeNode; + type: Type; + }[] + >((uniqConstituents, constituentNode) => { + const type = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(constituentNode), + ); + const duplicatePreviousConstituent = uniqConstituents.find( + ele => isSameAstNode(ele.node, constituentNode) || ele.type === type, + ); + if (duplicatePreviousConstituent) { + duplicateConstituents.push({ + duplicated: constituentNode, + duplicatePrevious: duplicatePreviousConstituent.node, + }); + return uniqConstituents; + } + return [...uniqConstituents, { node: constituentNode, type }]; + }, []); const fix: TSESLint.ReportFixFunction = fixer => { - return duplicateConstituentNodes - .map(duplicateConstituentNode => { + return duplicateConstituents + .map(duplicateConstituent => { const fixes: TSESLint.RuleFix[] = []; const beforeTokens = sourceCode.getTokensBefore( - duplicateConstituentNode, + duplicateConstituent.duplicated, ); const afterTokens = sourceCode.getTokensAfter( - duplicateConstituentNode, + duplicateConstituent.duplicated, ); const beforeUnionOrIntersectionToken = beforeTokens .reverse() @@ -149,7 +153,7 @@ export default util.createRule({ if (beforeUnionOrIntersectionToken) { const bracketBeforeTokens = sourceCode.getTokensBetween( beforeUnionOrIntersectionToken, - duplicateConstituentNode, + duplicateConstituent.duplicated, ); const bracketAfterTokens = afterTokens.slice( 0, @@ -158,7 +162,7 @@ export default util.createRule({ [ beforeUnionOrIntersectionToken, ...bracketBeforeTokens, - duplicateConstituentNode, + duplicateConstituent.duplicated, ...bracketAfterTokens, ].forEach(token => fixes.push(fixer.remove(token))); } @@ -167,14 +171,17 @@ export default util.createRule({ .flat(); }; - duplicateConstituentNodes.forEach(duplicateConstituentNode => { + duplicateConstituents.forEach(duplicateConstituent => { context.report({ data: { - name: sourceCode.getText(duplicateConstituentNode), + duplicated: sourceCode.getText(duplicateConstituent.duplicated), type: node.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' : 'Union', + previous: sourceCode.getText( + duplicateConstituent.duplicatePrevious, + ), }, messageId: 'duplicate', node, diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index fdf7dac9874a..ca53c8220001 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -141,7 +141,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: '1', + duplicated: '1', + previous: '1', }, }, ], @@ -154,7 +155,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'true', + duplicated: 'true', + previous: 'true', }, }, ], @@ -167,7 +169,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'null', + duplicated: 'null', + previous: 'null', }, }, ], @@ -180,7 +183,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'any', + duplicated: 'any', + previous: 'any', }, }, ], @@ -193,7 +197,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'string', + duplicated: 'string', + previous: 'string', }, }, ], @@ -206,7 +211,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: '{ a : string }', + duplicated: '{ a : string }', + previous: '{ a : string }', }, }, ], @@ -219,7 +225,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: '{ a : string, b : number }', + duplicated: '{ a : string, b : number }', + previous: '{ a : string, b : number }', }, }, ], @@ -232,7 +239,24 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'Set', + duplicated: 'Set', + previous: 'Set', + }, + }, + ], + }, + { + code: `type IsArray = T extends any[] ? true : false; +type ActuallyDuplicated = IsArray ${operator} IsArray;`, + output: `type IsArray = T extends any[] ? true : false; +type ActuallyDuplicated = IsArray ;`, + errors: [ + { + messageId: 'duplicate', + data: { + type, + duplicated: 'IsArray', + previous: 'IsArray', }, }, ], @@ -245,7 +269,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'Class', + duplicated: 'Class', + previous: 'Class', }, }, ], @@ -258,7 +283,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'string[]', + duplicated: 'string[]', + previous: 'string[]', }, }, ], @@ -271,7 +297,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'string[][]', + duplicated: 'string[][]', + previous: 'string[][]', }, }, ], @@ -284,7 +311,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: '[1, 2, 3]', + duplicated: '[1, 2, 3]', + previous: '[1, 2, 3]', }, }, ], @@ -297,7 +325,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'string', + duplicated: 'string', + previous: 'string', }, }, ], @@ -310,7 +339,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'null', + duplicated: 'null', + previous: 'null', }, }, ], @@ -323,7 +353,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: 'string', + duplicated: 'string', + previous: 'string', }, }, ], @@ -336,7 +367,8 @@ const invalid = ( messageId: 'duplicate', data: { type, - name: '"A"', + duplicated: '"A"', + previous: "'A'", }, }, ], @@ -351,7 +383,8 @@ type T = A ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -366,7 +399,8 @@ const a : A = 'A';`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -381,7 +415,8 @@ type T = (A) ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -396,7 +431,8 @@ type T = (A) ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -411,7 +447,8 @@ type T = A /* comment */ ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -428,7 +465,8 @@ type T = A1 ;`, messageId: 'duplicate', data: { type, - name: 'A2', + duplicated: 'A2', + previous: 'A1', }, }, ], @@ -445,7 +483,8 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -462,14 +501,16 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, { messageId: 'duplicate', data: { type, - name: 'B', + duplicated: 'B', + previous: 'B', }, }, ], @@ -486,14 +527,16 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, { messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -512,7 +555,8 @@ type T = A ${operator} B ${operator} C;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -529,7 +573,8 @@ type T = (A | B) ;`, messageId: 'duplicate', data: { type, - name: 'A | B', + duplicated: 'A | B', + previous: 'A | B', }, }, ], @@ -544,14 +589,16 @@ type T = A ;`, messageId: 'duplicate', data: { type, - name: `A ${operator} A`, + duplicated: `A ${operator} A`, + previous: `A`, }, }, { messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], @@ -566,7 +613,8 @@ type T = Record;`, messageId: 'duplicate', data: { type, - name: 'A', + duplicated: 'A', + previous: 'A', }, }, ], From a1b36e458a4eb77521f5a113f488cd5c92e11eb7 Mon Sep 17 00:00:00 2001 From: sajikix Date: Mon, 12 Dec 2022 10:39:15 +0900 Subject: [PATCH 28/50] fix: use word constituents --- .../eslint-plugin/docs/rules/no-duplicate-type-constituents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index 0c67083b5f37..25de2ef7d170 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -6,7 +6,7 @@ description: 'Disallow duplicate constituents of union or intersection types.' > > See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation. -Although TypeScript supports duplicate union and intersection member values, people usually expect members to have unique values within the same intersection and union. Duplicate values make the code redundant and generally reduce readability. +Although TypeScript supports duplicate union and intersection constituents, people usually expect members to have unique values within the same intersection and union. Duplicate values make the code redundant and generally reduce readability. ## Rule Details From 22c60f859c5e8f1df1a0705a3c333875dd29b0c1 Mon Sep 17 00:00:00 2001 From: sajikix Date: Mon, 12 Dec 2022 10:43:07 +0900 Subject: [PATCH 29/50] fix: sample case --- .../eslint-plugin/docs/rules/no-duplicate-type-constituents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index 25de2ef7d170..c0ab8ff557ae 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -49,7 +49,7 @@ type T3 = { a: string } & { b: string }; type T4 = [1, 2, 3] & [1, 2, 3, 4]; type StringA = string; -type NumberB = string; +type NumberB = number; type T5 = StringA | NumberB; ``` From f2651167ba63cf1a2609cb6da097ee12e22fa6a4 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 4 Feb 2023 22:11:36 +0900 Subject: [PATCH 30/50] fix: lint message --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 106ee8a82c68..b935a43b085e 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -79,7 +79,7 @@ export default util.createRule({ fixable: 'code', messages: { duplicate: - '{{type}} type member {{duplicated}} is duplicated with {{previous}}.', + '{{type}} type constituents {{duplicated}} is duplicated with {{previous}}.', }, schema: [ { From 05cc529d38d146c659160626a90c3314d9b7d34a Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 4 Feb 2023 23:02:07 +0900 Subject: [PATCH 31/50] fix: rule docs --- .../rules/no-duplicate-type-constituents.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index c0ab8ff557ae..3fd6f66c3f69 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -6,18 +6,14 @@ description: 'Disallow duplicate constituents of union or intersection types.' > > See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation. -Although TypeScript supports duplicate union and intersection constituents, people usually expect members to have unique values within the same intersection and union. Duplicate values make the code redundant and generally reduce readability. +Although TypeScript supports types ("constituents") in union and intersection types being duplicates of each other, developers typically expect each constituent to be unique within the same intersection and union. +Duplicate values make the code overly verbose and generally reduce readability. ## Rule Details This rule disallows duplicate union or intersection constituents. - -It determines whether two types are equivalent in the following way. - -1. whether the syntax is exactly the same. -2. whether TypeScript treats them as the same type. - -If either of the two conditions is satisfied, It treats the two types as duplicates. +We consider types to be duplicate if they evaluate to the same result in the type system. +For example, given `type A = string` and `type T = string | A`, this rule would flag `string | A`. @@ -30,7 +26,7 @@ type T2 = A | A | B; type T3 = { a: string } & { a: string }; -type T4 = [1, 2, 3] & [1, 2, 3]; +type T4 = [1, 2, 3] | [1, 2, 3]; type StringA = string; type StringB = string; @@ -57,8 +53,8 @@ type T5 = StringA | NumberB; ### `ignoreIntersections` -When set to true, duplicate checks on intersection type members are ignored. +When set to true, duplicate checks on intersection type constituents are ignored. ### `ignoreUnions` -When set to true, duplicate checks on union type members are ignored. +When set to true, duplicate checks on union type constituents are ignored. From b924fdd38a13d2b32a69c272e401025eefc2e9e0 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 4 Feb 2023 23:28:45 +0900 Subject: [PATCH 32/50] fix: use === & !== --- .../src/rules/no-duplicate-type-constituents.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index b935a43b085e..71534c2ba8d5 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -22,11 +22,11 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { if ( actualNode && expectedNode && - typeof actualNode == 'object' && - typeof expectedNode == 'object' + typeof actualNode === 'object' && + typeof expectedNode === 'object' ) { if (Array.isArray(actualNode) && Array.isArray(expectedNode)) { - if (actualNode.length != expectedNode.length) { + if (actualNode.length !== expectedNode.length) { return false; } return !actualNode.some( From d9489adbec3b79104fd68e2394e5e76fed98ec10 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 18 Feb 2023 19:02:17 +0900 Subject: [PATCH 33/50] fix: No `noFormat` in test. --- .../rules/no-duplicate-type-constituents.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index ca53c8220001..7fb9a35899bf 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -5,7 +5,7 @@ import type { Options, } from '../../src/rules/no-duplicate-type-constituents'; import rule from '../../src/rules/no-duplicate-type-constituents'; -import { getFixturesRootDir, noFormat, RuleTester } from '../RuleTester'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; const rootPath = getFixturesRootDir(); @@ -96,7 +96,7 @@ type B = "B"; type T = 'A' ${operator} 'B';`, }, { - code: noFormat`type A = "A"; + code: `type A = "A"; type B = "B"; type T = (A) ${operator} (B);`, }, @@ -406,9 +406,9 @@ const a : A = 'A';`, ], }, { - code: noFormat`type A = "A"; + code: `type A = "A"; type T = (A) ${operator} (A);`, - output: noFormat`type A = "A"; + output: `type A = "A"; type T = (A) ;`, errors: [ { @@ -422,9 +422,9 @@ type T = (A) ;`, ], }, { - code: noFormat`type A = "A"; + code: `type A = "A"; type T = (A) ${operator} ((A));`, - output: noFormat`type A = "A"; + output: `type A = "A"; type T = (A) ;`, errors: [ { From 17fcdf9b9bac1ca5bb02df4175595534836125d7 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 18 Feb 2023 19:04:16 +0900 Subject: [PATCH 34/50] fix: correct examples --- .../eslint-plugin/docs/rules/no-duplicate-type-constituents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index 3fd6f66c3f69..467807a3cf50 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -42,7 +42,7 @@ type T2 = A | B | C; type T3 = { a: string } & { b: string }; -type T4 = [1, 2, 3] & [1, 2, 3, 4]; +type T4 = [1, 2, 3] | [1, 2, 3, 4]; type StringA = string; type NumberB = number; From 9f6f2aa229af747275ee9359442e13c6b93c1e5a Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 18 Feb 2023 19:07:39 +0900 Subject: [PATCH 35/50] refactor: use `flatMap` --- .../rules/no-duplicate-type-constituents.ts | 56 +++++++++---------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 71534c2ba8d5..6748b7fa5d13 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -137,38 +137,36 @@ export default util.createRule({ }, []); const fix: TSESLint.ReportFixFunction = fixer => { - return duplicateConstituents - .map(duplicateConstituent => { - const fixes: TSESLint.RuleFix[] = []; - const beforeTokens = sourceCode.getTokensBefore( + return duplicateConstituents.flatMap(duplicateConstituent => { + const fixes: TSESLint.RuleFix[] = []; + const beforeTokens = sourceCode.getTokensBefore( + duplicateConstituent.duplicated, + ); + const afterTokens = sourceCode.getTokensAfter( + duplicateConstituent.duplicated, + ); + const beforeUnionOrIntersectionToken = beforeTokens + .reverse() + .find(token => token.value === '|' || token.value === '&'); + + if (beforeUnionOrIntersectionToken) { + const bracketBeforeTokens = sourceCode.getTokensBetween( + beforeUnionOrIntersectionToken, duplicateConstituent.duplicated, ); - const afterTokens = sourceCode.getTokensAfter( - duplicateConstituent.duplicated, + const bracketAfterTokens = afterTokens.slice( + 0, + bracketBeforeTokens.length, ); - const beforeUnionOrIntersectionToken = beforeTokens - .reverse() - .find(token => token.value === '|' || token.value === '&'); - - if (beforeUnionOrIntersectionToken) { - const bracketBeforeTokens = sourceCode.getTokensBetween( - beforeUnionOrIntersectionToken, - duplicateConstituent.duplicated, - ); - const bracketAfterTokens = afterTokens.slice( - 0, - bracketBeforeTokens.length, - ); - [ - beforeUnionOrIntersectionToken, - ...bracketBeforeTokens, - duplicateConstituent.duplicated, - ...bracketAfterTokens, - ].forEach(token => fixes.push(fixer.remove(token))); - } - return fixes; - }) - .flat(); + [ + beforeUnionOrIntersectionToken, + ...bracketBeforeTokens, + duplicateConstituent.duplicated, + ...bracketAfterTokens, + ].forEach(token => fixes.push(fixer.remove(token))); + } + return fixes; + }); }; duplicateConstituents.forEach(duplicateConstituent => { From 0489f17f4b235760d5051ea4ee3115391a1723ba Mon Sep 17 00:00:00 2001 From: sajikix Date: Sat, 18 Feb 2023 19:15:47 +0900 Subject: [PATCH 36/50] refactor: Do not use temporary `fixes` variable. --- .../rules/no-duplicate-type-constituents.ts | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 6748b7fa5d13..a15e99035c61 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -138,7 +138,6 @@ export default util.createRule({ const fix: TSESLint.ReportFixFunction = fixer => { return duplicateConstituents.flatMap(duplicateConstituent => { - const fixes: TSESLint.RuleFix[] = []; const beforeTokens = sourceCode.getTokensBefore( duplicateConstituent.duplicated, ); @@ -148,24 +147,23 @@ export default util.createRule({ const beforeUnionOrIntersectionToken = beforeTokens .reverse() .find(token => token.value === '|' || token.value === '&'); - - if (beforeUnionOrIntersectionToken) { - const bracketBeforeTokens = sourceCode.getTokensBetween( - beforeUnionOrIntersectionToken, - duplicateConstituent.duplicated, - ); - const bracketAfterTokens = afterTokens.slice( - 0, - bracketBeforeTokens.length, - ); - [ - beforeUnionOrIntersectionToken, - ...bracketBeforeTokens, - duplicateConstituent.duplicated, - ...bracketAfterTokens, - ].forEach(token => fixes.push(fixer.remove(token))); + if (!beforeUnionOrIntersectionToken) { + return []; } - return fixes; + const bracketBeforeTokens = sourceCode.getTokensBetween( + beforeUnionOrIntersectionToken, + duplicateConstituent.duplicated, + ); + const bracketAfterTokens = afterTokens.slice( + 0, + bracketBeforeTokens.length, + ); + return [ + beforeUnionOrIntersectionToken, + ...bracketBeforeTokens, + duplicateConstituent.duplicated, + ...bracketAfterTokens, + ].map(token => fixer.remove(token)); }); }; From 7b1712b9473942ec0e656da1e2770e761e4df623 Mon Sep 17 00:00:00 2001 From: sajikix Date: Sun, 19 Feb 2023 00:32:16 +0900 Subject: [PATCH 37/50] refactor: make type comparison lazy and use cache --- .../rules/no-duplicate-type-constituents.ts | 55 +++++++++++-------- .../no-duplicate-type-constituents.test.ts | 14 ++++- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index a15e99035c61..422348f00ca8 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -113,28 +113,39 @@ export default util.createRule({ duplicated: TSESTree.TypeNode; duplicatePrevious: TSESTree.TypeNode; }[] = []; - - node.types.reduce< - { - node: TSESTree.TypeNode; - type: Type; - }[] - >((uniqConstituents, constituentNode) => { - const type = checker.getTypeAtLocation( - parserServices.esTreeNodeToTSNodeMap.get(constituentNode), - ); - const duplicatePreviousConstituent = uniqConstituents.find( - ele => isSameAstNode(ele.node, constituentNode) || ele.type === type, - ); - if (duplicatePreviousConstituent) { - duplicateConstituents.push({ - duplicated: constituentNode, - duplicatePrevious: duplicatePreviousConstituent.node, - }); - return uniqConstituents; - } - return [...uniqConstituents, { node: constituentNode, type }]; - }, []); + const cachedTypeMap: Map = new Map(); + node.types.reduce( + (uniqConstituents, constituentNode) => { + const duplicatedPreviousConstituentInAst = uniqConstituents.find( + ele => isSameAstNode(ele, constituentNode), + ); + if (duplicatedPreviousConstituentInAst) { + duplicateConstituents.push({ + duplicated: constituentNode, + duplicatePrevious: duplicatedPreviousConstituentInAst, + }); + return uniqConstituents; + } + const constituentNodeType = checker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(constituentNode), + ); + const duplicatedPreviousConstituentInType = [ + ...cachedTypeMap.entries(), + ].find(([, type]) => { + return type === constituentNodeType; + })?.[0]; + if (duplicatedPreviousConstituentInType) { + duplicateConstituents.push({ + duplicated: constituentNode, + duplicatePrevious: duplicatedPreviousConstituentInType, + }); + return uniqConstituents; + } + cachedTypeMap.set(constituentNode, constituentNodeType); + return [...uniqConstituents, constituentNode]; + }, + [], + ); const fix: TSESLint.ReportFixFunction = fixer => { return duplicateConstituents.flatMap(duplicateConstituent => { diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index 7fb9a35899bf..f48e733b3519 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -456,10 +456,12 @@ type T = A /* comment */ ;`, { code: `type A1 = "A"; type A2 = "A"; -type T = A1 ${operator} A2;`, +type A3 = "A"; +type T = A1 ${operator} A2 ${operator} A3;`, output: `type A1 = "A"; type A2 = "A"; -type T = A1 ;`, +type A3 = "A"; +type T = A1 ;`, errors: [ { messageId: 'duplicate', @@ -469,6 +471,14 @@ type T = A1 ;`, previous: 'A1', }, }, + { + messageId: 'duplicate', + data: { + type, + duplicated: 'A3', + previous: 'A1', + }, + }, ], }, { From 2e00a2c7093d3d76f7ab6935496c6834e55fe10f Mon Sep 17 00:00:00 2001 From: sajikix Date: Sun, 19 Feb 2023 01:56:57 +0900 Subject: [PATCH 38/50] refactor: no unnecessary loop in `fix` function. --- .../rules/no-duplicate-type-constituents.ts | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 422348f00ca8..c05bf1ea9c0f 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -1,4 +1,4 @@ -import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; +import type { TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; import type { Type } from 'typescript'; @@ -146,38 +146,6 @@ export default util.createRule({ }, [], ); - - const fix: TSESLint.ReportFixFunction = fixer => { - return duplicateConstituents.flatMap(duplicateConstituent => { - const beforeTokens = sourceCode.getTokensBefore( - duplicateConstituent.duplicated, - ); - const afterTokens = sourceCode.getTokensAfter( - duplicateConstituent.duplicated, - ); - const beforeUnionOrIntersectionToken = beforeTokens - .reverse() - .find(token => token.value === '|' || token.value === '&'); - if (!beforeUnionOrIntersectionToken) { - return []; - } - const bracketBeforeTokens = sourceCode.getTokensBetween( - beforeUnionOrIntersectionToken, - duplicateConstituent.duplicated, - ); - const bracketAfterTokens = afterTokens.slice( - 0, - bracketBeforeTokens.length, - ); - return [ - beforeUnionOrIntersectionToken, - ...bracketBeforeTokens, - duplicateConstituent.duplicated, - ...bracketAfterTokens, - ].map(token => fixer.remove(token)); - }); - }; - duplicateConstituents.forEach(duplicateConstituent => { context.report({ data: { @@ -192,7 +160,34 @@ export default util.createRule({ }, messageId: 'duplicate', node, - fix, + fix: fixer => { + const beforeTokens = sourceCode.getTokensBefore( + duplicateConstituent.duplicated, + ); + const afterTokens = sourceCode.getTokensAfter( + duplicateConstituent.duplicated, + ); + const beforeUnionOrIntersectionToken = beforeTokens + .reverse() + .find(token => token.value === '|' || token.value === '&'); + if (!beforeUnionOrIntersectionToken) { + return []; + } + const bracketBeforeTokens = sourceCode.getTokensBetween( + beforeUnionOrIntersectionToken, + duplicateConstituent.duplicated, + ); + const bracketAfterTokens = afterTokens.slice( + 0, + bracketBeforeTokens.length, + ); + return [ + beforeUnionOrIntersectionToken, + ...bracketBeforeTokens, + duplicateConstituent.duplicated, + ...bracketAfterTokens, + ].map(token => fixer.remove(token)); + }, }); }); } From 2bbd34deb959b156c6e989f081c9630cdcf181aa Mon Sep 17 00:00:00 2001 From: sajikix Date: Mon, 20 Feb 2023 19:17:27 +0900 Subject: [PATCH 39/50] refactor: get logic of tokens to be deleted --- .../rules/no-duplicate-type-constituents.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index c05bf1ea9c0f..a95e72f73925 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -163,23 +163,17 @@ export default util.createRule({ fix: fixer => { const beforeTokens = sourceCode.getTokensBefore( duplicateConstituent.duplicated, + { filter: token => token.value === '|' || token.value === '&' }, ); - const afterTokens = sourceCode.getTokensAfter( - duplicateConstituent.duplicated, - ); - const beforeUnionOrIntersectionToken = beforeTokens - .reverse() - .find(token => token.value === '|' || token.value === '&'); - if (!beforeUnionOrIntersectionToken) { - return []; - } + const beforeUnionOrIntersectionToken = + beforeTokens[beforeTokens.length - 1]; const bracketBeforeTokens = sourceCode.getTokensBetween( beforeUnionOrIntersectionToken, duplicateConstituent.duplicated, ); - const bracketAfterTokens = afterTokens.slice( - 0, - bracketBeforeTokens.length, + const bracketAfterTokens = sourceCode.getTokensAfter( + duplicateConstituent.duplicated, + { count: bracketBeforeTokens.length }, ); return [ beforeUnionOrIntersectionToken, From 73b66a7807f8524b6dd8dfe3b35c42129ed2dded Mon Sep 17 00:00:00 2001 From: sajikix Date: Wed, 22 Mar 2023 17:05:23 +0900 Subject: [PATCH 40/50] refactor: separate report function and solve fixer range problem --- .../rules/no-duplicate-type-constituents.ts | 114 ++++++++++-------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index a95e72f73925..c9b321d53a4d 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -102,17 +102,12 @@ export default util.createRule({ }, ], create(context, [{ ignoreIntersections, ignoreUnions }]) { - const sourceCode = context.getSourceCode(); const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const duplicateConstituents: { - duplicated: TSESTree.TypeNode; - duplicatePrevious: TSESTree.TypeNode; - }[] = []; const cachedTypeMap: Map = new Map(); node.types.reduce( (uniqConstituents, constituentNode) => { @@ -120,10 +115,13 @@ export default util.createRule({ ele => isSameAstNode(ele, constituentNode), ); if (duplicatedPreviousConstituentInAst) { - duplicateConstituents.push({ - duplicated: constituentNode, - duplicatePrevious: duplicatedPreviousConstituentInAst, - }); + reportDuplicate( + { + duplicated: constituentNode, + duplicatePrevious: duplicatedPreviousConstituentInAst, + }, + node, + ); return uniqConstituents; } const constituentNodeType = checker.getTypeAtLocation( @@ -135,10 +133,13 @@ export default util.createRule({ return type === constituentNodeType; })?.[0]; if (duplicatedPreviousConstituentInType) { - duplicateConstituents.push({ - duplicated: constituentNode, - duplicatePrevious: duplicatedPreviousConstituentInType, - }); + reportDuplicate( + { + duplicated: constituentNode, + duplicatePrevious: duplicatedPreviousConstituentInType, + }, + node, + ); return uniqConstituents; } cachedTypeMap.set(constituentNode, constituentNodeType); @@ -146,43 +147,56 @@ export default util.createRule({ }, [], ); - duplicateConstituents.forEach(duplicateConstituent => { - context.report({ - data: { - duplicated: sourceCode.getText(duplicateConstituent.duplicated), - type: - node.type === AST_NODE_TYPES.TSIntersectionType - ? 'Intersection' - : 'Union', - previous: sourceCode.getText( - duplicateConstituent.duplicatePrevious, - ), - }, - messageId: 'duplicate', - node, - fix: fixer => { - const beforeTokens = sourceCode.getTokensBefore( - duplicateConstituent.duplicated, - { filter: token => token.value === '|' || token.value === '&' }, - ); - const beforeUnionOrIntersectionToken = - beforeTokens[beforeTokens.length - 1]; - const bracketBeforeTokens = sourceCode.getTokensBetween( - beforeUnionOrIntersectionToken, - duplicateConstituent.duplicated, - ); - const bracketAfterTokens = sourceCode.getTokensAfter( - duplicateConstituent.duplicated, - { count: bracketBeforeTokens.length }, - ); - return [ - beforeUnionOrIntersectionToken, - ...bracketBeforeTokens, - duplicateConstituent.duplicated, - ...bracketAfterTokens, - ].map(token => fixer.remove(token)); - }, - }); + } + function reportDuplicate( + duplicateConstituent: { + duplicated: TSESTree.TypeNode; + duplicatePrevious: TSESTree.TypeNode; + }, + parentNode: TSESTree.TSIntersectionType | TSESTree.TSUnionType, + ): void { + const sourceCode = context.getSourceCode(); + const beforeTokens = sourceCode.getTokensBefore( + duplicateConstituent.duplicated, + { filter: token => token.value === '|' || token.value === '&' }, + ); + const beforeUnionOrIntersectionToken = + beforeTokens[beforeTokens.length - 1]; + const bracketBeforeTokens = sourceCode.getTokensBetween( + beforeUnionOrIntersectionToken, + duplicateConstituent.duplicated, + ); + const bracketAfterTokens = sourceCode.getTokensAfter( + duplicateConstituent.duplicated, + { count: bracketBeforeTokens.length }, + ); + const reportLocation: TSESTree.SourceLocation = { + start: beforeUnionOrIntersectionToken.loc.start, + end: + bracketAfterTokens.length > 0 + ? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end + : duplicateConstituent.duplicated.loc.end, + }; + context.report({ + data: { + duplicated: sourceCode.getText(duplicateConstituent.duplicated), + type: + parentNode.type === AST_NODE_TYPES.TSIntersectionType + ? 'Intersection' + : 'Union', + previous: sourceCode.getText(duplicateConstituent.duplicatePrevious), + }, + messageId: 'duplicate', + node: duplicateConstituent.duplicated, + loc: reportLocation, + fix: fixer => { + return [ + beforeUnionOrIntersectionToken, + ...bracketBeforeTokens, + duplicateConstituent.duplicated, + ...bracketAfterTokens, + ].map(token => fixer.remove(token)); + }, }); } return { From f161c6183aa40ebbdeb8b82c8adf6ce10e62a1c1 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 10:34:28 +0900 Subject: [PATCH 41/50] refactor: improved documentation. --- .../docs/rules/no-duplicate-type-constituents.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md index 467807a3cf50..879d0c6ca74c 100644 --- a/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md +++ b/packages/eslint-plugin/docs/rules/no-duplicate-type-constituents.md @@ -6,14 +6,15 @@ description: 'Disallow duplicate constituents of union or intersection types.' > > See **https://typescript-eslint.io/rules/no-duplicate-type-constituents** for documentation. -Although TypeScript supports types ("constituents") in union and intersection types being duplicates of each other, developers typically expect each constituent to be unique within the same intersection and union. +TypeScript supports types ("constituents") within union and intersection types being duplicates of each other. +However, developers typically expect each constituent to be unique within its intersection or union. Duplicate values make the code overly verbose and generally reduce readability. ## Rule Details This rule disallows duplicate union or intersection constituents. We consider types to be duplicate if they evaluate to the same result in the type system. -For example, given `type A = string` and `type T = string | A`, this rule would flag `string | A`. +For example, given `type A = string` and `type T = string | A`, this rule would flag that `A` is the same type as `string`. From 255cc8c9a1f366372e470171019db96b204a0f89 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 10:52:52 +0900 Subject: [PATCH 42/50] fix: make additionalProperties false --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index c9b321d53a4d..52af31a89ba1 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -83,6 +83,7 @@ export default util.createRule({ }, schema: [ { + additionalProperties: false, type: 'object', properties: { ignoreIntersections: { From e8d1aa1c7be77061700c33f5a9b59d9eba05e586 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 10:58:31 +0900 Subject: [PATCH 43/50] fix: delete printing message {{duplicated}} --- .../rules/no-duplicate-type-constituents.ts | 4 +-- .../no-duplicate-type-constituents.test.ts | 34 ------------------- 2 files changed, 1 insertion(+), 37 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 52af31a89ba1..03b8d12b6161 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -78,8 +78,7 @@ export default util.createRule({ }, fixable: 'code', messages: { - duplicate: - '{{type}} type constituents {{duplicated}} is duplicated with {{previous}}.', + duplicate: '{{type}} type constituent is duplicated with {{previous}}.', }, schema: [ { @@ -180,7 +179,6 @@ export default util.createRule({ }; context.report({ data: { - duplicated: sourceCode.getText(duplicateConstituent.duplicated), type: parentNode.type === AST_NODE_TYPES.TSIntersectionType ? 'Intersection' diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index f48e733b3519..f378a9fd1b71 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -141,7 +141,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: '1', previous: '1', }, }, @@ -155,7 +154,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: 'true', previous: 'true', }, }, @@ -169,7 +167,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: 'null', previous: 'null', }, }, @@ -183,7 +180,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: 'any', previous: 'any', }, }, @@ -197,7 +193,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: 'string', previous: 'string', }, }, @@ -211,7 +206,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: '{ a : string }', previous: '{ a : string }', }, }, @@ -225,7 +219,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: '{ a : string, b : number }', previous: '{ a : string, b : number }', }, }, @@ -239,7 +232,6 @@ const invalid = ( messageId: 'duplicate', data: { type, - duplicated: 'Set', previous: 'Set', }, }, @@ -255,7 +247,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'IsArray', previous: 'IsArray', }, }, @@ -269,7 +260,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'Class', previous: 'Class', }, }, @@ -283,7 +273,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'string[]', previous: 'string[]', }, }, @@ -297,7 +286,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'string[][]', previous: 'string[][]', }, }, @@ -311,7 +299,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: '[1, 2, 3]', previous: '[1, 2, 3]', }, }, @@ -325,7 +312,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'string', previous: 'string', }, }, @@ -339,7 +325,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'null', previous: 'null', }, }, @@ -353,7 +338,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: 'string', previous: 'string', }, }, @@ -367,7 +351,6 @@ type ActuallyDuplicated = IsArray ;`, messageId: 'duplicate', data: { type, - duplicated: '"A"', previous: "'A'", }, }, @@ -383,7 +366,6 @@ type T = A ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -399,7 +381,6 @@ const a : A = 'A';`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -415,7 +396,6 @@ type T = (A) ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -431,7 +411,6 @@ type T = (A) ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -447,7 +426,6 @@ type T = A /* comment */ ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -467,7 +445,6 @@ type T = A1 ;`, messageId: 'duplicate', data: { type, - duplicated: 'A2', previous: 'A1', }, }, @@ -475,7 +452,6 @@ type T = A1 ;`, messageId: 'duplicate', data: { type, - duplicated: 'A3', previous: 'A1', }, }, @@ -493,7 +469,6 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -511,7 +486,6 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -519,7 +493,6 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - duplicated: 'B', previous: 'B', }, }, @@ -537,7 +510,6 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -545,7 +517,6 @@ type T = A ${operator} B ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -565,7 +536,6 @@ type T = A ${operator} B ${operator} C;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -583,7 +553,6 @@ type T = (A | B) ;`, messageId: 'duplicate', data: { type, - duplicated: 'A | B', previous: 'A | B', }, }, @@ -599,7 +568,6 @@ type T = A ;`, messageId: 'duplicate', data: { type, - duplicated: `A ${operator} A`, previous: `A`, }, }, @@ -607,7 +575,6 @@ type T = A ;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, @@ -623,7 +590,6 @@ type T = Record;`, messageId: 'duplicate', data: { type, - duplicated: 'A', previous: 'A', }, }, From 47c2b4c42301f3c3359e54ce532d7252c7297dba Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 11:03:30 +0900 Subject: [PATCH 44/50] fix: do not abbreviate "unique" --- .../src/rules/no-duplicate-type-constituents.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 03b8d12b6161..764bda64f94f 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -110,8 +110,8 @@ export default util.createRule({ ): void { const cachedTypeMap: Map = new Map(); node.types.reduce( - (uniqConstituents, constituentNode) => { - const duplicatedPreviousConstituentInAst = uniqConstituents.find( + (uniqueConstituents, constituentNode) => { + const duplicatedPreviousConstituentInAst = uniqueConstituents.find( ele => isSameAstNode(ele, constituentNode), ); if (duplicatedPreviousConstituentInAst) { @@ -122,7 +122,7 @@ export default util.createRule({ }, node, ); - return uniqConstituents; + return uniqueConstituents; } const constituentNodeType = checker.getTypeAtLocation( parserServices.esTreeNodeToTSNodeMap.get(constituentNode), @@ -140,10 +140,10 @@ export default util.createRule({ }, node, ); - return uniqConstituents; + return uniqueConstituents; } cachedTypeMap.set(constituentNode, constituentNodeType); - return [...uniqConstituents, constituentNode]; + return [...uniqueConstituents, constituentNode]; }, [], ); From 911366326d2ac215588d8a12d34b74508aa85ed4 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 11:14:45 +0900 Subject: [PATCH 45/50] refactor: reverse the key and value in cachedTypeMap to reduce the amount of calculation. --- .../src/rules/no-duplicate-type-constituents.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 764bda64f94f..833a60197c25 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -108,7 +108,7 @@ export default util.createRule({ function checkDuplicate( node: TSESTree.TSIntersectionType | TSESTree.TSUnionType, ): void { - const cachedTypeMap: Map = new Map(); + const cachedTypeMap: Map = new Map(); node.types.reduce( (uniqueConstituents, constituentNode) => { const duplicatedPreviousConstituentInAst = uniqueConstituents.find( @@ -127,11 +127,8 @@ export default util.createRule({ const constituentNodeType = checker.getTypeAtLocation( parserServices.esTreeNodeToTSNodeMap.get(constituentNode), ); - const duplicatedPreviousConstituentInType = [ - ...cachedTypeMap.entries(), - ].find(([, type]) => { - return type === constituentNodeType; - })?.[0]; + const duplicatedPreviousConstituentInType = + cachedTypeMap.get(constituentNodeType); if (duplicatedPreviousConstituentInType) { reportDuplicate( { @@ -142,7 +139,7 @@ export default util.createRule({ ); return uniqueConstituents; } - cachedTypeMap.set(constituentNode, constituentNodeType); + cachedTypeMap.set(constituentNodeType, constituentNode); return [...uniqueConstituents, constituentNode]; }, [], From a55e36fa2fb07d43c37c2a98ae6f1b0be0becac5 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 11:30:44 +0900 Subject: [PATCH 46/50] fix: reportLocation start --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 833a60197c25..08de5df590ed 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -168,7 +168,7 @@ export default util.createRule({ { count: bracketBeforeTokens.length }, ); const reportLocation: TSESTree.SourceLocation = { - start: beforeUnionOrIntersectionToken.loc.start, + start: duplicateConstituent.duplicated.loc.start, end: bracketAfterTokens.length > 0 ? bracketAfterTokens[bracketAfterTokens.length - 1].loc.end From 7a85718bfc96646cd4b31b9b47c05ff167641e66 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 14:03:54 +0900 Subject: [PATCH 47/50] refactor: stop test generation and write tests naively. --- .../no-duplicate-type-constituents.test.ts | 619 ++++++++++-------- 1 file changed, 331 insertions(+), 288 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts index f378a9fd1b71..c1b57d8914ba 100644 --- a/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts +++ b/packages/eslint-plugin/tests/rules/no-duplicate-type-constituents.test.ts @@ -1,9 +1,3 @@ -import type { TSESLint } from '@typescript-eslint/utils'; - -import type { - MessageIds, - Options, -} from '../../src/rules/no-duplicate-type-constituents'; import rule from '../../src/rules/no-duplicate-type-constituents'; import { getFixturesRootDir, RuleTester } from '../RuleTester'; @@ -17,607 +11,656 @@ const ruleTester = new RuleTester({ }, }); -const valid = (operator: '|' | '&'): TSESLint.ValidTestCase[] => [ - { - code: `type T = 1 ${operator} 2;`, - }, - { - code: `type T = 1 ${operator} '1';`, - }, - { - code: `type T = true ${operator} boolean;`, - }, - { - code: `type T = null ${operator} undefined;`, - }, - { - code: `type T = any ${operator} unknown;`, - }, - { - code: `type T = { a: string } ${operator} { b: string };`, - }, - { - code: `type T = { a: string, b: number } ${operator} { b: number, a: string };`, - }, - { - code: `type T = { a: string ${operator} number };`, - }, - { - code: `type T = Set ${operator} Set;`, - }, - { - code: `type T = Class ${operator} Class;`, - }, - { - code: `type T = string[] ${operator} number[];`, - }, - { - code: `type T = string[][] ${operator} string[];`, - }, - { - code: `type T = [1, 2, 3] ${operator} [1, 2, 4];`, - }, - { - code: `type T = [1, 2, 3] ${operator} [1, 2, 3, 4];`, - }, - { - code: `type T = "A" ${operator} string[];`, - }, - { - code: `type T = (() => string) ${operator} (() => void);`, - }, - { - code: `type T = () => string ${operator} void;`, - }, - { - code: `type T = () => null ${operator} undefined;`, - }, - { - code: `type T = (arg : string ${operator} number) => void;`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} B;`, - }, - { - code: `type A = "A"; -type B = "B"; -const a : A ${operator} B = "A";`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} /* comment */ B;`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = 'A' ${operator} 'B';`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = (A) ${operator} (B);`, - }, - { - code: `type A = "A"; -type B = "B"; -type C = "C"; -type T = A ${operator} B ${operator} C;`, - }, - { - code: `type T = readonly string[] ${operator} string[];`, - }, - { - code: `type A = "A"; -type B = "B"; -type C = "C"; -type D = "D"; -type T = (A | B) ${operator} (C | D); -`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = (A | B) ${operator} (A & B);`, - }, - { - code: `type A = "A"; -type B = "B"; -type T = Record;`, - }, -]; -const invalid = ( - operator: '|' | '&', -): TSESLint.InvalidTestCase[] => { - const type = operator === '|' ? 'Union' : 'Intersection'; - return [ - { - code: `type T = 1 ${operator} 1;`, +ruleTester.run('no-duplicate-type-constituents', rule, { + valid: [ + { + code: 'type T = 1 | 2;', + }, + { + code: "type T = 1 | '1';", + }, + { + code: 'type T = true & boolean;', + }, + { + code: 'type T = null | undefined;', + }, + { + code: 'type T = any | unknown;', + }, + { + code: 'type T = { a: string } | { b: string };', + }, + { + code: 'type T = { a: string; b: number } | { b: number; a: string };', + }, + { + code: 'type T = { a: string | number };', + }, + { + code: 'type T = Set | Set;', + }, + { + code: 'type T = Class | Class;', + }, + { + code: 'type T = string[] | number[];', + }, + { + code: 'type T = string[][] | string[];', + }, + { + code: 'type T = [1, 2, 3] | [1, 2, 4];', + }, + { + code: 'type T = [1, 2, 3] | [1, 2, 3, 4];', + }, + { + code: "type T = 'A' | string[];", + }, + { + code: 'type T = (() => string) | (() => void);', + }, + { + code: 'type T = () => string | void;', + }, + { + code: 'type T = () => null | undefined;', + }, + { + code: 'type T = (arg: string | number) => void;', + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type T = A | B; + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +const a: A | B = 'A'; + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type T = A | /* comment */ B; + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type T = 'A' | 'B'; + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type T = A | B | C; + `, + }, + { + code: 'type T = readonly string[] | string[];', + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type D = 'D'; +type T = (A | B) | (C | D); + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type T = (A | B) | (A & B); + `, + }, + { + code: ` +type A = 'A'; +type B = 'B'; +type T = Record; + `, + }, + { + code: 'type T = A | A;', + options: [ + { + ignoreUnions: true, + }, + ], + }, + { + code: 'type T = A & A;', + options: [ + { + ignoreIntersections: true, + }, + ], + }, + ], + invalid: [ + { + code: 'type T = 1 | 1;', output: `type T = 1 ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: '1', }, }, ], }, { - code: `type T = true ${operator} true;`, + code: 'type T = true & true;', output: `type T = true ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Intersection', previous: 'true', }, }, ], }, { - code: `type T = null ${operator} null;`, + code: 'type T = null | null;', output: `type T = null ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'null', }, }, ], }, { - code: `type T = any ${operator} any;`, + code: 'type T = any | any;', output: `type T = any ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'any', }, }, ], }, { - code: `type T = { a: string ${operator} string };`, + code: 'type T = { a: string | string };', output: `type T = { a: string };`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'string', }, }, ], }, { - code: `type T = { a : string } ${operator} { a : string };`, - output: `type T = { a : string } ;`, + code: 'type T = { a: string } | { a: string };', + output: `type T = { a: string } ;`, errors: [ { messageId: 'duplicate', data: { - type, - previous: '{ a : string }', + type: 'Union', + previous: '{ a: string }', }, }, ], }, { - code: `type T = { a : string, b : number } ${operator} { a : string, b : number };`, - output: `type T = { a : string, b : number } ;`, + code: 'type T = { a: string; b: number } | { a: string; b: number };', + output: `type T = { a: string; b: number } ;`, errors: [ { messageId: 'duplicate', data: { - type, - previous: '{ a : string, b : number }', + type: 'Union', + previous: '{ a: string; b: number }', }, }, ], }, { - code: `type T = Set ${operator} Set;`, + code: 'type T = Set | Set;', output: `type T = Set ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'Set', }, }, ], }, { - code: `type IsArray = T extends any[] ? true : false; -type ActuallyDuplicated = IsArray ${operator} IsArray;`, - output: `type IsArray = T extends any[] ? true : false; -type ActuallyDuplicated = IsArray ;`, + code: ` +type IsArray = T extends any[] ? true : false; +type ActuallyDuplicated = IsArray | IsArray; + `, + output: ` +type IsArray = T extends any[] ? true : false; +type ActuallyDuplicated = IsArray ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'IsArray', }, }, ], }, { - code: `type T = Class ${operator} Class;`, + code: 'type T = Class | Class;', output: `type T = Class ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'Class', }, }, ], }, { - code: `type T = string[] ${operator} string[];`, + code: 'type T = string[] | string[];', output: `type T = string[] ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'string[]', }, }, ], }, { - code: `type T = string[][] ${operator} string[][];`, + code: 'type T = string[][] | string[][];', output: `type T = string[][] ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'string[][]', }, }, ], }, { - code: `type T = [1, 2, 3] ${operator} [1, 2, 3];`, + code: 'type T = [1, 2, 3] | [1, 2, 3];', output: `type T = [1, 2, 3] ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: '[1, 2, 3]', }, }, ], }, { - code: `type T = () => string ${operator} string;`, + code: 'type T = () => string | string;', output: `type T = () => string ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'string', }, }, ], }, { - code: `type T = () => null ${operator} null;`, + code: 'type T = () => null | null;', output: `type T = () => null ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'null', }, }, ], }, { - code: `type T = (arg : string ${operator} string) => void;`, - output: `type T = (arg : string ) => void;`, + code: 'type T = (arg: string | string) => void;', + output: `type T = (arg: string ) => void;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'string', }, }, ], }, { - code: `type T = 'A' ${operator} "A";`, + code: "type T = 'A' | 'A';", output: `type T = 'A' ;`, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: "'A'", }, }, ], }, { - code: `type A = "A"; -type T = A ${operator} A;`, - output: `type A = "A"; -type T = A ;`, - errors: [ - { - messageId: 'duplicate', - data: { - type, - previous: 'A', - }, - }, - ], - }, - { - code: `type A = "A"; -const a : A ${operator} A = 'A';`, - output: `type A = "A"; -const a : A = 'A';`, + code: ` +type A = 'A'; +type T = A | A; + `, + output: ` +type A = 'A'; +type T = A ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type T = (A) ${operator} (A);`, - output: `type A = "A"; -type T = (A) ;`, + code: ` +type A = 'A'; +const a: A | A = 'A'; + `, + output: ` +type A = 'A'; +const a: A = 'A'; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type T = (A) ${operator} ((A));`, - output: `type A = "A"; -type T = (A) ;`, + code: ` +type A = 'A'; +type T = A | /* comment */ A; + `, + output: ` +type A = 'A'; +type T = A /* comment */ ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type T = A ${operator} /* comment */ A;`, - output: `type A = "A"; -type T = A /* comment */ ;`, + code: ` +type A1 = 'A'; +type A2 = 'A'; +type A3 = 'A'; +type T = A1 | A2 | A3; + `, + output: ` +type A1 = 'A'; +type A2 = 'A'; +type A3 = 'A'; +type T = A1 ; + `, errors: [ { messageId: 'duplicate', data: { - type, - previous: 'A', - }, - }, - ], - }, - { - code: `type A1 = "A"; -type A2 = "A"; -type A3 = "A"; -type T = A1 ${operator} A2 ${operator} A3;`, - output: `type A1 = "A"; -type A2 = "A"; -type A3 = "A"; -type T = A1 ;`, - errors: [ - { - messageId: 'duplicate', - data: { - type, + type: 'Union', previous: 'A1', }, }, { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A1', }, }, ], }, { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} B ${operator} A;`, - output: `type A = "A"; -type B = "B"; -type T = A ${operator} B ;`, + code: ` +type A = 'A'; +type B = 'B'; +type T = A | B | A; + `, + output: ` +type A = 'A'; +type B = 'B'; +type T = A | B ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} B ${operator} A ${operator} B;`, - output: `type A = "A"; -type B = "B"; -type T = A ${operator} B ;`, + code: ` +type A = 'A'; +type B = 'B'; +type T = A | B | A | B; + `, + output: ` +type A = 'A'; +type B = 'B'; +type T = A | B ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'B', }, }, ], }, { - code: `type A = "A"; -type B = "B"; -type T = A ${operator} B ${operator} A ${operator} A;`, - output: `type A = "A"; -type B = "B"; -type T = A ${operator} B ;`, + code: ` +type A = 'A'; +type B = 'B'; +type T = A | B | A | A; + `, + output: ` +type A = 'A'; +type B = 'B'; +type T = A | B ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type B = "B"; -type C = "C"; -type T = A ${operator} B ${operator} A ${operator} C;`, - output: `type A = "A"; -type B = "B"; -type C = "C"; -type T = A ${operator} B ${operator} C;`, + code: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type T = A | B | A | C; + `, + output: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type T = A | B | C; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type B = "B"; -type T = (A | B) ${operator} (A | B);`, - output: `type A = "A"; -type B = "B"; -type T = (A | B) ;`, + code: ` +type A = 'A'; +type B = 'B'; +type T = (A | B) | (A | B); + `, + output: ` +type A = 'A'; +type B = 'B'; +type T = (A | B) ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A | B', }, }, ], }, { - code: `type A = "A"; -type T = A ${operator} (A ${operator} A);`, - output: `type A = "A"; -type T = A ;`, + code: ` +type A = 'A'; +type T = A | (A | A); + `, + output: ` +type A = 'A'; +type T = A ; + `, errors: [ { messageId: 'duplicate', data: { - type, + type: 'Union', previous: `A`, }, }, { messageId: 'duplicate', data: { - type, + type: 'Union', previous: 'A', }, }, ], }, { - code: `type A = "A"; -type T = Record;`, - output: `type A = "A"; -type T = Record;`, + code: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type D = 'D'; +type F = (A | B) | (A | B) | ((C | D) & (A | B)) | (A | B); + `, + output: ` +type A = 'A'; +type B = 'B'; +type C = 'C'; +type D = 'D'; +type F = (A | B) | ((C | D) & (A | B)) ; + `, errors: [ { messageId: 'duplicate', data: { - type, - previous: 'A', + type: 'Union', + previous: 'A | B', }, }, - ], - }, - ]; -}; - -ruleTester.run('no-duplicate-type-constituents', rule, { - valid: [ - ...valid('|'), - { - code: 'type T = A | A;', - options: [ { - ignoreUnions: true, + messageId: 'duplicate', + data: { + type: 'Union', + previous: 'A | B', + }, }, ], }, - ...valid('&'), { - code: 'type T = A & A;', - options: [ + code: ` +type A = 'A'; +type T = Record; + `, + output: ` +type A = 'A'; +type T = Record; + `, + errors: [ { - ignoreIntersections: true, + messageId: 'duplicate', + data: { + type: 'Union', + previous: 'A', + }, }, ], }, ], - invalid: [...invalid('|'), ...invalid('&')], }); From a6b23824543f36d6b549d45ebf6b7c9f23d9efce Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 14:08:53 +0900 Subject: [PATCH 48/50] refactor: Narrowing the type of options --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 08de5df590ed..a2cee2eb333a 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -83,7 +83,6 @@ export default util.createRule({ schema: [ { additionalProperties: false, - type: 'object', properties: { ignoreIntersections: { type: 'boolean', From 9de38e4df0b0ddc84c3edc3af1764e1b9ca74f5a Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 14:09:52 +0900 Subject: [PATCH 49/50] Revert "refactor: Narrowing the type of options" This reverts commit a6b23824543f36d6b549d45ebf6b7c9f23d9efce. --- .../eslint-plugin/src/rules/no-duplicate-type-constituents.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index a2cee2eb333a..08de5df590ed 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -83,6 +83,7 @@ export default util.createRule({ schema: [ { additionalProperties: false, + type: 'object', properties: { ignoreIntersections: { type: 'boolean', From abb92d836ca3ab972bd7bfe21e2e3a05467ca5a7 Mon Sep 17 00:00:00 2001 From: sajikix Date: Fri, 24 Mar 2023 14:18:50 +0900 Subject: [PATCH 50/50] refactor: use Set instead of array --- .../src/rules/no-duplicate-type-constituents.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts index 08de5df590ed..180ad3b340fc 100644 --- a/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts +++ b/packages/eslint-plugin/src/rules/no-duplicate-type-constituents.ts @@ -13,7 +13,7 @@ export type Options = [ export type MessageIds = 'duplicate'; -const astIgnoreKeys = ['range', 'loc', 'parent']; +const astIgnoreKeys = new Set(['range', 'loc', 'parent']); const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { if (actualNode === expectedNode) { @@ -34,10 +34,10 @@ const isSameAstNode = (actualNode: unknown, expectedNode: unknown): boolean => { ); } const actualNodeKeys = Object.keys(actualNode).filter( - key => !astIgnoreKeys.includes(key), + key => !astIgnoreKeys.has(key), ); const expectedNodeKeys = Object.keys(expectedNode).filter( - key => !astIgnoreKeys.includes(key), + key => !astIgnoreKeys.has(key), ); if (actualNodeKeys.length !== expectedNodeKeys.length) { return false;