From c3d6b80ab5c1f81eff289c8a37076261a7b26ac8 Mon Sep 17 00:00:00 2001 From: Kamil Dubiel <166366002+kamdubiel@users.noreply.github.com> Date: Tue, 14 May 2024 15:15:37 +0200 Subject: [PATCH] feat: forbid enums in React (#129) --- .changeset/chilly-seas-cross.md | 5 ++ README.md | 1 + package.json | 5 ++ pnpm-lock.yaml | 14 +++++ src/configs/react.ts | 9 ++- .../typescript-enum-no-const-enum.test.ts | 30 ++++++++++ .../react/typescript-enum-no-enum.test.ts | 58 +++++++++++++++++++ tests/configs/react/utils.ts | 11 +++- 8 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 .changeset/chilly-seas-cross.md create mode 100644 tests/configs/react/typescript-enum-no-const-enum.test.ts create mode 100644 tests/configs/react/typescript-enum-no-enum.test.ts diff --git a/.changeset/chilly-seas-cross.md b/.changeset/chilly-seas-cross.md new file mode 100644 index 0000000..20fdd21 --- /dev/null +++ b/.changeset/chilly-seas-cross.md @@ -0,0 +1,5 @@ +--- +"@infinum/eslint-plugin": major +--- + +Added `eslint-plugin-typescript-enum` dependency and rules to React config diff --git a/README.md b/README.md index 0c93b9d..a37d46c 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ eslint-plugin-jsx-a11y@6.8 \ @typescript-eslint/eslint-plugin@7.8 \ @typescript-eslint/parser@7.8 \ @next/eslint-plugin-next@14.2 \ +eslint-plugin-typescript-enum@2.1 \ eslint-plugin-chakra-ui@0.11 ``` diff --git a/package.json b/package.json index b953ce9..5ec7611 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "eslint-plugin-react": "~7.34.0", "eslint-plugin-react-hooks": "~4.6.0", "eslint-plugin-rxjs": "~5.0.3", + "eslint-plugin-typescript-enum": "~2.1.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -75,6 +76,9 @@ "eslint-plugin-chakra-ui": { "optional": true }, + "eslint-plugin-typescript-enum": { + "optional": true + }, "typescript": { "optional": true } @@ -100,6 +104,7 @@ "eslint-plugin-react": "7.34.1", "eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-rxjs": "5.0.3", + "eslint-plugin-typescript-enum": "2.1.0", "husky": "9.0.11", "lint-staged": "15.2.2", "plop": "~3.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5ff6c3e..9544d16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: eslint-plugin-rxjs: specifier: 5.0.3 version: 5.0.3(eslint@8.57.0)(typescript@5.4.5) + eslint-plugin-typescript-enum: + specifier: 2.1.0 + version: 2.1.0(eslint@8.57.0)(typescript@5.4.5) husky: specifier: 9.0.11 version: 9.0.11 @@ -1229,6 +1232,9 @@ packages: eslint: ^8.0.0 typescript: '>=4.0.0' + eslint-plugin-typescript-enum@2.1.0: + resolution: {integrity: sha512-n6RO89KJ2V2nHVAdIq1q3IBeYZSNZjBreqXOzpjmsBtw+NNhSTTSQXqwO00VYOce9Gy8cr2cDEYpj0Km+Ij90Q==} + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -4376,6 +4382,14 @@ snapshots: transitivePeerDependencies: - supports-color + eslint-plugin-typescript-enum@2.1.0(eslint@8.57.0)(typescript@5.4.5): + dependencies: + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.0)(typescript@5.4.5) + transitivePeerDependencies: + - eslint + - supports-color + - typescript + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 diff --git a/src/configs/react.ts b/src/configs/react.ts index 28a6413..e52d27d 100644 --- a/src/configs/react.ts +++ b/src/configs/react.ts @@ -5,7 +5,12 @@ export default { jest: true, es2022: true, }, - extends: ['plugin:react/recommended', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended'], + extends: [ + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:typescript-enum/recommended', + ], settings: { react: { version: process.env.NODE_ENV === 'test' ? 'v18.2.0' : 'detect', @@ -29,5 +34,7 @@ export default { 'react/react-in-jsx-scope': 'off', 'react/self-closing-comp': ['warn', { component: true, html: true }], 'react/no-unknown-property': ['error', { ignore: ['css'] }], + 'typescript-enum/no-enum': 'warn', + 'typescript-enum/no-const-enum': 'warn', }, } satisfies TSESLint.Linter.Config; diff --git a/tests/configs/react/typescript-enum-no-const-enum.test.ts b/tests/configs/react/typescript-enum-no-const-enum.test.ts new file mode 100644 index 0000000..2af08ed --- /dev/null +++ b/tests/configs/react/typescript-enum-no-const-enum.test.ts @@ -0,0 +1,30 @@ +import { getReactTester } from './utils'; + +const ruleName = 'typescript-enum/no-const-enum'; + +const { test, validate } = getReactTester(ruleName, { parser: '@typescript-eslint/parser' }); + +test('should warn for const enums', () => + validate( + ` + const enum Foo { + Bar = "Bar", + Baz = "Baz", + } + `, + [], + [ + 'Unexpected `const` enum, use regular enum instead. As a side note, in modern TypeScript, you may not need an enum when an object with `as const` could suffice.', + ] + )); + +test('should not warn for union types', () => + validate( + ` + type Foo = "Bar" | "Baz"; + `, + [], + [] + )); + +test.run(); diff --git a/tests/configs/react/typescript-enum-no-enum.test.ts b/tests/configs/react/typescript-enum-no-enum.test.ts new file mode 100644 index 0000000..af28fda --- /dev/null +++ b/tests/configs/react/typescript-enum-no-enum.test.ts @@ -0,0 +1,58 @@ +import { getReactTester } from './utils'; + +const ruleName = 'typescript-enum/no-enum'; + +const { test, validate } = getReactTester(ruleName, { parser: '@typescript-eslint/parser' }); + +test('should warn for enums without specified values', () => + validate( + ` + enum Foo { + Bar, + Baz + } + `, + [], + ['In modern TypeScript, you may not need an enum when an object with `as const` could suffice.'] + )); + +test('should warn for enums with specified values', () => + validate( + ` + enum Foo { + Bar = 'Bar', + Baz = 'Baz' + } + + enum Foo { + Bar = "BAR", + Baz = "BAZ", + } + `, + [], + [ + 'In modern TypeScript, you may not need an enum when an object with `as const` could suffice.', + 'In modern TypeScript, you may not need an enum when an object with `as const` could suffice.', + ] + )); + +test('should not warn for consts', () => + validate( + ` + const Foo = { + Bar: 0, + Baz: 1, + } as const; + + type Foo = "Bar" | "Baz"; + + const Foo = { + Bar: "BAR", + Baz: "BAZ", + } as const; + `, + [], + [] + )); + +test.run(); diff --git a/tests/configs/react/utils.ts b/tests/configs/react/utils.ts index c4a60c2..ec11150 100644 --- a/tests/configs/react/utils.ts +++ b/tests/configs/react/utils.ts @@ -1,10 +1,17 @@ +import { TSESLint } from '@typescript-eslint/utils'; import eslintConfig from '../../../src/configs/react'; import { getTester } from '../../utils'; -export const getReactTester = (ruleName: string): ReturnType => { +export const getReactTester = ( + ruleName: string, + configOverride: TSESLint.ESLint.ESLintOptions['baseConfig'] = {} +): ReturnType => { return getTester({ filePath: __filename, - eslintConfig, + eslintConfig: { + ...eslintConfig, + ...configOverride, + }, ruleName: ruleName, }); };