From 30186458dba498311de81f330ca6ae5c1d3a2f6c Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 13:13:03 +0000 Subject: [PATCH 1/4] Start no-empty-object-type --- .../docs/rules/no-empty-object-type.mdx | 171 ++++++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/no-empty-object-type.ts | 186 ++++++ .../tests/rules/no-empty-object-type.test.ts | 574 ++++++++++++++++++ 5 files changed, 934 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/no-empty-object-type.mdx create mode 100644 packages/eslint-plugin/src/rules/no-empty-object-type.ts create mode 100644 packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx new file mode 100644 index 000000000000..732f31aca267 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -0,0 +1,171 @@ +--- +description: 'Disallow accidentally using the "empty object" type.' +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-empty-object-type** for documentation. + +The `{}`, or "empty object" type in TypeScript is a common source of confusion for developers unfamiliar with TypeScript's structural typing. +`{}` represents any _non-nullish value_, including literals like `0` and `""`: + +```ts +let anyNonNullishValue: {} = 'Intentionally allowed by TypeScript.'; +``` + +Often, developers writing `{}` actually mean either: + +- `object`: representing any _object_ value +- `unknown`: representing any value at all, including `null` and `undefined` + +In other words, the "empty object" type `{}` really means _"any value that is defined"_. +That includes arrays, class instances, functions, and primitives such as `string` and `symbol`. + +To avoid confusion around the `{}` type allowing any _non-nullish value_, this rule bans usage of the `{}` type. +That includes interfaces and object type aliases with no fields. + +:::tip +If you do have a use case for an API allowing `{}`, you can always configure the [rule's options](#options), use an [ESLint disable comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1), or [disable the rule in your ESLint config](https://eslint.org/docs/latest/use/configure/rules#using-configuration-files-1). +::: + +Note that this rule does not report on: + +- `{}` as a type constituent in an intersection type (e.g. types like TypeScript's built-in `type NonNullable = T & {}`), as this can be useful in type system operations. +- Interfaces that extend from multiple other interfaces. + +## Examples + + + + +```ts +let anyObject: {}; +let anyValue: {}; + +interface AnyObjectA {} +interface AnyValueA {} + +type AnyObjectB = {}; +type AnyValueB = {}; +``` + + + + +```ts +let anyObject: object; +let anyValue: unknown; + +type AnyObjectA = object; +type AnyValueA = unknown; + +type AnyObjectB = object; +type AnyValueB = unknown; + +let objectWith: { property: boolean }; + +interface InterfaceWith { + property: boolean; +} + +type TypeWith = { property: boolean }; +``` + + + + +## Options + +By default, this rule flags both interfaces and object types. + +### `allowInterfaces` + +Whether to allow empty interfaces, as one of: + +- `'always'`: to always allow interfaces with no fields +- `'never'` _(default)_: to never allow interfaces with no fields +- `'with-single-extends'`: to allow empty interfaces that `extend` from a single base interface + +Examples of code for this rule with `{ allowInterfaces: 'with-single-extends' }`: + + + +```ts option='{ "allowInterfaces": "with-single-extends" }' showPlaygroundButton +interface Foo {} +``` + + +```ts option='{ "allowInterfaces": "with-single-extends" }' showPlaygroundButton +interface Base { + value: boolean; +} + +interface Derived extends Base {} + +```` + + + +### `allowObjectTypes` + +Whether to allow empty object type literals, as one of: + +- `'always'`: to always allow object type literals with no fields +- `'never'` _(default)_: to never allow object type literals with no fields + +Examples of code for this rule with `{ allowObjectTypes: 'always' }`: + + + +```ts option='{ "allowObjectTypes": "always" }' showPlaygroundButton +interface Base {} +```` + + + +```ts option='{ "allowObjectTypes": "always" }' showPlaygroundButton +type Base = {}; +``` + + + +### `allowWithName` + +A stringified regular expression to allow interfaces and object type aliases with the configured name. +This can be useful if your existing code style includes a pattern of declaring empty types with `{}` instead of `object`. + +Examples of code for this rule with `{ allowWithName: 'Props$' }`: + + + + +```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton +interface InterfaceValue {} + +type TypeValue = {}; +``` + + + + +```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton +interface InterfaceProps {} + +type TypeProps = {}; +``` + + + + +## When Not To Use It + +If your code commonly needs to represent the _"any non-nullish value"_ type, this rule may not be for you. +Projects that extensively use type operations such as conditional types and mapped types oftentimes benefit from disabling this rule. + +## Further Reading + +- [Enhancement: [ban-types] Split the {} ban into a separate, better-phrased rule](https://github.com/typescript-eslint/typescript-eslint/issues/8700) +- [The Empty Object Type in TypeScript](https://www.totaltypescript.com/the-empty-object-type-in-typescript) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index ee778e7e48cd..228b01034839 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -55,6 +55,7 @@ export = { 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index fe0d36026bd1..62358555d7f3 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -47,6 +47,7 @@ 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'; +import noEmptyObjectType from './no-empty-object-type'; import noExplicitAny from './no-explicit-any'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; @@ -193,6 +194,7 @@ export default { 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, + 'no-empty-object-type': noEmptyObjectType, 'no-explicit-any': noExplicitAny, 'no-extra-non-null-assertion': noExtraNonNullAssertion, 'no-extra-parens': noExtraParens, diff --git a/packages/eslint-plugin/src/rules/no-empty-object-type.ts b/packages/eslint-plugin/src/rules/no-empty-object-type.ts new file mode 100644 index 000000000000..21db86ba2033 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-empty-object-type.ts @@ -0,0 +1,186 @@ +import type { TSESLint } from '@typescript-eslint/utils'; +import { AST_NODE_TYPES } from '@typescript-eslint/utils'; + +import { createRule } from '../util'; + +export type AllowInterfaces = 'always' | 'never' | 'with-single-extends'; + +export type AllowObjectTypes = 'always' | 'never'; + +export type Options = [ + { + allowInterfaces?: AllowInterfaces; + allowObjectTypes?: AllowObjectTypes; + allowWithName?: string; + }, +]; + +export type MessageIds = + | 'noEmptyInterface' + | 'noEmptyObject' + | 'noEmptyInterfaceWithSuper' + | 'replaceEmptyInterface' + | 'replaceEmptyInterfaceWithSuper' + | 'replaceEmptyObjectType'; + +const noEmptyMessage = (emptyType: string): string => + [ + `${emptyType} allows any non-nullish value, including literals like \`0\` and \`""\`.`, + "- If that's what you want, disable this lint rule with an inline comment or configure the '{{ option }}' rule option.", + '- If you want a type meaning "any object", you probably want `object` instead.', + '- If you want a type meaning "any value", you probably want `unknown` instead.', + ].join('\n'); + +export default createRule({ + name: 'no-empty-object-type', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow accidentally using the "empty object" type', + }, + hasSuggestions: true, + messages: { + noEmptyInterface: noEmptyMessage('An empty interface declaration'), + noEmptyObject: noEmptyMessage('The `{}` ("empty object") type'), + noEmptyInterfaceWithSuper: + 'An interface declaring no members is equivalent to its supertype.', + replaceEmptyInterface: 'Replace empty interface with `{{replacement}}`.', + replaceEmptyInterfaceWithSuper: + 'Replace empty interface with a type alias.', + replaceEmptyObjectType: 'Replace `{}` with `{{replacement}}`.', + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + allowInterfaces: { + enum: ['always', 'never', 'with-single-extends'], + type: 'string', + }, + allowObjectTypes: { + enum: ['always', 'in-type-alias-with-name', 'never'], + type: 'string', + }, + allowWithName: { + type: 'string', + }, + }, + }, + ], + }, + defaultOptions: [ + { + allowInterfaces: 'never', + allowObjectTypes: 'never', + }, + ], + create(context, [{ allowInterfaces, allowWithName, allowObjectTypes }]) { + const allowWithNameTester = allowWithName + ? new RegExp(allowWithName, 'u') + : undefined; + + return { + ...(allowInterfaces !== 'always' && { + TSInterfaceDeclaration(node): void { + if (allowWithNameTester?.test(node.id.name)) { + return; + } + + const extend = node.extends; + if ( + node.body.body.length !== 0 || + (extend.length === 1 && + allowInterfaces === 'with-single-extends') || + extend.length > 1 + ) { + return; + } + + const scope = context.sourceCode.getScope(node); + + const mergedWithClassDeclaration = scope.set + .get(node.id.name) + ?.defs.some( + def => def.node.type === AST_NODE_TYPES.ClassDeclaration, + ); + + if (extend.length === 0) { + context.report({ + data: { option: 'allowInterfaces' }, + node: node.id, + messageId: 'noEmptyInterface', + ...(!mergedWithClassDeclaration && { + suggest: ['object', 'unknown'].map(replacement => ({ + data: { replacement }, + fix(fixer): TSESLint.RuleFix { + const id = context.sourceCode.getText(node.id); + const typeParam = node.typeParameters + ? context.sourceCode.getText(node.typeParameters) + : ''; + + return fixer.replaceText( + node, + `type ${id}${typeParam} = ${replacement}`, + ); + }, + messageId: 'replaceEmptyInterface', + })), + }), + }); + return; + } + + context.report({ + node: node.id, + messageId: 'noEmptyInterfaceWithSuper', + ...(!mergedWithClassDeclaration && { + suggest: [ + { + fix(fixer): TSESLint.RuleFix { + const extended = context.sourceCode.getText(extend[0]); + const id = context.sourceCode.getText(node.id); + const typeParam = node.typeParameters + ? context.sourceCode.getText(node.typeParameters) + : ''; + + return fixer.replaceText( + node, + `type ${id}${typeParam} = ${extended}`, + ); + }, + messageId: 'replaceEmptyInterfaceWithSuper', + }, + ], + }), + }); + }, + }), + ...(allowObjectTypes !== 'always' && { + TSTypeLiteral(node): void { + if ( + node.members.length || + node.parent.type === AST_NODE_TYPES.TSIntersectionType || + (allowWithNameTester && + node.parent.type === AST_NODE_TYPES.TSTypeAliasDeclaration && + allowWithNameTester.test(node.parent.id.name)) + ) { + return; + } + + context.report({ + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + node, + suggest: ['object', 'unknown'].map(replacement => ({ + data: { replacement }, + messageId: 'replaceEmptyObjectType', + fix: (fixer): TSESLint.RuleFix => + fixer.replaceText(node, replacement), + })), + }); + }, + }), + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts new file mode 100644 index 000000000000..8bc0928ca1ba --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -0,0 +1,574 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-empty-object-type'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-empty-object-type', rule, { + valid: [ + ` +interface Base { + name: string; +} + `, + ` +interface Base { + name: string; +} +interface Derived { + age: number; +} +// valid because extending multiple interfaces can be used instead of a union type +interface Both extends Base, Derived {} + `, + { + code: 'interface Base {}', + options: [{ allowInterfaces: 'always' }], + }, + { + code: ` +interface Base { + name: string; +} +interface Derived extends Base {} + `, + options: [{ allowInterfaces: 'with-single-extends' }], + }, + { + code: ` +interface Base { + props: string; +} +interface Derived extends Base {} +class Derived {} + `, + options: [{ allowInterfaces: 'with-single-extends' }], + }, + 'let value: object;', + 'let value: Object;', + 'let value: { inner: true };', + 'type MyNonNullable = T & {};', + { + code: 'type Base = {};', + options: [{ allowObjectTypes: 'always' }], + }, + { + code: 'type Base = {};', + options: [{ allowWithName: 'Base' }], + }, + { + code: 'type BaseProps = {};', + options: [{ allowWithName: 'Props$' }], + }, + { + code: 'interface Base {}', + options: [{ allowWithName: 'Base' }], + }, + { + code: 'interface BaseProps {}', + options: [{ allowWithName: 'Props$' }], + }, + ], + invalid: [ + { + code: 'interface Base {}', + errors: [ + { + column: 11, + data: { option: 'allowInterfaces' }, + line: 1, + messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], + }, + ], + }, + { + code: 'interface Base {}', + errors: [ + { + column: 11, + data: { option: 'allowInterfaces' }, + line: 1, + messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], + }, + ], + options: [{ allowInterfaces: 'never' }], + }, + { + code: ` +interface Base { + props: string; +} +interface Derived extends Base {} +class Other {} + `, + errors: [ + { + line: 5, + column: 11, + messageId: 'noEmptyInterfaceWithSuper', + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + props: string; +} +type Derived = Base +class Other {} + `, + }, + ], + }, + ], + }, + { + code: ` +interface Base { + props: string; +} +interface Derived extends Base {} +class Derived {} + `, + errors: [ + { + line: 5, + column: 11, + messageId: 'noEmptyInterfaceWithSuper', + }, + ], + }, + { + code: ` +interface Base { + props: string; +} +interface Derived extends Base {} +const derived = class Derived {}; + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 5, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + props: string; +} +type Derived = Base +const derived = class Derived {}; + `, + }, + ], + }, + ], + }, + { + code: ` +interface Base { + name: string; +} +interface Derived extends Base {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 5, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Base { + name: string; +} +type Derived = Base + `, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Array {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Array`, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Array {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + endColumn: 15, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Array`, + }, + ], + }, + { + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + line: 1, + column: 39, + endColumn: 41, + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: `interface Base extends Array {}`, + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: `interface Base extends Array {}`, + }, + ], + }, + ], + }, + { + code: ` +interface Derived { + property: string; +} +interface Base extends Array {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 5, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +interface Derived { + property: string; +} +type Base = Array + `, + }, + ], + }, + ], + }, + { + code: ` +type R = Record; +interface Base extends R {} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 3, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +type R = Record; +type Base = R + `, + }, + ], + }, + ], + }, + { + code: 'interface Base extends Derived {}', + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 1, + column: 11, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: `type Base = Derived`, + }, + ], + }, + ], + }, + { + filename: 'test.d.ts', + code: ` +declare namespace BaseAndDerived { + type Base = typeof base; + export interface Derived extends Base {} +} + `, + errors: [ + { + messageId: 'noEmptyInterfaceWithSuper', + line: 4, + column: 20, + endLine: 4, + endColumn: 27, + suggestions: [ + { + messageId: 'replaceEmptyInterfaceWithSuper', + output: ` +declare namespace BaseAndDerived { + type Base = typeof base; + export type Derived = Base +} + `, + }, + ], + }, + ], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], + }, + ], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type Base = unknown;', + }, + ], + }, + ], + options: [{ allowObjectTypes: 'never' }], + }, + { + code: 'let value: {};', + errors: [ + { + column: 12, + line: 1, + endColumn: 14, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: unknown;', + }, + ], + }, + ], + }, + { + code: 'let value: {};', + errors: [ + { + column: 12, + line: 1, + endColumn: 14, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'let value: unknown;', + }, + ], + }, + ], + options: [{ allowObjectTypes: 'never' }], + }, + { + code: ` +let value: { + /* ... */ +}; + `, + errors: [ + { + line: 2, + endLine: 4, + column: 12, + endColumn: 2, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: ` +let value: object; + `, + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: ` +let value: unknown; + `, + }, + ], + }, + ], + }, + { + code: 'type MyUnion = T | {};', + errors: [ + { + column: 23, + line: 1, + endColumn: 25, + endLine: 1, + data: { option: 'allowObjectTypes' }, + messageId: 'noEmptyObject', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyObjectType', + output: 'type MyUnion = T | object;', + }, + { + data: { replacement: 'unknown' }, + messageId: 'replaceEmptyObjectType', + output: 'type MyUnion = T | unknown;', + }, + ], + }, + ], + }, + { + code: 'type Base = {} | null;', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyObject', + }, + ], + options: [{ allowWithName: 'Base' }], + }, + { + code: 'type Base = {};', + errors: [ + { + column: 13, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyObject', + }, + ], + options: [{ allowWithName: 'Mismatch' }], + }, + { + code: 'interface Base {}', + errors: [ + { + column: 11, + line: 1, + endColumn: 15, + endLine: 1, + messageId: 'noEmptyInterface', + suggestions: [ + { + data: { replacement: 'object' }, + messageId: 'replaceEmptyInterface', + output: `type Base = object`, + }, + { + messageId: 'replaceEmptyInterface', + output: `type Base = unknown`, + }, + ], + }, + ], + options: [{ allowWithName: '.*Props$' }], + }, + ], +}); From 983ae70ddb6f486f13b3f2d8a0d7ef091edd71ea Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 13:16:21 +0000 Subject: [PATCH 2/4] Snapshots --- .../no-empty-object-type.shot | 136 ++++++++++++++++++ .../no-empty-object-type.shot | 38 +++++ 2 files changed, 174 insertions(+) create mode 100644 packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot create mode 100644 packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot new file mode 100644 index 000000000000..0c63badf7756 --- /dev/null +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -0,0 +1,136 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 1`] = ` +"Incorrect + +let anyObject: {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +let anyValue: {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +interface AnyObjectA {} + ~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +interface AnyValueA {} + ~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +type AnyObjectB = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +type AnyValueB = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 2`] = ` +"Correct + +let anyObject: object; +let anyValue: unknown; + +type AnyObjectA = object; +type AnyValueA = unknown; + +type AnyObjectB = object; +type AnyValueB = unknown; + +let objectWith: { property: boolean }; + +interface InterfaceWith { +property: boolean; +} + +type TypeWith = { property: boolean }; +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 3`] = ` +"Options: { "allowInterfaces": "with-single-extends" } + +interface Base { + value: boolean; +} + +interface Derived extends Base {} +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 4`] = ` +"Incorrect +Options: { "allowWithName": "Props$" } + +interface InterfaceValue {} + ~~~~~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +type TypeValue = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 5`] = ` +"Correct +Options: { "allowWithName": "Props$" } + +interface InterfaceProps {} + +type TypeProps = {}; +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 6`] = ` +"Correct +Options: { "allowObjectTypes": "always" } + +type Base = {}; +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 7`] = ` +"Incorrect +Options: { "allowWithName": "Props$" } + +interface InterfaceValue {} + ~~~~~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. + +type TypeValue = {}; + ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. +" +`; + +exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 8`] = ` +"Correct +Options: { "allowWithName": "Props$" } + +interface InterfaceProps {} + +type TypeProps = {}; +" +`; diff --git a/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot new file mode 100644 index 000000000000..632b3ce83ca9 --- /dev/null +++ b/packages/eslint-plugin/tests/schema-snapshots/no-empty-object-type.shot @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Rule schemas should be convertible to TS types for documentation purposes no-empty-object-type 1`] = ` +" +# SCHEMA: + +[ + { + "additionalProperties": false, + "properties": { + "allowInterfaces": { + "enum": ["always", "never", "with-single-extends"], + "type": "string" + }, + "allowObjectTypes": { + "enum": ["always", "in-type-alias-with-name", "never"], + "type": "string" + }, + "allowWithName": { + "type": "string" + } + }, + "type": "object" + } +] + + +# TYPES: + +type Options = [ + { + allowInterfaces?: 'always' | 'never' | 'with-single-extends'; + allowObjectTypes?: 'always' | 'in-type-alias-with-name' | 'never'; + allowWithName?: string; + }, +]; +" +`; From 07d597ee4a4f335eb13b76f8e5034d112d11d192 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 14:31:43 +0000 Subject: [PATCH 3/4] Fixed tests and doc --- .../docs/rules/no-empty-object-type.mdx | 22 +++++---- .../no-empty-object-type.shot | 45 +++++++++---------- .../tests/rules/no-empty-object-type.test.ts | 29 +++++++++--- 3 files changed, 58 insertions(+), 38 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx index 732f31aca267..6a0e27bd8834 100644 --- a/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx +++ b/packages/eslint-plugin/docs/rules/no-empty-object-type.mdx @@ -93,19 +93,22 @@ Examples of code for this rule with `{ allowInterfaces: 'with-single-extends' }` -```ts option='{ "allowInterfaces": "with-single-extends" }' showPlaygroundButton + +```ts option='{ "allowInterfaces": "with-single-extends" }' interface Foo {} ``` + -```ts option='{ "allowInterfaces": "with-single-extends" }' showPlaygroundButton + +```ts option='{ "allowInterfaces": "with-single-extends" }' interface Base { value: boolean; } interface Derived extends Base {} +``` -```` @@ -120,15 +123,18 @@ Examples of code for this rule with `{ allowObjectTypes: 'always' }`: -```ts option='{ "allowObjectTypes": "always" }' showPlaygroundButton + +```ts option='{ "allowObjectTypes": "always" }' interface Base {} -```` +``` -```ts option='{ "allowObjectTypes": "always" }' showPlaygroundButton + +```ts option='{ "allowObjectTypes": "always" }' type Base = {}; ``` + @@ -142,7 +148,7 @@ Examples of code for this rule with `{ allowWithName: 'Props$' }`: -```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton +```ts option='{ "allowWithName": "Props$" }' interface InterfaceValue {} type TypeValue = {}; @@ -151,7 +157,7 @@ type TypeValue = {}; -```ts option='{ "allowWithName": "Props$" }' showPlaygroundButton +```ts option='{ "allowWithName": "Props$" }' interface InterfaceProps {} type TypeProps = {}; diff --git a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot index 0c63badf7756..1dacb3b537d8 100644 --- a/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot +++ b/packages/eslint-plugin/tests/docs-eslint-output-snapshots/no-empty-object-type.shot @@ -53,7 +53,7 @@ type AnyValueB = unknown; let objectWith: { property: boolean }; interface InterfaceWith { -property: boolean; + property: boolean; } type TypeWith = { property: boolean }; @@ -61,41 +61,38 @@ type TypeWith = { property: boolean }; `; exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 3`] = ` -"Options: { "allowInterfaces": "with-single-extends" } - -interface Base { - value: boolean; -} +"Incorrect +Options: { "allowInterfaces": "with-single-extends" } -interface Derived extends Base {} +interface Foo {} + ~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. " `; exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 4`] = ` -"Incorrect -Options: { "allowWithName": "Props$" } +"Correct +Options: { "allowInterfaces": "with-single-extends" } -interface InterfaceValue {} - ~~~~~~~~~~~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. - - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. +interface Base { + value: boolean; +} -type TypeValue = {}; - ~~ The \`{}\` ("empty object") type allows any non-nullish value, including literals like \`0\` and \`""\`. - - If that's what you want, disable this lint rule with an inline comment or configure the 'allowObjectTypes' rule option. - - If you want a type meaning "any object", you probably want \`object\` instead. - - If you want a type meaning "any value", you probably want \`unknown\` instead. +interface Derived extends Base {} " `; exports[`Validating rule docs no-empty-object-type.mdx code examples ESLint output 5`] = ` -"Correct -Options: { "allowWithName": "Props$" } - -interface InterfaceProps {} +"Incorrect +Options: { "allowObjectTypes": "always" } -type TypeProps = {}; +interface Base {} + ~~~~ An empty interface declaration allows any non-nullish value, including literals like \`0\` and \`""\`. + - If that's what you want, disable this lint rule with an inline comment or configure the 'allowInterfaces' rule option. + - If you want a type meaning "any object", you probably want \`object\` instead. + - If you want a type meaning "any value", you probably want \`unknown\` instead. " `; diff --git a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts index 8bc0928ca1ba..6494b8d6372f 100644 --- a/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts +++ b/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts @@ -17,9 +17,11 @@ interface Base { interface Base { name: string; } + interface Derived { age: number; } + // valid because extending multiple interfaces can be used instead of a union type interface Both extends Base, Derived {} `, @@ -32,6 +34,7 @@ interface Both extends Base, Derived {} interface Base { name: string; } + interface Derived extends Base {} `, options: [{ allowInterfaces: 'with-single-extends' }], @@ -41,7 +44,9 @@ interface Derived extends Base {} interface Base { props: string; } + interface Derived extends Base {} + class Derived {} `, options: [{ allowInterfaces: 'with-single-extends' }], @@ -76,9 +81,9 @@ class Derived {} code: 'interface Base {}', errors: [ { + line: 1, column: 11, data: { option: 'allowInterfaces' }, - line: 1, messageId: 'noEmptyInterface', suggestions: [ { @@ -98,9 +103,9 @@ class Derived {} code: 'interface Base {}', errors: [ { + line: 1, column: 11, data: { option: 'allowInterfaces' }, - line: 1, messageId: 'noEmptyInterface', suggestions: [ { @@ -122,12 +127,14 @@ class Derived {} interface Base { props: string; } + interface Derived extends Base {} + class Other {} `, errors: [ { - line: 5, + line: 6, column: 11, messageId: 'noEmptyInterfaceWithSuper', suggestions: [ @@ -137,7 +144,9 @@ class Other {} interface Base { props: string; } + type Derived = Base + class Other {} `, }, @@ -150,12 +159,14 @@ class Other {} interface Base { props: string; } + interface Derived extends Base {} + class Derived {} `, errors: [ { - line: 5, + line: 6, column: 11, messageId: 'noEmptyInterfaceWithSuper', }, @@ -166,13 +177,15 @@ class Derived {} interface Base { props: string; } + interface Derived extends Base {} + const derived = class Derived {}; `, errors: [ { messageId: 'noEmptyInterfaceWithSuper', - line: 5, + line: 6, column: 11, suggestions: [ { @@ -181,7 +194,9 @@ const derived = class Derived {}; interface Base { props: string; } + type Derived = Base + const derived = class Derived {}; `, }, @@ -194,12 +209,13 @@ const derived = class Derived {}; interface Base { name: string; } + interface Derived extends Base {} `, errors: [ { messageId: 'noEmptyInterfaceWithSuper', - line: 5, + line: 6, column: 11, suggestions: [ { @@ -208,6 +224,7 @@ interface Derived extends Base {} interface Base { name: string; } + type Derived = Base `, }, From 3cd8a82544d4b948ffe191c7b9404801bbc621a6 Mon Sep 17 00:00:00 2001 From: Vincent Girard Date: Thu, 27 Jun 2024 14:50:19 +0000 Subject: [PATCH 4/4] Forgotten to add to all rules --- packages/typescript-eslint/src/configs/all.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typescript-eslint/src/configs/all.ts b/packages/typescript-eslint/src/configs/all.ts index c92e80a1f275..f14a7a51cf45 100644 --- a/packages/typescript-eslint/src/configs/all.ts +++ b/packages/typescript-eslint/src/configs/all.ts @@ -64,6 +64,7 @@ export default ( 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'error', '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-empty-object-type': 'error', '@typescript-eslint/no-explicit-any': 'error', '@typescript-eslint/no-extra-non-null-assertion': 'error', '@typescript-eslint/no-extraneous-class': 'error', 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