From 9b40bb39129c5fd6d6c572f4f6a5dbbd44e9584b Mon Sep 17 00:00:00 2001 From: roikoren755 Date: Thu, 11 Nov 2021 18:23:59 +0200 Subject: [PATCH] feat: add `es-roikoren/no-object-hasown` rule --- .changeset/thin-cows-notice.md | 5 +++++ docs/rules/README.md | 1 + docs/rules/no-object-hasown.md | 20 ++++++++++++++++++++ scripts/new-rule.ts | 15 ++++++++++----- src/configs/no-new-in-esnext.ts | 6 +++++- src/index.ts | 2 ++ src/rules/no-object-hasown.ts | 19 +++++++++++++++++++ src/util/reference-tracker.ts | 2 +- tests/src/rules/no-object-hasown.ts | 21 +++++++++++++++++++++ 9 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 .changeset/thin-cows-notice.md create mode 100644 docs/rules/no-object-hasown.md create mode 100644 src/rules/no-object-hasown.ts create mode 100644 tests/src/rules/no-object-hasown.ts diff --git a/.changeset/thin-cows-notice.md b/.changeset/thin-cows-notice.md new file mode 100644 index 00000000..b1a62b38 --- /dev/null +++ b/.changeset/thin-cows-notice.md @@ -0,0 +1,5 @@ +--- +'eslint-plugin-es-roikoren': patch +--- + +feat: add `es-roikoren/no-object-hasown` rule diff --git a/docs/rules/README.md b/docs/rules/README.md index b2e799b4..74c3ca3a 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -11,6 +11,7 @@ There is a config that enables the rules in this category: `plugin:es-roikoren/n | Rule ID | Description | | |:--------|:------------|:--:| | [es-roikoren/no-class-fields](./no-class-fields.md) | disallow class fields. | | +| [es-roikoren/no-object-hasown](./no-object-hasown.md) | disallow the `Object.hasOwn` method. | | | [es-roikoren/no-regexp-d-flag](./no-regexp-d-flag.md) | disallow RegExp `d` flag. | | ## ES2021 diff --git a/docs/rules/no-object-hasown.md b/docs/rules/no-object-hasown.md new file mode 100644 index 00000000..8e0895e8 --- /dev/null +++ b/docs/rules/no-object-hasown.md @@ -0,0 +1,20 @@ +# es-roikoren/no-object-hasown +> disallow the `Object.hasOwn` method. + +- ✅ The following configurations enable this rule: `plugin:es-roikoren/no-new-in-esnext` + +This rule reports ES2022 `Object.hasOwn` method as errors. + +## Examples + +⛔ Examples of **incorrect** code for this rule: + +```js +/*eslint es-roikoren/no-object-hasown: error */ +const hasFoo = Object.hasOwn(obj, 'foo'); +``` + +## 📚 References + +- [Rule source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.3/src/rules/no-object-hasown.ts) +- [Test source](https://github.com/roikoren755/eslint-plugin-es/blob/v0.0.3/tests/src/rules/no-object-hasown.ts) diff --git a/scripts/new-rule.ts b/scripts/new-rule.ts index bbb0c9fc..367c9f9e 100644 --- a/scripts/new-rule.ts +++ b/scripts/new-rule.ts @@ -2,21 +2,22 @@ import { writeFileSync } from 'fs'; import path from 'path'; import { TSESLint } from '@typescript-eslint/experimental-utils'; -const run = async (ruleId: string): Promise => { - if (!ruleId) { +const run = async (appliedRuleId: string): Promise => { + if (!appliedRuleId) { console.error('Usage: npm run new '); process.exitCode = 1; return; } - if (!/^[a-z0-9-]+$/u.test(ruleId)) { - console.error("Invalid RuleID '%s'.", ruleId); + if (!/^[a-z0-9-]+$/u.test(appliedRuleId)) { + console.error("Invalid RuleID '%s'.", appliedRuleId); process.exitCode = 1; return; } + const ruleId = `no-${appliedRuleId}`; const ruleFile = path.resolve(__dirname, `../src/rules/${ruleId}.ts`); const testFile = path.resolve(__dirname, `../tests/src/rules/${ruleId}.ts`); const docFile = path.resolve(__dirname, `../docs/rules/${ruleId}.md`); @@ -43,9 +44,13 @@ export default createRule<[], 'forbidden'>({ ); writeFileSync( testFile, - `import { RuleTester } from '../../tester'; + `import { AST_NODE_TYPES } from '@typescript-eslint/types'; + +import { RuleTester } from '../../tester'; import rule from '../../../src/rules/${ruleId}'; +const error = { messageId: 'forbidden' as const, line: 1, column: 1, type: AST_NODE_TYPES.MemberExpression, data: {} }; + if (!RuleTester.isSupported(2022)) { console.log('Skip the tests of ${ruleId}.'); } else { diff --git a/src/configs/no-new-in-esnext.ts b/src/configs/no-new-in-esnext.ts index bdaa5367..c11dd341 100644 --- a/src/configs/no-new-in-esnext.ts +++ b/src/configs/no-new-in-esnext.ts @@ -4,5 +4,9 @@ */ export default { plugins: ['es-roikoren'], - rules: { 'es-roikoren/no-class-fields': 'error', 'es-roikoren/no-regexp-d-flag': 'error' }, + rules: { + 'es-roikoren/no-class-fields': 'error', + 'es-roikoren/no-object-hasown': 'error', + 'es-roikoren/no-regexp-d-flag': 'error', + }, }; diff --git a/src/index.ts b/src/index.ts index c4280349..8f2e204f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -111,6 +111,7 @@ import noObjectGetownpropertydescriptors from './rules/no-object-getownpropertyd import noObjectGetownpropertynames from './rules/no-object-getownpropertynames'; import noObjectGetownpropertysymbols from './rules/no-object-getownpropertysymbols'; import noObjectGetprototypeof from './rules/no-object-getprototypeof'; +import noObjectHasown from './rules/no-object-hasown'; import noObjectIs from './rules/no-object-is'; import noObjectIsextensible from './rules/no-object-isextensible'; import noObjectIsfrozen from './rules/no-object-isfrozen'; @@ -283,6 +284,7 @@ export default { 'no-object-getownpropertynames': noObjectGetownpropertynames, 'no-object-getownpropertysymbols': noObjectGetownpropertysymbols, 'no-object-getprototypeof': noObjectGetprototypeof, + 'no-object-hasown': noObjectHasown, 'no-object-is': noObjectIs, 'no-object-isextensible': noObjectIsextensible, 'no-object-isfrozen': noObjectIsfrozen, diff --git a/src/rules/no-object-hasown.ts b/src/rules/no-object-hasown.ts new file mode 100644 index 00000000..39eb5398 --- /dev/null +++ b/src/rules/no-object-hasown.ts @@ -0,0 +1,19 @@ +import { ASTUtils } from '@typescript-eslint/experimental-utils'; + +import { createRule } from '../util/create-rule'; +import { referenceTracker } from '../util/reference-tracker'; + +export const category = 'ES2022'; +export default createRule<[], 'forbidden'>({ + name: 'no-object-hasown', + meta: { + type: 'problem', + docs: { description: 'disallow the `Object.hasOwn` method.', recommended: false }, + schema: [], + messages: { forbidden: "ES2022 '{{name}}' method is forbidden." }, + }, + defaultOptions: [], + create(context) { + return referenceTracker(context, { Object: { hasOwn: { [ASTUtils.ReferenceTracker.READ]: true } } }); + }, +}); diff --git a/src/util/reference-tracker.ts b/src/util/reference-tracker.ts index 4c230d41..68788919 100644 --- a/src/util/reference-tracker.ts +++ b/src/util/reference-tracker.ts @@ -3,7 +3,7 @@ import type { TSESLint } from '@typescript-eslint/experimental-utils'; export const referenceTracker = ( context: TSESLint.RuleContext<'forbidden', readonly []>, - traceMap: ASTUtils.ReferenceTracker.TraceMap, + traceMap: ASTUtils.ReferenceTracker.TraceMap, ): TSESLint.RuleListener => ({ 'Program:exit'() { const tracker = new ASTUtils.ReferenceTracker(context.getScope()); diff --git a/tests/src/rules/no-object-hasown.ts b/tests/src/rules/no-object-hasown.ts new file mode 100644 index 00000000..44266f32 --- /dev/null +++ b/tests/src/rules/no-object-hasown.ts @@ -0,0 +1,21 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/types'; + +import { RuleTester } from '../../tester'; +import rule from '../../../src/rules/no-object-hasown'; + +const error = { + messageId: 'forbidden' as const, + line: 1, + column: 1, + type: AST_NODE_TYPES.MemberExpression, + data: { name: 'Object.hasOwn' }, +}; + +if (!RuleTester.isSupported(2022)) { + console.log('Skip the tests of no-object-hasown.'); +} else { + new RuleTester().run('no-object-hasown', rule, { + valid: ['Object', 'Object.assign', 'let Object = 0; Object.is'], + invalid: [{ code: 'Object.hasOwn', errors: [error] }], + }); +}