From 0471e125ca068df997157a89f79573da3e8aef19 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Sun, 24 Sep 2023 15:04:32 +0300 Subject: [PATCH 01/12] feat(eslint-plugin): [no-unsafe-enum-comparison] add switch suggestion Closes #7643 --- .../src/rules/no-unsafe-enum-comparison.ts | 66 +++++++++++++++++-- .../rules/no-unsafe-enum-comparison.test.ts | 20 ++++-- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts index 677eb918831..894afef48d7 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts @@ -25,6 +25,27 @@ function typeViolates(leftTypeParts: ts.Type[], right: ts.Type): boolean { ); } +function getEnumKeyForLiteral( + leftEnumTypes: ts.Type[], + rightNode: TSESTree.Node, +): string | null { + const right = util.getStaticValue(rightNode); + + if (right === null) { + return null; + } + + for (const leftEnumType of leftEnumTypes) { + if (leftEnumType.value === right.value) { + const enumParentName = leftEnumType.symbol.parent.name; + + return `${enumParentName}.${leftEnumType.symbol.name}`; + } + } + + return null; +} + /** * @returns What type a type's enum value is (number or string), if either. */ @@ -48,6 +69,8 @@ export default util.createRule({ messages: { mismatched: 'The two values in this comparison do not have a shared enum type.', + mismatchedSimilar: + 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `{{replacement}}`?', }, schema: [], }, @@ -100,11 +123,44 @@ export default util.createRule({ } } - if ( - typeViolates(leftTypeParts, right) || - typeViolates(rightTypeParts, left) - ) { - context.report({ + if (typeViolates(leftTypeParts, right)) { + const leftEnumKey = getEnumKeyForLiteral(leftEnumTypes, node.right); + + if (leftEnumKey) { + // TODO: Add fixer. + return context.report({ + messageId: 'mismatchedSimilar', + node, + data: { + replacement: leftEnumKey, + }, + }); + } + + return context.report({ + messageId: 'mismatched', + node, + }); + } + + if (typeViolates(rightTypeParts, left)) { + const rightEnumKey = getEnumKeyForLiteral( + [...rightEnumTypes.values()], + node.left, + ); + + if (rightEnumKey) { + // TODO: Add fixer. + return context.report({ + messageId: 'mismatchedSimilar', + node, + data: { + replacement: rightEnumKey, + }, + }); + } + + return context.report({ messageId: 'mismatched', node, }); diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index c31b742160c..c0751b7bde6 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -545,10 +545,22 @@ ruleTester.run('strict-enums-comparison', rule, { mixed === 1; `, errors: [ - { messageId: 'mismatched' }, - { messageId: 'mismatched' }, - { messageId: 'mismatched' }, - { messageId: 'mismatched' }, + { + message: + 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Str.A`?', + }, + { + message: + 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Num.B`?', + }, + { + message: + 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Mixed.A`?', + }, + { + message: + 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Mixed.B`?', + }, ], }, { From 930b14af679d80653b91157c52df1844f37d259a Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 27 Sep 2023 09:38:33 +0300 Subject: [PATCH 02/12] refactor + add tests --- .../src/rules/enum-utils/shared.ts | 52 +++++++- .../src/rules/no-unsafe-enum-comparison.ts | 125 +++++++++--------- .../rules/no-unsafe-enum-comparison.test.ts | 78 ++++++++--- 3 files changed, 173 insertions(+), 82 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index e629d00e7be..7528dc4ea8c 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -1,3 +1,4 @@ +import type { TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; @@ -20,6 +21,21 @@ function getBaseEnumType(typeChecker: ts.TypeChecker, type: ts.Type): ts.Type { return typeChecker.getTypeAtLocation(symbol.valueDeclaration!.parent); } +/** + * Retrieve only the Enum literals from a type. for example: + * - 123 --> [] + * - {} --> [] + * - Fruit.Apple --> [Fruit.Apple] + * - Fruit.Apple | Vegetable.Lettuce --> [Fruit.Apple, Vegetable.Lettuce] + * - Fruit.Apple | Vegetable.Lettuce | 123 --> [Fruit.Apple, Vegetable.Lettuce] + * - T extends Fruit --> [Fruit] + */ +export function getEnumLiterals(type: ts.Type): ts.Type[] { + return tsutils + .unionTypeParts(type) + .filter(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral)); +} + /** * A type can have 0 or more enum types. For example: * - 123 --> [] @@ -33,8 +49,36 @@ export function getEnumTypes( typeChecker: ts.TypeChecker, type: ts.Type, ): ts.Type[] { - return tsutils - .unionTypeParts(type) - .filter(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral)) - .map(type => getBaseEnumType(typeChecker, type)); + return getEnumLiterals(type).map(type => getBaseEnumType(typeChecker, type)); +} + +/** + * Returns the enum key that matches the given literal node, or null if none + * match. For example: + * ```ts + * enum Fruit { + * Apple = 'apple', + * Banana = 'banana', + * } + * + * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'apple') --> 'Fruit.Apple' + * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'banana') --> 'Fruit.Banana' + * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'orange') --> null + * ``` + */ +export function getEnumKeyForLiteral( + enumLiterals: ts.Type[], + literal: unknown, +): string | null { + for (const enumLiteral of enumLiterals) { + // @ts-expect-error Not sure why `value` is not on `enumLiteral`. + if (enumLiteral.value === literal) { + const { symbol } = enumLiteral; + + // @ts-expect-error Not sure why `parent` is not on `symbol`. + return `${symbol.parent.name}.${symbol.name}`; + } + } + + return null; } diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts index 894afef48d7..0577fdd2fce 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts @@ -1,9 +1,13 @@ -import type { TSESTree } from '@typescript-eslint/utils'; +import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; import * as util from '../util'; -import { getEnumTypes } from './enum-utils/shared'; +import { + getEnumKeyForLiteral, + getEnumLiterals, + getEnumTypes, +} from './enum-utils/shared'; /** * @returns Whether the right type is an unsafe comparison against any left type. @@ -25,27 +29,6 @@ function typeViolates(leftTypeParts: ts.Type[], right: ts.Type): boolean { ); } -function getEnumKeyForLiteral( - leftEnumTypes: ts.Type[], - rightNode: TSESTree.Node, -): string | null { - const right = util.getStaticValue(rightNode); - - if (right === null) { - return null; - } - - for (const leftEnumType of leftEnumTypes) { - if (leftEnumType.value === right.value) { - const enumParentName = leftEnumType.symbol.parent.name; - - return `${enumParentName}.${leftEnumType.symbol.name}`; - } - } - - return null; -} - /** * @returns What type a type's enum value is (number or string), if either. */ @@ -60,6 +43,7 @@ function getEnumValueType(type: ts.Type): ts.TypeFlags | undefined { export default util.createRule({ name: 'no-unsafe-enum-comparison', meta: { + hasSuggestions: true, type: 'suggestion', docs: { description: 'Disallow comparing an enum value with a non-enum value', @@ -69,8 +53,7 @@ export default util.createRule({ messages: { mismatched: 'The two values in this comparison do not have a shared enum type.', - mismatchedSimilar: - 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `{{replacement}}`?', + replaceValueWithEnum: 'Replace with an enum value comparison.', }, schema: [], }, @@ -123,46 +106,64 @@ export default util.createRule({ } } - if (typeViolates(leftTypeParts, right)) { - const leftEnumKey = getEnumKeyForLiteral(leftEnumTypes, node.right); - - if (leftEnumKey) { - // TODO: Add fixer. - return context.report({ - messageId: 'mismatchedSimilar', - node, - data: { - replacement: leftEnumKey, - }, - }); - } - - return context.report({ + if ( + typeViolates(leftTypeParts, right) || + typeViolates(rightTypeParts, left) + ) { + context.report({ messageId: 'mismatched', node, - }); - } - - if (typeViolates(rightTypeParts, left)) { - const rightEnumKey = getEnumKeyForLiteral( - [...rightEnumTypes.values()], - node.left, - ); - - if (rightEnumKey) { - // TODO: Add fixer. - return context.report({ - messageId: 'mismatchedSimilar', - node, - data: { - replacement: rightEnumKey, + suggest: [ + { + messageId: 'replaceValueWithEnum', + fix(fixer): TSESLint.RuleFix { + const sourceCode = context.getSourceCode(); + const leftExpression = sourceCode.getText(node.left); + const rightExpression = sourceCode.getText(node.right); + + // Replace the right side with an enum key if possible: + // + // ```ts + // Fruit.Apple === 'apple'; // Fruit.Apple === Fruit.Apple + // ``` + const leftEnumKey = getEnumKeyForLiteral( + getEnumLiterals(left), + util.getStaticValue(node.right)?.value, + ); + + if (leftEnumKey != null) { + return fixer.replaceText( + node, + `${leftExpression} ${node.operator} ${leftEnumKey}`, + ); + } + + // Replace the left side with an enum key if possible: + // + // ```ts + // declare const fruit: Fruit; + // 'apple' === Fruit.Apple; // Fruit.Apple === Fruit.Apple + // ``` + const rightEnumKey = getEnumKeyForLiteral( + getEnumLiterals(right), + util.getStaticValue(node.left)?.value, + ); + + if (rightEnumKey != null) { + return fixer.replaceText( + node, + `${rightEnumKey} ${node.operator} ${rightExpression}`, + ); + } + + // TODO: Should it have a "void fix" here? Is there any other way to handle this? + return fixer.replaceText( + node, + `${leftExpression} ${node.operator} ${rightExpression}`, + ); + }, }, - }); - } - - return context.report({ - messageId: 'mismatched', - node, + ], }); } }, diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index c0751b7bde6..557f73d9216 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -545,22 +545,10 @@ ruleTester.run('strict-enums-comparison', rule, { mixed === 1; `, errors: [ - { - message: - 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Str.A`?', - }, - { - message: - 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Num.B`?', - }, - { - message: - 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Mixed.A`?', - }, - { - message: - 'The two values in this comparison do not have a shared enum type. Did you mean to compare to `Mixed.B`?', - }, + { messageId: 'mismatched' }, + { messageId: 'mismatched' }, + { messageId: 'mismatched' }, + { messageId: 'mismatched' }, ], }, { @@ -577,5 +565,63 @@ ruleTester.run('strict-enums-comparison', rule, { `, errors: [{ messageId: 'mismatched' }], }, + { + code: + `enum Str {A = 'a', B = 'b'} ` + + `declare const str: Str; ` + + `str === 'b';`, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: `enum Str {A = 'a', B = 'b'} declare const str: Str; str === Str.B;`, + }, + ], + }, + ], + }, + { + code: + `enum Num {A = 1, B = 2} ` + `declare const num: Num; ` + `num === 1;`, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: `enum Num {A = 1, B = 2} declare const num: Num; num === Num.A;`, + }, + ], + }, + ], + }, + { + code: + `enum Mixed {A = 1, B = 'b'} ` + + `declare const mixed: Mixed; ` + + `mixed === 1 || mixed === 'b';`, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: `enum Mixed {A = 1, B = 'b'} declare const mixed: Mixed; mixed === Mixed.A || mixed === 'b';`, + }, + ], + }, + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: `enum Mixed {A = 1, B = 'b'} declare const mixed: Mixed; mixed === 1 || mixed === Mixed.B;`, + }, + ], + }, + ], + }, ], }); From b800d1bf2e194d24614818e9ba12d684ea33521e Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 27 Sep 2023 09:47:52 +0300 Subject: [PATCH 03/12] remove "void fixer" --- .../eslint-plugin/src/rules/no-unsafe-enum-comparison.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts index 0577fdd2fce..7d45d4651e5 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts @@ -116,7 +116,7 @@ export default util.createRule({ suggest: [ { messageId: 'replaceValueWithEnum', - fix(fixer): TSESLint.RuleFix { + fix(fixer): TSESLint.RuleFix | null { const sourceCode = context.getSourceCode(); const leftExpression = sourceCode.getText(node.left); const rightExpression = sourceCode.getText(node.right); @@ -156,11 +156,7 @@ export default util.createRule({ ); } - // TODO: Should it have a "void fix" here? Is there any other way to handle this? - return fixer.replaceText( - node, - `${leftExpression} ${node.operator} ${rightExpression}`, - ); + return null; }, }, ], From d901495cc9650468a62822298c504a37d390dde0 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 27 Sep 2023 10:28:55 +0300 Subject: [PATCH 04/12] fix lint --- packages/eslint-plugin/src/rules/enum-utils/shared.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index 7528dc4ea8c..bd1c0ba06bc 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -1,4 +1,3 @@ -import type { TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; From b49c446040f7df8e0ed8d480a5b8d1a3e68f3cf9 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 20:22:49 +0300 Subject: [PATCH 05/12] wip --- .../src/rules/enum-utils/shared.ts | 22 +++-- .../src/rules/no-unsafe-enum-comparison.ts | 37 ++++----- .../rules/no-unsafe-enum-comparison.test.ts | 82 +++++++++++++++---- 3 files changed, 98 insertions(+), 43 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index bd1c0ba06bc..c37eebfe49e 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -29,10 +29,12 @@ function getBaseEnumType(typeChecker: ts.TypeChecker, type: ts.Type): ts.Type { * - Fruit.Apple | Vegetable.Lettuce | 123 --> [Fruit.Apple, Vegetable.Lettuce] * - T extends Fruit --> [Fruit] */ -export function getEnumLiterals(type: ts.Type): ts.Type[] { +export function getEnumLiterals(type: ts.Type): ts.LiteralType[] { return tsutils .unionTypeParts(type) - .filter(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral)); + .filter((subType): subType is ts.LiteralType => + util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral), + ); } /** @@ -62,20 +64,24 @@ export function getEnumTypes( * * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'apple') --> 'Fruit.Apple' * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'banana') --> 'Fruit.Banana' - * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'orange') --> null + * getEnumKeyForLiteral([Fruit.Apple, Fruit.Banana], 'cherry') --> null * ``` */ export function getEnumKeyForLiteral( - enumLiterals: ts.Type[], + enumLiterals: ts.LiteralType[], literal: unknown, ): string | null { for (const enumLiteral of enumLiterals) { - // @ts-expect-error Not sure why `value` is not on `enumLiteral`. if (enumLiteral.value === literal) { - const { symbol } = enumLiteral; + const memberSymbol = enumLiteral.symbol; - // @ts-expect-error Not sure why `parent` is not on `symbol`. - return `${symbol.parent.name}.${symbol.name}`; + const enumSymbol = (memberSymbol.valueDeclaration as ts.EnumMember) + .parent; + + const enumName = enumSymbol.name.getText(); + const memberName = memberSymbol.name; + + return `${enumName}.${memberName}`; } } diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts index 7d45d4651e5..a68558b9409 100644 --- a/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-comparison.ts @@ -2,7 +2,12 @@ import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'ts-api-utils'; import * as ts from 'typescript'; -import * as util from '../util'; +import { + createRule, + getParserServices, + getStaticValue, + isTypeFlagSet, +} from '../util'; import { getEnumKeyForLiteral, getEnumLiterals, @@ -33,14 +38,14 @@ function typeViolates(leftTypeParts: ts.Type[], right: ts.Type): boolean { * @returns What type a type's enum value is (number or string), if either. */ function getEnumValueType(type: ts.Type): ts.TypeFlags | undefined { - return util.isTypeFlagSet(type, ts.TypeFlags.EnumLike) - ? util.isTypeFlagSet(type, ts.TypeFlags.NumberLiteral) + return isTypeFlagSet(type, ts.TypeFlags.EnumLike) + ? isTypeFlagSet(type, ts.TypeFlags.NumberLiteral) ? ts.TypeFlags.Number : ts.TypeFlags.String : undefined; } -export default util.createRule({ +export default createRule({ name: 'no-unsafe-enum-comparison', meta: { hasSuggestions: true, @@ -59,7 +64,7 @@ export default util.createRule({ }, defaultOptions: [], create(context) { - const parserServices = util.getParserServices(context); + const parserServices = getParserServices(context); const typeChecker = parserServices.program.getTypeChecker(); return { @@ -117,10 +122,6 @@ export default util.createRule({ { messageId: 'replaceValueWithEnum', fix(fixer): TSESLint.RuleFix | null { - const sourceCode = context.getSourceCode(); - const leftExpression = sourceCode.getText(node.left); - const rightExpression = sourceCode.getText(node.right); - // Replace the right side with an enum key if possible: // // ```ts @@ -128,14 +129,11 @@ export default util.createRule({ // ``` const leftEnumKey = getEnumKeyForLiteral( getEnumLiterals(left), - util.getStaticValue(node.right)?.value, + getStaticValue(node.right)?.value, ); - if (leftEnumKey != null) { - return fixer.replaceText( - node, - `${leftExpression} ${node.operator} ${leftEnumKey}`, - ); + if (leftEnumKey) { + return fixer.replaceText(node.right, leftEnumKey); } // Replace the left side with an enum key if possible: @@ -146,14 +144,11 @@ export default util.createRule({ // ``` const rightEnumKey = getEnumKeyForLiteral( getEnumLiterals(right), - util.getStaticValue(node.left)?.value, + getStaticValue(node.left)?.value, ); - if (rightEnumKey != null) { - return fixer.replaceText( - node, - `${rightEnumKey} ${node.operator} ${rightExpression}`, - ); + if (rightEnumKey) { + return fixer.replaceText(node.left, rightEnumKey); } return null; diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index 557f73d9216..716a293a38e 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -566,58 +566,112 @@ ruleTester.run('strict-enums-comparison', rule, { errors: [{ messageId: 'mismatched' }], }, { - code: - `enum Str {A = 'a', B = 'b'} ` + - `declare const str: Str; ` + - `str === 'b';`, + code: ` + enum Str { + A = 'a', + B = 'b', + } + declare const str: Str; + str === 'b'; + `, errors: [ { messageId: 'mismatched', suggestions: [ { messageId: 'replaceValueWithEnum', - output: `enum Str {A = 'a', B = 'b'} declare const str: Str; str === Str.B;`, + output: ` + enum Str { + A = 'a', + B = 'b', + } + declare const str: Str; + str === Str.B; + `, }, ], }, ], }, { - code: - `enum Num {A = 1, B = 2} ` + `declare const num: Num; ` + `num === 1;`, + code: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + 1 === num; + `, errors: [ { messageId: 'mismatched', suggestions: [ { messageId: 'replaceValueWithEnum', - output: `enum Num {A = 1, B = 2} declare const num: Num; num === Num.A;`, + output: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + Num.A === num; + `, }, ], }, ], }, { - code: - `enum Mixed {A = 1, B = 'b'} ` + - `declare const mixed: Mixed; ` + - `mixed === 1 || mixed === 'b';`, + code: ` + enum Mixed { + A = 1, + B = 'b', + } + declare const mixed: Mixed; + mixed === 1; + `, errors: [ { messageId: 'mismatched', suggestions: [ { messageId: 'replaceValueWithEnum', - output: `enum Mixed {A = 1, B = 'b'} declare const mixed: Mixed; mixed === Mixed.A || mixed === 'b';`, + output: ` + enum Mixed { + A = 1, + B = 'b', + } + declare const mixed: Mixed; + mixed === Mixed.A; + `, }, ], }, + ], + }, + { + code: ` + enum Mixed { + A = 1, + B = 'b', + } + declare const mixed: Mixed; + mixed === 'b'; + `, + errors: [ { messageId: 'mismatched', suggestions: [ { messageId: 'replaceValueWithEnum', - output: `enum Mixed {A = 1, B = 'b'} declare const mixed: Mixed; mixed === 1 || mixed === Mixed.B;`, + output: ` + enum Mixed { + A = 1, + B = 'b', + } + declare const mixed: Mixed; + mixed === Mixed.B; + `, }, ], }, From 42928a5342bdf7e7dcaafcd5dc0b27127a5d1fa9 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 20:31:16 +0300 Subject: [PATCH 06/12] beautify --- packages/eslint-plugin/src/rules/enum-utils/shared.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index d78ed965156..a8f19039c77 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -74,7 +74,6 @@ export function getEnumKeyForLiteral( for (const enumLiteral of enumLiterals) { if (enumLiteral.value === literal) { const memberSymbol = enumLiteral.symbol; - const enumSymbol = (memberSymbol.valueDeclaration as ts.EnumMember) .parent; From 32c3d7575b46308a6d190d14d3a29d50143131fd Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 20:38:01 +0300 Subject: [PATCH 07/12] just changing a wrong variable name --- packages/eslint-plugin/src/rules/enum-utils/shared.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index a8f19039c77..f59b92f9362 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -74,10 +74,10 @@ export function getEnumKeyForLiteral( for (const enumLiteral of enumLiterals) { if (enumLiteral.value === literal) { const memberSymbol = enumLiteral.symbol; - const enumSymbol = (memberSymbol.valueDeclaration as ts.EnumMember) + const enumDeclaration = (memberSymbol.valueDeclaration as ts.EnumMember) .parent; - const enumName = enumSymbol.name.getText(); + const enumName = enumDeclaration.name.getText(); const memberName = memberSymbol.name; return `${enumName}.${memberName}`; From 3a923ca6b583e570747d5be578d6dd87ec1e584d Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 20:40:12 +0300 Subject: [PATCH 08/12] just making it look & feel better and consistent --- packages/eslint-plugin/src/rules/enum-utils/shared.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index f59b92f9362..9e7763d03f7 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -73,12 +73,13 @@ export function getEnumKeyForLiteral( ): string | null { for (const enumLiteral of enumLiterals) { if (enumLiteral.value === literal) { - const memberSymbol = enumLiteral.symbol; - const enumDeclaration = (memberSymbol.valueDeclaration as ts.EnumMember) - .parent; + const { symbol } = enumLiteral; + + const memberDeclaration = symbol.valueDeclaration as ts.EnumMember; + const enumDeclaration = memberDeclaration.parent; const enumName = enumDeclaration.name.getText(); - const memberName = memberSymbol.name; + const memberName = memberDeclaration.name.getText(); return `${enumName}.${memberName}`; } From a20fe15a9c252754011c9b1d919f80c5aaffdf4d Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 21:48:39 +0300 Subject: [PATCH 09/12] add failing test --- .../rules/no-unsafe-enum-comparison.test.ts | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index 716a293a38e..4503df379ee 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -677,5 +677,31 @@ ruleTester.run('strict-enums-comparison', rule, { }, ], }, + { + code: ` + enum StringKey { + 'a' = 1, + } + declare const stringKey: StringKey; + stringKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum StringKey { + 'a' = 1, + } + declare const stringKey: StringKey; + stringKey === StringKey['a']; + `, + }, + ], + }, + ], + }, ], }); From 07f93067635c8d2483b1beabda93bf8ec399e674 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Tue, 17 Oct 2023 22:56:30 +0300 Subject: [PATCH 10/12] wip --- .../src/rules/enum-utils/shared.ts | 18 +++++++++-- .../rules/no-unsafe-enum-comparison.test.ts | 32 +++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index 9e7763d03f7..8c6093739da 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -78,10 +78,22 @@ export function getEnumKeyForLiteral( const memberDeclaration = symbol.valueDeclaration as ts.EnumMember; const enumDeclaration = memberDeclaration.parent; - const enumName = enumDeclaration.name.getText(); - const memberName = memberDeclaration.name.getText(); + const memberNameIdentifier = memberDeclaration.name; + const enumName = enumDeclaration.name.text; - return `${enumName}.${memberName}`; + switch (memberNameIdentifier.kind) { + case ts.SyntaxKind.Identifier: + return `${enumName}.${memberNameIdentifier.text}`; + + case ts.SyntaxKind.StringLiteral: + return `${enumName}['${memberNameIdentifier.text}']`; + + case ts.SyntaxKind.ComputedPropertyName: + return `${enumName}[${memberNameIdentifier.expression.getText()}]`; + + default: + break; + } } } diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index 4503df379ee..b79c923a043 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -680,7 +680,7 @@ ruleTester.run('strict-enums-comparison', rule, { { code: ` enum StringKey { - 'a' = 1, + 'test-key' /* with comment */ = 1, } declare const stringKey: StringKey; stringKey === 1; @@ -693,10 +693,36 @@ ruleTester.run('strict-enums-comparison', rule, { messageId: 'replaceValueWithEnum', output: ` enum StringKey { - 'a' = 1, + 'test-key' /* with comment */ = 1, } declare const stringKey: StringKey; - stringKey === StringKey['a']; + stringKey === StringKey['test-key']; + `, + }, + ], + }, + ], + }, + { + code: ` + enum ComputedKey { + ['test-key' /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum ComputedKey { + ['test-key' /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === ComputedKey['test-key']; `, }, ], From 9c492f7fd68c564f87749c92515cbbd73bcc39dc Mon Sep 17 00:00:00 2001 From: StyleShit Date: Wed, 18 Oct 2023 19:43:48 +0300 Subject: [PATCH 11/12] added some more tests to cover stupid cases --- .../rules/no-unsafe-enum-comparison.test.ts | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index b79c923a043..26a564b7dc8 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -593,6 +593,34 @@ ruleTester.run('strict-enums-comparison', rule, { }, ], }, + { + code: ` + enum Str { + A = 'a', + AB = 'ab', + } + declare const str: Str; + str === 'a' + 'b'; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Str { + A = 'a', + AB = 'ab', + } + declare const str: Str; + str === Str.AB; + `, + }, + ], + }, + ], + }, { code: ` enum Num { @@ -621,6 +649,62 @@ ruleTester.run('strict-enums-comparison', rule, { }, ], }, + { + code: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + 1 /* with */ === /* comment */ num; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + Num.A /* with */ === /* comment */ num; + `, + }, + ], + }, + ], + }, + { + code: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + 1 + 1 === num; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum Num { + A = 1, + B = 2, + } + declare const num: Num; + Num.B === num; + `, + }, + ], + }, + ], + }, { code: ` enum Mixed { @@ -729,5 +813,60 @@ ruleTester.run('strict-enums-comparison', rule, { }, ], }, + { + code: ` + enum ComputedKey { + [\`test-key\` /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum ComputedKey { + [\`test-key\` /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === ComputedKey[\`test-key\`]; + `, + }, + ], + }, + ], + }, + { + code: ` + enum ComputedKey { + [\`test- + key\` /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum ComputedKey { + [\`test- + key\` /* with comment */] = 1, + } + declare const computedKey: ComputedKey; + computedKey === ComputedKey[\`test- + key\`]; + `, + }, + ], + }, + ], + }, ], }); From cf560404a626abebff0dc29fcb19d8c21c4cfda4 Mon Sep 17 00:00:00 2001 From: StyleShit Date: Thu, 19 Oct 2023 20:29:02 +0300 Subject: [PATCH 12/12] moar stupid tests --- .../src/rules/enum-utils/shared.ts | 7 +- .../rules/no-unsafe-enum-comparison.test.ts | 78 +++++++++++++++++++ 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts index 8c6093739da..b2a93b4f116 100644 --- a/packages/eslint-plugin/src/rules/enum-utils/shared.ts +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -85,8 +85,11 @@ export function getEnumKeyForLiteral( case ts.SyntaxKind.Identifier: return `${enumName}.${memberNameIdentifier.text}`; - case ts.SyntaxKind.StringLiteral: - return `${enumName}['${memberNameIdentifier.text}']`; + case ts.SyntaxKind.StringLiteral: { + const memberName = memberNameIdentifier.text.replace(/'/g, "\\'"); + + return `${enumName}['${memberName}']`; + } case ts.SyntaxKind.ComputedPropertyName: return `${enumName}[${memberNameIdentifier.expression.getText()}]`; diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts index 26a564b7dc8..ff1211b257b 100644 --- a/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-comparison.test.ts @@ -787,6 +787,84 @@ ruleTester.run('strict-enums-comparison', rule, { }, ], }, + { + code: ` + enum StringKey { + "key-'with-single'-quotes" = 1, + } + declare const stringKey: StringKey; + stringKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum StringKey { + "key-'with-single'-quotes" = 1, + } + declare const stringKey: StringKey; + stringKey === StringKey['key-\\'with-single\\'-quotes']; + `, + }, + ], + }, + ], + }, + { + code: ` + enum StringKey { + 'key-"with-double"-quotes' = 1, + } + declare const stringKey: StringKey; + stringKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum StringKey { + 'key-"with-double"-quotes' = 1, + } + declare const stringKey: StringKey; + stringKey === StringKey['key-"with-double"-quotes']; + `, + }, + ], + }, + ], + }, + { + code: ` + enum StringKey { + 'key-\`with-backticks\`-quotes' = 1, + } + declare const stringKey: StringKey; + stringKey === 1; + `, + errors: [ + { + messageId: 'mismatched', + suggestions: [ + { + messageId: 'replaceValueWithEnum', + output: ` + enum StringKey { + 'key-\`with-backticks\`-quotes' = 1, + } + declare const stringKey: StringKey; + stringKey === StringKey['key-\`with-backticks\`-quotes']; + `, + }, + ], + }, + ], + }, { code: ` enum ComputedKey {