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 677eb918831f..894afef48d7d 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 c31b742160ce..c0751b7bde67 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 e629d00e7be7..7528dc4ea8c8 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 894afef48d7d..0577fdd2fcea 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 c0751b7bde67..557f73d92161 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 0577fdd2fcea..7d45d4651e5d 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 7528dc4ea8c8..bd1c0ba06bc5 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 bd1c0ba06bc5..c37eebfe49e2 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 7d45d4651e5d..a68558b9409b 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 557f73d92161..716a293a38e3 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 d78ed9651566..a8f19039c77b 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 a8f19039c77b..f59b92f93623 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 f59b92f93623..9e7763d03f7b 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 716a293a38e3..4503df379ee8 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 9e7763d03f7b..8c6093739da5 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 4503df379ee8..b79c923a043f 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 b79c923a043f..26a564b7dc8b 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 8c6093739da5..b2a93b4f1162 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 26a564b7dc8b..ff1211b257b4 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 { pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy