diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index fe0d36026bd1..8dec4b435981 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -64,6 +64,7 @@ import noLossOfPrecision from './no-loss-of-precision'; import noMagicNumbers from './no-magic-numbers'; import noMeaninglessVoidOperator from './no-meaningless-void-operator'; import noMisusedNew from './no-misused-new'; +import noMisusedObjectLikes from './no-misused-object-likes'; import noMisusedPromises from './no-misused-promises'; import noMixedEnums from './no-mixed-enums'; import noNamespace from './no-namespace'; @@ -210,6 +211,7 @@ export default { 'no-magic-numbers': noMagicNumbers, 'no-meaningless-void-operator': noMeaninglessVoidOperator, 'no-misused-new': noMisusedNew, + 'no-misused-object-likes': noMisusedObjectLikes, 'no-misused-promises': noMisusedPromises, 'no-mixed-enums': noMixedEnums, 'no-namespace': noNamespace, diff --git a/packages/eslint-plugin/src/rules/no-misused-object-likes.ts b/packages/eslint-plugin/src/rules/no-misused-object-likes.ts new file mode 100644 index 000000000000..e0ccbdd0c597 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-misused-object-likes.ts @@ -0,0 +1,149 @@ +import type { Identifier, MemberExpression } from '@typescript-eslint/ast-spec'; +import type { TSESTree } from '@typescript-eslint/utils'; + +import { createRule, getParserServices } from '../util'; + +export type Options = [ + { + checkObjectKeysForMap?: boolean; + checkObjectValuesForMap?: boolean; + checkObjectEntriesForMap?: boolean; + checkObjectKeysForSet?: boolean; + checkObjectValuesForSet?: boolean; + checkObjectEntriesForSet?: boolean; + }, +]; +export type MessageIds = + | 'objectKeysForMap' + | 'objectValuesForMap' + | 'objectEntriesForMap' + | 'objectKeysForSet' + | 'objectValuesForSet' + | 'objectEntriesForSet'; + +export default createRule({ + name: 'no-misused-object-likes', + defaultOptions: [ + { + checkObjectKeysForMap: true, + checkObjectValuesForMap: true, + checkObjectEntriesForMap: true, + checkObjectKeysForSet: true, + checkObjectValuesForSet: true, + checkObjectEntriesForSet: true, + }, + ], + + meta: { + type: 'problem', + docs: { + description: + 'Enforce check `Object.values(...)`, `Object.keys(...)`, `Object.entries(...)` usage with Map/Set objects', + requiresTypeChecking: false, + }, + messages: { + objectKeysForMap: "Don't use `Object.keys()` for Map objects", + objectValuesForMap: "Don't use `Object.values()` for Map objects", + objectEntriesForMap: "Don't use `Object.entries()` for Map objects", + objectKeysForSet: "Don't use `Object.keys()` for Set", + objectValuesForSet: "Don't use `Object.values()` for Set", + objectEntriesForSet: "Don't use `Object.entries()` for Set", + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + checkObjectKeysForMap: { + description: 'Check usage Object.keys for Map object', + type: 'boolean', + }, + checkObjectValuesForMap: { + description: 'Check usage Object.values for Map object', + type: 'boolean', + }, + checkObjectEntriesForMap: { + description: 'Check usage Object.entries for Map object', + type: 'boolean', + }, + checkObjectKeysForSet: { + description: 'Check usage Object.keys for Set object', + type: 'boolean', + }, + checkObjectValuesForSet: { + description: 'Check usage Object.values for Set object', + type: 'boolean', + }, + checkObjectEntriesForSet: { + description: 'Check usage Object.entries for Set object', + type: 'boolean', + }, + }, + }, + ], + }, + + create(context, [options]) { + const services = getParserServices(context); + + function checkObjectMethodCall( + callExpression: TSESTree.CallExpression, + ): void { + const argument = callExpression.arguments[0]; + const type = services.getTypeAtLocation(argument); + const argumentTypeName = type.getSymbol()?.name; + const callee = callExpression.callee as MemberExpression; + const objectMethod = (callee.property as Identifier).name; + + if (argumentTypeName === 'Map') { + if (objectMethod === 'keys' && options.checkObjectKeysForMap) { + context.report({ + node: callExpression, + messageId: 'objectKeysForMap', + }); + } + if (objectMethod === 'values' && options.checkObjectValuesForMap) { + context.report({ + node: callExpression, + messageId: 'objectValuesForMap', + }); + } + if (objectMethod === 'entries' && options.checkObjectEntriesForMap) { + context.report({ + node: callExpression, + messageId: 'objectEntriesForMap', + }); + } + } + if (argumentTypeName === 'Set') { + if (objectMethod === 'keys' && options.checkObjectKeysForSet) { + context.report({ + node: callExpression, + messageId: 'objectKeysForSet', + }); + } + if (objectMethod === 'values' && options.checkObjectValuesForSet) { + context.report({ + node: callExpression, + messageId: 'objectValuesForSet', + }); + } + if (objectMethod === 'entries' && options.checkObjectEntriesForSet) { + context.report({ + node: callExpression, + messageId: 'objectEntriesForSet', + }); + } + } + } + + return { + 'CallExpression[callee.object.name=Object][callee.property.name=keys][arguments.length=1]': + checkObjectMethodCall, + 'CallExpression[callee.object.name=Object][callee.property.name=values][arguments.length=1]': + checkObjectMethodCall, + 'CallExpression[callee.object.name=Object][callee.property.name=entries][arguments.length=1]': + checkObjectMethodCall, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts b/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts new file mode 100644 index 000000000000..e4fe49033601 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-misused-object-likes.test.ts @@ -0,0 +1,174 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; + +import rule from '../../src/rules/no-misused-object-likes'; +import { getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-misused-object-likes', rule, { + valid: [ + { + code: ` + class ExMap extends Map {} + const map = new ExMap(); + Object.keys(map); + `, + }, + { + code: ` + class ExMap extends Map {} + const map = new ExMap(); + Object.values(map); + `, + }, + { + code: ` + class ExMap extends Map {} + const map = new ExMap(); + Object.entries(map); + `, + }, + { + code: ` + const test = {}; + Object.entries(test); + `, + }, + { + code: ` + const test = {}; + Object.keys(test); + `, + }, + { + code: ` + const test = {}; + Object.values(test); + `, + }, + { + code: ` + const test = []; + Object.keys(test); + `, + }, + { + code: ` + const test = []; + Object.values(test); + `, + }, + { + code: ` + const test = []; + Object.entries(test); + `, + }, + { + options: [{ checkObjectKeysForMap: false }], + code: ` + const map = new Map(); + const result = Object.keys(map); + `, + }, + { + options: [{ checkObjectEntriesForMap: false }], + code: ` + const map = new Map(); + const result = Object.entries(map); + `, + }, + { + options: [{ checkObjectValuesForMap: false }], + code: ` + const map = new Map(); + const result = Object.values(map); + `, + }, + { + options: [{ checkObjectKeysForSet: false }], + code: ` + const set = new Set(); + const result = Object.keys(set); + `, + }, + { + options: [{ checkObjectEntriesForSet: false }], + code: ` + const set = new Set(); + const result = Object.entries(set); + `, + }, + { + options: [{ checkObjectValuesForSet: false }], + code: ` + const set = new Set(); + const result = Object.values(set); + `, + }, + { + code: ` + const test = 123; + Object.keys(test); + `, + }, + { + code: ` + const test = new WeakMap(); + Object.keys(test); + `, + }, + ], + invalid: [ + { + code: ` + const map = new Map(); + const result = Object.keys(map); + `, + errors: [{ messageId: 'objectKeysForMap' }], + }, + { + code: ` + const map = new Map(); + const result = Object.entries(map); + `, + errors: [{ messageId: 'objectEntriesForMap' }], + }, + { + code: ` + const map = new Map(); + const result = Object.values(map); + `, + errors: [{ messageId: 'objectValuesForMap' }], + }, + { + code: ` + const set = new Set(); + const result = Object.keys(set); + `, + errors: [{ messageId: 'objectKeysForSet' }], + }, + { + code: ` + const set = new Set(); + const result = Object.entries(set); + `, + errors: [{ messageId: 'objectEntriesForSet' }], + }, + { + code: ` + const set = new Set(); + const result = Object.values(set); + `, + errors: [{ messageId: 'objectValuesForSet' }], + }, + ], +});